From 62b8e72e65f81dbae730846d7b6dafc2c904f780 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 09:36:03 +0800 Subject: [PATCH 01/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E4=BA=86=E5=85=B3?= =?UTF-8?q?=E8=8A=82=E8=BD=AF=E9=99=90=E4=BD=8D=E5=92=8C=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E8=AF=9D=E9=A2=98=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/joint_motion_limit.yaml | 46 ++++++++++++++++++++++++++++++++ config/joint_soft_limits.yaml | 47 +++++++++++++++++++++++++++++++++ src/msgs/msg/ControlActions.msg | 6 +++++ 3 files changed, 99 insertions(+) create mode 100644 config/joint_motion_limit.yaml create mode 100644 config/joint_soft_limits.yaml create mode 100644 src/msgs/msg/ControlActions.msg diff --git a/config/joint_motion_limit.yaml b/config/joint_motion_limit.yaml new file mode 100644 index 0000000..75ebdb8 --- /dev/null +++ b/config/joint_motion_limit.yaml @@ -0,0 +1,46 @@ +arm_joint_limits: + right_arm: + joint_1: + vec: 4 + acc: 4 + joint_2: + vec: 4 + acc: 4 + joint_3: + vec: 4 + acc: 4 + joint_4: + vec: 4 + acc: 4 + joint_5: + vec: 4 + acc: 4 + joint_6: + vec: 4 + acc: 4 + joint_7: + vec: 4 + acc: 4 + + left_arm: + joint_1: + vec: 4 + acc: 4 + joint_2: + vec: 4 + acc: 4 + joint_3: + vec: 4 + acc: 4 + joint_4: + vec: 4 + acc: 4 + joint_5: + vec: 4 + acc: 4 + joint_6: + vec: 4 + acc: 4 + joint_7: + vec: 4 + acc: 4 \ No newline at end of file diff --git a/config/joint_soft_limits.yaml b/config/joint_soft_limits.yaml new file mode 100644 index 0000000..ba4c70a --- /dev/null +++ b/config/joint_soft_limits.yaml @@ -0,0 +1,47 @@ +arm_joint_limits: + right_arm: + joint_1: + min: -167 + max: 167 + joint_2: + min: -4 + max: 183 + joint_3: + min: -154 + max: 154 + joint_4: + min: -117 + max: 117 + joint_5: + min: -154 + max: 154 + joint_6: + min: -91 + max: 91 + joint_7: + min: -91 + max: 91 + + left_arm: + joint_1: + min: -167 + max: 167 + joint_2: + min: -183 + max: 4 + joint_3: + min: -154 + max: 154 + joint_4: + min: -117 + max: 117 + joint_5: + min: -154 + max: 154 + joint_6: + min: -91 + max: 91 + joint_7: + min: -91 + max: 91 + \ No newline at end of file diff --git a/src/msgs/msg/ControlActions.msg b/src/msgs/msg/ControlActions.msg new file mode 100644 index 0000000..98a5121 --- /dev/null +++ b/src/msgs/msg/ControlActions.msg @@ -0,0 +1,6 @@ +std_msgs/Header header + +uint64 system_timestamp +string arm_ip + +float64[12] \ No newline at end of file -- 2.52.0 From 3955956604eeb3dc0b8c5d1e18aef5a1ea225187 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 09:44:19 +0800 Subject: [PATCH 02/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E5=AE=98=E6=96=B9sdk,?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=B0=86=E5=9C=A8=E6=AD=A4=E5=9F=BA=E7=A1=80?= =?UTF-8?q?=E4=B8=8A=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lbot_arm_api | 1 + 1 file changed, 1 insertion(+) create mode 160000 src/lbot_arm_api diff --git a/src/lbot_arm_api b/src/lbot_arm_api new file mode 160000 index 0000000..e1c4a45 --- /dev/null +++ b/src/lbot_arm_api @@ -0,0 +1 @@ +Subproject commit e1c4a4523a46e80e40c5dbe8cc651f2cf5fa4a51 -- 2.52.0 From c649b258451ecc7abc600ec2661a74972c3b5faa Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 09:48:36 +0800 Subject: [PATCH 03/18] =?UTF-8?q?=E5=88=A0=E6=8E=89=E4=BA=86=E5=AE=98?= =?UTF-8?q?=E6=96=B9sdk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lbot_arm_api | 1 - 1 file changed, 1 deletion(-) delete mode 160000 src/lbot_arm_api diff --git a/src/lbot_arm_api b/src/lbot_arm_api deleted file mode 160000 index e1c4a45..0000000 --- a/src/lbot_arm_api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e1c4a4523a46e80e40c5dbe8cc651f2cf5fa4a51 -- 2.52.0 From 9479a8ccb2f81f60deda471d80deeb7095b5f572 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 10:50:14 +0800 Subject: [PATCH 04/18] =?UTF-8?q?=E6=9C=BA=E6=A2=B0=E8=87=82=E4=B8=8E?= =?UTF-8?q?=E7=81=B5=E5=B7=A7=E6=89=8B=E4=BD=8D=E6=8E=A7=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E5=B7=B2=E5=AE=8C=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 255 +++ scripts/lbot_arm_control.sh | 138 ++ .../lbot_arm_interfaces/CMakeLists.txt | 71 + .../lbot_arm_interfaces/msg/ArmState.msg | 3 + .../lbot_arm_interfaces/msg/FollowJoint.msg | 2 + .../lbot_arm_interfaces/msg/LbotFrame.msg | 3 + .../lbot_arm_interfaces/msg/LbotPose.msg | 2 + .../lbot_arm_interfaces/package.xml | 25 + .../lbot_arm_interfaces/srv/ChangeFrame.srv | 3 + .../lbot_arm_interfaces/srv/DeleteFrame.srv | 3 + .../srv/ForwardKinematics.srv | 7 + .../lbot_arm_interfaces/srv/GetAllFrames.srv | 4 + .../srv/GetCurrentFrame.srv | 5 + .../lbot_arm_interfaces/srv/GetFrame.srv | 4 + .../srv/InverseKinematics.srv | 6 + .../lbot_arm_interfaces/srv/MoveC.srv | 9 + .../lbot_arm_interfaces/srv/MoveJ.srv | 7 + .../lbot_arm_interfaces/srv/MoveJP.srv | 9 + .../lbot_arm_interfaces/srv/MoveL.srv | 9 + .../lbot_arm_interfaces/srv/SetEmergency.srv | 5 + .../lbot_arm_interfaces/srv/SetEnable.srv | 5 + .../lbot_arm_interfaces/srv/SetFrame.srv | 3 + .../lbot_arm_interfaces/srv/SetString.srv | 3 + .../lbot_arm_interfaces/srv/SetZero.srv | 3 + src/officer_sdk/lbot_driver/CMakeLists.txt | 64 + .../lbot_driver/config/lbot_config.yaml | 4 + .../include/lbot_driver/lbot_api.h | 485 +++++ .../include/lbot_driver/lbot_api_cpp.h | 660 ++++++ .../include/lbot_driver/lbot_driver.h | 431 ++++ .../include/lbot_driver/lbot_types.h | 99 + .../include/lbot_driver/lbot_version.h | 23 + .../launch/lbot_start_driver.launch.py | 41 + .../lbot_driver/lib/lib_install.sh | 36 + .../lbot_driver/lib/liblbot_api_cpp.so | 1 + .../lbot_driver/lib/liblbot_api_cpp.so.1 | 1 + .../lbot_driver/lib/liblbot_api_cpp.so.1.0.0 | Bin 0 -> 139864 bytes .../lib/linux_arm64/liblbot_api.so | Bin 0 -> 84824 bytes .../lib/linux_arm64/liblbot_api.so.1 | Bin 0 -> 84824 bytes .../lib/linux_arm64/liblbot_api.so.1.0.5 | Bin 0 -> 84824 bytes .../lib/linux_arm64/liblbot_api_cpp.so | Bin 0 -> 118032 bytes .../lib/linux_arm64/liblbot_api_cpp.so.1 | Bin 0 -> 118032 bytes .../lib/linux_arm64/liblbot_api_cpp.so.1.0.5 | Bin 0 -> 118032 bytes .../lbot_driver/lib/linux_x64/liblbot_api.so | Bin 0 -> 106976 bytes .../lib/linux_x64/liblbot_api.so.1 | Bin 0 -> 106976 bytes .../lib/linux_x64/liblbot_api.so.1.0.5 | Bin 0 -> 106976 bytes .../lib/linux_x64/liblbot_api_cpp.so | Bin 0 -> 139864 bytes .../lib/linux_x64/liblbot_api_cpp.so.1 | Bin 0 -> 139864 bytes .../lib/linux_x64/liblbot_api_cpp.so.1.0.5 | Bin 0 -> 139864 bytes src/officer_sdk/lbot_driver/package.xml | 26 + .../lbot_driver/src/lbot_driver.cpp | 1782 +++++++++++++++++ 50 files changed, 4237 insertions(+) create mode 100755 scripts/lbot_arm_control.sh create mode 100755 src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt create mode 100755 src/officer_sdk/lbot_arm_interfaces/msg/ArmState.msg create mode 100755 src/officer_sdk/lbot_arm_interfaces/msg/FollowJoint.msg create mode 100644 src/officer_sdk/lbot_arm_interfaces/msg/LbotFrame.msg create mode 100755 src/officer_sdk/lbot_arm_interfaces/msg/LbotPose.msg create mode 100755 src/officer_sdk/lbot_arm_interfaces/package.xml create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/ChangeFrame.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/DeleteFrame.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/ForwardKinematics.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/GetAllFrames.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/GetCurrentFrame.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/GetFrame.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/InverseKinematics.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/MoveC.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/MoveJP.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/MoveL.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/SetEmergency.srv create mode 100755 src/officer_sdk/lbot_arm_interfaces/srv/SetEnable.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/SetFrame.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/SetString.srv create mode 100644 src/officer_sdk/lbot_arm_interfaces/srv/SetZero.srv create mode 100644 src/officer_sdk/lbot_driver/CMakeLists.txt create mode 100755 src/officer_sdk/lbot_driver/config/lbot_config.yaml create mode 100644 src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api.h create mode 100644 src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api_cpp.h create mode 100644 src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h create mode 100644 src/officer_sdk/lbot_driver/include/lbot_driver/lbot_types.h create mode 100644 src/officer_sdk/lbot_driver/include/lbot_driver/lbot_version.h create mode 100755 src/officer_sdk/lbot_driver/launch/lbot_start_driver.launch.py create mode 100755 src/officer_sdk/lbot_driver/lib/lib_install.sh create mode 120000 src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so create mode 120000 src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1 create mode 100755 src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1.0.0 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1.0.5 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1.0.5 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api.so create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api.so.1 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api.so.1.0.5 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1 create mode 100755 src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1.0.5 create mode 100644 src/officer_sdk/lbot_driver/package.xml create mode 100644 src/officer_sdk/lbot_driver/src/lbot_driver.cpp diff --git a/README.md b/README.md index e69de29..bd44893 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,255 @@ +# wpz_LbotArm 接口说明 + +本工程当前使用 `src/officer_sdk/lbot_driver` 作为机器人网口连接和 ROS2 bridge。启动脚本为: + +```bash +cd ~/workspace/wpz_LbotArm/scripts +./lbot_arm_control.sh start +``` + +停止服务: + +```bash +cd ~/workspace/wpz_LbotArm/scripts +./lbot_arm_control.sh stop +``` + +默认启动的机器人 namespace 是 `/robot1`,默认连接控制器 IP 为 `192.168.10.21`。 + +## 机械臂反馈订阅 + +外部软件如果需要订阅机器人当前机械臂各关节角度、速度、电流/力矩,使用下面两个 topic: + +```text +/robot1/left_arm/joint_states +/robot1/right_arm/joint_states +``` + +消息类型: + +```text +sensor_msgs/msg/JointState +``` + +字段含义: + +```text +name[] 关节名称 +position[] 当前 7 个关节角度,单位 rad +velocity[] 当前 7 个关节速度,单位 rad/s +effort[] 当前 7 个关节 effort 字段;当前 driver 直接填入 SDK 的 effort[7],按项目使用可视为关节电流/力矩反馈字段 +``` + +示例: + +```bash +ros2 topic echo /robot1/left_arm/joint_states +ros2 topic echo /robot1/right_arm/joint_states +``` + +当前反馈中没有关节加速度字段。如果外部软件必须获取关节加速度,需要在上游根据 `velocity[]` 和时间戳自行差分,或者让厂家提供 SDK 原生加速度反馈接口。 + +## 机械臂关节空间控制 + +外部软件如果需要发送机械臂各关节目标位置,使用 MoveJ 服务,不是 topic。 + +左臂: + +```text +/robot1/left_arm/move_joint +``` + +右臂: + +```text +/robot1/right_arm/move_joint +``` + +服务类型: + +```text +lbot_arm_interfaces/srv/MoveJ +``` + +服务定义: + +```srv +float32[] joints +float32 speed +float32 acce +bool block +--- +bool success +``` + +字段含义: + +```text +joints 7 个目标关节角,单位 rad +speed 整条机械臂统一目标速度,单位 rad/s +acce 整条机械臂统一目标加速度,单位 rad/s^2 +block true 表示阻塞等待运动完成,false 表示非阻塞下发 +``` + +示例: + +```bash +ros2 service call /robot1/left_arm/move_joint lbot_arm_interfaces/srv/MoveJ \ +"{joints: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], speed: 1.0, acce: 1.0, block: true}" +``` + +当前限制: + +```text +MoveJ 支持每个关节独立目标位置 joints[7] +MoveJ 不支持每个关节独立目标速度 velocity[7] +MoveJ 不支持每个关节独立目标加速度 acceleration[7] +speed 和 acce 是整条机械臂统一标量 +``` + +如果后续必须支持每个关节单独目标速度和加速度,需要修改: + +```text +src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv +src/officer_sdk/lbot_driver/src/lbot_driver.cpp +src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h +``` + +但当前 SDK 的 `lbot_move_joint()` 接口本身只接收: + +```cpp +const double joints[7], double speed, double accel, bool block +``` + +因此即使 ROS service 增加 `velocity[7]`、`acceleration[7]` 字段,也不能直接传给现有 SDK。要真正实现每关节速度/加速度控制,需要厂家提供更底层的控制接口,或者在 ROS 层自己做轨迹插值,再周期性下发关节位置。 + +## L20/R20 灵巧手整手位置控制 + +外部软件如果需要一次性控制机器人灵巧手所有关节位置,使用下面两个 topic: + +```text +/robot1/left_hand/set_l20_joint +/robot1/right_hand/set_l20_joint +``` + +消息类型: + +```text +std_msgs/msg/Int32MultiArray +``` + +字段含义: + +```text +data[] L20/R20 灵巧手目标位置数组 +``` + +当前 driver 中该 topic 直接调用 SDK: + +```cpp +lbot_l20_set_all_position(lbot_handle, LBOT_LEFT_ARM, hand_joints) +lbot_l20_set_all_position(lbot_handle, LBOT_RIGHT_ARM, hand_joints) +``` + +SDK 头文件注释写明该接口参数是: + +```text +16 个自由度的目标位置,单位 degree +``` + +因此当前整手控制按 16 个整数位置发送。 + +示例: + +```bash +ros2 topic pub --once /robot1/left_hand/set_l20_joint std_msgs/msg/Int32MultiArray \ +"{data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}" +``` + +右手示例: + +```bash +ros2 topic pub --once /robot1/right_hand/set_l20_joint std_msgs/msg/Int32MultiArray \ +"{data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}" +``` + +## L20/R20 单指串联位置控制 + +如果只需要控制单根手指,可以使用: + +```text +/robot1/left_hand/set_l20_series_position +/robot1/right_hand/set_l20_series_position +``` + +消息类型: + +```text +std_msgs/msg/Int32MultiArray +``` + +当前约定的数据格式: + +```text +data[0] finger 编号 +data[1..6] 该手指的 6 个位置参数 +``` + +finger 编号: + +```text +0 thumb +1 index +2 middle +3 ring +4 little +``` + +示例,控制左手食指: + +```bash +ros2 topic pub --once /robot1/left_hand/set_l20_series_position std_msgs/msg/Int32MultiArray \ +"{data: [1, 0, 30, 30, 0, 0, 0]}" +``` + +## 相关代码位置 + +反馈发布: + +```text +src/officer_sdk/lbot_driver/src/lbot_driver.cpp +``` + +主要位置: + +```text +LBot::state_publish_timer_callback() +``` + +机械臂 MoveJ 服务: + +```text +LeftArmServiceNode::move_joint_callback() +RightArmServiceNode::move_joint_callback() +``` + +L20/R20 灵巧手 topic: + +```text +LeftArmServiceNode::left_hand_l20_set_joint_callback() +RightArmServiceNode::right_hand_l20_set_joint_callback() +LeftArmServiceNode::left_hand_l20_set_series_position_callback() +RightArmServiceNode::right_hand_l20_set_series_position_callback() +``` + +SDK API 声明: + +```text +src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api_cpp.h +``` + +ROS 接口定义: + +```text +src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv +``` diff --git a/scripts/lbot_arm_control.sh b/scripts/lbot_arm_control.sh new file mode 100755 index 0000000..ca10ac5 --- /dev/null +++ b/scripts/lbot_arm_control.sh @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WS_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +STATE_DIR="/tmp/lbot_arm_control" +PID_FILE="${STATE_DIR}/lbot_driver.pid" +PGID_FILE="${STATE_DIR}/lbot_driver.pgid" +LOG_FILE="${WS_DIR}/log/lbot_arm_control.log" + +usage() { + echo "Usage: $0 {start|stop|status}" +} + +is_running() { + [[ -f "${PID_FILE}" ]] || return 1 + local pid + pid="$(cat "${PID_FILE}")" + [[ -n "${pid}" ]] || return 1 + kill -0 "${pid}" 2>/dev/null +} + +source_workspace() { + set +u + if [[ -n "${ROS_DISTRO:-}" && -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then + # shellcheck disable=SC1090 + source "/opt/ros/${ROS_DISTRO}/setup.bash" + elif [[ -f "/opt/ros/jazzy/setup.bash" ]]; then + # shellcheck disable=SC1091 + source "/opt/ros/jazzy/setup.bash" + fi + set -u + + if [[ ! -f "${WS_DIR}/install/setup.bash" ]]; then + echo "Workspace is not built: ${WS_DIR}/install/setup.bash not found" + echo "Run: cd ${WS_DIR} && colcon build --packages-select lbot_arm_interfaces lbot_driver" + exit 1 + fi + + # shellcheck disable=SC1091 + set +u + source "${WS_DIR}/install/setup.bash" + set -u +} + +start_driver() { + mkdir -p "${STATE_DIR}" "$(dirname "${LOG_FILE}")" + + if is_running; then + echo "lbot_driver is already running, pid=$(cat "${PID_FILE}")" + return 0 + fi + + source_workspace + + echo "Starting lbot_driver from officer_sdk..." + echo "Log: ${LOG_FILE}" + + : > "${LOG_FILE}" + setsid bash -lc "set +u; source '${WS_DIR}/install/setup.bash'; set -u; exec ros2 launch lbot_driver lbot_start_driver.launch.py" \ + >> "${LOG_FILE}" 2>&1 & + + local pid=$! + local pgid + pgid="$(ps -o pgid= -p "${pid}" | tr -d ' ')" + echo "${pid}" > "${PID_FILE}" + echo "${pgid:-${pid}}" > "${PGID_FILE}" + + sleep 1 + if is_running; then + echo "lbot_driver started, pid=${pid}, pgid=$(cat "${PGID_FILE}")" + else + echo "lbot_driver failed to start. Check log: ${LOG_FILE}" + rm -f "${PID_FILE}" "${PGID_FILE}" + exit 1 + fi +} + +stop_driver() { + if ! [[ -f "${PID_FILE}" ]]; then + echo "lbot_driver is not running" + return 0 + fi + + local pid pgid + pid="$(cat "${PID_FILE}")" + pgid="${pid}" + if [[ -f "${PGID_FILE}" ]]; then + pgid="$(cat "${PGID_FILE}")" + fi + + if ! kill -0 "${pid}" 2>/dev/null; then + echo "lbot_driver process is not alive, cleaning state" + rm -f "${PID_FILE}" "${PGID_FILE}" + return 0 + fi + + echo "Stopping lbot_driver, pid=${pid}, pgid=${pgid}..." + kill -TERM -- "-${pgid}" 2>/dev/null || kill -TERM "${pid}" 2>/dev/null || true + + for _ in {1..30}; do + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${PID_FILE}" "${PGID_FILE}" + echo "lbot_driver stopped" + return 0 + fi + sleep 0.2 + done + + echo "lbot_driver did not exit after SIGTERM, forcing stop..." + kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true + rm -f "${PID_FILE}" "${PGID_FILE}" + echo "lbot_driver stopped" +} + +status_driver() { + if is_running; then + echo "lbot_driver is running, pid=$(cat "${PID_FILE}"), pgid=$(cat "${PGID_FILE}" 2>/dev/null || cat "${PID_FILE}")" + else + echo "lbot_driver is not running" + fi +} + +case "${1:-}" in + start) + start_driver + ;; + stop) + stop_driver + ;; + status) + status_driver + ;; + *) + usage + exit 1 + ;; +esac diff --git a/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt b/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt new file mode 100755 index 0000000..87b542c --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.5) +project(lbot_arm_interfaces) + +# Default to C99 +if(NOT CMAKE_C_STANDARD) + set(CMAKE_C_STANDARD 99) +endif() + +# Default to C++14 +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 14) +endif() + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(std_msgs REQUIRED) +find_package(sensor_msgs REQUIRED) + +find_package(rosidl_default_generators REQUIRED) + +rosidl_generate_interfaces(${PROJECT_NAME} + # srv + "srv/MoveJ.srv" + "srv/MoveL.srv" + "srv/MoveC.srv" + "srv/MoveJP.srv" + "srv/InverseKinematics.srv" + "srv/ForwardKinematics.srv" + "srv/SetFrame.srv" + "srv/SetString.srv" + "srv/GetFrame.srv" + "srv/GetCurrentFrame.srv" + "srv/ChangeFrame.srv" + "srv/DeleteFrame.srv" + "srv/GetAllFrames.srv" + "srv/SetZero.srv" + "srv/SetEmergency.srv" + "srv/SetEnable.srv" + # msg + "msg/ArmState.msg" + "msg/LbotPose.msg" + "msg/LbotFrame.msg" + "msg/FollowJoint.msg" + + DEPENDENCIES std_msgs geometry_msgs sensor_msgs + ) + + ament_export_dependencies(rosidl_default_runtime) + + +# uncomment the following section in order to fill in +# further dependencies manually. +# find_package( REQUIRED) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # the following line skips the linter which checks for copyrights + # uncomment the line when a copyright and license is not present in all source files + #set(ament_cmake_copyright_FOUND TRUE) + # the following line skips cpplint (only works in a git repo) + # uncomment the line when this package is not in a git repo + #set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/src/officer_sdk/lbot_arm_interfaces/msg/ArmState.msg b/src/officer_sdk/lbot_arm_interfaces/msg/ArmState.msg new file mode 100755 index 0000000..68e26f0 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/msg/ArmState.msg @@ -0,0 +1,3 @@ +float32[] joints +geometry_msgs/Vector3 euler +geometry_msgs/Pose pose diff --git a/src/officer_sdk/lbot_arm_interfaces/msg/FollowJoint.msg b/src/officer_sdk/lbot_arm_interfaces/msg/FollowJoint.msg new file mode 100755 index 0000000..c8f6760 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/msg/FollowJoint.msg @@ -0,0 +1,2 @@ +float32[] joints +bool follow diff --git a/src/officer_sdk/lbot_arm_interfaces/msg/LbotFrame.msg b/src/officer_sdk/lbot_arm_interfaces/msg/LbotFrame.msg new file mode 100644 index 0000000..886d08c --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/msg/LbotFrame.msg @@ -0,0 +1,3 @@ +string name +geometry_msgs/Vector3 euler +geometry_msgs/Vector3 position \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/msg/LbotPose.msg b/src/officer_sdk/lbot_arm_interfaces/msg/LbotPose.msg new file mode 100755 index 0000000..e279aa2 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/msg/LbotPose.msg @@ -0,0 +1,2 @@ +geometry_msgs/Vector3 euler +geometry_msgs/Vector3 position diff --git a/src/officer_sdk/lbot_arm_interfaces/package.xml b/src/officer_sdk/lbot_arm_interfaces/package.xml new file mode 100755 index 0000000..eb8b176 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/package.xml @@ -0,0 +1,25 @@ + + + + lbot_arm_interfaces + 0.0.0 + TODO: Package description + Ross + TODO: License declaration + + ament_cmake + + rosidl_default_generators + rosidl_default_runtime + rosidl_interface_packages + + ament_lint_auto + ament_lint_common + + + ament_cmake + + std_msgs + geometry_msgs + sensor_msgs + diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/ChangeFrame.srv b/src/officer_sdk/lbot_arm_interfaces/srv/ChangeFrame.srv new file mode 100644 index 0000000..59e4a8a --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/ChangeFrame.srv @@ -0,0 +1,3 @@ +string name +--- +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/DeleteFrame.srv b/src/officer_sdk/lbot_arm_interfaces/srv/DeleteFrame.srv new file mode 100644 index 0000000..59e4a8a --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/DeleteFrame.srv @@ -0,0 +1,3 @@ +string name +--- +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/ForwardKinematics.srv b/src/officer_sdk/lbot_arm_interfaces/srv/ForwardKinematics.srv new file mode 100755 index 0000000..9622f3d --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/ForwardKinematics.srv @@ -0,0 +1,7 @@ +float32[] joints + +--- +geometry_msgs/Vector3 position +geometry_msgs/Vector3 euler + +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/GetAllFrames.srv b/src/officer_sdk/lbot_arm_interfaces/srv/GetAllFrames.srv new file mode 100644 index 0000000..f468109 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/GetAllFrames.srv @@ -0,0 +1,4 @@ + +--- +string[] names +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/GetCurrentFrame.srv b/src/officer_sdk/lbot_arm_interfaces/srv/GetCurrentFrame.srv new file mode 100644 index 0000000..824338f --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/GetCurrentFrame.srv @@ -0,0 +1,5 @@ +--- +string name +lbot_arm_interfaces/LbotFrame frame +bool success + diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/GetFrame.srv b/src/officer_sdk/lbot_arm_interfaces/srv/GetFrame.srv new file mode 100644 index 0000000..2562797 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/GetFrame.srv @@ -0,0 +1,4 @@ +string name +--- +lbot_arm_interfaces/LbotFrame frame +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/InverseKinematics.srv b/src/officer_sdk/lbot_arm_interfaces/srv/InverseKinematics.srv new file mode 100755 index 0000000..fc91edd --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/InverseKinematics.srv @@ -0,0 +1,6 @@ +float32[] joints # 此关节角度不设置会默认从机械臂读取当前角度,如果设置则基于此值为初始角度进行逆解 +geometry_msgs/Vector3 position +geometry_msgs/Vector3 euler +--- +float32[] joints +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/MoveC.srv b/src/officer_sdk/lbot_arm_interfaces/srv/MoveC.srv new file mode 100755 index 0000000..e543615 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/MoveC.srv @@ -0,0 +1,9 @@ +geometry_msgs/Vector3 position +geometry_msgs/Vector3 euler +float32 speed +float32 acce +bool block + +--- + +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv b/src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv new file mode 100755 index 0000000..5ebaa0a --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv @@ -0,0 +1,7 @@ +float32[] joints +float32 speed +float32 acce +bool block + +--- +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/MoveJP.srv b/src/officer_sdk/lbot_arm_interfaces/srv/MoveJP.srv new file mode 100755 index 0000000..e543615 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/MoveJP.srv @@ -0,0 +1,9 @@ +geometry_msgs/Vector3 position +geometry_msgs/Vector3 euler +float32 speed +float32 acce +bool block + +--- + +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/MoveL.srv b/src/officer_sdk/lbot_arm_interfaces/srv/MoveL.srv new file mode 100755 index 0000000..e543615 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/MoveL.srv @@ -0,0 +1,9 @@ +geometry_msgs/Vector3 position +geometry_msgs/Vector3 euler +float32 speed +float32 acce +bool block + +--- + +bool success \ No newline at end of file diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/SetEmergency.srv b/src/officer_sdk/lbot_arm_interfaces/srv/SetEmergency.srv new file mode 100755 index 0000000..6781332 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/SetEmergency.srv @@ -0,0 +1,5 @@ +bool emergency + +--- + +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/SetEnable.srv b/src/officer_sdk/lbot_arm_interfaces/srv/SetEnable.srv new file mode 100755 index 0000000..f5d9de0 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/SetEnable.srv @@ -0,0 +1,5 @@ +bool enable + +--- + +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/SetFrame.srv b/src/officer_sdk/lbot_arm_interfaces/srv/SetFrame.srv new file mode 100644 index 0000000..1eadc65 --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/SetFrame.srv @@ -0,0 +1,3 @@ +lbot_arm_interfaces/LbotFrame frame +--- +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/SetString.srv b/src/officer_sdk/lbot_arm_interfaces/srv/SetString.srv new file mode 100644 index 0000000..59e4a8a --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/SetString.srv @@ -0,0 +1,3 @@ +string name +--- +bool success diff --git a/src/officer_sdk/lbot_arm_interfaces/srv/SetZero.srv b/src/officer_sdk/lbot_arm_interfaces/srv/SetZero.srv new file mode 100644 index 0000000..d857c6a --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/srv/SetZero.srv @@ -0,0 +1,3 @@ + +--- +bool success diff --git a/src/officer_sdk/lbot_driver/CMakeLists.txt b/src/officer_sdk/lbot_driver/CMakeLists.txt new file mode 100644 index 0000000..9aa9b81 --- /dev/null +++ b/src/officer_sdk/lbot_driver/CMakeLists.txt @@ -0,0 +1,64 @@ +cmake_minimum_required(VERSION 3.8) +project(lbot_driver) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_compile_options(-Wall -Wextra -Wpedantic) + +# ROS2 dependencies +find_package(ament_cmake REQUIRED) +find_package(rclcpp REQUIRED) +find_package(std_msgs REQUIRED) +find_package(std_srvs REQUIRED) +find_package(sensor_msgs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(tf2 REQUIRED) +find_package(tf2_geometry_msgs REQUIRED) +find_package(lbot_arm_interfaces REQUIRED) + +# include directories +include_directories( + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/include/${PROJECT_NAME} +) + +# library directory +link_directories(${PROJECT_SOURCE_DIR}/lib) + +# executable +add_executable(lbot_driver src/lbot_driver.cpp) + +# link the API library (name only! no lib prefix, no .so suffix) +target_link_libraries(lbot_driver + lbot_api_cpp +) + +# ros deps +ament_target_dependencies(lbot_driver + rclcpp + std_msgs + std_srvs + sensor_msgs + geometry_msgs + tf2 + tf2_geometry_msgs + lbot_arm_interfaces +) + +# install binary +install(TARGETS lbot_driver + DESTINATION lib/${PROJECT_NAME} +) + +# install launch & config +install(DIRECTORY launch config + DESTINATION share/${PROJECT_NAME} +) + +# install .so libraries +install(DIRECTORY lib/ + DESTINATION lib +) + +ament_package() diff --git a/src/officer_sdk/lbot_driver/config/lbot_config.yaml b/src/officer_sdk/lbot_driver/config/lbot_config.yaml new file mode 100755 index 0000000..af42273 --- /dev/null +++ b/src/officer_sdk/lbot_driver/config/lbot_config.yaml @@ -0,0 +1,4 @@ +lbot_driver: + ros__parameters: + #robot param + arm_ip: "192.168.10.21" #设置TCP连接时的IP \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api.h new file mode 100644 index 0000000..4634746 --- /dev/null +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api.h @@ -0,0 +1,485 @@ +/** + * @file lbot_api.h + * @brief LBot机器人控制API接口 + * @date 2026.1.19 + * @copyright 灵心巧手科技有限公司 + */ +#ifndef LBOT_API_H +#define LBOT_API_H + +#ifdef __cplusplus +extern "C" { +#endif + +// 跨平台导出宏 +#ifdef _WIN32 + #ifdef LBOT_API_EXPORTS + #define LBOT_API __declspec(dllexport) + #else + #define LBOT_API __declspec(dllimport) + #endif +#else + #define LBOT_API __attribute__((visibility("default"))) +#endif + +#include "lbot_types.h" +#include "lbot_version.h" + +// ============================================== +// API初始化和清理函数 +// ============================================== +/** + * @brief 初始化LBot API连接 + * @param tcp_host TCP服务器地址格式:"192.168.10.21" + * @return lbot_handle_t 连接句柄,连接成功句柄ID>0,失败返回NULL + */ +LBOT_API lbot_handle_t *lbot_init(const char* tcp_host); + +/** + * @brief 断开指定连接 + * @param handle 机械臂句柄 + * @return true 断开成功,false 断开失败 + */ +LBOT_API bool lbot_disconnect(lbot_handle_t *handle); + +/** + * @brief 清理API资源,断开连接 + */ +LBOT_API void lbot_cleanup(); + +/** + * @brief 获取API版本信息 + * @return 版本字符串 + */ +LBOT_API const char* lbot_get_api_version(); +// ============================================== +// 系统信息获取函数 +// ============================================== +/** + * @brief 获取控制器信息 + * @param handle 机械臂句柄 + * @param robot_model 返回的机器人型号字符串(需要调用者释放) + * @param controller_version 返回的控制器版本字符串(需要调用者释放) + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_controller_info(lbot_handle_t *handle, char** robot_model, char** controller_version); + +// ============================================== +// 状态监控和管理函数 +// ============================================== +/** + * @brief 启动状态监控 + * @param state_cb 状态回调函数,当机器人状态更新时调用 + * @param error_cb 错误回调函数,当发生错误时调用 + * @return true 启动成功,false 启动失败 + */ +LBOT_API bool lbot_start_state_monitor(lbot_state_callback_t state_cb, lbot_error_callback_t error_cb); + +/** + * @brief 停止状态监控 + */ +LBOT_API void lbot_stop_state_monitor(); + +/** + * @brief 获取当前机器人完整状态,此接口需要在启动状态监控后才能调用 + * @param handle 机械臂句柄 + * @param state 返回的机器人状态结构体指针 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_current_state(lbot_handle_t *handle, lbot_full_state_t* state); + +// ============================================== +// 运动控制函数 +// ============================================== +/** + * @brief 关节空间运动 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节的目标角度(弧度) + * @param speed 运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 加速度(0.0~20.0) 单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_move_joint(lbot_handle_t *handle, lbot_arm_t arm, const double joints[7], double speed, double accel, bool block); + +/** + * @brief 笛卡尔空间姿态运动(关节插值) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米) + * @param euler 机械臂末端目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param speed 机械臂末端运动速度(0.0~20.0)单位m/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 机械臂末端加速度(0.0~20.0)单位m/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_move_pose(lbot_handle_t *handle, lbot_arm_t arm, const lbot_position_t* position, const lbot_euler_t* euler, + double speed, double accel, bool block); + +/** + * @brief 笛卡尔空间直线运动(直线插值) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米) + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param speed 关节运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 关节运动加速度(0.0~20.0) 单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_move_linear(lbot_handle_t *handle, lbot_arm_t arm, const lbot_position_t* position, const lbot_euler_t* euler, + double speed, double accel, bool block); + +// ============================================== +// 关节跟随函数(用于遥操作) +// ============================================== +/** + * @brief 关节跟随控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节的目标角度(弧度) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_joint_follow(lbot_handle_t *handle, lbot_arm_t arm, const double joints[7]); + +/** + * @brief 笛卡尔空间姿态跟随运动 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param pos 目标位置(x, y, z,单位:米) + * @param eul 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_pose_follow(lbot_handle_t *handle, lbot_arm_t arm, lbot_position_t pos, lbot_euler_t eul); +// ============================================== +// l6 手控制接口 +// ============================================== +/** + * @brief 设置L6手的位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 6个手指的目标位置(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l6_set_position(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t position[6]); + +/** + * @brief 设置L6手的速度控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param velocity 6个手指的目标速度(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l6_set_velocity(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t velocity[6]); + +/** + * @brief 设置L6手的力矩控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param torque 6个手指的目标力矩(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l6_set_effort(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t torque[6]); + +// ============================================== +// l10 手控制接口(10个自由度) +// ============================================== +/** + * @brief 设置L10手的位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 10个手指的目标位置(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l10_set_position(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t position[10]); + +/** + * @brief 设置L10手的速度控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param velocity 10个手指的目标速度(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l10_set_velocity(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t velocity[10]); + +/** + * @brief 设置L10手的力矩控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param torque 10个手指的目标力矩(0~255) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l10_set_effort(lbot_handle_t *handle, lbot_arm_t arm, const uint8_t torque[10]); + +// ============================================== +// r20 手控制接口 +// ============================================== +/** + * @brief 设置R20手各手指位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param cmd 目标位置命令 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l20_set_series_position(lbot_handle_t *handle, lbot_arm_t arm, const lbot_l20_series_cmd_t* cmd); +/** + * @brief 设置R20手所有自由度位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param cmd 目标位置命令, 16个自由度的目标位置(度),分别为:拇指指根(0~120)、 + * 拇指指尖(0~150)、拇指侧摆(0~180)、拇指旋转(0~130)、食指侧摆(-30~30)、 + * 食指指根(0~180)、食指指尖(0~180)、中指侧摆(-30~30)、中指指根(0~180)、 + * 中指指尖(0~180)、无名指侧摆(-20~20)、无名指指根(0~180)、无名指指尖(0~180)、 + * 小指侧摆(-20~20)、小指指根(0~180)、小指指尖(0~180) + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_l20_set_all_position(lbot_handle_t *handle, lbot_arm_t arm, const int cmd[16]); + + +// ============================================== +// 运动学计算函数 +// ============================================== +/** + * @brief 正运动学计算 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节角度(弧度) + * @param position 返回的末端位置(x, y, z,单位:米) + * @param euler 返回的末端欧拉角(roll, pitch, yaw,单位:弧度) + * @return true 计算成功,false 计算失败 + */ +LBOT_API bool lbot_forward_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const double joints[7], + lbot_position_t* position, lbot_euler_t* euler); + +/** + * @brief 逆运动学计算 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param initial_joints 初始关节角度(弧度),用于求解器迭代 + * @param position 目标位置(x, y, z,单位:米) + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param result_joints 返回的7个关节角度解(弧度) + * @return true 求解成功,false 求解失败 + */ +LBOT_API bool lbot_inverse_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const double initial_joints[7], + const lbot_position_t* position, const lbot_euler_t* euler, + double result_joints[7]); + +// ============================================== +// 工具坐标系管理函数 +// ============================================== +/** + * @brief 设置工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称(最大32字符) + * @param position 工具坐标系相对于法兰盘的位置偏移(x, y, z,单位:米) + * @param euler 工具坐标系相对于法兰盘的欧拉角偏移(roll, pitch, yaw,单位:弧度) + * @return true 设置成功,false 设置失败 + */ +LBOT_API bool lbot_set_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + const lbot_position_t* position, const lbot_euler_t* euler); + +/** + * @brief 获取工具坐标系参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称 + * @param position 返回的工具坐标系位置偏移 + * @param euler 返回的工具坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + lbot_position_t* position, lbot_euler_t* euler); + +/** + * @brief 获取当前使用的工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 返回的当前工具坐标系名称(需要调用者释放) + * @param position 返回的当前工具坐标系位置偏移 + * @param euler 返回的当前工具坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_current_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, + char** name, + lbot_position_t* position, + lbot_euler_t* euler); + +/** + * @brief 切换当前工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工具坐标系名称 + * @return true 切换成功,false 切换失败 + */ +LBOT_API bool lbot_change_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + +/** + * @brief 删除工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工具坐标系名称 + * @return true 删除成功,false 删除失败 + */ +LBOT_API bool lbot_delete_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + +/** + * @brief 获取所有工具坐标系名称 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param names 返回的工具坐标系名称数组(需要调用lbot_free_string_array释放) + * @param count 返回的工具坐标系数量 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_all_tool_frames(lbot_handle_t *handle, lbot_arm_t arm, char*** names, int* count); + +// ============================================== +// 工作坐标系管理函数 +// ============================================== +/** + * @brief 设置工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称(最大32字符) + * @param position 工作坐标系相对于基坐标系的位置偏移(x, y, z,单位:米) + * @param euler 工作坐标系相对于基坐标系的欧拉角偏移(roll, pitch, yaw,单位:弧度) + * @return true 设置成功,false 设置失败 + */ +LBOT_API bool lbot_set_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + const lbot_position_t* position, const lbot_euler_t* euler); + +/** + * @brief 获取工作坐标系参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称 + * @param position 返回的工作坐标系位置偏移 + * @param euler 返回的工作坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + lbot_position_t* position, lbot_euler_t* euler); + +/** + * @brief 切换当前工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工作坐标系名称 + * @return true 切换成功,false 切换失败 + */ +LBOT_API bool lbot_change_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + +/** + * @brief 删除工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工作坐标系名称 + * @return true 删除成功,false 删除失败 + */ +LBOT_API bool lbot_delete_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + +/** + * @brief 获取所有工作坐标系名称 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param names 返回的工作坐标系名称数组(需要调用lbot_free_string_array释放) + * @param count 返回的工作坐标系数量 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_all_work_frames(lbot_handle_t *handle, lbot_arm_t arm, char*** names, int* count); + +// ============================================== +// 系统功能函数 +// ============================================== +/** + * @brief 重新标定电机零位,设置当前位置为零位 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @return true 设置成功,false 设置失败 + */ +LBOT_API bool lbot_set_zero(lbot_handle_t *handle, lbot_arm_t arm); + +/** + * @brief 使能/掉使能机械臂 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param enable true 使能,false 掉使能 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_enable_arm(lbot_handle_t *handle, lbot_arm_t arm, bool enable); + +/** + * @brief 紧急停止/恢复机械臂运行 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param enable true 紧急停止,false 恢复运行 + * @return true 指令发送成功,false 发送失败 + */ +LBOT_API bool lbot_emergency_stop(lbot_handle_t *handle, lbot_arm_t arm, bool enable); + +/** + * @brief 清除所有错误 + * @param handle 机械臂句柄 + * @return true 清除成功,false 清除失败 + */ +LBOT_API bool lbot_clear_errors(lbot_handle_t *handle); + +/** + * @brief 设置机械臂关节限位 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 最大关节限位参数 + * @param lower_joint_limit 最小关节限位参数 + * @return true 设置成功,false 设置失败 + */ +LBOT_API bool lbot_set_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, const double upper_joint_limit[7], const double lower_joint_limit[7]); +/** + * @brief 获取机械臂关节限位参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 返回的最大关节限位参数 + * @param lower_joint_limit 返回的最小关节限位参数 + * @return true 获取成功,false 获取失败 + */ +LBOT_API bool lbot_get_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, double upper_joint_limit[7], double lower_joint_limit[7]); +/** + * @brief 恢复默认关节限位参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 返回的默认最大关节限位参数 + * @param lower_joint_limit 返回的默认最小关节限位参数 + * @return true 恢复成功,false 恢复失败 + */ +LBOT_API bool lbot_get_default_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, double upper_joint_limit[7], double lower_joint_limit[7]); +// ============================================== +// 内存管理辅助函数 +// ============================================== +/** + * @brief 释放字符串数组内存 + * @param array 要释放的字符串数组 + * @param count 数组元素数量 + */ +LBOT_API void lbot_free_string_array(char** array, int count); + +// ============================================== +// 工具函数 +// ============================================== +/** + * @brief 获取最后一次错误信息 + * @return 错误信息字符串指针 + */ +LBOT_API const char* lbot_get_last_error(lbot_handle_t *handle); + +/** + * @brief 设置日志级别 + * @param level 日志级别:0-ERROR, 1-WARN, 2-INFO, 3-DEBUG + */ +LBOT_API void lbot_set_log_level(int level); + +#ifdef __cplusplus +} +#endif + +#endif // LBOT_API_H \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api_cpp.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api_cpp.h new file mode 100644 index 0000000..20c46ea --- /dev/null +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_api_cpp.h @@ -0,0 +1,660 @@ +/** + * @file lbot_api_cpp.h + * @brief 灵心巧手机械臂控制API C++封装头文件 + * @date 2026.1.19 + * @copyright 灵心巧手科技有限公司 + */ +#ifndef LBOT_API_CPP_H +#define LBOT_API_CPP_H + +#include "lbot_api.h" +#include +#include +#include + +/** + * @brief 灵心巧手机械臂控制API C++封装类 + * @details 提供C++友好的接口封装,使用std::string和std::vector简化内存管理 + */ +namespace lbot { + +class LbotApi { +public: + // ============================================== + // 构造函数和析构函数 + // ============================================== + /** + * @brief 构造函数 + */ + LbotApi(); + + /** + * @brief 析构函数,自动清理资源 + */ + ~LbotApi(); + + // ============================================== + // API初始化和清理函数 + // ============================================== + /** + * @brief 初始化LBot API连接 + * @param tcp_host TCP服务器地址格式:"192.168.10.21" + * @return lbot_handle_t 连接句柄,连接成功句柄ID>0,失败返回NULL + */ + LBOT_API lbot_handle_t *lbot_init(const char* tcp_host); + + /** + * @brief 断开指定连接 + * @param handle 机械臂句柄 + * @return true 断开成功,false 断开失败 + */ + LBOT_API bool lbot_disconnect(lbot_handle_t *handle); + + /** + * @brief 清理API资源,断开连接 + */ + LBOT_API void lbot_cleanup(); + + /** + * @brief 获取API版本信息 + * @return 版本字符串 + */ + LBOT_API std::string lbot_get_api_version(); + + // ============================================== + // 系统信息获取函数 + // ============================================== + /** + * @brief 获取控制器信息 + * @param handle 机械臂句柄 + * @param robot_model 返回的机器人型号字符串 + * @param controller_version 返回的控制器版本字符串 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_controller_info(lbot_handle_t *handle, std::string& robot_model, std::string& controller_version); + + // ============================================== + // 状态监控和管理函数 + // ============================================== + /** + * @brief 启动状态监控 + * @param state_cb 状态回调函数,当机器人状态更新时调用 + * @param error_cb 错误回调函数,当发生错误时调用 + * @return true 启动成功,false 启动失败 + */ + LBOT_API bool lbot_start_state_monitor(lbot_state_callback_t state_cb, lbot_error_callback_t error_cb); + + /** + * @brief 停止状态监控 + */ + LBOT_API void lbot_stop_state_monitor(); + + /** + * @brief 获取当前机器人完整状态,此接口需要在启动状态监控后才能调用 + * @param handle 机械臂句柄 + * @param state 返回的机器人状态结构体指针 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_current_state(lbot_handle_t *handle, lbot_full_state_t* state); + + // ============================================== + // 运动控制函数 + // ============================================== + /** + * @brief 关节空间运动 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节的目标角度(弧度) + * @param speed 运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 加速度(0.0~20.0)单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_joint(lbot_handle_t *handle, lbot_arm_t arm, const double joints[7], double speed, double accel, bool block); + + /** + * @brief 关节空间运动(使用vector) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节的目标角度(弧度)向量 + * @param speed 运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 加速度(0.0~20.0)单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_joint(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& joints, double speed, double accel, bool block); + + /** + * @brief 笛卡尔空间姿态运动(关节插值) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米) + * @param euler 机械臂末端目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param speed 机械臂末端运动速度(0.0~20.0)单位m/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 机械臂末端加速度(0.0~20.0)单位m/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_pose(lbot_handle_t *handle, lbot_arm_t arm, const lbot_position_t* position, const lbot_euler_t* euler, + double speed, double accel, bool block); + + /** + * @brief 笛卡尔空间姿态运动(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米)向量 + * @param euler 机械臂末端目标欧拉角(roll, pitch, yaw,单位:弧度)向量 + * @param speed 机械臂末端运动速度(0.0~20.0)单位m/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 机械臂末端加速度(0.0~20.0)单位m/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_pose(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& position, const std::vector& euler, + double speed, double accel, bool block); + + /** + * @brief 笛卡尔空间直线运动(直线插值) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米) + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param speed 关节运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 关节运动加速度(0.0~20.0)单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_linear(lbot_handle_t *handle, lbot_arm_t arm, const lbot_position_t* position, const lbot_euler_t* euler, + double speed, double accel, bool block); + + /** + * @brief 笛卡尔空间直线运动(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 目标位置(x, y, z,单位:米)向量 + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度)向量 + * @param speed 关节运动速度(0.0~20.0)单位是rad/s, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param accel 关节运动加速度(0.0~20.0)单位是rad/s^2, 建议从(0.0~2.0)开始使用后续如有需要逐步提高 + * @param block 是否阻塞执行:true 等待运动完成,false 立即返回 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_move_linear(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& position, const std::vector& euler, + double speed, double accel, bool block); + + // ============================================== + // 关节跟随函数(用于遥操作) + // ============================================== + /** + * @brief 关节跟随控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 包含7个关节目标角度(弧度)的向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_joint_follow(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& joints); + + // ============================================== + // 姿态跟随函数 + // ============================================== + /** + * @brief 笛卡尔空间姿态跟随运动 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param pos 目标位置(x, y, z,单位:米) + * @param eul 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_pose_follow(lbot_handle_t *handle, lbot_arm_t arm, lbot_position_t pos, lbot_euler_t eul); + + // ============================================== + // L6手控制接口 + // ============================================== + /** + * @brief 设置L6手的位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 6个手指的目标位置(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l6_set_position(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& position); + + /** + * @brief 设置L6手的速度控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param velocity 6个手指的目标速度(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l6_set_velocity(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& velocity); + + /** + * @brief 设置L6手的力矩控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param torque 6个手指的目标力矩(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l6_set_effort(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& torque); + + // ============================================== + // L10手控制接口(10个自由度) + // ============================================== + /** + * @brief 设置L10手的位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 10个手指的目标位置(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l10_set_position(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& position); + + /** + * @brief 设置L10手的速度控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param velocity 10个手指的目标速度(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l10_set_velocity(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& velocity); + + /** + * @brief 设置L10手的力矩控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param torque 10个手指的目标力矩(0~255)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l10_set_effort(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& torque); + + // ============================================== + // L20手控制接口(20个自由度) + // ============================================== + /** + * @brief 设置R20手的串联位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param cmd 目标位置命令,包含手指标识和6个电机位置 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l20_set_series_position(lbot_handle_t *handle, lbot_arm_t arm, const lbot_l20_series_cmd_t* cmd); + + /** + * @brief 设置R20手所有自由度位置控制 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param position 16个自由度的目标位置(度)向量 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_l20_set_all_position(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& position); + + // ============================================== + // 运动学计算函数 + // ============================================== + /** + * @brief 正运动学计算 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节角度(弧度) + * @param position 返回的末端位置(x, y, z,单位:米) + * @param euler 返回的末端欧拉角(roll, pitch, yaw,单位:弧度) + * @return true 计算成功,false 计算失败 + */ + LBOT_API bool lbot_forward_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const double joints[7], + lbot_position_t* position, lbot_euler_t* euler); + + /** + * @brief 正运动学计算(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param joints 7个关节角度(弧度)向量 + * @param position 返回的末端位置(x, y, z,单位:米)向量 + * @param euler 返回的末端欧拉角(roll, pitch, yaw,单位:弧度)向量 + * @return true 计算成功,false 计算失败 + */ + LBOT_API bool lbot_forward_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& joints, + std::vector& position, std::vector& euler); + + /** + * @brief 逆运动学计算 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param initial_joints 初始关节角度(弧度),用于求解器迭代 + * @param position 目标位置(x, y, z,单位:米) + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度) + * @param result_joints 返回的7个关节角度解(弧度) + * @return true 求解成功,false 求解失败 + */ + LBOT_API bool lbot_inverse_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const double initial_joints[7], + const lbot_position_t* position, const lbot_euler_t* euler, + double result_joints[7]); + + /** + * @brief 逆运动学计算(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param initial_joints 初始关节角度(弧度)向量,用于求解器迭代 + * @param position 目标位置(x, y, z,单位:米)向量 + * @param euler 目标欧拉角(roll, pitch, yaw,单位:弧度)向量 + * @param result_joints 返回的7个关节角度解(弧度)向量 + * @return true 求解成功,false 求解失败 + */ + LBOT_API bool lbot_inverse_kinematics(lbot_handle_t *handle, lbot_arm_t arm, const std::vector& initial_joints, + const std::vector& position, const std::vector& euler, + std::vector& result_joints); + + // ============================================== + // 工具坐标系管理函数 + // ============================================== + /** + * @brief 设置工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称(最大32字符) + * @param position 工具坐标系相对于法兰盘的位置偏移(x, y, z,单位:米) + * @param euler 工具坐标系相对于法兰盘的欧拉角偏移(roll, pitch, yaw,单位:弧度) + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + const lbot_position_t* position, const lbot_euler_t* euler); + + /** + * @brief 设置工具坐标系(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称(最大32字符) + * @param position 工具坐标系相对于法兰盘的位置偏移(x, y, z,单位:米)向量 + * @param euler 工具坐标系相对于法兰盘的欧拉角偏移(roll, pitch, yaw,单位:弧度)向量 + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name, + const std::vector& position, const std::vector& euler); + + /** + * @brief 获取工具坐标系参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称 + * @param position 返回的工具坐标系位置偏移 + * @param euler 返回的工具坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + lbot_position_t* position, lbot_euler_t* euler); + + /** + * @brief 获取工具坐标系参数(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工具坐标系名称 + * @param position 返回的工具坐标系位置偏移向量 + * @param euler 返回的工具坐标系欧拉角偏移向量 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name, + std::vector& position, std::vector& euler); + + /** + * @brief 获取当前使用的工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 返回的当前工具坐标系名称 + * @param position 返回的当前工具坐标系位置偏移 + * @param euler 返回的当前工具坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_current_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, + std::string& name, + lbot_position_t& position, + lbot_euler_t& euler); + + /** + * @brief 获取当前使用的工具坐标系(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 返回的当前工具坐标系名称 + * @param position 返回的当前工具坐标系位置偏移向量 + * @param euler 返回的当前工具坐标系欧拉角偏移向量 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_current_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, std::string& name, + std::vector& position, + std::vector& euler); + + /** + * @brief 切换当前工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工具坐标系名称 + * @return true 切换成功,false 切换失败 + */ + LBOT_API bool lbot_change_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + + /** + * @brief 切换当前工具坐标系(使用string) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工具坐标系名称 + * @return true 切换成功,false 切换失败 + */ + LBOT_API bool lbot_change_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name); + + /** + * @brief 删除工具坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工具坐标系名称 + * @return true 删除成功,false 删除失败 + */ + LBOT_API bool lbot_delete_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + + /** + * @brief 删除工具坐标系(使用string) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工具坐标系名称 + * @return true 删除成功,false 删除失败 + */ + LBOT_API bool lbot_delete_tool_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name); + + /** + * @brief 获取所有工具坐标系名称 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param names 返回的工具坐标系名称向量 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_all_tool_frames(lbot_handle_t *handle, lbot_arm_t arm, std::vector& names); + + // ============================================== + // 工作坐标系管理函数 + // ============================================== + /** + * @brief 设置工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称(最大32字符) + * @param position 工作坐标系相对于基坐标系的位置偏移(x, y, z,单位:米) + * @param euler 工作坐标系相对于基坐标系的欧拉角偏移(roll, pitch, yaw,单位:弧度) + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + const lbot_position_t* position, const lbot_euler_t* euler); + + /** + * @brief 设置工作坐标系(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称(最大32字符) + * @param position 工作坐标系相对于基坐标系的位置偏移(x, y, z,单位:米)向量 + * @param euler 工作坐标系相对于基坐标系的欧拉角偏移(roll, pitch, yaw,单位:弧度)向量 + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name, + const std::vector& position, const std::vector& euler); + + /** + * @brief 获取工作坐标系参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称 + * @param position 返回的工作坐标系位置偏移 + * @param euler 返回的工作坐标系欧拉角偏移 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name, + lbot_position_t* position, lbot_euler_t* euler); + + /** + * @brief 获取工作坐标系参数(使用向量) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 工作坐标系名称 + * @param position 返回的工作坐标系位置偏移向量 + * @param euler 返回的工作坐标系欧拉角偏移向量 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name, + std::vector& position, std::vector& euler); + + /** + * @brief 切换当前工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工作坐标系名称 + * @return true 切换成功,false 切换失败 + */ + LBOT_API bool lbot_change_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + + /** + * @brief 切换当前工作坐标系(使用string) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要切换到的工作坐标系名称 + * @return true 切换成功,false 切换失败 + */ + LBOT_API bool lbot_change_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name); + + /** + * @brief 删除工作坐标系 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工作坐标系名称 + * @return true 删除成功,false 删除失败 + */ + LBOT_API bool lbot_delete_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const char* name); + + /** + * @brief 删除工作坐标系(使用string) + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param name 要删除的工作坐标系名称 + * @return true 删除成功,false 删除失败 + */ + LBOT_API bool lbot_delete_work_frame(lbot_handle_t *handle, lbot_arm_t arm, const std::string& name); + + /** + * @brief 获取所有工作坐标系名称 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param names 返回的工作坐标系名称向量 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_all_work_frames(lbot_handle_t *handle, lbot_arm_t arm, std::vector& names); + + // ============================================== + // 系统功能函数 + // ============================================== + /** + * @brief 重新标定电机零位,设置当前位置为零位 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_zero(lbot_handle_t *handle, lbot_arm_t arm); + + /** + * @brief 使能/掉使能机械臂 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param enable true 使能,false 掉使能 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_enable_arm(lbot_handle_t *handle, lbot_arm_t arm, bool enable); + + /** + * @brief 紧急停止/恢复 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param enable true 紧急停止,false 恢复运行 + * @return true 指令发送成功,false 发送失败 + */ + LBOT_API bool lbot_emergency_stop(lbot_handle_t *handle, lbot_arm_t arm, bool enable); + + /** + * @brief 清除所有错误 + * @param handle 机械臂句柄 + * @return true 清除成功,false 清除失败 + */ + LBOT_API bool lbot_clear_errors(lbot_handle_t *handle); + + /** + * @brief 设置机械臂关节限位 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 最大关节限位参数 + * @param lower_joint_limit 最小关节限位参数 + * @return true 设置成功,false 设置失败 + */ + LBOT_API bool lbot_set_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, const double upper_joint_limit[7], const double lower_joint_limit[7]); + /** + * @brief 获取机械臂关节限位参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 返回的最大关节限位参数 + * @param lower_joint_limit 返回的最小关节限位参数 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, double upper_joint_limit[7], double lower_joint_limit[7]); + /** + * @brief 获取机械臂默认关节限位参数 + * @param handle 机械臂句柄 + * @param arm 机械臂选择:LBOT_LEFT_ARM 或 LBOT_RIGHT_ARM + * @param upper_joint_limit 返回的最大关节限位参数 + * @param lower_joint_limit 返回的最小关节限位参数 + * @return true 获取成功,false 获取失败 + */ + LBOT_API bool lbot_get_default_joint_limit(lbot_handle_t *handle, lbot_arm_t arm, double upper_joint_limit[7], double lower_joint_limit[7]); + + // ============================================== + // 工具函数 + // ============================================== + /** + * @brief 获取最后一次错误信息 + * @return 错误信息字符串 + */ + LBOT_API std::string lbot_get_last_error(lbot_handle_t *handle); + + /** + * @brief 设置日志级别 + * @param level 日志级别:0-ERROR, 1-WARN, 2-INFO, 3-DEBUG + */ + LBOT_API void lbot_set_log_level(int level); + + // ============================================== + // 内存管理辅助函数 + // ============================================== + /** + * @brief 释放字符串数组内存 + * @param array 要释放的字符串数组 + * @param count 数组元素数量 + */ + LBOT_API void lbot_free_string_array(char** array, int count); + +private: + // 禁用拷贝构造和赋值操作 + LbotApi(const LbotApi&) = delete; + LbotApi& operator=(const LbotApi&) = delete; +}; + +} // namespace lbot + +#endif // LBOT_API_CPP_H \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h new file mode 100644 index 0000000..2aeb98d --- /dev/null +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h @@ -0,0 +1,431 @@ +// Copyright (c) 2025 LingSmart Tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef LBOT_DRIVER_H +#define LBOT_DRIVER_H + +#include +#include "rclcpp/rclcpp.hpp" +#include "rclcpp/clock.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "lbot_api_cpp.h" + +// ROS2 标准消息类型 +#include +#include +#include +#include +#include +#include +#include +#include "std_msgs/msg/u_int8_multi_array.hpp" +#include "std_msgs/msg/int32_multi_array.hpp" + +// 自定义 Message 类型 +#include "lbot_arm_interfaces/msg/arm_state.hpp" +#include "lbot_arm_interfaces/msg/lbot_pose.hpp" +#include "lbot_arm_interfaces/msg/lbot_frame.hpp" +#include "lbot_arm_interfaces/msg/follow_joint.hpp" + +// 自定义 Service 类型 +#include "lbot_arm_interfaces/srv/change_frame.hpp" +#include "lbot_arm_interfaces/srv/delete_frame.hpp" +#include "lbot_arm_interfaces/srv/forward_kinematics.hpp" +#include "lbot_arm_interfaces/srv/inverse_kinematics.hpp" +#include "lbot_arm_interfaces/srv/move_c.hpp" +#include "lbot_arm_interfaces/srv/move_j.hpp" +#include "lbot_arm_interfaces/srv/move_jp.hpp" +#include "lbot_arm_interfaces/srv/move_l.hpp" +#include "lbot_arm_interfaces/srv/set_frame.hpp" +#include "lbot_arm_interfaces/srv/set_string.hpp" +#include "lbot_arm_interfaces/srv/set_zero.hpp" +#include "lbot_arm_interfaces/srv/set_enable.hpp" +#include "lbot_arm_interfaces/srv/set_emergency.hpp" +#include "lbot_arm_interfaces/srv/get_frame.hpp" +#include "lbot_arm_interfaces/srv/get_current_frame.hpp" +#include "lbot_arm_interfaces/srv/get_all_frames.hpp" + +#define RAD_DEGREE 57.295791433 +#define DEGREE_RAD 0.01745329252 + +using namespace std::chrono_literals; + +// 状态回调函数 +void lbot_state_callback_wrapper(const lbot_full_state_t* state); +void lbot_error_callback_wrapper(int error_code, const char* error_msg); + +// 全局连接状态枚举 +enum class GlobalConnState { + DISCONNECTED, + CONNECTING, + CONNECTED +}; + +// 全局变量 +extern bool lbot_ctrl_flag; +extern lbot::LbotApi lbot_api; +extern lbot_handle_t *lbot_handle; +extern std::atomic g_conn_state; // 全局连接状态 + +namespace lbot_driver { + +// 主节点 - 负责连接管理、状态发布、心跳重连、关节跟随 +class LBot: public rclcpp::Node +{ +public: + LBot(const std::string& node_name = "lbot_main_node"); + ~LBot(); + + // 线程安全的状态缓存 + std::mutex state_mutex_, conn_mutex_; + lbot_full_state_t current_lbot_state_; + // 节点关闭状态变量 + std::atomic shutting_down_{false}; + // 单例类指针 + static LBot* g_instance; + + // 重连机制相关线程与变量 + std::thread reconnect_thread_; + std::atomic reconnect_thread_running_{false}; + std::mutex reconnect_thread_mutex_; + + void disconnect_robot(); + +private: + // 初始化和连接相关 + bool connect_robot(); + void get_robot_info(); + void state_publish_timer_callback(); + + // 心跳与重连机制回调函数 + void heartbeat_timer_callback(); + void reconnect_timer_callback(); + + // 关节跟随订阅回调函数(移到主节点,避免阻塞) + void left_joint_follow_callback(const lbot_arm_interfaces::msg::FollowJoint::SharedPtr msg); + void right_joint_follow_callback(const lbot_arm_interfaces::msg::FollowJoint::SharedPtr msg); + + /****************************** 发布器 ******************************/ + + // 状态发布器 (50Hz) + rclcpp::Publisher::SharedPtr left_joint_pub_; + rclcpp::Publisher::SharedPtr right_joint_pub_; + rclcpp::Publisher::SharedPtr left_pose_pub_; + rclcpp::Publisher::SharedPtr right_pose_pub_; + + /****************************** 订阅器 ******************************/ + + // 关节跟随订阅器 (用于遥操作) - 移到主节点 + rclcpp::Subscription::SharedPtr left_joint_follow_sub_; + rclcpp::Subscription::SharedPtr right_joint_follow_sub_; + + /****************************** 定时器 ******************************/ + + // 状态发布定时器 (50Hz) + rclcpp::TimerBase::SharedPtr state_publish_timer_; + rclcpp::TimerBase::SharedPtr heartbeat_timer_; + rclcpp::TimerBase::SharedPtr reconnect_timer_; + + // 参数 + std::string arm_ip_ = "192.168.10.21"; + + // 回调组 + rclcpp::CallbackGroup::SharedPtr callback_group_timer_; + rclcpp::CallbackGroup::SharedPtr callback_group_subscribers_; + + // 连接状态 + bool is_state_monitor_started_ = false; + + GlobalConnState conn_state_ = GlobalConnState::DISCONNECTED; + + // 心跳计数器(连续失败次数) + int heartbeat_fail_count_ = 0; +}; + +// 左臂服务节点 - 负责左臂所有服务 +class LeftArmServiceNode : public rclcpp::Node +{ +public: + LeftArmServiceNode(const std::string& node_name = "lbot_left_arm_node"); + ~LeftArmServiceNode() = default; + +private: + rclcpp::CallbackGroup::SharedPtr callback_group_, callback_group_subscribers_; + + void create_services(); + + // 灵巧手设置回调函数 + void left_hand_l6_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l6_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l6_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l10_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void left_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); + void left_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); + + /****************************** 连接状态检查函数 ******************************/ + + bool check_connection_state(const std::string& service_name) { + if (g_conn_state.load() != GlobalConnState::CONNECTED) { + RCLCPP_WARN(this->get_logger(), "%s: Robot not connected, service rejected", service_name.c_str()); + return false; + } + return true; + } + + /****************************** 左臂服务回调函数 ******************************/ + + // 运动控制 + void move_joint_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void move_pose_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void move_linear_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 运动学计算 + void forward_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void inverse_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 工具坐标系管理 + void set_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_current_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void change_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void delete_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_all_tool_frames_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 系统设置 + void set_zero_callback( + const std::shared_ptr request, + std::shared_ptr response); + void set_enable_callback( + const std::shared_ptr request, + std::shared_ptr response); + void set_emergency_callback( + const std::shared_ptr request, + std::shared_ptr response); + + /****************************** 服务器 ******************************/ + + // 运动控制服务器 + rclcpp::Service::SharedPtr move_joint_service_; + rclcpp::Service::SharedPtr move_pose_service_; + rclcpp::Service::SharedPtr move_linear_service_; + + // 运动学计算服务器 + rclcpp::Service::SharedPtr forward_kinematics_service_; + rclcpp::Service::SharedPtr inverse_kinematics_service_; + + // 工具坐标系管理服务器 + rclcpp::Service::SharedPtr set_tool_frame_service_; + rclcpp::Service::SharedPtr get_tool_frame_service_; + rclcpp::Service::SharedPtr get_current_tool_frame_service_; + rclcpp::Service::SharedPtr change_tool_frame_service_; + rclcpp::Service::SharedPtr delete_tool_frame_service_; + rclcpp::Service::SharedPtr get_all_tool_frames_service_; + + // 系统设置服务器 + rclcpp::Service::SharedPtr set_zero_service_; + rclcpp::Service::SharedPtr set_enable_service_; + rclcpp::Service::SharedPtr set_emergency_service_; + + /****************************** 灵巧手Topic ******************************/ + + rclcpp::Subscription::SharedPtr left_hand_l6_joint_sub_; + rclcpp::Subscription::SharedPtr left_hand_l6_force_sub_; + rclcpp::Subscription::SharedPtr left_hand_l6_speed_sub_; + rclcpp::Subscription::SharedPtr left_hand_l10_joint_sub_; + rclcpp::Subscription::SharedPtr left_hand_l10_force_sub_; + rclcpp::Subscription::SharedPtr left_hand_l10_speed_sub_; + rclcpp::Subscription::SharedPtr left_hand_l20_joint_sub_; + rclcpp::Subscription::SharedPtr left_hand_l20_series_position_sub_; +}; + +// 右臂服务节点 - 负责右臂所有服务 +class RightArmServiceNode : public rclcpp::Node +{ +public: + RightArmServiceNode(const std::string& node_name = "lbot_right_arm_node"); + ~RightArmServiceNode() = default; + +private: + rclcpp::CallbackGroup::SharedPtr callback_group_, callback_group_subscribers_; + + void create_services(); + + // 灵巧手设置回调函数 + void right_hand_l6_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l6_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l6_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l10_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); + void right_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); + void right_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); + + /****************************** 连接状态检查函数 ******************************/ + + bool check_connection_state(const std::string& service_name) { + if (g_conn_state.load() != GlobalConnState::CONNECTED) { + RCLCPP_WARN(this->get_logger(), "%s: Robot not connected, service rejected", service_name.c_str()); + return false; + } + return true; + } + + /****************************** 右臂服务回调函数 ******************************/ + + // 运动控制 + void move_joint_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void move_pose_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void move_linear_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 运动学计算 + void forward_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void inverse_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 工具坐标系管理 + void set_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_current_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void change_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void delete_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response); + + void get_all_tool_frames_callback( + const std::shared_ptr request, + std::shared_ptr response); + + // 系统设置 + void set_zero_callback( + const std::shared_ptr request, + std::shared_ptr response); + void set_enable_callback( + const std::shared_ptr request, + std::shared_ptr response); + void set_emergency_callback( + const std::shared_ptr request, + std::shared_ptr response); + + /****************************** 服务器 ******************************/ + + // 运动控制服务器 + rclcpp::Service::SharedPtr move_joint_service_; + rclcpp::Service::SharedPtr move_pose_service_; + rclcpp::Service::SharedPtr move_linear_service_; + + // 运动学计算服务器 + rclcpp::Service::SharedPtr forward_kinematics_service_; + rclcpp::Service::SharedPtr inverse_kinematics_service_; + + // 工具坐标系管理服务器 + rclcpp::Service::SharedPtr set_tool_frame_service_; + rclcpp::Service::SharedPtr get_tool_frame_service_; + rclcpp::Service::SharedPtr get_current_tool_frame_service_; + rclcpp::Service::SharedPtr change_tool_frame_service_; + rclcpp::Service::SharedPtr delete_tool_frame_service_; + rclcpp::Service::SharedPtr get_all_tool_frames_service_; + + // 系统设置服务器 + rclcpp::Service::SharedPtr set_zero_service_; + rclcpp::Service::SharedPtr set_enable_service_; + rclcpp::Service::SharedPtr set_emergency_service_; + + /****************************** 灵巧手Topic ******************************/ + + rclcpp::Subscription::SharedPtr right_hand_l6_joint_sub_; + rclcpp::Subscription::SharedPtr right_hand_l6_force_sub_; + rclcpp::Subscription::SharedPtr right_hand_l6_speed_sub_; + rclcpp::Subscription::SharedPtr right_hand_l10_joint_sub_; + rclcpp::Subscription::SharedPtr right_hand_l10_force_sub_; + rclcpp::Subscription::SharedPtr right_hand_l10_speed_sub_; + rclcpp::Subscription::SharedPtr right_hand_l20_joint_sub_; + rclcpp::Subscription::SharedPtr right_hand_l20_series_position_sub_; +}; + +} // namespace lbot_driver + +#endif // LBOT_DRIVER_H diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_types.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_types.h new file mode 100644 index 0000000..219b921 --- /dev/null +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_types.h @@ -0,0 +1,99 @@ +/** + * @file lbot_types.h + * @brief 该文件定义了结构和枚举定义 + * @date 2026.1.19 + * @copyright 灵心巧手科技有限公司 + */ +#ifndef LBOT_TYPES_H +#define LBOT_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + + +// 机械臂类型枚举 +typedef enum { + LBOT_LEFT_ARM = 0, + LBOT_RIGHT_ARM = 1 +} lbot_arm_t; + +// 机械臂控制句柄 +typedef struct { + uint64_t id; // 句柄ID,连接成功返回>0的值,0表示无效句柄 +}lbot_handle_t; + +// 运动类型枚举 +typedef enum { + LBOT_MOVE_JOINT = 0, // 关节空间运动 + LBOT_MOVE_POSE = 1, // 笛卡尔空间点到点 + LBOT_MOVE_LINEAR = 2 // 笛卡尔空间直线运动 +} lbot_move_type_t; + +// 坐标系结构体 +typedef struct { + double x, y, z; +} lbot_position_t; + +typedef struct { + double x, y, z, w; +} lbot_orientation_t; + +typedef struct { + double x, y, z; +} lbot_euler_t; + +// 关节状态结构体 +typedef struct { + // 关节数据 + char name[7][32]; // 7个关节名称 + double joint_position[7]; // 7个关节位置 + double velocity[7]; // 7个关节速度 + double effort[7]; // 7个关节力矩 + double temperature[7]; // 7个关节温度 + + // 时间戳 + int32_t sec; // 秒 + uint32_t nanosec; // 纳秒 + char frame_id[64]; // 工作坐标系 + + // 末端状态 + lbot_position_t end_effector_position; // 末端位置 + lbot_euler_t euler; // 欧拉角 + lbot_orientation_t orientation; // 四元数姿态 +} lbot_arm_state_t; + +// 机械臂完整状态结构体 +typedef struct { + lbot_arm_state_t left_arm; + lbot_arm_state_t right_arm; + uint64_t system_timestamp; // 系统时间戳(纳秒) + char arm_ip[16]; // 机械臂IP地址 +} lbot_full_state_t; + +// 回调函数类型定义 +typedef void (*lbot_state_callback_t)(const lbot_full_state_t* state); +typedef void (*lbot_error_callback_t)(int error_code, const char* error_msg); + + +typedef enum { + LBOT_FINGER_THUMB = 0, // 大拇指 + LBOT_FINGER_INDEX = 1, // 食指 + LBOT_FINGER_MID = 2, // 中指 + LBOT_FINGER_RING = 3, // 无名指 + LBOT_FINGER_LITTLE = 4 // 小指 +} lbot_finger_type_t; + +typedef struct { + uint8_t data[6]; // 单根手指各电机位置(指根、指尖、侧摆、旋转) + lbot_finger_type_t finger; // 手指标识,0~4表示thumb,index,mid,ring,little +} lbot_l20_series_cmd_t; + +#ifdef __cplusplus +} +#endif + +#endif // LBOT_TYPES_H diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_version.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_version.h new file mode 100644 index 0000000..fbeb086 --- /dev/null +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_version.h @@ -0,0 +1,23 @@ +/** + * @file lbot_version.h + * @brief 该文件指定API版本号 + * @date 2026.1.19 + * @copyright 灵心巧手科技有限公司 + */ +#ifndef LBOT_VERSION_H +#define LBOT_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + +#define SDK_VERSION ("1.0.5") +#define SDK_BUILD_TIME ("2026.4.30") + + +#ifdef __cplusplus +} +#endif + +#endif + \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/launch/lbot_start_driver.launch.py b/src/officer_sdk/lbot_driver/launch/lbot_start_driver.launch.py new file mode 100755 index 0000000..645d675 --- /dev/null +++ b/src/officer_sdk/lbot_driver/launch/lbot_start_driver.launch.py @@ -0,0 +1,41 @@ +import launch +import os +from launch import LaunchDescription +from launch_ros.actions import Node +from ament_index_python.packages import get_package_share_directory + +def generate_launch_description(): + + # YAML 默认模板文件 + base_yaml_file = os.path.join( + get_package_share_directory('lbot_driver'), + 'config', 'lbot_config.yaml' + ) + + # 定义机器人列表,每个机器人名字和 IP + robots = [ + {"name": "robot1", "arm_ip": "192.168.10.21"}, + # {"name": "robot2", "arm_ip": "192.168.10.22"}, + ] + + nodes = [] + + for robot in robots: + # 每个机器人只需要启动一个 lbot_driver 可执行文件 + # 这个可执行文件内部会创建三个节点:主节点、左臂服务节点、右臂服务节点 + driver_node = Node( + package='lbot_driver', + executable='lbot_driver', + namespace=robot["name"], # 设置 namespace + parameters=[ + base_yaml_file, # 默认 YAML 文件 + { # 覆盖参数 + "arm_ip": robot["arm_ip"] + } + ], + output='screen', + emulate_tty=True, # 更好的日志输出格式 + ) + nodes.append(driver_node) + + return LaunchDescription(nodes) diff --git a/src/officer_sdk/lbot_driver/lib/lib_install.sh b/src/officer_sdk/lbot_driver/lib/lib_install.sh new file mode 100755 index 0000000..2a908fc --- /dev/null +++ b/src/officer_sdk/lbot_driver/lib/lib_install.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -e + +# 功能包 lib 目录(脚本所在目录就是 lib) +LIB_DIR="$(cd "$(dirname "$0")" && pwd)" + +echo "Target lib directory: $LIB_DIR" + +# 根据系统架构选择对应文件夹 +ARCH=$(uname -m) +if [ "$ARCH" = "x86_64" ]; then + SRC_DIR="$LIB_DIR/linux_x64" +elif [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ]; then + SRC_DIR="$LIB_DIR/linux_arm64" +else + echo "Unsupported architecture: $ARCH" + exit 1 +fi + +TARGET_SO="$LIB_DIR/liblbot_api_cpp.so.1.0.0" + +echo "Using source directory: $SRC_DIR" +echo "Removing old files..." +rm -f "$LIB_DIR/liblbot_api_cpp.so" \ + "$LIB_DIR/liblbot_api_cpp.so.1" \ + "$TARGET_SO" + +echo "Copying..." +cp "$SRC_DIR/liblbot_api_cpp.so" "$TARGET_SO" + +echo "Creating symlinks..." +ln -s "liblbot_api_cpp.so.1.0.0" "$LIB_DIR/liblbot_api_cpp.so.1" +ln -s "liblbot_api_cpp.so.1.0.0" "$LIB_DIR/liblbot_api_cpp.so" + +echo "[SUCCESS] Installed." + diff --git a/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so b/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so new file mode 120000 index 0000000..df3db5a --- /dev/null +++ b/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so @@ -0,0 +1 @@ +liblbot_api_cpp.so.1.0.0 \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1 b/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1 new file mode 120000 index 0000000..df3db5a --- /dev/null +++ b/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1 @@ -0,0 +1 @@ +liblbot_api_cpp.so.1.0.0 \ No newline at end of file diff --git a/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1.0.0 b/src/officer_sdk/lbot_driver/lib/liblbot_api_cpp.so.1.0.0 new file mode 100755 index 0000000000000000000000000000000000000000..0b8870e7e4e42d00cade29c782fba2a2dce8ee05 GIT binary patch literal 139864 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 qt^L~cFTd=<%6~ug6G?xaRWQw0S@bv$hui literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so new file mode 100755 index 0000000000000000000000000000000000000000..bc6ec60a5513cd026de479385b49be51dddbc7d3 GIT binary patch literal 84824 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 E0~@o%vj6}9 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1 b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1 new file mode 100755 index 0000000000000000000000000000000000000000..bc6ec60a5513cd026de479385b49be51dddbc7d3 GIT binary patch literal 84824 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 E0~@o%vj6}9 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1.0.5 b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api.so.1.0.5 new file mode 100755 index 0000000000000000000000000000000000000000..bc6ec60a5513cd026de479385b49be51dddbc7d3 GIT binary patch literal 84824 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 E0~@o%vj6}9 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so new file mode 100755 index 0000000000000000000000000000000000000000..32a085ca328a8ad49ab4a9c8c817c8e9a9f209a1 GIT binary patch literal 118032 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 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1 b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1 new file mode 100755 index 0000000000000000000000000000000000000000..32a085ca328a8ad49ab4a9c8c817c8e9a9f209a1 GIT binary patch literal 118032 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 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1.0.5 b/src/officer_sdk/lbot_driver/lib/linux_arm64/liblbot_api_cpp.so.1.0.5 new file mode 100755 index 0000000000000000000000000000000000000000..32a085ca328a8ad49ab4a9c8c817c8e9a9f209a1 GIT binary patch literal 118032 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 literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api.so b/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api.so new file mode 100755 index 0000000000000000000000000000000000000000..d0ed1c157fc9680aa6439d888b6ab4ce6567f4a5 GIT binary patch literal 106976 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 qt^L~cFTd=<%6~ug6G?xaRWQw0S@bv$hui literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1 b/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1 new file mode 100755 index 0000000000000000000000000000000000000000..0b8870e7e4e42d00cade29c782fba2a2dce8ee05 GIT binary patch literal 139864 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 qt^L~cFTd=<%6~ug6G?xaRWQw0S@bv$hui literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1.0.5 b/src/officer_sdk/lbot_driver/lib/linux_x64/liblbot_api_cpp.so.1.0.5 new file mode 100755 index 0000000000000000000000000000000000000000..0b8870e7e4e42d00cade29c782fba2a2dce8ee05 GIT binary patch literal 139864 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 qt^L~cFTd=<%6~ug6G?xaRWQw0S@bv$hui literal 0 HcmV?d00001 diff --git a/src/officer_sdk/lbot_driver/package.xml b/src/officer_sdk/lbot_driver/package.xml new file mode 100644 index 0000000..ccfb6f5 --- /dev/null +++ b/src/officer_sdk/lbot_driver/package.xml @@ -0,0 +1,26 @@ + + + + lbot_driver + 0.0.0 + TODO: Package description + wxp + TODO: License declaration + + ament_cmake + + rclcpp + std_msgs + sensor_msgs + geometry_msgs + tf2 + tf2_geometry_msgs + lbot_arm_interfaces + + ament_lint_auto + ament_lint_common + + + ament_cmake + + diff --git a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp new file mode 100644 index 0000000..9d6affc --- /dev/null +++ b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp @@ -0,0 +1,1782 @@ +// Copyright (c) 2025 LinkerRobot Tech +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "lbot_driver.h" +#include + +using namespace std::chrono_literals; +using lbot_driver::LBot; + +// 全局变量 +lbot::LbotApi lbot_api; +lbot_handle_t *lbot_handle{}; +LBot* LBot::g_instance = nullptr; +std::atomic g_conn_state{GlobalConnState::DISCONNECTED}; // 全局连接状态 + +// 静态状态回调函数 +void lbot_state_callback_wrapper(const lbot_full_state_t* state) { + if (state && lbot_driver::LBot::g_instance) { + std::lock_guard lock(lbot_driver::LBot::g_instance->state_mutex_); + lbot_driver::LBot::g_instance->current_lbot_state_ = *state; + } +} + +// 错误回调函数 +void lbot_error_callback_wrapper(int error_code, const char* error_msg) { + if (LBot::g_instance && LBot::g_instance->shutting_down_) return; + + if (error_msg) { + RCLCPP_ERROR( + rclcpp::get_logger("lbot_driver"), + "LBot Error [%d]: %s", + error_code, + error_msg + ); + } else { + RCLCPP_ERROR( + rclcpp::get_logger("lbot_driver"), + "LBot Error [%d]: (null message)", + error_code + ); + } +} + +namespace lbot_driver { + +// ========== 主节点实现 ========== +LBot::LBot(const std::string& node_name) : rclcpp::Node(node_name) { + // 设置独特的logger名称 + this->get_logger() = rclcpp::get_logger(node_name); + + // 参数初始化 + this->declare_parameter("arm_ip", "192.168.10.21"); + arm_ip_ = this->get_parameter("arm_ip").as_string(); + + // 全局指针 + g_instance = this; + + // 创建回调组 + callback_group_timer_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + callback_group_subscribers_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + + // 创建发布器 (50Hz状态发布) + left_joint_pub_ = this->create_publisher("left_arm/joint_states", 10); + right_joint_pub_ = this->create_publisher("right_arm/joint_states", 10); + left_pose_pub_ = this->create_publisher("left_arm/pose_states", 10); + right_pose_pub_ = this->create_publisher("right_arm/pose_states", 10); + + // 创建关节跟随订阅器(避免阻塞服务) + auto sub_opt = rclcpp::SubscriptionOptions(); + sub_opt.callback_group = callback_group_subscribers_; + + left_joint_follow_sub_ = this->create_subscription( + "left_arm/joint_follow", 10, + std::bind(&LBot::left_joint_follow_callback, this, std::placeholders::_1), + sub_opt); + + right_joint_follow_sub_ = this->create_subscription( + "right_arm/joint_follow", 10, + std::bind(&LBot::right_joint_follow_callback, this, std::placeholders::_1), + sub_opt); + + // 心跳与重连定时器 + heartbeat_timer_ = create_wall_timer(1s, std::bind(&LBot::heartbeat_timer_callback, this)); + reconnect_timer_ = create_wall_timer(2s, std::bind(&LBot::reconnect_timer_callback, this)); + + // 创建状态发布定时器 (50Hz) + state_publish_timer_ = this->create_wall_timer( + 20ms, + std::bind(&LBot::state_publish_timer_callback, this), + callback_group_timer_); + + // 连接机器人 + connect_robot(); + + RCLCPP_INFO(this->get_logger(), "LBot main node initialized successfully"); +} + +// 析构函数 +LBot::~LBot() { + shutting_down_ = true; + + // 停止 reconnect 线程 + { + std::lock_guard lock(reconnect_thread_mutex_); + if (reconnect_thread_.joinable()) + reconnect_thread_.join(); + } + + disconnect_robot(); +} + +bool LBot::connect_robot() { + std::lock_guard lock(conn_mutex_); + if(conn_state_ == GlobalConnState::CONNECTED) return true; + + g_conn_state.store(GlobalConnState::CONNECTING); // 更新全局状态 + + lbot_handle = lbot_api.lbot_init(arm_ip_.c_str()); + if (lbot_handle->id > 0) { + RCLCPP_INFO(this->get_logger(), "Connected to LBot at %s", arm_ip_.c_str()); + + // 获取机械臂信息 + get_robot_info(); + + // 启动状态监控 + if (lbot_api.lbot_start_state_monitor(lbot_state_callback_wrapper, lbot_error_callback_wrapper)) { + is_state_monitor_started_ = true; + RCLCPP_INFO(this->get_logger(), "State monitor started successfully"); + } else { + is_state_monitor_started_ = false; + RCLCPP_ERROR(this->get_logger(), "Failed to start state monitor"); + } + + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to connect to LBot"); + } + + bool success = lbot_handle->id > 0; + conn_state_ = success ? GlobalConnState::CONNECTED : GlobalConnState::DISCONNECTED; + g_conn_state.store(conn_state_); // 更新全局状态 + + return success; +} + +void LBot::disconnect_robot() { + std::lock_guard lock(conn_mutex_); + if (conn_state_ == GlobalConnState::DISCONNECTED) return; + + RCLCPP_INFO(this->get_logger(), "Disconnecting from LBot..."); + + // 先设置状态,避免其他线程继续操作 + conn_state_ = GlobalConnState::DISCONNECTED; + g_conn_state.store(GlobalConnState::DISCONNECTED); // 更新全局状态 + + // 停止状态监控 + if (is_state_monitor_started_) { + RCLCPP_INFO(this->get_logger(), "Stopping state monitor..."); + lbot_api.lbot_stop_state_monitor(); + is_state_monitor_started_ = false; + } + + // 清理连接 + RCLCPP_INFO(this->get_logger(), "Cleaning up LBot connection..."); + lbot_api.lbot_cleanup(); + + RCLCPP_INFO(this->get_logger(), "Disconnected from LBot successfully"); +} + +void LBot::get_robot_info() { + std::string robot_model, controller_version; + if (lbot_api.lbot_get_controller_info(lbot_handle, robot_model, controller_version)) { + RCLCPP_INFO(this->get_logger(), "Robot Model: %s, Controller Version: %s", robot_model.c_str(), controller_version.c_str()); + } +} + +// 状态发布定时器回调函数 (50Hz) +void LBot::state_publish_timer_callback() { + if (conn_state_ != GlobalConnState::CONNECTED || !is_state_monitor_started_) return; + + lbot_full_state_t state; + { + std::lock_guard lock(state_mutex_); + state = current_lbot_state_; + } + + rclcpp::Time time_now = this->get_clock()->now(); + + // ----------------------- 左臂 ----------------------- + + sensor_msgs::msg::JointState left_joint; + left_joint.header.stamp = time_now; + left_joint.header.frame_id = state.left_arm.frame_id; + + // 关节名称 + left_joint.name.resize(7); + for (size_t i = 0; i < 7; i++) + left_joint.name[i] = state.left_arm.name[i]; + + // 关节角度 + left_joint.position.resize(7); + for (size_t i = 0; i < 7; i++) + left_joint.position[i] = state.left_arm.joint_position[i]; + + // 关节速度 + left_joint.velocity.resize(7); + for (size_t i = 0; i < 7; i++) + left_joint.velocity[i] = state.left_arm.velocity[i]; + + // 关节力矩 effort + left_joint.effort.resize(7); + for (size_t i = 0; i < 7; i++) + left_joint.effort[i] = state.left_arm.effort[i]; + + left_joint_pub_->publish(left_joint); + + // PoseStamped(末端状态) + geometry_msgs::msg::PoseStamped left_pose; + left_pose.header.stamp = time_now; + left_pose.header.frame_id = state.left_arm.frame_id; + + left_pose.pose.position.x = state.left_arm.end_effector_position.x; + left_pose.pose.position.y = state.left_arm.end_effector_position.y; + left_pose.pose.position.z = state.left_arm.end_effector_position.z; + + left_pose.pose.orientation.x = state.left_arm.orientation.x; + left_pose.pose.orientation.y = state.left_arm.orientation.y; + left_pose.pose.orientation.z = state.left_arm.orientation.z; + left_pose.pose.orientation.w = state.left_arm.orientation.w; + + left_pose_pub_->publish(left_pose); + + // ----------------------- 右臂 ----------------------- + + sensor_msgs::msg::JointState right_joint; + right_joint.header.stamp = time_now; + right_joint.header.frame_id = state.right_arm.frame_id; + + right_joint.name.resize(7); + for (size_t i = 0; i < 7; i++) + right_joint.name[i] = state.right_arm.name[i]; + + right_joint.position.resize(7); + for (size_t i = 0; i < 7; i++) + right_joint.position[i] = state.right_arm.joint_position[i]; + + right_joint.velocity.resize(7); + for (size_t i = 0; i < 7; i++) + right_joint.velocity[i] = state.right_arm.velocity[i]; + + right_joint.effort.resize(7); + for (size_t i = 0; i < 7; i++) + right_joint.effort[i] = state.right_arm.effort[i]; + + right_joint_pub_->publish(right_joint); + + geometry_msgs::msg::PoseStamped right_pose; + right_pose.header.stamp = time_now; + right_pose.header.frame_id = state.right_arm.frame_id; + + right_pose.pose.position.x = state.right_arm.end_effector_position.x; + right_pose.pose.position.y = state.right_arm.end_effector_position.y; + right_pose.pose.position.z = state.right_arm.end_effector_position.z; + + right_pose.pose.orientation.x = state.right_arm.orientation.x; + right_pose.pose.orientation.y = state.right_arm.orientation.y; + right_pose.pose.orientation.z = state.right_arm.orientation.z; + right_pose.pose.orientation.w = state.right_arm.orientation.w; + + right_pose_pub_->publish(right_pose); +} + + +/****************************** 关节跟随订阅器(遥操作) ******************************/ + +// 左臂关节跟随回调 +void LBot::left_joint_follow_callback(const std::shared_ptr msg) { + if (!msg || msg->joints.empty()) return; + + if (g_conn_state.load() != GlobalConnState::CONNECTED) { + RCLCPP_WARN_THROTTLE(this->get_logger(), *this->get_clock(), 1000, "Left joint follow: Robot not connected"); + return; + } + + std::vector joints(msg->joints.begin(), msg->joints.end()); + + if (!lbot_api.lbot_joint_follow(lbot_handle, LBOT_LEFT_ARM, joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to execute left_joint_follow"); + } +} + +// 右臂关节跟随回调 +void LBot::right_joint_follow_callback(const std::shared_ptr msg) { + if (!msg || msg->joints.empty()) return; + + if (g_conn_state.load() != GlobalConnState::CONNECTED) { + RCLCPP_WARN_THROTTLE(this->get_logger(), *this->get_clock(), 1000, "Right joint follow: Robot not connected"); + return; + } + + std::vector joints(msg->joints.begin(), msg->joints.end()); + + if (!lbot_api.lbot_joint_follow(lbot_handle, LBOT_RIGHT_ARM, joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to execute right_joint_follow"); + } +} + +/****************************** 心跳与重连机制 ******************************/ + +// 心跳机制回调函数 +void LBot::heartbeat_timer_callback() +{ + if (conn_state_ != GlobalConnState::CONNECTED) + return; + + std::string model, version; + // bool ok = lbot_api.lbot_get_controller_info(lbot_handle, model, version); + bool ok = true; + + if (!ok) + { + heartbeat_fail_count_++; + + RCLCPP_WARN(get_logger(), "Heartbeat failed (%d/3)", heartbeat_fail_count_); + + if (heartbeat_fail_count_ >= 3) + { + RCLCPP_ERROR(get_logger(), "Heartbeat lost. Disconnecting..."); + disconnect_robot(); + } + } + else + { + heartbeat_fail_count_ = 0; + } +} + +// 自动重连机制回调函数 +void LBot::reconnect_timer_callback() +{ + if (shutting_down_) return; + if (conn_state_ != GlobalConnState::DISCONNECTED) return; + + std::lock_guard lock(reconnect_thread_mutex_); + + if (reconnect_thread_running_) + return; // 线程正在运行,不重复启动 + + reconnect_thread_running_ = true; + + reconnect_thread_ = std::thread([this]() { + if (shutting_down_) { + reconnect_thread_running_ = false; + return; + } + + RCLCPP_INFO(this->get_logger(), "Trying to reconnect to LBot..."); + + bool success = connect_robot(); + + { + std::lock_guard lock(conn_mutex_); + if (success) { + conn_state_ = GlobalConnState::CONNECTED; + heartbeat_fail_count_ = 0; + + if (!shutting_down_) + RCLCPP_INFO(this->get_logger(), "Arm reconnected successfully"); + } else { + conn_state_ = GlobalConnState::DISCONNECTED; + + if (!shutting_down_) + RCLCPP_WARN(this->get_logger(), "Reconnection failed. Will retry..."); + } + } + + reconnect_thread_running_ = false; + }); +} + +// ========== 左臂服务节点实现 ========== +LeftArmServiceNode::LeftArmServiceNode(const std::string& node_name) : rclcpp::Node(node_name) { + // 设置独特的logger名称 + this->get_logger() = rclcpp::get_logger(node_name); + + // 创建互斥回调组确保串行执行 + callback_group_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + callback_group_subscribers_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + + create_services(); + RCLCPP_INFO(this->get_logger(), "Left arm service node initialized"); +} + +void LeftArmServiceNode::create_services() { + auto service_qos = rclcpp::QoS(rclcpp::ServicesQoS()); + auto sub_opt = rclcpp::SubscriptionOptions(); + sub_opt.callback_group = callback_group_subscribers_; + + // 运动控制服务 + move_joint_service_ = this->create_service( + "left_arm/move_joint", + std::bind(&LeftArmServiceNode::move_joint_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + move_pose_service_ = this->create_service( + "left_arm/move_pose", + std::bind(&LeftArmServiceNode::move_pose_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + move_linear_service_ = this->create_service( + "left_arm/move_linear", + std::bind(&LeftArmServiceNode::move_linear_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 运动学计算服务 + forward_kinematics_service_ = this->create_service( + "left_arm/forward_kinematics", + std::bind(&LeftArmServiceNode::forward_kinematics_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + inverse_kinematics_service_ = this->create_service( + "left_arm/inverse_kinematics", + std::bind(&LeftArmServiceNode::inverse_kinematics_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 工具坐标系管理服务 + set_tool_frame_service_ = this->create_service( + "left_arm/set_tool_frame", + std::bind(&LeftArmServiceNode::set_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_tool_frame_service_ = this->create_service( + "left_arm/get_tool_frame", + std::bind(&LeftArmServiceNode::get_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_current_tool_frame_service_ = this->create_service( + "left_arm/get_current_tool_frame", + std::bind(&LeftArmServiceNode::get_current_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + change_tool_frame_service_ = this->create_service( + "left_arm/change_tool_frame", + std::bind(&LeftArmServiceNode::change_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + delete_tool_frame_service_ = this->create_service( + "left_arm/delete_tool_frame", + std::bind(&LeftArmServiceNode::delete_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_all_tool_frames_service_ = this->create_service( + "left_arm/get_all_tool_frames", + std::bind(&LeftArmServiceNode::get_all_tool_frames_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 零点设置服务 + set_zero_service_ = this->create_service( + "left_arm/set_zero", + std::bind(&LeftArmServiceNode::set_zero_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + set_enable_service_ = this->create_service( + "left_arm/set_enable", + std::bind(&LeftArmServiceNode::set_enable_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + set_emergency_service_ = this->create_service( + "left_arm/set_emergency_stop", + std::bind(&LeftArmServiceNode::set_emergency_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 灵巧手设置Topic回调函数 + left_hand_l6_joint_sub_ = this->create_subscription( + "left_hand/set_l6_joint", 10, + std::bind(&LeftArmServiceNode::left_hand_l6_set_joint_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l6_force_sub_ = this->create_subscription( + "left_hand/set_l6_force", 10, + std::bind(&LeftArmServiceNode::left_hand_l6_set_force_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l6_speed_sub_ = this->create_subscription( + "left_hand/set_l6_speed", 10, + std::bind(&LeftArmServiceNode::left_hand_l6_set_speed_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l10_joint_sub_ = this->create_subscription( + "left_hand/set_l10_joint", 10, + std::bind(&LeftArmServiceNode::left_hand_l10_set_joint_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l10_force_sub_ = this->create_subscription( + "left_hand/set_l10_force", 10, + std::bind(&LeftArmServiceNode::left_hand_l10_set_force_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l10_speed_sub_ = this->create_subscription( + "left_hand/set_l10_speed", 10, + std::bind(&LeftArmServiceNode::left_hand_l10_set_speed_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l20_joint_sub_ = this->create_subscription( + "left_hand/set_l20_joint", 10, + std::bind(&LeftArmServiceNode::left_hand_l20_set_joint_callback, this, std::placeholders::_1), + sub_opt); + left_hand_l20_series_position_sub_ = this->create_subscription( + "left_hand/set_l20_series_position", 10, + std::bind(&LeftArmServiceNode::left_hand_l20_set_series_position_callback, this, std::placeholders::_1), + sub_opt); +} + +/****************************** 左臂服务回调函数实现 ******************************/ + +// 关节运动回调函数 +void LeftArmServiceNode::move_joint_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + auto start_time = std::chrono::steady_clock::now(); + RCLCPP_INFO(this->get_logger(), "[%ld ms] Left arm move_joint started", + std::chrono::duration_cast(start_time.time_since_epoch()).count()); + + if (!check_connection_state("left_arm/move_joint")) { + response->success = false; + return; + } + + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Left arm move_joint: size must be 7"); + return; + } + double joints[7]; + std::copy(request->joints.begin(), request->joints.end(), joints); + response->success = lbot_api.lbot_move_joint(lbot_handle, LBOT_LEFT_ARM, joints, request->speed, request->acce, request->block); + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm move_joint executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Left arm move_joint failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + + auto end_time = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast(end_time - start_time); + RCLCPP_INFO(this->get_logger(), "[%ld ms] Left arm move_joint returned after %ld ms", + std::chrono::duration_cast(end_time.time_since_epoch()).count(), + duration.count()); +} + +// 位姿运动回调函数 +void LeftArmServiceNode::move_pose_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/move_pose")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + response->success = lbot_api.lbot_move_pose(lbot_handle, LBOT_LEFT_ARM, &position, &euler, request->speed, request->acce, request->block); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm move_pose executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Left arm move_pose failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 线性运动回调函数 +void LeftArmServiceNode::move_linear_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/move_linear")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + response->success = lbot_api.lbot_move_linear(lbot_handle, LBOT_LEFT_ARM, &position, &euler, request->speed, request->acce, request->block); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm move_linear executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Left arm move_linear failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 正运动学回调函数 +void LeftArmServiceNode::forward_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/forward_kinematics")) { + response->success = false; + return; + } + + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Left arm forward_kinematics: Joint array must contain exactly 7 values"); + return; + } + + double joints[7]; + std::copy(request->joints.begin(), request->joints.end(), joints); + + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_forward_kinematics(lbot_handle, LBOT_LEFT_ARM, joints, &position, &euler); + + if (response->success) { + response->position.x = position.x; + response->position.y = position.y; + response->position.z = position.z; + + response->euler.x = euler.x; + response->euler.y = euler.y; + response->euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Left arm forward kinematics calculated successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Left arm forward kinematics calculation failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 逆运动学回调函数 +void LeftArmServiceNode::inverse_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/inverse_kinematics")) { + response->success = false; + return; + } + + double initial_joints[7]; + double* initial_ptr = nullptr; + + if (request->joints.empty()) { + initial_ptr = nullptr; + } else { + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Left arm inverse_kinematics: Initial joint array must contain exactly 7 values"); + return; + } + std::copy(request->joints.begin(), request->joints.end(), initial_joints); + initial_ptr = initial_joints; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + double result_joints[7]; + response->success = lbot_api.lbot_inverse_kinematics(lbot_handle, LBOT_LEFT_ARM, initial_ptr, &position, &euler, result_joints); + + if (response->success) { + response->joints.resize(7); + std::copy(result_joints, result_joints + 7, response->joints.begin()); + RCLCPP_INFO(this->get_logger(), "Left arm inverse kinematics calculated successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Left arm inverse kinematics calculation failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 设置工具坐标系回调函数 +void LeftArmServiceNode::set_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/set_tool_frame")) { + response->success = false; + return; + } + + const auto &frame = request->frame; + + lbot_position_t position; + position.x = frame.position.x; + position.y = frame.position.y; + position.z = frame.position.z; + + lbot_euler_t euler; + euler.x = frame.euler.x; + euler.y = frame.euler.y; + euler.z = frame.euler.z; + + response->success = lbot_api.lbot_set_tool_frame(lbot_handle, LBOT_LEFT_ARM, frame.name.c_str(), &position, &euler); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm tool frame '%s' set successfully", frame.name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to set left arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 获取指定工具坐标系回调函数 +void LeftArmServiceNode::get_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/get_tool_frame")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_get_tool_frame(lbot_handle, LBOT_LEFT_ARM, request->name.c_str(), &position, &euler); + + if (response->success) { + response->frame.name = request->name; + response->frame.position.x = position.x; + response->frame.position.y = position.y; + response->frame.position.z = position.z; + response->frame.euler.x = euler.x; + response->frame.euler.y = euler.y; + response->frame.euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Left arm tool frame '%s' retrieved successfully", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get left arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 获取当前工具坐标系回调函数 +void LeftArmServiceNode::get_current_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("left_arm/get_current_tool_frame")) { + response->success = false; + return; + } + + std::string name = ""; + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_get_current_tool_frame( + lbot_handle, + LBOT_LEFT_ARM, + name, + position, + euler + ); + + if (response->success) { + response->frame.name = name; + response->frame.position.x = position.x; + response->frame.position.y = position.y; + response->frame.position.z = position.z; + response->frame.euler.x = euler.x; + response->frame.euler.y = euler.y; + response->frame.euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Left arm current tool frame '%s' retrieved successfully", response->name.c_str() + ); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get current tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str() + ); + } +} + +// 切换工具坐标系回调函数 +void LeftArmServiceNode::change_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/change_tool_frame")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_change_tool_frame(lbot_handle, LBOT_LEFT_ARM, request->name.c_str()); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm changed to tool frame '%s'", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to change left arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 删除工具坐标系回调函数 +void LeftArmServiceNode::delete_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/delete_tool_frame")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_delete_tool_frame(lbot_handle, LBOT_LEFT_ARM, request->name.c_str()); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm tool frame '%s' deleted successfully", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to delete left arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 获取所有工具坐标系回调函数 +void LeftArmServiceNode::get_all_tool_frames_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("left_arm/get_all_tool_frames")) { + response->success = false; + return; + } + + std::vector frame_names; + response->success = lbot_api.lbot_get_all_tool_frames(lbot_handle, LBOT_LEFT_ARM, frame_names); + + if (response->success) { + response->names = frame_names; + RCLCPP_INFO(this->get_logger(), "Left arm retrieved %zu tool frames", frame_names.size()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get left arm tool frames: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 零点设置回调函数 +void LeftArmServiceNode::set_zero_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("left_arm/set_zero")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_set_zero(lbot_handle, LBOT_LEFT_ARM); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Left arm joint zero set successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to set left arm zero: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 使能设置回调函数 +void LeftArmServiceNode::set_enable_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("left_arm/set_enable")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_enable_arm(lbot_handle, LBOT_LEFT_ARM, request->enable); + + if (response->success) { + if (request->enable) { + RCLCPP_INFO(this->get_logger(), "Left arm enable ON successfully"); + } else { + RCLCPP_INFO(this->get_logger(), "Left arm enable OFF successfully"); + } + } else { + if (request->enable) { + RCLCPP_ERROR(this->get_logger(), + "Failed to enable (ON) left arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), + "Failed to disable (OFF) left arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + } + +} + +// 急停设置回调函数 +void LeftArmServiceNode::set_emergency_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("left_arm/set_emergency_stop")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_emergency_stop(lbot_handle, LBOT_LEFT_ARM, request->emergency); + + if (response->success) { + if (request->emergency) { + RCLCPP_INFO(this->get_logger(), "Left arm emergency stop activated successfully"); + } else { + RCLCPP_INFO(this->get_logger(), "Left arm emergency stop released successfully"); + } + } else { + if (request->emergency) { + RCLCPP_ERROR(this->get_logger(), + "Failed to activate emergency stop for left arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), + "Failed to release emergency stop for left arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + } +} + +void LeftArmServiceNode::left_hand_l6_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l6_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_position(lbot_handle, LBOT_LEFT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand joint"); + } +} + +void LeftArmServiceNode::left_hand_l6_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l6_force")) { + return; + } + std::vector hand_force(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_effort(lbot_handle, LBOT_LEFT_ARM, hand_force)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand force"); + } +} + +void LeftArmServiceNode::left_hand_l6_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l6_speed")) { + return; + } + std::vector hand_speed(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_velocity(lbot_handle, LBOT_LEFT_ARM, hand_speed)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand speed"); + } +} + +void LeftArmServiceNode::left_hand_l10_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l10_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_position(lbot_handle, LBOT_LEFT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand joint"); + } +} + +void LeftArmServiceNode::left_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l10_force")) { + return; + } + std::vector hand_force(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_effort(lbot_handle, LBOT_LEFT_ARM, hand_force)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand force"); + } +} + +void LeftArmServiceNode::left_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l10_speed")) { + return; + } + std::vector hand_speed(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_velocity(lbot_handle, LBOT_LEFT_ARM, hand_speed)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left hand speed"); + } +} + +void LeftArmServiceNode::left_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("left_hand/set_l20_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l20_set_all_position(lbot_handle, LBOT_LEFT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left L20 hand joint"); + } +} + +void LeftArmServiceNode::left_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { + if (!msg || msg->data.size() < 7) return; + if (!check_connection_state("left_hand/set_l20_series_position")) { + return; + } + + lbot_l20_series_cmd_t cmd{}; + cmd.finger = static_cast(msg->data[0]); + for (size_t i = 0; i < 6; ++i) { + cmd.data[i] = static_cast(msg->data[i + 1]); + } + + if (!lbot_api.lbot_l20_set_series_position(lbot_handle, LBOT_LEFT_ARM, &cmd)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set left L20 hand series position"); + } +} + +// ========== 右臂服务节点实现 ========== +RightArmServiceNode::RightArmServiceNode(const std::string& node_name) : rclcpp::Node(node_name) { + // 设置独特的logger名称 + this->get_logger() = rclcpp::get_logger(node_name); + + // 创建互斥回调组确保串行执行 + callback_group_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + callback_group_subscribers_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive); + + create_services(); + RCLCPP_INFO(this->get_logger(), "Right arm service node initialized"); +} + +void RightArmServiceNode::create_services() { + auto service_qos = rclcpp::QoS(rclcpp::ServicesQoS()); + auto sub_opt = rclcpp::SubscriptionOptions(); + sub_opt.callback_group = callback_group_subscribers_; + + // 运动控制服务 + move_joint_service_ = this->create_service( + "right_arm/move_joint", + std::bind(&RightArmServiceNode::move_joint_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + move_pose_service_ = this->create_service( + "right_arm/move_pose", + std::bind(&RightArmServiceNode::move_pose_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + move_linear_service_ = this->create_service( + "right_arm/move_linear", + std::bind(&RightArmServiceNode::move_linear_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 运动学计算服务 + forward_kinematics_service_ = this->create_service( + "right_arm/forward_kinematics", + std::bind(&RightArmServiceNode::forward_kinematics_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + inverse_kinematics_service_ = this->create_service( + "right_arm/inverse_kinematics", + std::bind(&RightArmServiceNode::inverse_kinematics_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 工具坐标系管理服务 + set_tool_frame_service_ = this->create_service( + "right_arm/set_tool_frame", + std::bind(&RightArmServiceNode::set_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_tool_frame_service_ = this->create_service( + "right_arm/get_tool_frame", + std::bind(&RightArmServiceNode::get_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_current_tool_frame_service_ = this->create_service( + "right_arm/get_current_tool_frame", + std::bind(&RightArmServiceNode::get_current_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + change_tool_frame_service_ = this->create_service( + "right_arm/change_tool_frame", + std::bind(&RightArmServiceNode::change_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + delete_tool_frame_service_ = this->create_service( + "right_arm/delete_tool_frame", + std::bind(&RightArmServiceNode::delete_tool_frame_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + get_all_tool_frames_service_ = this->create_service( + "right_arm/get_all_tool_frames", + std::bind(&RightArmServiceNode::get_all_tool_frames_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 零点设置服务 + set_zero_service_ = this->create_service( + "right_arm/set_zero", + std::bind(&RightArmServiceNode::set_zero_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + set_enable_service_ = this->create_service( + "right_arm/set_enable", + std::bind(&RightArmServiceNode::set_enable_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + set_emergency_service_ = this->create_service( + "right_arm/set_emergency_stop", + std::bind(&RightArmServiceNode::set_emergency_callback, this, std::placeholders::_1, std::placeholders::_2), + service_qos, callback_group_); + + // 灵巧手设置Topic回调函数 + right_hand_l6_joint_sub_ = this->create_subscription( + "right_hand/set_l6_joint", 10, + std::bind(&RightArmServiceNode::right_hand_l6_set_joint_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l6_force_sub_ = this->create_subscription( + "right_hand/set_l6_force", 10, + std::bind(&RightArmServiceNode::right_hand_l6_set_force_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l6_speed_sub_ = this->create_subscription( + "right_hand/set_l6_speed", 10, + std::bind(&RightArmServiceNode::right_hand_l6_set_speed_callback, this, std::placeholders::_1), + sub_opt); + + right_hand_l10_joint_sub_ = this->create_subscription( + "right_hand/set_l10_joint", 10, + std::bind(&RightArmServiceNode::right_hand_l10_set_joint_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l10_force_sub_ = this->create_subscription( + "right_hand/set_l10_force", 10, + std::bind(&RightArmServiceNode::right_hand_l10_set_force_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l10_speed_sub_ = this->create_subscription( + "right_hand/set_l10_speed", 10, + std::bind(&RightArmServiceNode::right_hand_l10_set_speed_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l20_joint_sub_ = this->create_subscription( + "right_hand/set_l20_joint", 10, + std::bind(&RightArmServiceNode::right_hand_l20_set_joint_callback, this, std::placeholders::_1), + sub_opt); + right_hand_l20_series_position_sub_ = this->create_subscription( + "right_hand/set_l20_series_position", 10, + std::bind(&RightArmServiceNode::right_hand_l20_set_series_position_callback, this, std::placeholders::_1), + sub_opt); +} + +/****************************** 右臂服务回调函数实现 ******************************/ + +// 关节运动回调函数 +void RightArmServiceNode::move_joint_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/move_joint")) { + response->success = false; + return; + } + + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Right arm move_joint: size must be 7"); + return; + } + double joints[7]; + std::copy(request->joints.begin(), request->joints.end(), joints); + response->success = lbot_api.lbot_move_joint(lbot_handle, LBOT_RIGHT_ARM, joints, request->speed, request->acce, request->block); + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm move_joint executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Right arm move_joint failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 位姿运动回调函数 +void RightArmServiceNode::move_pose_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/move_pose")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + response->success = lbot_api.lbot_move_pose(lbot_handle, LBOT_RIGHT_ARM, &position, &euler, request->speed, request->acce, request->block); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm move_pose executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Right arm move_pose failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 线性运动回调函数 +void RightArmServiceNode::move_linear_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/move_linear")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + response->success = lbot_api.lbot_move_linear(lbot_handle, LBOT_RIGHT_ARM, &position, &euler, request->speed, request->acce, request->block); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm move_linear executed successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Right arm move_linear failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 正运动学回调函数 +void RightArmServiceNode::forward_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/forward_kinematics")) { + response->success = false; + return; + } + + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Right arm forward_kinematics: Joint array must contain exactly 7 values"); + return; + } + + double joints[7]; + std::copy(request->joints.begin(), request->joints.end(), joints); + + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_forward_kinematics(lbot_handle, LBOT_RIGHT_ARM, joints, &position, &euler); + + if (response->success) { + response->position.x = position.x; + response->position.y = position.y; + response->position.z = position.z; + + response->euler.x = euler.x; + response->euler.y = euler.y; + response->euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Right arm forward kinematics calculated successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Right arm forward kinematics calculation failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 逆运动学回调函数 +void RightArmServiceNode::inverse_kinematics_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/inverse_kinematics")) { + response->success = false; + return; + } + + double initial_joints[7]; + double* initial_ptr = nullptr; + + if (request->joints.empty()) { + initial_ptr = nullptr; + } else { + if (request->joints.size() != 7) { + response->success = false; + RCLCPP_ERROR(this->get_logger(), "Right arm inverse_kinematics: Initial joint array must contain exactly 7 values"); + return; + } + std::copy(request->joints.begin(), request->joints.end(), initial_joints); + initial_ptr = initial_joints; + } + + lbot_position_t position; + lbot_euler_t euler; + + position.x = request->position.x; + position.y = request->position.y; + position.z = request->position.z; + + euler.x = request->euler.x; + euler.y = request->euler.y; + euler.z = request->euler.z; + + double result_joints[7]; + response->success = lbot_api.lbot_inverse_kinematics(lbot_handle, LBOT_RIGHT_ARM, initial_ptr, &position, &euler, result_joints); + + if (response->success) { + response->joints.resize(7); + std::copy(result_joints, result_joints + 7, response->joints.begin()); + RCLCPP_INFO(this->get_logger(), "Right arm inverse kinematics calculated successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Right arm inverse kinematics calculation failed: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 设置工具坐标系回调函数 +void RightArmServiceNode::set_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/set_tool_frame")) { + response->success = false; + return; + } + + const auto &frame = request->frame; + + lbot_position_t position; + position.x = frame.position.x; + position.y = frame.position.y; + position.z = frame.position.z; + + lbot_euler_t euler; + euler.x = frame.euler.x; + euler.y = frame.euler.y; + euler.z = frame.euler.z; + + response->success = lbot_api.lbot_set_tool_frame(lbot_handle, LBOT_RIGHT_ARM, frame.name.c_str(), &position, &euler); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm tool frame '%s' set successfully", frame.name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to set right arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 获取指定工具坐标系回调函数 +void RightArmServiceNode::get_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/get_tool_frame")) { + response->success = false; + return; + } + + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_get_tool_frame(lbot_handle, LBOT_RIGHT_ARM, request->name.c_str(), &position, &euler); + + if (response->success) { + response->frame.name = request->name; + response->frame.position.x = position.x; + response->frame.position.y = position.y; + response->frame.position.z = position.z; + response->frame.euler.x = euler.x; + response->frame.euler.y = euler.y; + response->frame.euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Right arm tool frame '%s' retrieved successfully", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get right arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + + +// 获取当前工具坐标系回调函数 +void RightArmServiceNode::get_current_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("right_arm/get_current_tool_frame")) { + response->success = false; + return; + } + + std::string name = ""; + lbot_position_t position; + lbot_euler_t euler; + + response->success = lbot_api.lbot_get_current_tool_frame( + lbot_handle, + LBOT_RIGHT_ARM, + name, + position, + euler + ); + + if (response->success) { + response->frame.name = name; + response->frame.position.x = position.x; + response->frame.position.y = position.y; + response->frame.position.z = position.z; + response->frame.euler.x = euler.x; + response->frame.euler.y = euler.y; + response->frame.euler.z = euler.z; + + RCLCPP_INFO(this->get_logger(), "Right arm current tool frame '%s' retrieved successfully", response->name.c_str() + ); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get current tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str() + ); + } +} + +// 切换工具坐标系回调函数 +void RightArmServiceNode::change_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/change_tool_frame")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_change_tool_frame(lbot_handle, LBOT_RIGHT_ARM, request->name.c_str()); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm changed to tool frame '%s'", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to change right arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 删除工具坐标系回调函数 +void RightArmServiceNode::delete_tool_frame_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/delete_tool_frame")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_delete_tool_frame(lbot_handle, LBOT_RIGHT_ARM, request->name.c_str()); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm tool frame '%s' deleted successfully", request->name.c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to delete right arm tool frame: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 获取所有工具坐标系回调函数 +void RightArmServiceNode::get_all_tool_frames_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("right_arm/get_all_tool_frames")) { + response->success = false; + return; + } + + std::vector frame_names; + response->success = lbot_api.lbot_get_all_tool_frames(lbot_handle, LBOT_RIGHT_ARM, frame_names); + + if (response->success) { + response->names = frame_names; + RCLCPP_INFO(this->get_logger(), "Right arm retrieved %zu tool frames", frame_names.size()); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to get right arm tool frames: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 零点设置回调函数 +void RightArmServiceNode::set_zero_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("right_arm/set_zero")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_set_zero(lbot_handle, LBOT_RIGHT_ARM); + + if (response->success) { + RCLCPP_INFO(this->get_logger(), "Right arm joint zero set successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to set right arm zero: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } +} + +// 使能设置回调函数 +void RightArmServiceNode::set_enable_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + if (!check_connection_state("right_arm/set_enable")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_enable_arm(lbot_handle, LBOT_RIGHT_ARM, request->enable); + + if (response->success) { + if (request->enable) { + RCLCPP_INFO(this->get_logger(), "Right arm enable ON successfully"); + } else { + RCLCPP_INFO(this->get_logger(), "Right arm enable OFF successfully"); + } + } else { + if (request->enable) { + RCLCPP_ERROR(this->get_logger(), + "Failed to enable (ON) right arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), + "Failed to disable (OFF) right arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + } +} + +// 急停设置回调函数 +void RightArmServiceNode::set_emergency_callback( + const std::shared_ptr request, + std::shared_ptr response) +{ + (void)request; + + if (!check_connection_state("right_arm/set_emergency_stop")) { + response->success = false; + return; + } + + response->success = lbot_api.lbot_emergency_stop(lbot_handle, LBOT_RIGHT_ARM, request->emergency); + + if (response->success) { + if (request->emergency) { + RCLCPP_INFO(this->get_logger(), "Right arm emergency stop activated successfully"); + } else { + RCLCPP_INFO(this->get_logger(), "Right arm emergency stop released successfully"); + } + } else { + if (request->emergency) { + RCLCPP_ERROR(this->get_logger(), + "Failed to activate emergency stop for right arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } else { + RCLCPP_ERROR(this->get_logger(), + "Failed to release emergency stop for right arm: %s", + lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + } +} + +void RightArmServiceNode::right_hand_l6_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l6_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_position(lbot_handle, LBOT_RIGHT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand joint"); + } +} + +void RightArmServiceNode::right_hand_l6_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l6_force")) { + return; + } + std::vector hand_force(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_effort(lbot_handle, LBOT_RIGHT_ARM, hand_force)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand force"); + } +} + +void RightArmServiceNode::right_hand_l6_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l6_speed")) { + return; + } + std::vector hand_speed(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l6_set_velocity(lbot_handle, LBOT_RIGHT_ARM, hand_speed)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand speed"); + } +} + +void RightArmServiceNode::right_hand_l10_set_joint_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l10_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_position(lbot_handle, LBOT_RIGHT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand joint"); + } +} + +void RightArmServiceNode::right_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l10_force")) { + return; + } + std::vector hand_force(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_effort(lbot_handle, LBOT_RIGHT_ARM, hand_force)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand force"); + } +} + +void RightArmServiceNode::right_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l10_speed")) { + return; + } + std::vector hand_speed(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l10_set_velocity(lbot_handle, LBOT_RIGHT_ARM, hand_speed)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right hand speed"); + } +} + +void RightArmServiceNode::right_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { + if (!msg || msg->data.empty()) return; + if (!check_connection_state("right_hand/set_l20_joint")) { + return; + } + std::vector hand_joints(msg->data.begin(), msg->data.end()); + + if (!lbot_api.lbot_l20_set_all_position(lbot_handle, LBOT_RIGHT_ARM, hand_joints)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right L20 hand joint"); + } +} + +void RightArmServiceNode::right_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { + if (!msg || msg->data.size() < 7) return; + if (!check_connection_state("right_hand/set_l20_series_position")) { + return; + } + + lbot_l20_series_cmd_t cmd{}; + cmd.finger = static_cast(msg->data[0]); + for (size_t i = 0; i < 6; ++i) { + cmd.data[i] = static_cast(msg->data[i + 1]); + } + + if (!lbot_api.lbot_l20_set_series_position(lbot_handle, LBOT_RIGHT_ARM, &cmd)) { + RCLCPP_ERROR(this->get_logger(), "Failed to set right L20 hand series position"); + } +} + +} // namespace lbot_driver + +/****************************** 主函数 ******************************/ + +int main(int argc, char ** argv) { + rclcpp::init(argc, argv); + + RCLCPP_INFO(rclcpp::get_logger("lbot_driver"), "Starting LBot Driver with separate service nodes..."); + + // 创建三个节点 + auto main_node = std::make_shared("lbot_main_node"); // 主节点:连接管理+状态发布+关节跟随 + auto left_arm_node = std::make_shared("lbot_left_arm_node"); // 左臂服务节点 + auto right_arm_node = std::make_shared("lbot_right_arm_node"); // 右臂服务节点 + + rclcpp::on_shutdown([&]() { + RCLCPP_INFO(rclcpp::get_logger("lbot_driver"), "Shutdown signal received, stopping..."); + if (main_node) { + main_node->shutting_down_ = true; + main_node->disconnect_robot(); + } + RCLCPP_INFO(rclcpp::get_logger("lbot_driver"), "Cleanup completed"); + }); + + // 创建三个单线程执行器 + rclcpp::executors::SingleThreadedExecutor main_executor; // 主节点执行器 + rclcpp::executors::SingleThreadedExecutor left_arm_executor; // 左臂服务执行器 + rclcpp::executors::SingleThreadedExecutor right_arm_executor; // 右臂服务执行器 + + // 将节点添加到对应执行器 + main_executor.add_node(main_node); + left_arm_executor.add_node(left_arm_node); + right_arm_executor.add_node(right_arm_node); + + // 创建三个线程分别运行执行器 + std::thread main_thread([&main_executor]() { + try { + RCLCPP_INFO(rclcpp::get_logger("main_executor"), "Main node executor spinning..."); + main_executor.spin(); + } catch (const std::exception& e) { + RCLCPP_ERROR(rclcpp::get_logger("main_executor"), "Exception in main executor: %s", e.what()); + } + }); + + std::thread left_arm_thread([&left_arm_executor]() { + try { + RCLCPP_INFO(rclcpp::get_logger("left_arm_executor"), "Left arm service executor spinning..."); + left_arm_executor.spin(); + } catch (const std::exception& e) { + RCLCPP_ERROR(rclcpp::get_logger("left_arm_executor"), "Exception in left arm executor: %s", e.what()); + } + }); + + std::thread right_arm_thread([&right_arm_executor]() { + try { + RCLCPP_INFO(rclcpp::get_logger("right_arm_executor"), "Right arm service executor spinning..."); + right_arm_executor.spin(); + } catch (const std::exception& e) { + RCLCPP_ERROR(rclcpp::get_logger("right_arm_executor"), "Exception in right arm executor: %s", e.what()); + } + }); + + RCLCPP_INFO(rclcpp::get_logger("lbot_driver"), "All nodes ready, spinning..."); + + // 等待所有线程结束 + main_thread.join(); + left_arm_thread.join(); + right_arm_thread.join(); + + RCLCPP_INFO(rclcpp::get_logger("lbot_driver"), "LBot Driver stopped"); + return 0; +} -- 2.52.0 From 50aba5217fe8544a5fa031db47b342d648fb138c Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 13:42:11 +0800 Subject: [PATCH 05/18] =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=89=8B=E9=83=A8?= =?UTF-8?q?=E4=B8=B2=E8=81=94=E6=8E=A7=E5=88=B6=E9=A9=B1=E5=8A=A8,?= =?UTF-8?q?=E5=8A=A0=E5=85=A5=E7=B3=BB=E7=BB=9F=E6=8A=A5=E9=94=99=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E8=AF=9D=E9=A2=98=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 115 +++++++++++------- .../lbot_arm_interfaces/CMakeLists.txt | 1 + .../lbot_arm_interfaces/msg/SystemError.msg | 4 + .../include/lbot_driver/lbot_driver.h | 7 +- .../lbot_driver/src/lbot_driver.cpp | 61 +++------- 5 files changed, 101 insertions(+), 87 deletions(-) create mode 100644 src/officer_sdk/lbot_arm_interfaces/msg/SystemError.msg diff --git a/README.md b/README.md index bd44893..a39cb43 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,80 @@ ros2 topic echo /robot1/right_arm/joint_states 当前反馈中没有关节加速度字段。如果外部软件必须获取关节加速度,需要在上游根据 `velocity[]` 和时间戳自行差分,或者让厂家提供 SDK 原生加速度反馈接口。 +## 机器人健康/故障状态 + +当前 `officer_sdk/lbot_driver` 已发布系统错误信息 topic: + +```text +/robot1/system_error +``` + +消息类型: + +```text +lbot_arm_interfaces/msg/SystemError +``` + +消息定义: + +```text +std_msgs/Header header +int32 error_code +string error_msg +bool connected +``` + +字段含义: + +```text +error_code SDK 或 driver 上报的错误码 +error_msg 错误信息 +connected 发布错误时 driver 是否认为机器人处于连接状态 +``` + +示例: + +```bash +ros2 topic echo /robot1/system_error +``` + +目前该 topic 会在以下情况发布: + +```text +SDK error callback 触发时 +连接机器人失败时,error_code = -1 +状态监控启动失败时,error_code = -2 +``` + +注意:SDK 当前状态结构 `lbot_full_state_t` 只包含左右臂关节状态、末端位姿、时间戳和 IP;没有完整的故障码、告警码、使能状态、急停状态等字段。因此 `/robot1/system_error` 当前是错误事件 topic,不是完整健康状态快照。 + +仍然可以辅助查看 ROS 日志: + +```text +/rosout +``` + +示例: + +```bash +ros2 topic echo /rosout +``` + +也可以查看启动脚本日志: + +```bash +tail -f ~/workspace/wpz_LbotArm/log/lbot_arm_control.log +``` + +如果后续需要稳定给外部软件使用的完整健康状态快照,建议在 `SystemError.msg` 之外再新增 `RobotHealth.msg`,并在连接/重连逻辑、使能/急停服务回调中维护并周期发布该状态。需要修改的位置: + +```text +src/officer_sdk/lbot_arm_interfaces/msg/RobotHealth.msg +src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt +src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h +src/officer_sdk/lbot_driver/src/lbot_driver.cpp +``` + ## 机械臂关节空间控制 外部软件如果需要发送机械臂各关节目标位置,使用 MoveJ 服务,不是 topic。 @@ -173,45 +247,6 @@ ros2 topic pub --once /robot1/right_hand/set_l20_joint std_msgs/msg/Int32MultiAr "{data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}" ``` -## L20/R20 单指串联位置控制 - -如果只需要控制单根手指,可以使用: - -```text -/robot1/left_hand/set_l20_series_position -/robot1/right_hand/set_l20_series_position -``` - -消息类型: - -```text -std_msgs/msg/Int32MultiArray -``` - -当前约定的数据格式: - -```text -data[0] finger 编号 -data[1..6] 该手指的 6 个位置参数 -``` - -finger 编号: - -```text -0 thumb -1 index -2 middle -3 ring -4 little -``` - -示例,控制左手食指: - -```bash -ros2 topic pub --once /robot1/left_hand/set_l20_series_position std_msgs/msg/Int32MultiArray \ -"{data: [1, 0, 30, 30, 0, 0, 0]}" -``` - ## 相关代码位置 反馈发布: @@ -238,8 +273,6 @@ L20/R20 灵巧手 topic: ```text LeftArmServiceNode::left_hand_l20_set_joint_callback() RightArmServiceNode::right_hand_l20_set_joint_callback() -LeftArmServiceNode::left_hand_l20_set_series_position_callback() -RightArmServiceNode::right_hand_l20_set_series_position_callback() ``` SDK API 声明: diff --git a/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt b/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt index 87b542c..74e7793 100755 --- a/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt +++ b/src/officer_sdk/lbot_arm_interfaces/CMakeLists.txt @@ -46,6 +46,7 @@ rosidl_generate_interfaces(${PROJECT_NAME} "msg/LbotPose.msg" "msg/LbotFrame.msg" "msg/FollowJoint.msg" + "msg/SystemError.msg" DEPENDENCIES std_msgs geometry_msgs sensor_msgs ) diff --git a/src/officer_sdk/lbot_arm_interfaces/msg/SystemError.msg b/src/officer_sdk/lbot_arm_interfaces/msg/SystemError.msg new file mode 100644 index 0000000..f46a83f --- /dev/null +++ b/src/officer_sdk/lbot_arm_interfaces/msg/SystemError.msg @@ -0,0 +1,4 @@ +std_msgs/Header header +int32 error_code +string error_msg +bool connected diff --git a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h index 2aeb98d..ed28611 100644 --- a/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h +++ b/src/officer_sdk/lbot_driver/include/lbot_driver/lbot_driver.h @@ -54,6 +54,7 @@ #include "lbot_arm_interfaces/msg/lbot_pose.hpp" #include "lbot_arm_interfaces/msg/lbot_frame.hpp" #include "lbot_arm_interfaces/msg/follow_joint.hpp" +#include "lbot_arm_interfaces/msg/system_error.hpp" // 自定义 Service 类型 #include "lbot_arm_interfaces/srv/change_frame.hpp" @@ -118,6 +119,7 @@ public: std::mutex reconnect_thread_mutex_; void disconnect_robot(); + void publish_system_error(int error_code, const char* error_msg); private: // 初始化和连接相关 @@ -140,6 +142,7 @@ private: rclcpp::Publisher::SharedPtr right_joint_pub_; rclcpp::Publisher::SharedPtr left_pose_pub_; rclcpp::Publisher::SharedPtr right_pose_pub_; + rclcpp::Publisher::SharedPtr system_error_pub_; /****************************** 订阅器 ******************************/ @@ -190,7 +193,6 @@ private: void left_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); void left_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); void left_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); - void left_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); /****************************** 连接状态检查函数 ******************************/ @@ -295,7 +297,6 @@ private: rclcpp::Subscription::SharedPtr left_hand_l10_force_sub_; rclcpp::Subscription::SharedPtr left_hand_l10_speed_sub_; rclcpp::Subscription::SharedPtr left_hand_l20_joint_sub_; - rclcpp::Subscription::SharedPtr left_hand_l20_series_position_sub_; }; // 右臂服务节点 - 负责右臂所有服务 @@ -318,7 +319,6 @@ private: void right_hand_l10_set_force_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); void right_hand_l10_set_speed_callback(const std_msgs::msg::UInt8MultiArray::SharedPtr msg); void right_hand_l20_set_joint_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); - void right_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg); /****************************** 连接状态检查函数 ******************************/ @@ -423,7 +423,6 @@ private: rclcpp::Subscription::SharedPtr right_hand_l10_force_sub_; rclcpp::Subscription::SharedPtr right_hand_l10_speed_sub_; rclcpp::Subscription::SharedPtr right_hand_l20_joint_sub_; - rclcpp::Subscription::SharedPtr right_hand_l20_series_position_sub_; }; } // namespace lbot_driver diff --git a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp index 9d6affc..2ca3868 100644 --- a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp +++ b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp @@ -36,6 +36,10 @@ void lbot_state_callback_wrapper(const lbot_full_state_t* state) { void lbot_error_callback_wrapper(int error_code, const char* error_msg) { if (LBot::g_instance && LBot::g_instance->shutting_down_) return; + if (LBot::g_instance) { + LBot::g_instance->publish_system_error(error_code, error_msg); + } + if (error_msg) { RCLCPP_ERROR( rclcpp::get_logger("lbot_driver"), @@ -75,6 +79,7 @@ LBot::LBot(const std::string& node_name) : rclcpp::Node(node_name) { right_joint_pub_ = this->create_publisher("right_arm/joint_states", 10); left_pose_pub_ = this->create_publisher("left_arm/pose_states", 10); right_pose_pub_ = this->create_publisher("right_arm/pose_states", 10); + system_error_pub_ = this->create_publisher("system_error", 10); // 创建关节跟随订阅器(避免阻塞服务) auto sub_opt = rclcpp::SubscriptionOptions(); @@ -120,6 +125,18 @@ LBot::~LBot() { disconnect_robot(); } +void LBot::publish_system_error(int error_code, const char* error_msg) { + if (!system_error_pub_) return; + + lbot_arm_interfaces::msg::SystemError msg; + msg.header.stamp = this->get_clock()->now(); + msg.header.frame_id = "lbot_driver"; + msg.error_code = error_code; + msg.error_msg = error_msg ? error_msg : ""; + msg.connected = g_conn_state.load() == GlobalConnState::CONNECTED; + system_error_pub_->publish(msg); +} + bool LBot::connect_robot() { std::lock_guard lock(conn_mutex_); if(conn_state_ == GlobalConnState::CONNECTED) return true; @@ -140,10 +157,12 @@ bool LBot::connect_robot() { } else { is_state_monitor_started_ = false; RCLCPP_ERROR(this->get_logger(), "Failed to start state monitor"); + publish_system_error(-2, "Failed to start state monitor"); } } else { RCLCPP_ERROR(this->get_logger(), "Failed to connect to LBot"); + publish_system_error(-1, "Failed to connect to LBot"); } bool success = lbot_handle->id > 0; @@ -507,10 +526,6 @@ void LeftArmServiceNode::create_services() { "left_hand/set_l20_joint", 10, std::bind(&LeftArmServiceNode::left_hand_l20_set_joint_callback, this, std::placeholders::_1), sub_opt); - left_hand_l20_series_position_sub_ = this->create_subscription( - "left_hand/set_l20_series_position", 10, - std::bind(&LeftArmServiceNode::left_hand_l20_set_series_position_callback, this, std::placeholders::_1), - sub_opt); } /****************************** 左臂服务回调函数实现 ******************************/ @@ -1036,23 +1051,6 @@ void LeftArmServiceNode::left_hand_l20_set_joint_callback(const std_msgs::msg::I } } -void LeftArmServiceNode::left_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { - if (!msg || msg->data.size() < 7) return; - if (!check_connection_state("left_hand/set_l20_series_position")) { - return; - } - - lbot_l20_series_cmd_t cmd{}; - cmd.finger = static_cast(msg->data[0]); - for (size_t i = 0; i < 6; ++i) { - cmd.data[i] = static_cast(msg->data[i + 1]); - } - - if (!lbot_api.lbot_l20_set_series_position(lbot_handle, LBOT_LEFT_ARM, &cmd)) { - RCLCPP_ERROR(this->get_logger(), "Failed to set left L20 hand series position"); - } -} - // ========== 右臂服务节点实现 ========== RightArmServiceNode::RightArmServiceNode(const std::string& node_name) : rclcpp::Node(node_name) { // 设置独特的logger名称 @@ -1173,10 +1171,6 @@ void RightArmServiceNode::create_services() { "right_hand/set_l20_joint", 10, std::bind(&RightArmServiceNode::right_hand_l20_set_joint_callback, this, std::placeholders::_1), sub_opt); - right_hand_l20_series_position_sub_ = this->create_subscription( - "right_hand/set_l20_series_position", 10, - std::bind(&RightArmServiceNode::right_hand_l20_set_series_position_callback, this, std::placeholders::_1), - sub_opt); } /****************************** 右臂服务回调函数实现 ******************************/ @@ -1692,23 +1686,6 @@ void RightArmServiceNode::right_hand_l20_set_joint_callback(const std_msgs::msg: } } -void RightArmServiceNode::right_hand_l20_set_series_position_callback(const std_msgs::msg::Int32MultiArray::SharedPtr msg) { - if (!msg || msg->data.size() < 7) return; - if (!check_connection_state("right_hand/set_l20_series_position")) { - return; - } - - lbot_l20_series_cmd_t cmd{}; - cmd.finger = static_cast(msg->data[0]); - for (size_t i = 0; i < 6; ++i) { - cmd.data[i] = static_cast(msg->data[i + 1]); - } - - if (!lbot_api.lbot_l20_set_series_position(lbot_handle, LBOT_RIGHT_ARM, &cmd)) { - RCLCPP_ERROR(this->get_logger(), "Failed to set right L20 hand series position"); - } -} - } // namespace lbot_driver /****************************** 主函数 ******************************/ -- 2.52.0 From f0633b6ff73d773639a15aad600d10c6f196cbc5 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 13:44:10 +0800 Subject: [PATCH 06/18] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1345657 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build/ +install/ +log/ -- 2.52.0 From 8b997e1e7958c3c1d934560e397e1ffc0208ecad Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 15:30:07 +0800 Subject: [PATCH 07/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E9=9A=8Fsh=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E7=9A=84=E4=BD=BF?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E5=A4=B1=E8=83=BD=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/officer_sdk/lbot_driver/src/lbot_driver.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp index 2ca3868..028aef6 100644 --- a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp +++ b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp @@ -150,6 +150,14 @@ bool LBot::connect_robot() { // 获取机械臂信息 get_robot_info(); + bool left_enable_ok = lbot_api.lbot_enable_arm(lbot_handle, LBOT_LEFT_ARM, true); + bool right_enable_ok = lbot_api.lbot_enable_arm(lbot_handle, LBOT_RIGHT_ARM, true); + if (left_enable_ok && right_enable_ok) { + RCLCPP_INFO(this->get_logger(), "Left and right arms enabled successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to enable arms: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } + // 启动状态监控 if (lbot_api.lbot_start_state_monitor(lbot_state_callback_wrapper, lbot_error_callback_wrapper)) { is_state_monitor_started_ = true; @@ -177,6 +185,14 @@ void LBot::disconnect_robot() { if (conn_state_ == GlobalConnState::DISCONNECTED) return; RCLCPP_INFO(this->get_logger(), "Disconnecting from LBot..."); + + bool left_disable_ok = lbot_api.lbot_enable_arm(lbot_handle, LBOT_LEFT_ARM, false); + bool right_disable_ok = lbot_api.lbot_enable_arm(lbot_handle, LBOT_RIGHT_ARM, false); + if (left_disable_ok && right_disable_ok) { + RCLCPP_INFO(this->get_logger(), "Left and right arms disabled successfully"); + } else { + RCLCPP_ERROR(this->get_logger(), "Failed to disable arms: %s", lbot_api.lbot_get_last_error(lbot_handle).c_str()); + } // 先设置状态,避免其他线程继续操作 conn_state_ = GlobalConnState::DISCONNECTED; -- 2.52.0 From c3613d9e41c2ad9dc954de68434a6fc5b5df12a2 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 16:17:50 +0800 Subject: [PATCH 08/18] =?UTF-8?q?=E5=B7=B2=E5=8A=A0=E5=85=A5=E5=AF=B9?= =?UTF-8?q?=E6=9C=BA=E6=A2=B0=E8=87=82=E5=8F=8D=E9=A6=88=E5=80=BC=E5=92=8C?= =?UTF-8?q?=E6=8E=A7=E5=88=B6=E9=87=8F=E7=9A=84=E5=8F=82=E6=95=B0=E5=BD=95?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lbot_arm_control.sh | 120 ++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/scripts/lbot_arm_control.sh b/scripts/lbot_arm_control.sh index ca10ac5..a0bb1b9 100755 --- a/scripts/lbot_arm_control.sh +++ b/scripts/lbot_arm_control.sh @@ -6,7 +6,10 @@ WS_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" STATE_DIR="/tmp/lbot_arm_control" PID_FILE="${STATE_DIR}/lbot_driver.pid" PGID_FILE="${STATE_DIR}/lbot_driver.pgid" +BAG_PID_FILE="${STATE_DIR}/rosbag.pid" +BAG_PGID_FILE="${STATE_DIR}/rosbag.pgid" LOG_FILE="${WS_DIR}/log/lbot_arm_control.log" +BAG_LOG_FILE="${WS_DIR}/log/lbot_arm_rosbag.log" usage() { echo "Usage: $0 {start|stop|status}" @@ -20,6 +23,14 @@ is_running() { kill -0 "${pid}" 2>/dev/null } +is_bag_running() { + [[ -f "${BAG_PID_FILE}" ]] || return 1 + local pid + pid="$(cat "${BAG_PID_FILE}")" + [[ -n "${pid}" ]] || return 1 + kill -0 "${pid}" 2>/dev/null +} + source_workspace() { set +u if [[ -n "${ROS_DISTRO:-}" && -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then @@ -43,11 +54,109 @@ source_workspace() { set -u } +start_bag_recorder() { + mkdir -p "${STATE_DIR}" "$(dirname "${BAG_LOG_FILE}")" + + if is_bag_running; then + echo "rosbag recorder is already running, pid=$(cat "${BAG_PID_FILE}")" + return 0 + fi + + echo "Starting rosbag recorder..." + echo "Rosbag log: ${BAG_LOG_FILE}" + + : > "${BAG_LOG_FILE}" + setsid bash -lc " + set -euo pipefail + set +u + source '${WS_DIR}/install/setup.bash' + set -u + while true; do + start_time=\\\$(date +%Y%m%d_%H%M%S) + output=/tmp/lbot_arm_\\${start_time}.mcap + echo \"Starting rosbag: \\${output}\" + set +e + timeout 3600 ros2 bag record \ + --storage mcap \ + --output \"\\${output}\" \ + --polling-interval 2 \ + --include-unpublished-topics \ + --disable-keyboard-controls \ + --log-level warn \ + --topics \ + /robot1/left_arm/joint_states \ + /robot1/right_arm/joint_states \ + /robot1/system_error \ + /robot1/left_hand/set_l20_joint \ + /robot1/right_hand/set_l20_joint \ + --services \ + /robot1/left_arm/move_joint \ + /robot1/right_arm/move_joint + rc=\\$? + set -e + if [[ \\${rc} -ne 124 ]]; then + exit \\${rc} + fi + done + " >> "${BAG_LOG_FILE}" 2>&1 & + + local pid=$! + local pgid + pgid="$(ps -o pgid= -p "${pid}" | tr -d ' ')" + echo "${pid}" > "${BAG_PID_FILE}" + echo "${pgid:-${pid}}" > "${BAG_PGID_FILE}" + + sleep 1 + if is_bag_running; then + echo "rosbag recorder started, pid=${pid}, pgid=$(cat "${BAG_PGID_FILE}")" + else + echo "rosbag recorder failed to start. Check log: ${BAG_LOG_FILE}" + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + return 1 + fi +} + +stop_bag_recorder() { + if ! [[ -f "${BAG_PID_FILE}" ]]; then + return 0 + fi + + local pid pgid + pid="$(cat "${BAG_PID_FILE}")" + pgid="${pid}" + if [[ -f "${BAG_PGID_FILE}" ]]; then + pgid="$(cat "${BAG_PGID_FILE}")" + fi + + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + return 0 + fi + + echo "Stopping rosbag recorder, pid=${pid}, pgid=${pgid}..." + kill -TERM -- "-${pgid}" 2>/dev/null || kill -TERM "${pid}" 2>/dev/null || true + + for _ in {1..30}; do + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + echo "rosbag recorder stopped" + return 0 + fi + sleep 0.2 + done + + echo "rosbag recorder did not exit after SIGTERM, forcing stop..." + kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + echo "rosbag recorder stopped" +} + start_driver() { mkdir -p "${STATE_DIR}" "$(dirname "${LOG_FILE}")" if is_running; then echo "lbot_driver is already running, pid=$(cat "${PID_FILE}")" + start_bag_recorder return 0 fi @@ -69,6 +178,7 @@ start_driver() { sleep 1 if is_running; then echo "lbot_driver started, pid=${pid}, pgid=$(cat "${PGID_FILE}")" + start_bag_recorder else echo "lbot_driver failed to start. Check log: ${LOG_FILE}" rm -f "${PID_FILE}" "${PGID_FILE}" @@ -79,6 +189,7 @@ start_driver() { stop_driver() { if ! [[ -f "${PID_FILE}" ]]; then echo "lbot_driver is not running" + stop_bag_recorder return 0 fi @@ -92,6 +203,7 @@ stop_driver() { if ! kill -0 "${pid}" 2>/dev/null; then echo "lbot_driver process is not alive, cleaning state" rm -f "${PID_FILE}" "${PGID_FILE}" + stop_bag_recorder return 0 fi @@ -102,6 +214,7 @@ stop_driver() { if ! kill -0 "${pid}" 2>/dev/null; then rm -f "${PID_FILE}" "${PGID_FILE}" echo "lbot_driver stopped" + stop_bag_recorder return 0 fi sleep 0.2 @@ -111,6 +224,7 @@ stop_driver() { kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true rm -f "${PID_FILE}" "${PGID_FILE}" echo "lbot_driver stopped" + stop_bag_recorder } status_driver() { @@ -119,6 +233,12 @@ status_driver() { else echo "lbot_driver is not running" fi + + if is_bag_running; then + echo "rosbag recorder is running, pid=$(cat "${BAG_PID_FILE}"), pgid=$(cat "${BAG_PGID_FILE}" 2>/dev/null || cat "${BAG_PID_FILE}")" + else + echo "rosbag recorder is not running" + fi } case "${1:-}" in -- 2.52.0 From 9b2279f2d2f7cee2c04d215c2522c395547640a4 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Thu, 17 Sep 2026 16:34:27 +0800 Subject: [PATCH 09/18] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86rosbag?= =?UTF-8?q?=E5=BD=95=E5=88=B6=E6=9C=8D=E5=8A=A1=E5=B4=A9=E6=BA=83=E7=9A=84?= =?UTF-8?q?bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/lbot_arm_control.sh | 20 +++++++++---------- .../lbot_driver/src/lbot_driver.cpp | 12 +++++++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/scripts/lbot_arm_control.sh b/scripts/lbot_arm_control.sh index a0bb1b9..4c650cd 100755 --- a/scripts/lbot_arm_control.sh +++ b/scripts/lbot_arm_control.sh @@ -66,19 +66,19 @@ start_bag_recorder() { echo "Rosbag log: ${BAG_LOG_FILE}" : > "${BAG_LOG_FILE}" - setsid bash -lc " + setsid env WS_DIR="${WS_DIR}" bash -lc ' set -euo pipefail set +u - source '${WS_DIR}/install/setup.bash' + source "${WS_DIR}/install/setup.bash" set -u while true; do - start_time=\\\$(date +%Y%m%d_%H%M%S) - output=/tmp/lbot_arm_\\${start_time}.mcap - echo \"Starting rosbag: \\${output}\" + start_time=$(date +%Y%m%d_%H%M%S) + output=/tmp/lbot_arm_${start_time}.mcap + echo "Starting rosbag: ${output}" set +e timeout 3600 ros2 bag record \ --storage mcap \ - --output \"\\${output}\" \ + --output "${output}" \ --polling-interval 2 \ --include-unpublished-topics \ --disable-keyboard-controls \ @@ -92,13 +92,13 @@ start_bag_recorder() { --services \ /robot1/left_arm/move_joint \ /robot1/right_arm/move_joint - rc=\\$? + rc=$? set -e - if [[ \\${rc} -ne 124 ]]; then - exit \\${rc} + if [[ ${rc} -ne 124 ]]; then + exit ${rc} fi done - " >> "${BAG_LOG_FILE}" 2>&1 & + ' >> "${BAG_LOG_FILE}" 2>&1 & local pid=$! local pgid diff --git a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp index 028aef6..edd6bdd 100644 --- a/src/officer_sdk/lbot_driver/src/lbot_driver.cpp +++ b/src/officer_sdk/lbot_driver/src/lbot_driver.cpp @@ -144,7 +144,8 @@ bool LBot::connect_robot() { g_conn_state.store(GlobalConnState::CONNECTING); // 更新全局状态 lbot_handle = lbot_api.lbot_init(arm_ip_.c_str()); - if (lbot_handle->id > 0) { + bool success = lbot_handle != nullptr && lbot_handle->id > 0; + if (success) { RCLCPP_INFO(this->get_logger(), "Connected to LBot at %s", arm_ip_.c_str()); // 获取机械臂信息 @@ -169,11 +170,10 @@ bool LBot::connect_robot() { } } else { - RCLCPP_ERROR(this->get_logger(), "Failed to connect to LBot"); - publish_system_error(-1, "Failed to connect to LBot"); + RCLCPP_ERROR(this->get_logger(), "Failed to connect to LBot, UDP reconnecting"); + publish_system_error(-1, "UDP Reconnecting"); } - bool success = lbot_handle->id > 0; conn_state_ = success ? GlobalConnState::CONNECTED : GlobalConnState::DISCONNECTED; g_conn_state.store(conn_state_); // 更新全局状态 @@ -391,6 +391,10 @@ void LBot::reconnect_timer_callback() if (reconnect_thread_running_) return; // 线程正在运行,不重复启动 + if (reconnect_thread_.joinable()) { + reconnect_thread_.join(); + } + reconnect_thread_running_ = true; reconnect_thread_ = std::thread([this]() { -- 2.52.0 From d40d0c394073f1b25143297d75e71500dc30747e Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 09:57:33 +0800 Subject: [PATCH 10/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E7=81=B5=E5=B7=A7?= =?UTF-8?q?=E6=89=8B=E6=8E=A7=E5=88=B6=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../launch/linker_hand.launch.py | 21 + .../launch/linker_hand_double.launch.py | 35 + src/linker_hand_ros2_sdk/launch/test.bak | 31 + .../LinkerHand/__init__.py | 0 .../LinkerHand/config/L10_positions.yaml | 146 ++ .../LinkerHand/config/L20_positions.yaml | 0 .../LinkerHand/config/L21_positions.yaml | 110 ++ .../LinkerHand/config/L25_positions.yaml | 83 ++ .../LinkerHand/config/L6_positions.yaml | 26 + .../LinkerHand/config/L7_positions.yaml | 29 + .../LinkerHand/config/O6_positions.yaml | 26 + .../LinkerHand/config/__init__.py | 0 .../LinkerHand/config/setting.yaml | 58 + .../LinkerHand/core/__init__.py | 0 .../LinkerHand/core/can/__init__.py | 0 .../core/can/linker_hand_g20_can.py | 1269 +++++++++++++++++ .../core/can/linker_hand_l10_can.py | 531 +++++++ .../core/can/linker_hand_l20_can.py | 478 +++++++ .../core/can/linker_hand_l21_can.py | 822 +++++++++++ .../core/can/linker_hand_l24_can.py | 448 ++++++ .../core/can/linker_hand_l25_can.py | 848 +++++++++++ .../LinkerHand/core/can/linker_hand_l6_can.py | 426 ++++++ .../LinkerHand/core/can/linker_hand_l7_can.py | 419 ++++++ .../LinkerHand/core/can/linker_hand_o6_can.py | 447 ++++++ .../core/rs485/linker_hand_l10_rs485.py | 345 +++++ .../core/rs485/linker_hand_l6_rs485.py | 460 ++++++ .../core/rs485/linker_hand_l6_rs485.py.bak | 444 ++++++ .../core/rs485/linker_hand_l7_rs485.py | 423 ++++++ .../core/rs485/linker_hand_o6_rs485.py | 671 +++++++++ .../core/rs485/linker_hand_o6_rs485.py.bak | 1157 +++++++++++++++ .../LinkerHand/linker_hand_api.py | 355 +++++ .../LinkerHand/utils/__init__.py | 0 .../LinkerHand/utils/color_msg.py | 27 + .../LinkerHand/utils/init_linker_hand.py | 81 ++ .../LinkerHand/utils/load_write_yaml.py | 101 ++ .../LinkerHand/utils/mapping.py | 383 +++++ .../LinkerHand/utils/open_can.py | 145 ++ .../linker_hand_ros2_sdk/__init__.py | 0 .../linker_hand_ros2_sdk/linker_hand.py | 562 ++++++++ .../linker_hand_ros2_sdk/linker_hand.py.bak | 414 ++++++ .../linker_hand_advanced_g20.py | 258 ++++ .../linker_hand_advanced_l10.py | 246 ++++ .../linker_hand_advanced_l6.py | 251 ++++ .../linker_hand_advanced_l7.py | 281 ++++ .../linker_hand_advanced_o6.py | 252 ++++ .../linker_hand_g20_palm_touch.py | 268 ++++ .../linker_hand_ros2_sdk/o6_joint_curve.py | 331 +++++ src/linker_hand_ros2_sdk/package.xml | 24 + src/linker_hand_ros2_sdk/pyproject.toml | 3 + .../resource/linker_hand_ros2_sdk | 0 src/linker_hand_ros2_sdk/setup.cfg | 4 + src/linker_hand_ros2_sdk/setup.py | 51 + .../test/test_copyright.py | 25 + src/linker_hand_ros2_sdk/test/test_flake8.py | 25 + src/linker_hand_ros2_sdk/test/test_pep257.py | 23 + .../test/test_topic_prefix.py | 36 + 56 files changed, 13899 insertions(+) create mode 100644 src/linker_hand_ros2_sdk/launch/linker_hand.launch.py create mode 100644 src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py create mode 100644 src/linker_hand_ros2_sdk/launch/test.bak create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L10_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L20_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L21_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L25_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L6_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L7_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/O6_positions.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_g20_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l10_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l21_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l24_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l25_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l6_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l7_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_o6_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l10_rs485.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py.bak create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l7_rs485.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py.bak create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/linker_hand_api.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/color_msg.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/init_linker_hand.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/load_write_yaml.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/mapping.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/open_can.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__init__.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py.bak create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py create mode 100644 src/linker_hand_ros2_sdk/package.xml create mode 100644 src/linker_hand_ros2_sdk/pyproject.toml create mode 100644 src/linker_hand_ros2_sdk/resource/linker_hand_ros2_sdk create mode 100644 src/linker_hand_ros2_sdk/setup.cfg create mode 100644 src/linker_hand_ros2_sdk/setup.py create mode 100644 src/linker_hand_ros2_sdk/test/test_copyright.py create mode 100644 src/linker_hand_ros2_sdk/test/test_flake8.py create mode 100644 src/linker_hand_ros2_sdk/test/test_pep257.py create mode 100644 src/linker_hand_ros2_sdk/test/test_topic_prefix.py diff --git a/src/linker_hand_ros2_sdk/launch/linker_hand.launch.py b/src/linker_hand_ros2_sdk/launch/linker_hand.launch.py new file mode 100644 index 0000000..7c090a6 --- /dev/null +++ b/src/linker_hand_ros2_sdk/launch/linker_hand.launch.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from launch import LaunchDescription +from launch_ros.actions import Node + +def generate_launch_description(): + return LaunchDescription([ + Node( + package='linker_hand_ros2_sdk', + executable='linker_hand_sdk', + name='linker_hand_sdk', + output='screen', + parameters=[{ + 'hand_type': 'left', # 配置Linker Hand灵巧手类型 left | right 字母为小写 + 'hand_joint': "O6", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 + 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 + "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* 注意添加sudo chmod 777 /dev/ttyUSB*权限 + }], + ), + ]) diff --git a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py new file mode 100644 index 0000000..4d81d46 --- /dev/null +++ b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from launch import LaunchDescription +from launch_ros.actions import Node + +def generate_launch_description(): + return LaunchDescription([ + Node( + package='linker_hand_ros2_sdk', + executable='linker_hand_sdk', + name='linker_hand_sdk_left', + output='screen', + parameters=[{ + 'hand_type': 'left', # 配置Linker Hand灵巧手类型 left | right 字母为小写 + 'hand_joint': "G20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 + 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* + }], + ), + + Node( + package='linker_hand_ros2_sdk', + executable='linker_hand_sdk', + name='linker_hand_sdk_right', + output='screen', + parameters=[{ + 'hand_type': 'right', # 配置Linker Hand灵巧手类型 left | right 字母为小写 + 'hand_joint': "G20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 + 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False + 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* + }], + ), + ]) diff --git a/src/linker_hand_ros2_sdk/launch/test.bak b/src/linker_hand_ros2_sdk/launch/test.bak new file mode 100644 index 0000000..a4d8d10 --- /dev/null +++ b/src/linker_hand_ros2_sdk/launch/test.bak @@ -0,0 +1,31 @@ +from launch import LaunchDescription +from launch_ros.actions import Node + +def generate_launch_description(): + return LaunchDescription([ + Node( + package='linker_hand_ros2_sdk', + executable='linker_hand_sdk', + name='linker_hand_sdk_left', + output='screen', + parameters=[{ + 'hand_type': 'left', + 'hand_joint': "L10", # 这里需要修改为实际Linker Hand的型号 L7、L10、L20、L21、L25 + 'is_touch': True, # 是否带有压力传感器 + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 + }], + ), + + Node( + package='linker_hand_ros2_sdk', + executable='linker_hand_sdk', + name='linker_hand_sdk_right', + output='screen', + parameters=[{ + 'hand_type': 'right', + 'hand_joint': "L10", # 这里需要修改为实际Linker Hand的型号 L7、L10、L20、L21、L25 + 'is_touch': True, # 是否带有压力传感器 + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 + }], + ), + ]) diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L10_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L10_positions.yaml new file mode 100644 index 0000000..ea7439d --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L10_positions.yaml @@ -0,0 +1,146 @@ +LEFT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: 捏合5CM + POSITION: + - 165 + - 70 + - 165 + - 165 + - 255 + - 255 + - 113 + - 255 + - 255 + - 88 +- ACTION_NAME: 捏合1CM + POSITION: + - 150 + - 70 + - 155 + - 155 + - 255 + - 255 + - 113 + - 255 + - 255 + - 88 +- ACTION_NAME: 握3CM物品 + POSITION: + - 113 + - 70 + - 85 + - 85 + - 85 + - 85 + - 85 + - 255 + - 255 + - 88 +- ACTION_NAME: 准备抓握 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 121 +- ACTION_NAME: 拇指弯曲 + POSITION: + - 35 + - 140 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 30 +- ACTION_NAME: 食指弯曲 + POSITION: + - 255 + - 70 + - 0 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: shishi + POSITION: + - 85 + - 30 + - 255 + - 0 + - 0 + - 255 + - 0 + - 0 + - 0 + - 66 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: 捏合5CM + POSITION: + - 165 + - 70 + - 165 + - 165 + - 255 + - 255 + - 113 + - 255 + - 255 + - 88 +- ACTION_NAME: 捏合1CM + POSITION: + - 150 + - 70 + - 155 + - 155 + - 255 + - 255 + - 113 + - 255 + - 255 + - 88 +- ACTION_NAME: 准备抓握 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 121 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L20_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L20_positions.yaml new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L21_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L21_positions.yaml new file mode 100644 index 0000000..a23101c --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L21_positions.yaml @@ -0,0 +1,110 @@ +LEFT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 96 + - 255 + - 255 + - 255 + - 255 + - 150 + - 114 + - 151 + - 189 + - 255 + - 180 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: 握拳 + POSITION: + - 177 + - 0 + - 0 + - 0 + - 0 + - 51 + - 114 + - 151 + - 189 + - 255 + - 79 + - 255 + - 255 + - 255 + - 255 + - 131 + - 222 + - 244 + - 255 + - 255 + - 0 + - 0 + - 0 + - 0 + - 0 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 96 + - 255 + - 255 + - 255 + - 255 + - 150 + - 114 + - 151 + - 189 + - 255 + - 180 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: 握拳 + POSITION: + - 230 + - 80 + - 51 + - 42 + - 7 + - 35 + - 114 + - 151 + - 189 + - 255 + - 58 + - 255 + - 255 + - 255 + - 255 + - 133 + - 5 + - 0 + - 0 + - 0 + - 30 + - 0 + - 0 + - 0 + - 0 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L25_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L25_positions.yaml new file mode 100644 index 0000000..709b0c3 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L25_positions.yaml @@ -0,0 +1,83 @@ +LEFT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 96 + - 255 + - 255 + - 255 + - 255 + - 150 + - 114 + - 151 + - 189 + - 255 + - 180 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 96 + - 255 + - 255 + - 255 + - 255 + - 150 + - 114 + - 151 + - 189 + - 255 + - 180 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 + - 255 +- ACTION_NAME: 握拳 + POSITION: + - 230 + - 80 + - 51 + - 42 + - 7 + - 35 + - 114 + - 151 + - 189 + - 255 + - 58 + - 255 + - 255 + - 255 + - 255 + - 133 + - 5 + - 0 + - 0 + - 0 + - 30 + - 0 + - 0 + - 0 + - 0 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L6_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L6_positions.yaml new file mode 100644 index 0000000..9caaf4a --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L6_positions.yaml @@ -0,0 +1,26 @@ +LEFT_HAND: +- ACTION_NAME: 握拳 + POSITION: + - 67 + - 151 + - 0 + - 0 + - 0 + - 0 +- ACTION_NAME: 张开 + POSITION: + - 255 + - 179 + - 255 + - 255 + - 255 + - 255 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L7_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L7_positions.yaml new file mode 100644 index 0000000..d573967 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/L7_positions.yaml @@ -0,0 +1,29 @@ +LEFT_HAND: +- ACTION_NAME: 握拳 + POSITION: + - 67 + - 151 + - 0 + - 0 + - 0 + - 0 + - 37 +- ACTION_NAME: 张开 + POSITION: + - 255 + - 179 + - 255 + - 255 + - 255 + - 255 + - 83 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 + - 255 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/O6_positions.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/O6_positions.yaml new file mode 100644 index 0000000..9caaf4a --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/O6_positions.yaml @@ -0,0 +1,26 @@ +LEFT_HAND: +- ACTION_NAME: 握拳 + POSITION: + - 67 + - 151 + - 0 + - 0 + - 0 + - 0 +- ACTION_NAME: 张开 + POSITION: + - 255 + - 179 + - 255 + - 255 + - 255 + - 255 +RIGHT_HAND: +- ACTION_NAME: 张开 + POSITION: + - 255 + - 70 + - 255 + - 255 + - 255 + - 255 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml new file mode 100644 index 0000000..f3a7511 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml @@ -0,0 +1,58 @@ +VERSION: 3.1.1 # 支持O6、L6在RS485模式 +LINKER_HAND: # 手部配置信息 + LEFT_HAND: + EXISTS: True # 是否存在左手 + TOUCH: True # 是否有压力传感器 + CAN: "can0" # 配置CAN端口 默认can0 如果MODUBS不为"None",则CAN配置失效。 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 + MODBUS: "None" # 通讯协议是否为485 默认None 如果启动485,则是设备端口 /dev/ttyUSB* CAN配置失效 当前只支持O6/L6.后续版本正在努力增加中 + JOINT: L10 # 左手型号 O6/L6/L7/L10/L20/G20/L21/L25/ + NAME: # 默认值,不用修改 + - joint41 + - joint42 + - joint43 + - joint44 + - joint45 + - joint46 + - joint47 + - joint48 + - joint49 + - joint50 + - joint51 + - joint52 + - joint53 + - joint54 + - joint55 + - joint56 + - joint57 + - joint58 + - joint59 + - joint60 + + RIGHT_HAND: + EXISTS: False # 是否存在右手 + TOUCH: False # 是否有压力传感器 + CAN: "can0" # 配置CAN端口 默认can0 如果MODUBS不为"None",则CAN配置失效。 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 + MODBUS: "None" # 通讯协议是否为485 默认None 如果启动485,则是设备端口 /dev/ttyUSB* CAN配置失效 当前只支持O6/L6.后续版本正在努力增加中 + JOINT: L10 # 右手型号 O6/L6/L7/L10/L20/G20/L21/L25/ + NAME: # 默认值,不用修改 + - joint71 + - joint72 + - joint73 + - joint77 + - joint75 + - joint76 + - joint77 + - joint78 + - joint79 + - joint80 + - joint81 + - joint82 + - joint83 + - joint84 + - joint88 + - joint86 + - joint87 + - joint88 + - joint89 + - joint90 +PASSWORD: "12345678" # 由于与can通讯,需要激活通讯接口用到系统管理员密码。只有Linux系统需要,windows系统不需要。RS485不需要修改,RS485需要给/dev/ttyUSB* 777权限 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_g20_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_g20_can.py new file mode 100644 index 0000000..9553aed --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_g20_can.py @@ -0,0 +1,1269 @@ +#!/usr/bin/env python3 +import can +import time, sys, os +import threading +import numpy as np +from enum import Enum +from utils.open_can import OpenCan +from can.exceptions import CanError +from utils.color_msg import ColorMsg +current_dir = os.path.dirname(os.path.abspath(__file__)) +target_dir = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(target_dir) +""" +拇指41: [拇指侧摆, 拇指横摆, 拇指根部, 预留, 预留, 拇指尖部] +食指42: [食指侧摆, 预留, 食指根部, 预留, 预留, 食指末端] +中指43: [中指侧摆, 预留, 中指根部, 预留, 预留, 中指末端] +无名指44: [无名指侧摆, 预留, 无名指根部, 预留, 预留, 无名指末端] +小指45: [小指侧摆, 预留, 小指根部, 预留, 预留, 小指末端] +""" +CMD_MAP = [ + "拇指根部", + "食指根部", + "中指根部", + "无名指根部", + "小指根部", + "拇指侧摆", + "食指侧摆", + "中指侧摆", + "无名指侧摆", + "小指侧摆", + "拇指横摆", + "预留", + "预留", + "预留", + "预留", + "拇指尖部", + "食指末端", + "中指末端", + "无名指末端", + "小指末端" +] + +class FrameProperty(Enum): + # 手指运动控制 - 并联型控制指令(控制所有手指同一关节) + ROLL_POS = 0x01 # 横滚关节位置 + YAW_POS = 0x02 # 航向关节位置 + ROOT1_POS = 0x03 # 指根1关节位置 + ROOT2_POS = 0x04 # 指根2关节位置 + ROOT3_POS = 0x05 # 指根3关节位置 + TIP_POS = 0x06 # 指尖关节位置 + + # 关节速度指令 + ROLL_SPEED = 0x09 # 横滚关节速度 + YAW_SPEED = 0x0A # 航向关节速度 + ROOT1_SPEED = 0x0B # 指根1关节速度 + ROOT2_SPEED = 0x0C # 指根2关节速度 + ROOT3_SPEED = 0x0D # 指根3关节速度 + TIP_SPEED = 0x0E # 指尖关节速度 + + # 关节扭矩指令 + ROLL_TORQUE = 0x11 # 横滚关节扭矩 + YAW_TORQUE = 0x12 # 航向关节扭矩 + ROOT1_TORQUE = 0x13 # 指根1关节扭矩 + ROOT2_TORQUE = 0x14 # 指根2关节扭矩 + ROOT3_TORQUE = 0x15 # 指根3关节扭矩 + TIP_TORQUE = 0x16 # 指尖关节扭矩 + + # 关节故障码 + ROLL_FAULT = 0x19 # 横滚关节故障码 + YAW_FAULT = 0x1A # 航向关节故障码 + ROOT1_FAULT = 0x1B # 指根1关节故障码 + ROOT2_FAULT = 0x1C # 指根2关节故障码 + ROOT3_FAULT = 0x1D # 指根3关节故障码 + TIP_FAULT = 0x1E # 指尖关节故障码 + + # 关节温度 + ROLL_TEMPERATURE = 0x21 # 横滚关节过温保护阈值 + YAW_TEMPERATURE = 0x22 # 航向关节过温保护阈值 + ROOT1_TEMPERATURE = 0x23 # 指根1关节过温保护阈值 + ROOT2_TEMPERATURE = 0x24 # 指根2关节过温保护阈值 + ROOT3_TEMPERATURE = 0x25 # 指根3关节过温保护阈值 + TIP_TEMPERATURE = 0x26 # 指尖关节过温保护阈值 + + # 手指运动控制 - 串联型控制指令(控制同一手指所有关节) + THUMB_POS = 0x41 # 大拇指指关节位置 + INDEX_POS = 0x42 # 食指关节位置 + MIDDLE_POS = 0x43 # 中指关节位置 + RING_POS = 0x44 # 无名指关节位置 + LITTLE_POS = 0x45 # 小拇指关节位置 + + # 手指速度 + THUMB_SPEED = 0x49 # 大拇指速度 + INDEX_SPEED = 0x4A # 食指速度 + MIDDLE_SPEED = 0x4B # 中指速度 + RING_SPEED = 0x4C # 无名指速度 + LITTLE_SPEED = 0x4D # 小拇指速度 + + # 手指扭矩 + THUMB_TORQUE = 0x51 # 大拇指扭矩 + INDEX_TORQUE = 0x52 # 食指扭矩 + MIDDLE_TORQUE = 0x53 # 中指扭矩 + RING_TORQUE = 0x54 # 无名指扭矩 + LITTLE_TORQUE = 0x55 # 小拇指扭矩 + + # 手指故障码 + THUMB_FAULT = 0x59 # 大拇指故障码 + INDEX_FAULT = 0x5A # 食指故障码 + MIDDLE_FAULT = 0x5B # 中指故障码 + RING_FAULT = 0x5C # 无名指故障码 + LITTLE_FAULT = 0x5D # 小拇指故障码 + + # 手指温度 + THUMB_TEMPERATURE = 0x61 # 大拇指过温保护阈值 + INDEX_TEMPERATURE = 0x62 # 食指过温保护阈值 + MIDDLE_TEMPERATURE = 0x63 # 中指过温保护阈值 + RING_TEMPERATURE = 0x64 # 无名指过温保护阈值 + LITTLE_TEMPERATURE = 0x65 # 小拇指过温保护阈值 + + # 手指运动控制 - 合并指令区域 + FINGER_SPEED = 0x81 # 设置手指速度 + FINGER_TORQUE = 0x82 # 设置手指输出扭矩 + FINGER_FAULT = 0x83 # 清除手指故障及故障码 + FINGER_TEMPERATURE = 0x84 # 手指各关节温度 + + # 指尖传感器数据 + HAND_NORMAL_FORCE = 0x90 # 五指法向压力 + HAND_TANGENTIAL_FORCE = 0x91 # 五指切向压力 + HAND_TANGENTIAL_FORCE_DIR = 0x92 # 五指切向方向 + HAND_APPROACH_INC = 0x93 # 五指接近感应 + + # 手指所有数据 + THUMB_ALL_DATA = 0x98 # 大拇指所有数据 + INDEX_ALL_DATA = 0x99 # 食指所有数据 + MIDDLE_ALL_DATA = 0x9A # 中指所有数据 + RING_ALL_DATA = 0x9B # 无名指所有数据 + LITTLE_ALL_DATA = 0x9C # 小拇指所有数据 + + # 触觉传感器 + TOUCH_SENSOR_TYPE = 0xB0 # 触觉传感器类型 + THUMB_TOUCH = 0xB1 # 大拇指触觉传感 + INDEX_TOUCH = 0xB2 # 食指触觉传感 + MIDDLE_TOUCH = 0xB3 # 中指触觉传感 + RING_TOUCH = 0xB4 # 无名指触觉传感 + LITTLE_TOUCH = 0xB5 # 小拇指触觉传感 + PALM_TOUCH = 0xB6 # 手掌指触觉传感 + + # 查询指令 + HAND_UID_GET = 0xC0 # 唯一标识码查询 + HAND_HARDWARE_VERSION_GET = 0xC1 # 硬件版本查询 + HAND_SOFTWARE_VERSION_GET = 0xC2 # 软件版本查询 + HAND_COMM_ID_GET = 0xC3 # 设备id查询 + HAND_STRUCT_VERSION_GET = 0xC4 # 结构版本号查询 + + # 出厂指令 + HOST_CMD_HAND_ERASE_POS_CALI = 0xCD # 擦除位置校准值 + HAND_COMM_ID_SET = 0xD1 # 通信ID设置 + HAND_UID_SET = 0xF0 # 唯一标识码设置 + +class LinkerHandG20Can: + def __init__(self, can_channel='can0', baudrate=1000000, can_id=0x28, yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.running = True + + # 初始化数据存储变量 + self.last_thumb_pos, self.last_index_pos, self.last_ring_pos, self.last_middle_pos, self.last_little_pos = None, None, None, None, None + self.last_root1, self.last_yaw, self.last_roll, self.last_root2, self.last_tip = None, None, None, None, None + + # 并联控制数据存储 + self.x01, self.x02, self.x03, self.x04, self.x05, self.x06 = [], [], [], [], [], [] + self.x09, self.x0A, self.x0B, self.x0C, self.x0D, self.x0E = [], [], [], [], [], [] + self.x11, self.x12, self.x13, self.x14, self.x15, self.x16 = [], [], [], [], [], [] + self.x19, self.x1A, self.x1B, self.x1C, self.x1D, self.x1E = [], [], [], [], [], [] + self.x21, self.x22, self.x23, self.x24, self.x25, self.x26 = [], [], [], [], [], [] + + # 串联控制数据存储 + self.x41, self.x42, self.x43, self.x44, self.x45 = [], [], [], [], [] + self.x49, self.x4A, self.x4B, self.x4C, self.x4D = [0] * 6, [0] * 6, [0] * 6, [0] * 6, [0] * 6 + self.x51, self.x52, self.x53, self.x54, self.x55 = [], [], [], [], [] + self.x59, self.x5A, self.x5B, self.x5C, self.x5D = [], [], [], [], [] + self.x61, self.x62, self.x63, self.x64, self.x65 = [], [], [], [], [] + + # 合并指令区域数据存储 + self.x81, self.x82, self.x83, self.x84 = [], [], [], [] + + # 传感器数据存储 + self.x90, self.x91, self.x92, self.x93 = [], [], [], [] + self.x98, self.x99, self.x9A, self.x9B, self.x9C = [], [], [], [], [] + self.xB0, self.xB1, self.xB2, self.xB3, self.xB4, self.xB5, self.xB6 = [], [], [], [], [], [], [] + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = [[-1] * 5 for _ in range(4)] + + # 查询指令数据存储 + self.xC0, self.xC1, self.xC2, self.xC3, self.xC4 = [], [], [], [], [] + self.serial_number = [] + self.serial_number_map = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + } + # 触觉传感器矩阵数据 + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, 16: 1, 32: 2, 48: 3, 64: 4, 80: 5, + 96: 6, 112: 7, 128: 8, 144: 9, 160: 10, 176: 11, + } + # 全掌触觉数据缓存 + self.thumb_matrix_palm = np.full((23, 9), -1) + self.thumb_matrix_palm_tmp = [] + self.thumb_matrix_palm_mass = [-1, -1, -1] + + self.index_matrix_palm = np.full((23, 9), -1) + self.index_matrix_palm_tmp = [] + self.index_matrix_palm_mass = [-1, -1, -1] + + self.middle_matrix_palm = np.full((23, 9), -1) + self.middle_matrix_palm_tmp = [] + self.middle_matrix_palm_mass = [-1, -1, -1] + + self.ring_matrix_palm = np.full((23, 9), -1) + self.ring_matrix_palm_tmp = [] + self.ring_matrix_palm_mass = [-1, -1, -1] + + self.little_matrix_palm = np.full((23, 9), -1) + self.little_matrix_palm_tmp = [] + self.little_matrix_palm_mass = [-1, -1, -1] + + self.palm_matrix_palm = np.full((28, 20), -1) + self.palm_matrix_palm_tmp = [] + self.palm_matrix_palm_mass = [-1, -1] + + self.bus = self.init_can_bus(channel=self.can_channel, baudrate=baudrate) + # 启动接收线程 + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + self._check_touch_type() + + self.xB0 = self.get_touch_sensor_type() # 获取触觉传感器类型,如果返回值为5:TSSP_JZG(全手掌,指尖11x9,指中6x9,指根6x9,数据以23行9列形式返回,五指各有3个合力值。手掌20x28,手掌有两个合力值,分为上掌上半部分和下半部分。) + + def _check_touch_type(self): + '''根据SN编码判断压感类型''' + self.sn = self.get_serial_number() + time.sleep(0.1) + if self.sn != "-1": + parts = self.sn.split("-") + if parts[4] == "A": + self.touch_type = 1 + elif parts[4] == "B": + self.touch_type = 2 + self.touch_code = 0xC6 # 6*12 + elif parts[4] == "J": + self.touch_type = 3 + elif parts[4] == "F": + self.touch_type = 4 + self.touch_code = 0xA4 # 4*10 + elif parts[4] == "Z": + self.touch_type = -1 + else: + # 如果没有SN编码则根据返回数据进行判断 + self.touch_type = self.get_touch_type() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_command(self, frame_property, data_list, sleep_time=0.003): + """ + 发送指令到CAN总线 + :param frame_property: 数据帧属性 + :param data_list: 数据载荷 + """ + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(sleep_time) + + def receive_response(self): + """ + 接收并处理CAN总线响应消息 + """ + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving message: {e}") + + def process_response(self, msg): + """ + 处理CAN响应消息 + """ + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + # 并联控制指令响应 + if frame_type == 0x01: self.x01 = list(response_data) + elif frame_type == 0x02: self.x02 = list(response_data) + elif frame_type == 0x03: self.x03 = list(response_data) + elif frame_type == 0x04: self.x04 = list(response_data) + elif frame_type == 0x05: self.x05 = list(response_data) + elif frame_type == 0x06: self.x06 = list(response_data) + elif frame_type == 0x09: self.x09 = list(response_data) + elif frame_type == 0x0A: self.x0A = list(response_data) + elif frame_type == 0x0B: self.x0B = list(response_data) + elif frame_type == 0x0C: self.x0C = list(response_data) + elif frame_type == 0x0D: self.x0D = list(response_data) + elif frame_type == 0x0E: self.x0E = list(response_data) + elif frame_type == 0x11: self.x11 = list(response_data) + elif frame_type == 0x12: self.x12 = list(response_data) + elif frame_type == 0x13: self.x13 = list(response_data) + elif frame_type == 0x14: self.x14 = list(response_data) + elif frame_type == 0x15: self.x15 = list(response_data) + elif frame_type == 0x16: self.x16 = list(response_data) + elif frame_type == 0x19: self.x19 = list(response_data) + elif frame_type == 0x1A: self.x1A = list(response_data) + elif frame_type == 0x1B: self.x1B = list(response_data) + elif frame_type == 0x1C: self.x1C = list(response_data) + elif frame_type == 0x1D: self.x1D = list(response_data) + elif frame_type == 0x1E: self.x1E = list(response_data) + elif frame_type == 0x21: self.x21 = list(response_data) + elif frame_type == 0x22: self.x22 = list(response_data) + elif frame_type == 0x23: self.x23 = list(response_data) + elif frame_type == 0x24: self.x24 = list(response_data) + elif frame_type == 0x25: self.x25 = list(response_data) + elif frame_type == 0x26: self.x26 = list(response_data) + + # 串联控制指令响应 + elif frame_type == 0x41: self.x41 = list(response_data) + elif frame_type == 0x42: self.x42 = list(response_data) + elif frame_type == 0x43: self.x43 = list(response_data) + elif frame_type == 0x44: self.x44 = list(response_data) + elif frame_type == 0x45: self.x45 = list(response_data) + elif frame_type == 0x49: self.x49 = list(response_data) + elif frame_type == 0x4A: self.x4A = list(response_data) + elif frame_type == 0x4B: self.x4B = list(response_data) + elif frame_type == 0x4C: self.x4C = list(response_data) + elif frame_type == 0x4D: self.x4D = list(response_data) + elif frame_type == 0x51: self.x51 = list(response_data) + elif frame_type == 0x52: self.x52 = list(response_data) + elif frame_type == 0x53: self.x53 = list(response_data) + elif frame_type == 0x54: self.x54 = list(response_data) + elif frame_type == 0x55: self.x55 = list(response_data) + elif frame_type == 0x59: self.x59 = list(response_data) + elif frame_type == 0x5A: self.x5A = list(response_data) + elif frame_type == 0x5B: self.x5B = list(response_data) + elif frame_type == 0x5C: self.x5C = list(response_data) + elif frame_type == 0x5D: self.x5D = list(response_data) + elif frame_type == 0x61: self.x61 = list(response_data) + elif frame_type == 0x62: self.x62 = list(response_data) + elif frame_type == 0x63: self.x63 = list(response_data) + elif frame_type == 0x64: self.x64 = list(response_data) + elif frame_type == 0x65: self.x65 = list(response_data) + + # 合并指令区域响应 + elif frame_type == 0x81: self.x81 = list(response_data) + elif frame_type == 0x82: self.x82 = list(response_data) + elif frame_type == 0x83: self.x83 = list(response_data) + elif frame_type == 0x84: self.x84 = list(response_data) + + # 传感器数据响应 + elif frame_type == 0x90: self.x90 = list(response_data) + elif frame_type == 0x91: self.x91 = list(response_data) + elif frame_type == 0x92: self.x92 = list(response_data) + elif frame_type == 0x93: self.x93 = list(response_data) + elif frame_type == 0x98: self.x98 = list(response_data) + elif frame_type == 0x99: self.x99 = list(response_data) + elif frame_type == 0x9A: self.x9A = list(response_data) + elif frame_type == 0x9B: self.x9B = list(response_data) + elif frame_type == 0x9C: self.x9C = list(response_data) + + # 触觉传感器响应 + elif frame_type == 0xB0: self.xB0 = list(response_data) + elif frame_type == 0xB1: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.thumb_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 4 and d[0] == 22 and d[1] == 7: # 如果是最后一帧 + self.thumb_matrix_palm = self.build_matrix(self.thumb_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.thumb_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 7 and d[0] == 255: + self.thumb_matrix_palm_mass = self.build_matrix_mass(d) # [指尖合力值, 指中合力值, 指根合力值] + else: + if len(d) == 2: + self.xB1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] + + elif frame_type == 0xB2: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.index_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 4 and d[0] == 22 and d[1] == 7: # 如果是最后一帧 + self.index_matrix_palm = self.build_matrix(self.index_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.index_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 7 and d[0] == 255: + self.index_matrix_palm_mass = self.build_matrix_mass(d) # [指尖合力值, 指中合力值, 指根合力值] + else: + if len(d) == 2: + self.xB2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] + elif frame_type == 0xB3: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.middle_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 4 and d[0] == 22 and d[1] == 7: # 如果是最后一帧 + self.middle_matrix_palm = self.build_matrix(self.middle_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.middle_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 7 and d[0] == 255: + self.middle_matrix_palm_mass = self.build_matrix_mass(d) # [指尖合力值, 指中合力值, 指根合力值] + else: + if len(d) == 2: + self.xB3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] + elif frame_type == 0xB4: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.ring_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 4 and d[0] == 22 and d[1] == 7: # 如果是最后一帧 + self.ring_matrix_palm = self.build_matrix(self.ring_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.ring_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 7 and d[0] == 255: + self.ring_matrix_palm_mass = self.build_matrix_mass(d) # [指尖合力值, 指中合力值, 指根合力值] + else: + if len(d) == 2: + self.xB4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] + elif frame_type == 0xB5: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.little_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 4 and d[0] == 22 and d[1] == 7: # 如果是最后一帧 + self.little_matrix_palm = self.build_matrix(self.little_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.little_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 7 and d[0] == 255: + self.little_matrix_palm_mass = self.build_matrix_mass(d) # [指尖合力值, 指中合力值, 指根合力值] + else: + if len(d) == 2: + self.xB5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] + elif frame_type == 0xB6: + d = list(response_data) + if self.xB0[0] == 5: + """全掌矩阵""" + self.palm_matrix_palm_tmp.append(d) # 将返回帧存储到缓存 + if len(d) == 7 and d[0] == 27 and d[1] == 5: # 如果是最后一帧 + self.palm_matrix_palm = self.build_matrix(self.palm_matrix_palm_tmp) # 将帧数据排列成23行9列矩阵 + self.palm_matrix_palm_tmp=[] # 重置缓存数据 + if len(d) == 5 and d[0] == 255: + self.palm_matrix_palm_mass = self.build_matrix_mass(d) + else: + self.xB6 = d + + + # 查询指令响应 + # elif frame_type == 0xC0: self.xC0 = list(response_data) + elif frame_type == 0xC0: + d = list(response_data) + index = self.serial_number_map.get(d[0]) + if index is not None: + self.serial_number=self.serial_number + d[1:] + else: + self.serial_number=self.serial_number + [-1] * 6 + elif frame_type == 0xC1: self.xC1 = list(response_data) + elif frame_type == 0xC2: self.xC2 = list(response_data) + elif frame_type == 0xC3: self.xC3 = list(response_data) + elif frame_type == 0xC4: self.xC4 = list(response_data) + + + + def build_matrix(self, data, r=23, c=9): + rows, cols = r, c + matrix = np.full((rows, cols), -1) + + for item in data: + if len(item) == 4: + # 最后一行:从列坐标开始放 + row, col, v1, v2 = item + start_col = col # 改为 col,而不是 col+1 + values = [v1, v2] + + cur_row, cur_col = row, start_col + for val in values: + if cur_col >= cols: + cur_row += 1 + cur_col = 0 + if cur_row < rows: + matrix[cur_row][cur_col] = val + cur_col += 1 + break + + elif len(item) == 7: + # 普通行:从列坐标开始放 + row, col, v1, v2, v3, v4, v5 = item + start_col = col # 改为 col,而不是 col+1 + values = [v1, v2, v3, v4, v5] + + cur_row, cur_col = row, start_col + for val in values: + if cur_col >= cols: + cur_row += 1 + cur_col = 0 + if cur_row < rows: + matrix[cur_row][cur_col] = val + cur_col += 1 + + return matrix + + def build_matrix_mass(self, hex_data): + """ + 处理返回的和力值的帧数据,手指返回合力值长度为3,[指尖,指中,指根] + 手掌返回为长度为2。[上半部,下半部] + 解析 CAN 数据(支持 5 字节或 7 字节) + + 参数: + hex_data: 十六进制列表,如 [0xFF, 0x27, 0x02, 0x51, 0x05, 0x20, 0x04] (7字节) + 或 [0xFF, 0x92, 0x09, 0xF8, 0x00] (5字节) + + 返回: + (id, values) 其中 id 是 int,values 是包含 int 的列表(2个或3个) + """ + if len(hex_data) not in [5, 7]: + raise ValueError(f"数据长度不支持,需要 5 或 7 字节,实际: {len(hex_data)}") + + # 获取 ID(第一个字节) + can_id = hex_data[0] + + # 计算有多少组数据(每组2字节) + data_bytes = hex_data[1:] # 去掉 ID + num_values = len(data_bytes) // 2 + + # 解析数据组(小端序) + values = [] + for i in range(num_values): + low_byte = data_bytes[i*2] # 低位字节 + high_byte = data_bytes[i*2 + 1] # 高位字节 + # 小端拼接:低位 + 高位<<8 + value = low_byte | (high_byte << 8) + values.append(value) + + return values + + + # 并联控制指令方法 + def set_roll_positions(self, joint_ranges): + """设置所有手指横滚关节位置""" + self.send_command(FrameProperty.ROLL_POS, joint_ranges) + + def set_yaw_positions(self, joint_ranges): + """设置所有手指航向关节位置""" + self.send_command(FrameProperty.YAW_POS, joint_ranges) + + def set_root1_positions(self, joint_ranges): + """设置所有手指指根1关节位置""" + self.send_command(FrameProperty.ROOT1_POS, joint_ranges) + + def set_root2_positions(self, joint_ranges): + """设置所有手指指根2关节位置""" + self.send_command(FrameProperty.ROOT2_POS, joint_ranges) + + def set_root3_positions(self, joint_ranges): + """设置所有手指指根3关节位置""" + self.send_command(FrameProperty.ROOT3_POS, joint_ranges) + + def set_tip_positions(self, joint_ranges=[80]*5): + """设置所有手指指尖关节位置""" + self.send_command(FrameProperty.TIP_POS, joint_ranges) + + # 串联控制指令方法 + def set_thumb_positions(self, joint_ranges): + """设置大拇指所有关节位置""" + self.send_command(FrameProperty.THUMB_POS, joint_ranges) + + def set_index_positions(self, joint_ranges): + """设置食指所有关节位置""" + self.send_command(FrameProperty.INDEX_POS, joint_ranges) + + def set_middle_positions(self, joint_ranges): + """设置中指所有关节位置""" + self.send_command(FrameProperty.MIDDLE_POS, joint_ranges) + + def set_ring_positions(self, joint_ranges): + """设置无名指所有关节位置""" + self.send_command(FrameProperty.RING_POS, joint_ranges) + + def set_little_positions(self, joint_ranges): + """设置小拇指所有关节位置""" + self.send_command(FrameProperty.LITTLE_POS, joint_ranges) + + # 扭矩设置方法 + def set_thumb_torque(self, torque_values): + """设置大拇指扭矩""" + self.send_command(FrameProperty.THUMB_TORQUE, torque_values) + + def set_index_torque(self, torque_values): + """设置食指扭矩""" + self.send_command(FrameProperty.INDEX_TORQUE, torque_values) + + def set_middle_torque(self, torque_values): + """设置中指扭矩""" + self.send_command(FrameProperty.MIDDLE_TORQUE, torque_values) + + def set_ring_torque(self, torque_values): + """设置无名指扭矩""" + self.send_command(FrameProperty.RING_TORQUE, torque_values) + + def set_little_torque(self, torque_values): + """设置小拇指扭矩""" + self.send_command(FrameProperty.LITTLE_TORQUE, torque_values) + + # 速度设置方法 + def set_thumb_speed(self, speed_values): + """设置大拇指速度""" + self.send_command(FrameProperty.THUMB_SPEED, speed_values) + + def set_index_speed(self, speed_values): + """设置食指速度""" + self.send_command(FrameProperty.INDEX_SPEED, speed_values) + + def set_middle_speed(self, speed_values): + """设置中指速度""" + self.send_command(FrameProperty.MIDDLE_SPEED, speed_values) + + def set_ring_speed(self, speed_values): + """设置无名指速度""" + self.send_command(FrameProperty.RING_SPEED, speed_values) + + def set_little_speed(self, speed_values): + """设置小拇指速度""" + self.send_command(FrameProperty.LITTLE_SPEED, speed_values) + + # 查询方法 + def get_thumb_positions(self): + """获取大拇指所有关节当前位置""" + self.send_command(FrameProperty.THUMB_POS, []) + return self.x41 + + def get_index_positions(self): + """获取食指所有关节当前位置""" + self.send_command(FrameProperty.INDEX_POS, []) + return self.x42 + + def get_middle_positions(self): + """获取中指所有关节当前位置""" + self.send_command(FrameProperty.MIDDLE_POS, []) + return self.x43 + + def get_ring_positions(self): + """获取无名指所有关节当前位置""" + self.send_command(FrameProperty.RING_POS, []) + return self.x44 + + def get_little_positions(self): + """获取小拇指所有关节当前位置""" + self.send_command(FrameProperty.LITTLE_POS, []) + return self.x45 + + + def get_thumb_speed(self): + """获取大拇指速度""" + self.send_command(FrameProperty.THUMB_SPEED, []) + + def get_index_speed(self): + """获取食指速度""" + self.send_command(FrameProperty.INDEX_SPEED, []) + + def get_middle_speed(self): + """获取中指速度""" + self.send_command(FrameProperty.MIDDLE_SPEED, []) + + def get_ring_speed(self): + """获取无名指速度""" + self.send_command(FrameProperty.RING_SPEED, []) + + def get_little_speed(self): + """获取小拇指速度""" + self.send_command(FrameProperty.LITTLE_SPEED, []) + + def get_thumb_torque(self): + """获取大拇指扭矩""" + self.send_command(FrameProperty.THUMB_TORQUE, []) + + def get_index_torque(self): + """获取食指扭矩""" + self.send_command(FrameProperty.INDEX_TORQUE, []) + + def get_middle_torque(self): + """获取中指扭矩""" + self.send_command(FrameProperty.MIDDLE_TORQUE, []) + + def get_ring_torque(self): + """获取无名指扭矩""" + self.send_command(FrameProperty.RING_TORQUE, []) + + def get_little_torque(self): + """获取小拇指扭矩""" + self.send_command(FrameProperty.LITTLE_TORQUE, []) + + def get_thumb_fault(self): + """获取大拇指所有关节故障码""" + self.send_command(FrameProperty.THUMB_FAULT, []) + return self.x59 + + def get_index_fault(self): + """获取食指所有关节故障码""" + self.send_command(FrameProperty.INDEX_FAULT, []) + return self.x5A + + def get_middle_fault(self): + """获取中指所有关节故障码""" + self.send_command(FrameProperty.MIDDLE_FAULT, []) + return self.x5B + + def get_ring_fault(self): + """获取无名指所有关节故障码""" + self.send_command(FrameProperty.RING_FAULT, []) + return self.x5C + + def get_little_fault(self): + """获取小拇指所有关节故障码""" + self.send_command(FrameProperty.LITTLE_FAULT, []) + return self.x5D + + def get_thumb_temperature(self): + """获取大拇指所有关节当前温度""" + self.send_command(FrameProperty.THUMB_TEMPERATURE, []) + return self.x61 + + def get_index_temperature(self): + """获取食指所有关节当前温度""" + self.send_command(FrameProperty.INDEX_TEMPERATURE, []) + return self.x62 + + def get_middle_temperature(self): + """获取中指所有关节当前温度""" + self.send_command(FrameProperty.MIDDLE_TEMPERATURE, []) + return self.x63 + + def get_ring_temperature(self): + """获取无名指所有关节当前温度""" + self.send_command(FrameProperty.RING_TEMPERATURE, []) + return self.x64 + + def get_little_temperature(self): + """获取小拇指所有关节当前温度""" + self.send_command(FrameProperty.LITTLE_TEMPERATURE, []) + return self.x65 + + # 合并指令区域方法 + def set_finger_speed(self, speed_values): + """设置手指速度""" + self.send_command(FrameProperty.FINGER_SPEED, speed_values) + + def set_finger_torque(self, torque_values): + """设置手指输出扭矩""" + self.send_command(FrameProperty.FINGER_TORQUE, torque_values) + + def clear_finger_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """清除手指故障及故障码""" + self.send_command(FrameProperty.FINGER_FAULT, finger_mask) + return self.x83 + + def get_finger_temperature(self): + """获取手指各关节温度""" + self.send_command(FrameProperty.FINGER_TEMPERATURE, []) + return self.x84 + + # 传感器数据获取方法 + def get_normal_force(self): + """获取五指法向力""" + self.send_command(FrameProperty.HAND_NORMAL_FORCE, []) + return self.x90 + + def get_tangential_force(self): + """获取五指切向力""" + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE, []) + return self.x91 + + def get_tangential_force_dir(self): + """获取五指切向力方向""" + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE_DIR, []) + return self.x92 + + def get_approach_inc(self): + """获取五指接近感应""" + self.send_command(FrameProperty.HAND_APPROACH_INC, []) + return self.x93 + + def get_force(self): + '''Get pressure sensor data''' + return [self.x90,self.x91,self.x92,self.x93] + + # 触觉传感器方法 + def get_touch_sensor_type(self): + """获取触觉传感器类型 暂仅支持G20""" + self.send_command(FrameProperty.TOUCH_SENSOR_TYPE, []) + return self.xB0[0] + + def get_thumb_touch(self): + """获取大拇指触觉传感数据""" + if self.xB0[0] == 5: + d = [23, 9, 1] + sleep_time = 0.015 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.THUMB_TOUCH, d, sleep_time=sleep_time) + #return self.thumb_matrix + + def get_index_touch(self): + """获取食指触觉传感数据""" + if self.xB0[0] == 5: + d = [23, 9, 1] + sleep_time = 0.015 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.INDEX_TOUCH, d, sleep_time=sleep_time) + #return self.xB2 + + def get_middle_touch(self): + """获取中指触觉传感数据""" + if self.xB0[0] == 5: + d = [23, 9, 1] + sleep_time = 0.015 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.MIDDLE_TOUCH, d, sleep_time=sleep_time) + #33333return self.xB3 + + def get_ring_touch(self): + """获取无名指触觉传感数据""" + if self.xB0[0] == 5: + d = [23, 9, 1] + sleep_time = 0.015 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.RING_TOUCH, d, sleep_time=sleep_time) + #return self.xB4 + + def get_little_touch(self): + """获取小拇指触觉传感数据""" + if self.xB0[0] == 5: + d = [23, 9, 1] + sleep_time = 0.015 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.LITTLE_TOUCH, d, sleep_time=sleep_time) + #return self.xB5 + + def get_palm_touch(self): + """获取手掌触觉传感数据""" + if self.xB0[0] == 5: + d = [28, 20, 1] + sleep_time = 0.035 + else: + d = [0xC6] + sleep_time = 0.007 + self.send_command(FrameProperty.PALM_TOUCH, d, sleep_time=sleep_time) + #return self.xB6 + + # 查询指令方法 + def get_uid(self): + """获取设备唯一标识码""" + self.send_command(FrameProperty.HAND_UID_GET, []) + return self.xC0 + + def get_hardware_version(self): + """获取硬件版本""" + self.send_command(FrameProperty.HAND_HARDWARE_VERSION_GET, []) + return self.xC1 + + def get_software_version(self): + """获取软件版本""" + self.send_command(FrameProperty.HAND_SOFTWARE_VERSION_GET, []) + return self.xC2 + + def get_comm_id(self): + """获取设备通信ID""" + self.send_command(FrameProperty.HAND_COMM_ID_GET, []) + return self.xC3 + + def get_struct_version(self): + """获取结构版本号""" + self.send_command(FrameProperty.HAND_STRUCT_VERSION_GET, []) + return self.xC4 + + # 出厂指令方法 + def erase_position_calibration(self): + """擦除位置校准值""" + self.send_command(FrameProperty.HOST_CMD_HAND_ERASE_POS_CALI, []) + + def set_comm_id(self, new_id): + """设置通信ID""" + self.send_command(FrameProperty.HAND_COMM_ID_SET, [new_id]) + + def set_uid(self, uid_data): + """设置唯一标识码(内部出厂使用)""" + self.send_command(FrameProperty.HAND_UID_SET, uid_data) + + # 辅助方法 + def slice_list(self, input_list, slice_size): + """将列表按指定大小切片""" + return [input_list[i:i + slice_size] for i in range(0, len(input_list), slice_size)] + # ----------------------------------------------------- + # API指令区域 + #------------------------------------------------------ + def set_joint_positions(self, joint_ranges): + """API接口:设置手指所有关节位置""" + j = self.cmd_range_to_joint_range(cmd_list=joint_ranges) + self.set_thumb_positions(j[0]) + self.set_index_positions(j[1]) + self.set_middle_positions(j[2]) + self.set_ring_positions(j[3]) + self.set_little_positions(j[4]) + + def set_speed(self, speed=[250] * 5): + """API接口:设置手指速度""" + self.set_thumb_speed(speed_values=[speed[0]] * 6) + self.set_index_speed(speed_values=[speed[1]] * 6) + self.set_middle_speed(speed_values=[speed[2]] * 6) + self.set_ring_speed(speed_values=[speed[3]] * 6) + self.set_little_speed(speed_values=[speed[4]] * 6) + + def set_torque(self, torque=[250] * 5): + """API接口:设置手指最大扭矩""" + self.set_thumb_torque(torque_values=[torque[0]] * 6) + self.set_index_torque(torque_values=[torque[1]] * 6) + self.set_middle_torque(torque_values=[torque[2]] * 6) + self.set_ring_torque(torque_values=[torque[3]] * 6) + self.set_little_torque(torque_values=[torque[4]] * 6) + + + def get_version(self): + """API接口:获取手指嵌入式版本信息""" + return self.get_software_version() + + def get_current_status(self): + """API接口:获取手指当前状态""" + self.get_thumb_positions() + self.get_index_positions() + self.get_middle_positions() + self.get_ring_positions() + self.get_little_positions() + time.sleep(0.002) + s = [self.x41, self.x42, self.x43, self.x44, self.x45] + cmd_state = self.joint_state_to_cmd_state(state=s) + return cmd_state + + def get_current_pub_status(self): + """API接口:获取手指当前状态""" + self.get_current_status() + + def get_speed(self): + """API接口:获取手指速度""" + self.get_thumb_speed() + self.get_index_speed() + self.get_middle_speed() + self.get_ring_speed() + self.get_little_speed() + time.sleep(0.002) + + joint_speed = [self.x49, self.x4A, self.x4B, self.x4C, self.x4D] + state_speed = self.joint_state_to_cmd_state(state=joint_speed) + return state_speed + + def get_touch_type(self): + """API接口:获取手指触觉传感器类型""" + self.send_command(0xb0,[],sleep_time=0.03) + self.send_command(0xb1,[],sleep_time=0.03) + t = [] + for i in range(3): + t = self.xB1 + time.sleep(0.01) + if len(t) == 2: + return 2 + else: + self.send_command(0x20,[],sleep_time=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + else: + return 1 + + def get_matrix_touch(self): + """API接口:获取手指触摸传感器数据""" + self.get_thumb_touch() + self.get_index_touch() + self.get_middle_touch() + self.get_ring_touch() + self.get_little_touch() + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_matrix_touch_v2(self): + """API接口:获取手指触摸传感器数据""" + return self.get_matrix_touch() + + def get_thumb_matrix_touch(self,sleep_time=0): + """API接口:获取[大拇指]指触摸传感器数据""" + self.get_thumb_touch() + if self.xB0[0] == 5: + data = self.thumb_matrix_palm + else: + data = self.thumb_matrix + return data + + def get_index_matrix_touch(self,sleep_time=0): + """API接口:获取[食指]指触摸传感器数据""" + self.get_index_touch() + if self.xB0[0] == 5: + data = self.index_matrix_palm + else: + data = self.index_matrix + return data + + def get_middle_matrix_touch(self,sleep_time=0): + """API接口:获取[中指]指触摸传感器数据""" + self.get_middle_touch() + if self.xB0[0] == 5: + data = self.middle_matrix_palm + else: + data = self.middle_matrix + return data + + def get_ring_matrix_touch(self,sleep_time=0): + """API接口:获取[无名指]指触摸传感器数据""" + self.get_ring_touch() + if self.xB0[0] == 5: + data = self.ring_matrix_palm + else: + data = self.ring_matrix + return data + + def get_little_matrix_touch(self,sleep_time=0): + """API接口:获取[小指]指触摸传感器数据""" + self.get_little_touch() + if self.xB0[0] == 5: + data = self.little_matrix_palm + else: + data = self.little_matrix + return data + + def get_palm_matrix_touch(self,sleep_time=0): + """API接口:获取[小指]指触摸传感器数据""" + self.get_palm_touch() + if self.xB0[0] == 5: + data = self.palm_matrix_palm + else: + data = self.palm_matrix + return data + + def get_torque(self): + """API接口:获取手指最大扭矩""" + self.get_thumb_torque() + self.get_index_torque() + self.get_middle_torque() + self.get_ring_torque() + self.get_little_torque() + time.sleep(0.003) + t = [self.x51, self.x52, self.x53, self.x54, self.x55] + cmd_torque = self.joint_state_to_cmd_state(state=t) + return cmd_torque + + def get_current(self): + """API接口:获取手指电流""" + return [-1] * 20 + + def get_temperature(self): + """API接口:获取手指温度""" + joint_temperature = [self.get_thumb_temperature(), self.get_index_temperature(), self.get_middle_temperature(), self.get_ring_temperature(), self.get_little_temperature()] + cmd_temperature = self.joint_state_to_cmd_state(state=joint_temperature) + return cmd_temperature + + + def get_fault(self): + """API接口:获取手指故障代码""" + joint_fault = [self.get_thumb_fault(), self.get_index_fault(), self.get_middle_fault(), self.get_ring_fault(), self.get_little_fault()] + cmd_fault = self.joint_state_to_cmd_state(state=joint_fault) + return cmd_fault + + def clear_faults(self): + """API接口:清除手指故障代码""" + self.clear_finger_faults(finger_mask=[1, 1, 1, 1, 1]) + + def cmd_range_to_joint_range(self,cmd_list): + """根据手指映射关系,将手指控制命令列表转换为手指分组数据形式""" + # 定义手指映射规则 + finger_mapping = { + '拇指': [10, 5, 0, 11, 12, 15], + '食指': [6, 11, 1, 13, 14, 16], + '中指': [7, 12, 2, 13, 14, 17], + '无名指': [8, 13, 3, 14, 15, 18], + '小指': [9, 14, 4, 15, 16, 19] + } + + result = [] + + for finger, indices in finger_mapping.items(): + finger_data = [cmd_list[i] for i in indices] + result.append(finger_data) + + return result + + + def joint_state_to_cmd_state(self, state): + """ + 将关节状态转换为命令状态 + :param state: list2 格式的数据,5×6 的二维列表 + :return: list1 格式的 20 维列表 + """ + # 初始化结果列表,20个位置,预留位默认为0 + result = [0] * 20 + + # list1 索引映射: + # 0:拇指根部, 1:食指根部, 2:中指根部, 3:无名指根部, 4:小指根部 + # 5:拇指侧摆, 6:食指侧摆, 7:中指侧摆, 8:无名指侧摆, 9:小指侧摆 + # 10:拇指横摆, 11-14:预留, 15:拇指尖部, 16:食指末端, 17:中指末端, 18:无名指末端, 19:小指末端 + + # list2 每行结构: [侧摆/横摆, 0, 根部, 0, 0, 末端/尖部] + # 拇指行: [横摆, 侧摆, 根部, 0, 0, 尖部] — 注意拇指特殊,第1列是横摆,第2列是侧摆 + # 其他指: [侧摆, 0, 根部, 0, 0, 末端] + + # 拇指 (第0行) — 特殊处理 + result[10] = state[0][0] # 拇指横摆 + result[5] = state[0][1] # 拇指侧摆 + result[0] = state[0][2] # 拇指根部 + result[15] = state[0][5] # 拇指尖部 + + # 食指 (第1行) + result[6] = state[1][0] # 食指侧摆 + result[1] = state[1][2] # 食指根部 + result[16] = state[1][5] # 食指末端 + + # 中指 (第2行) + result[7] = state[2][0] # 中指侧摆 + result[2] = state[2][2] # 中指根部 + result[17] = state[2][5] # 中指末端 + + # 无名指 (第3行) + result[8] = state[3][0] # 无名指侧摆 + result[3] = state[3][2] # 无名指根部 + result[18] = state[3][5] # 无名指末端 + + # 小指 (第4行) + result[9] = state[4][0] # 小指侧摆 + result[4] = state[4][2] # 小指根部 + result[19] = state[4][5] # 小指末端 + + # 预留位 11-14 保持为 0 + + return result + + + def _list_d_value(self, list1, list2): + """检查两个列表的值是否有显著差异""" + if list1 is None: + return True + for a, b in zip(list1, list2): + if abs(b - a) > 2: + return True + return False + + def close_can_interface(self): + """关闭CAN接口""" + if self.bus: + self.bus.shutdown() + self.running = False + + def get_serial_number(self): + try: + self.send_command(0xC0,[],sleep_time=0.005) + # 1. 使用 bytes() 函数将整数列表转换为字节对象 + # bytes() 接收一个由 0-255 之间的整数组成的列表。 + byte_data = bytes(self.serial_number) + # 2. 使用 .decode() 方法将字节对象解码为 ASCII 字符串 + result_string = byte_data.decode('ascii') + if result_string == "": + return "-1" + else: + # print(f"原始 ASCII 码列表: {self.serial_number}") + # print(f"解码后的字符串: {result_string}") + return result_string + except: + return "-1" + def get_finger_order(self): + return ["Thumb Base", "Index Finger Base", "Middle Finger Base", "Ring Finger Base", "Pinky Finger Base", "Thumb Abduction", "Index Finger Abduction", "Middle Finger Abduction", "Ring Finger Abduction", "Pinky Finger Abduction", "Thumb Horizontal Abduction", "Reserved", "Reserved", "Reserved", "Reserved", "Thumb Tip", "Index Finger Tip", "Middle Finger Tip", "Ring Finger Tip", "Pinky Finger Tip"] diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l10_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l10_can.py new file mode 100644 index 0000000..f445a58 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l10_can.py @@ -0,0 +1,531 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import can +import time,sys +import threading +import numpy as np +#from tabulate import tabulate +from enum import Enum +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError + + + +class FrameProperty(Enum): + INVALID_FRAME_PROPERTY = 0x00 + JOINT_POSITION_RCO = 0x01 + MAX_PRESS_RCO = 0x02 + MAX_PRESS_RCO2 = 0x03 + JOINT_POSITION2_RCO = 0x04 + JOINT_SPEED = 0x05 + JOINT_SPEED2 = 0x06 + REQUEST_DATA_RETURN = 0x09 + JOINT_POSITION_N = 0x11 + MAX_PRESS_N = 0x12 + HAND_NORMAL_FORCE = 0X20 + HAND_TANGENTIAL_FORCE = 0X21 + HAND_TANGENTIAL_FORCE_DIR = 0X22 + HAND_APPROACH_INC = 0X23 + MOTOR_TEMPERATURE_1 = 0x33 + MOTOR_TEMPERATURE_2 = 0x34 + +class LinkerHandL10Can: + def __init__(self,can_id, can_channel='can0', baudrate=1000000, yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + self.is_cmd = False + self.x01 = [-1] * 5 + self.x02 = [-1] * 5 + self.x03 = [-1] * 5 + self.x04 = [-1] * 5 + self.x05 = [-1] * 5 + self.x06 = [-1] * 5 + self.x33 = self.x34 = [0] * 5 + # Fault codes + self.x35,self.x36 = [0] * 5,[0] * 5 + # New pressure sensors + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + self.serial_number = [] + self.serial_number_map = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + } + self.can_id = can_id + self.joint_angles = [0] * 10 + self.pressures = [200] * 5 # Default torque 200 + self.bus = self.init_can_bus(can_channel, baudrate) + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = [[-1] * 5 for _ in range(4)] + self.version = None + # Start receiving thread + self.running = True + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + self.version = self.get_version() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_frame(self, frame_property, data_list,sleep=0.002): + """Send a single CAN frame with specified properties and data.""" + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + # time.sleep(1) + # + time.sleep(sleep) + + def set_joint_positions(self, joint_angles): + """Set the positions of 10 joints (joint_angles: list of 10 values).""" + self.joint_angles = joint_angles + self.is_cmd = True + # Send angle control in frames, L10 protocol splits into first 6 and last 4 + self.send_frame(FrameProperty.JOINT_POSITION2_RCO, self.joint_angles[6:]) + #time.sleep(0.001) + self.send_frame(FrameProperty.JOINT_POSITION_RCO, self.joint_angles[:6]) + #time.sleep(0.002) + self.is_cmd = False + + + def set_max_torque_limits(self, pressures,type="get"): + """Set maximum torque limits""" + if type == "get": + self.pressures = [0.0] + else: + self.pressures = pressures[:5] + #self.send_frame(FrameProperty.MAX_PRESS_RCO, self.pressures) + + + def set_joint_speed_l10(self,speed=[180]*5): + self.x05 = speed + for i in range(2): + time.sleep(0.01) + self.send_frame(0x05, speed) + def set_speed(self,speed=[180]*5): + if len(speed) == 5: + self.x05 = speed + for i in range(2): + time.sleep(0.01) + self.send_frame(0x05, speed) + elif len(speed) == 10: + for i in range(2): + time.sleep(0.01) + self.send_frame(0x05, speed[:5]) + self.send_frame(0x06, speed[5:]) + else: + raise ValueError("Speed list must have 10 elements.") + def request_all_status(self): + """Get all joint positions and pressures.""" + self.send_frame(FrameProperty.REQUEST_DATA_RETURN, []) + ''' -------------------Pressure Sensors---------------------- ''' + def get_normal_force(self): + self.send_frame(FrameProperty.HAND_NORMAL_FORCE,[],sleep=0.004) + + def get_tangential_force(self): + self.send_frame(FrameProperty.HAND_TANGENTIAL_FORCE,[],sleep=0.004) + + def get_tangential_force_dir(self): + self.send_frame(FrameProperty.HAND_TANGENTIAL_FORCE_DIR,[],sleep=0.004) + def get_approach_inc(self): + self.send_frame(FrameProperty.HAND_APPROACH_INC,[],sleep=0.004) + ''' -------------------Motor Temperature---------------------- ''' + def get_motor_temperature(self): + self.send_frame(FrameProperty.MOTOR_TEMPERATURE_1,[],sleep=0.01) + self.send_frame(FrameProperty.MOTOR_TEMPERATURE_2,[],sleep=0.01) + # Motor fault codes + def get_motor_fault_code(self): + self.send_frame(0x35,[],sleep=0.1) + self.send_frame(0x36,[],sleep=0.1) + def receive_response(self): + """Receive CAN responses and process them.""" + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving CAN message: {e}") + + def process_response(self, msg): + """Process received CAN messages.""" + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == FrameProperty.JOINT_POSITION_RCO.value: # 0x01 + self.x01 = list(response_data) + elif frame_type == FrameProperty.MAX_PRESS_RCO.value: # 0x02 + self.x02 = list(response_data) + elif frame_type == FrameProperty.MAX_PRESS_RCO2.value: # 0x03 + self.x03 = list(response_data) + elif frame_type == FrameProperty.JOINT_POSITION2_RCO.value: # 0x04 + self.x04 = list(response_data) + elif frame_type == 0x05: + self.x05 = list(response_data) + elif frame_type == 0x06: + self.x06 = list(response_data) + elif frame_type == 0x20: + # Five-finger normal force + d = list(response_data) + self.normal_force = [float(i) for i in d] + elif frame_type == 0x21: + # Five-finger tangential force + d = list(response_data) + self.tangential_force = [float(i) for i in d] + elif frame_type == 0x22: + # Five-finger tangential force direction + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + # Five-finger approach increment + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x33: + self.x33 = list(response_data) + elif frame_type == 0x34: + self.x34 = list(response_data) + elif frame_type == 0x35: + self.x35 = list(response_data) + elif frame_type == 0x36: + self.x36 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0x64: + self.version = list(response_data) + elif frame_type == 0xC2: # version number + self.version = list(response_data) + elif frame_type == 0xC0: + d = list(response_data) + index = self.serial_number_map.get(d[0]) + if index is not None: + self.serial_number=self.serial_number + d[1:] + else: + self.serial_number=self.serial_number + [-1] * 6 + + def get_version(self): + self.send_frame(0x64, [], sleep=0.1) + time.sleep(0.1) + if self.version is None: + self.send_frame(0xC2, [], sleep=0.1) + time.sleep(0.1) + return self.version + + def set_torque(self,torque=[]): + '''Set maximum torque''' + if len(torque) == 5: + self.send_frame(0x02, torque) + time.sleep(0.002) + self.send_frame(0x03,torque) + elif len(torque) > 5: + self.send_frame(0x02, torque[:5]) + time.sleep(0.002) + self.send_frame(0x03,torque[5:]) + + + def get_current_status(self): + '''Get current joint status''' + if self.is_cmd == False: + #if self.version != None and self.version[4] > 35: + self.send_frame(0x01,[],sleep=0.003) + self.send_frame(0x04,[],sleep=0.003) + state = self.x01 + self.x04 + return state + else: + state = self.x01 + self.x04 + return state + + def get_current_pub_status(self): + state = self.x01 + self.x04 + return state + + def get_speed(self): + '''Get current speed''' + self.send_frame(0x05,[],sleep=0.003) + self.send_frame(0x06,[],sleep=0.003) + return self.x05 + self.x06 + + def get_force(self): + '''Get pressure sensor data''' + return [self.normal_force,self.tangential_force , self.tangential_force_dir , self.approach_inc] + def get_temperature(self): + '''Get current motor temperature''' + self.get_motor_temperature() + return self.x33+self.x34 + + def get_touch_type(self): + '''Get touch type''' + self.send_frame(0xb0,[],sleep=0.03) + self.send_frame(0xb1,[],sleep=0.03) + t = [] + for i in range(3): + t = self.xb1 + time.sleep(0.01) + if len(t) == 2: + return 2 + else: + self.send_frame(0x20,[],sleep=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + else: + return 1 + + def get_touch(self): + '''Get touch data''' + self.send_frame(0xb1,[],sleep=0.03) + self.send_frame(0xb2,[],sleep=0.03) + self.send_frame(0xb3,[],sleep=0.03) + self.send_frame(0xb4,[],sleep=0.03) + self.send_frame(0xb5,[],sleep=0.03) + return [self.xb1[1],self.xb2[1],self.xb3[1],self.xb4[1],self.xb5[1],0] # The last digit is palm, currently not available + + def get_matrix_touch(self): + self.send_frame(0xb1,[0xc6],sleep=0.06) + self.send_frame(0xb2,[0xc6],sleep=0.06) + self.send_frame(0xb3,[0xc6],sleep=0.06) + self.send_frame(0xb4,[0xc6],sleep=0.06) + self.send_frame(0xb5,[0xc6],sleep=0.06) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_matrix_touch_v2(self): + self.send_frame(0xb1,[0xc6],sleep=0.005) + self.send_frame(0xb2,[0xc6],sleep=0.005) + self.send_frame(0xb3,[0xc6],sleep=0.005) + self.send_frame(0xb4,[0xc6],sleep=0.005) + self.send_frame(0xb5,[0xc6],sleep=0.005) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_thumb_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb1,[0xc6],sleep=sleep_time) + return self.thumb_matrix + + def get_index_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb2,[0xc6],sleep=sleep_time) + return self.index_matrix + + def get_middle_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb3,[0xc6],sleep=sleep_time) + return self.middle_matrix + + def get_ring_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb4,[0xc6],sleep=sleep_time) + return self.ring_matrix + + def get_little_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb5,[0xc6],sleep=sleep_time) + return self.little_matrix + + + def get_torque(self): + '''Get current motor torque''' + if self.version != None and self.version[4]< 36: + return [-1] * 5 + else: + self.send_frame(0x02, []) + time.sleep(0.002) + self.send_frame(0x03,[]) + time.sleep(0.002) + return self.x02+self.x03 + + def get_fault(self): + '''Get motor fault''' + self.get_motor_fault_code() + return self.x35+self.x36 + + def get_current(self): + '''Get current''' + #return [-1] * 5 + self.send_frame(0x02, []) + time.sleep(0.002) + self.send_frame(0x03,[]) + return self.x02+self.x03 + + def get_serial_number(self): + try: + self.send_frame(0xC0,[],sleep=0.005) + # 1. 使用 bytes() 函数将整数列表转换为字节对象 + # bytes() 接收一个由 0-255 之间的整数组成的列表。 + byte_data = bytes(self.serial_number) + # 2. 使用 .decode() 方法将字节对象解码为 ASCII 字符串 + result_string = byte_data.decode('ascii') + if result_string == "": + return "-1" + else: + # print(f"原始 ASCII 码列表: {self.serial_number}") + # print(f"解码后的字符串: {result_string}") + return result_string + except: + return "-1" + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch", + "index_mcp_roll", "ring_mcp_roll", "pinky_mcp_roll", "thumb_cmc_roll"] + + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """L10 暂不支持清除故障码""" + pass + + def show_fun_table(self): + # if len(data) != 8 or data[0] != 0x64: + # raise ValueError("数据格式不正确") + data = self.version + result = { + "自由度": data[0], + "机械版本": data[1], + "版本序号": data[2], + "手方向": chr(data[3]), # ASCII 转字符 + "软件版本": f"V{data[4] >> 4}.{data[4] & 0x0F}", + "硬件版本": f"V{data[5] >> 4}.{data[5] & 0x0F}", + "修订标志": data[6], + "set_position": "Y", + "set_torque": "Y", + "set_speed": "Y", + "get_version": "Y", + "get_current_status": "Y", + "get_speed": "Y", + "get_temperature": "Y", + "get_touch_type": "Y", + "get_matrix_touch": "Y", + "get_fault": "Y", + "get_current": "current == torque" + } + + #return [data[0],data[1],data[2],chr(data[3]),f"V{data[4] >> 4}.{data[4] & 0x0F}",f"V{data[5] >> 4}.{data[5] & 0x0F}",data[6]] + table = [[k, v] for k, v in result.items()] + #print(tabulate(table, tablefmt="grid"), flush=True) + + + # # 示例数据 + # data = [0x64, 0x15, 0x03, 0x0A, 0x4C, 0x11, 0x22, 0x01] + # parsed = parse_version_data(data) + + # # 打印结果 + # for k, v in parsed.items(): + # print(f"{k}: {v}") + + + def close_can_interface(self): + """Stop the CAN communication.""" + self.running = False + if self.receive_thread.is_alive(): + self.receive_thread.join() + if self.bus: + self.bus.shutdown() diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py new file mode 100644 index 0000000..5b5f36e --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py @@ -0,0 +1,478 @@ +import sys +import time +import can +import threading +from enum import Enum +import numpy as np +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError + +class FrameProperty(Enum): + INVALID_FRAME_PROPERTY = 0x00 # Invalid CAN frame property | No return + JOINT_PITCH_R = 0x01 # Short frame pitch angle - finger base flexion | Returns this type of data + JOINT_YAW_R = 0x02 # Short frame yaw angle - finger abduction/adduction | Returns this type of data + JOINT_ROLL_R = 0x03 # Short frame roll angle - only used for thumb | Returns this type of data + JOINT_TIP_R = 0x04 # Short frame fingertip angle control | Returns this type of data + JOINT_SPEED_R = 0x05 # Short frame speed - motor running speed control | Returns this type of data + JOINT_CURRENT_R = 0x06 # Short frame current - motor running current feedback | Returns this type of data + JOINT_FAULT_R = 0x07 # Short frame fault - motor running fault feedback | Returns this type of data + REQUEST_DATA_RETURN = 0x09 # Request data return | Returns all data + JOINT_PITCH_NR = 0x11 # Pitch angle - finger base flexion | No return for this type of data + JOINT_YAW_NR = 0x12 # Yaw angle - finger abduction/adduction | No return for this type of data + JOINT_ROLL_NR = 0x13 # Roll angle - only used for thumb | No return for this type of data + JOINT_TIP_NR = 0x14 # Fingertip angle control | No return for this type of data + JOINT_SPEED_NR = 0x15 # Speed - motor running speed control | No return for this type of data + JOINT_CURRENT_NR = 0x16 # Current - motor running current feedback | No return for this type of data + JOINT_FAULT_NR = 0x17 # Fault - motor running fault feedback | No return for this type of data + HAND_UID = 0xC0 # Device unique identifier Read only -------- + HAND_HARDWARE_VERSION = 0xC1 # Hardware version Read only -------- + HAND_SOFTWARE_VERSION = 0xC2 # Software version Read only -------- + HAND_COMM_ID = 0xC3 # Device ID Read/Write 1 byte + HAND_SAVE_PARAMETER = 0xCF # Save parameters Write only -------- + + +class LinkerHandL20Can: + def __init__(self, can_channel='can0', baudrate=1000000, can_id=0x28,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.running = True + self.x05 = [255] * 5 + self.x06, self.x07 = [],[] + # New pressure sensors + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + self.x09 = self.x0b = self.x0c = self.x0d = [-1] * 5 + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + + # Initialize CAN bus according to operating system + # try: + # if sys.platform == "linux": + # self.open_can.open_can(self.can_channel) + # time.sleep(0.1) + # self.bus = can.interface.Bus( + # channel=can_channel, interface="socketcan", bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # elif sys.platform == "win32": + # self.bus = can.interface.Bus( + # channel=can_channel, interface='pcan', bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # else: + # raise EnvironmentError("Unsupported platform for CAN interface") + # except: + # print("Please insert CAN device",flush=True) + self.bus = self.init_can_bus(channel=self.can_channel, baudrate=baudrate) + # Initialize data storage + self.x01, self.x02, self.x03, self.x04 = [[-1] * 5 for _ in range(4)] + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = \ + [[-1] * 5 for _ in range(4)] + + # Start receive thread + self.get_touch_type() + time.sleep(0.1) + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + # def send_command(self, frame_property, data_list): + # print("66666") + # """ + # Send command to CAN bus + # :param frame_property: Data frame property + # :param data_list: Data payload + # """ + # frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + # data = [frame_property_value] + [int(val) for val in data_list] + # msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + # try: + # self.bus.send(msg) + # print(f"Message sent: ID={hex(self.can_id)}, Data={data}") + # except can.CanError as e: + # print(f"Failed to send message: {e}") + + def receive_response(self): + """ + Receive and process CAN bus response messages + """ + while self.running: + try: + msg = self.bus.recv(timeout=1.0) # Blocking receive, 1 second timeout + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving message666: {e}",flush=True) + + + def set_finger_base(self, angles): + self.send_command(FrameProperty.JOINT_PITCH_NR, angles) + + def set_finger_tip(self, angles): + self.send_command(FrameProperty.JOINT_TIP_NR, angles) + + def set_finger_middle(self, angles): + self.send_command(FrameProperty.JOINT_YAW_NR, angles) + + def set_thumb_roll(self, angle): + self.send_command(FrameProperty.JOINT_ROLL_NR, angle) + + def send_command(self, frame_property, data_list,sleep=0.002): + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError: + print("Message NOT sent") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....",flush=True) + time.sleep(sleep) + + def set_joint_pitch(self, frame, angles): + self.send_command(frame, angles) + + def set_joint_yaw(self, angles): + self.send_command(0x02, angles) + + def set_joint_roll(self, thumb_roll): + self.send_command(0x03, [thumb_roll, 0, 0, 0, 0]) + + def set_joint_speed(self, speed): + self.x05 = speed + self.send_command(0x05, speed) + def set_electric_current(self, e_c=[]): + self.send_command(0x06, e_c) + + def get_normal_force(self): + self.send_command(0x20,[]) + + def get_tangential_force(self): + self.send_command(0x21,[]) + + + def get_tangential_force_dir(self): + self.send_command(0x22,[]) + + def get_approach_inc(self): + self.send_command(0x23,[]) + + + + + def get_electric_current(self, e_c=[]): + self.send_command(0x06, e_c) + + def request_device_info(self): + self.send_command(0xC0, [0]) + self.send_command(0xC1, [0]) + self.send_command(0xC2, [0]) + + def save_parameters(self): + self.send_command(0xCF, []) + def process_response(self, msg): + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == 0x01: + self.x01 = list(response_data) + elif frame_type == 0x02: + self.x02 = list(response_data) + elif frame_type == 0x03: + self.x03 = list(response_data) + elif frame_type == 0x04: + self.x04 = list(response_data) + elif frame_type == 0xC0: + print(f"Device ID info: {response_data}") + if self.can_id == 0x28: + self.right_hand_info = response_data + elif self.can_id == 0x27: + self.left_hand_info = response_data + elif frame_type == 0x05: + self.x05 = list(response_data) + elif frame_type == 0x06: + self.x06 = list(response_data) + elif frame_type == 0x07: + self.x07 = list(response_data) + elif frame_type == 0x09: + self.x09 = list(response_data) + elif frame_type == 0x0B: + self.x0b = list(response_data) + elif frame_type == 0x0C: + self.x0c = list(response_data) + elif frame_type == 0x0D: + self.x0d = list(response_data) + elif frame_type == 0x20: + d = list(response_data) + self.normal_force = [float(i) for i in d] + elif frame_type == 0x21: + d = list(response_data) + self.tangential_force = [float(i) for i in d] + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] # Remove the first flag bit + def pose_slice(self, p): + """Slice the joint array into finger action arrays""" + try: + finger_base = [int(val) for val in p[0:5]] # Finger base + yaw_angles = [int(val) for val in p[5:10]] # Yaw + thumb_yaw = [int(val) for val in p[10:15]] # Thumb yaw to palm, others are 0 + finger_tip = [int(val) for val in p[15:20]] # Fingertip flexion + return finger_base, yaw_angles, thumb_yaw, finger_tip + except Exception as e: + print(e) + def set_joint_positions(self, position): + if len(position) != 20: + print("L20 finger joint length is incorrect") + return + finger_base, yaw_angles, thumb_yaw, finger_tip = self.pose_slice(position) + self.set_thumb_roll(thumb_yaw) # Thumb yaw to palm movement + self.set_finger_tip(finger_tip) # Fingertip movement + self.set_finger_base(finger_base) # Finger base movement + self.set_finger_middle(yaw_angles) # Yaw movement + def set_speed(self, speed=[]): + if len(speed) != 5: + raise ValueError("Speed list must have 5 elements.") + return + self.send_command(0x05,speed) + def set_torque(self, torque=[]): + '''Set torque, not supported for L20''' + print("Set torque, not supported for L20") + def set_current(self, current=[]): + '''Set current''' + self.set_electric_current(e_c=current) + def get_version(self): + '''Get version, currently not supported''' + return [0] * 5 + def get_current_status(self): + '''Get current finger joint status''' + self.send_command(0x01,[],sleep=0.01) + self.send_command(0x02,[],sleep=0.01) + self.send_command(0x03,[],sleep=0.01) + self.send_command(0x04,[],sleep=0.01) + return self.x01 + self.x02 + self.x03 + self.x04 + + def get_current_pub_status(self): + time.sleep(0.01) + return self.x01 + self.x02 + self.x03 + self.x04 + + def get_speed(self): + '''Get current motor speed''' + self.send_command(0x05, [0]) + time.sleep(0.001) + return self.x05 + def get_current(self): + '''Get current threshold''' + self.send_command(0x06, [0]) + return self.x06 + def get_torque(self): + '''Get current motor torque, not supported for L20''' + return [0] * 5 + def get_fault(self): + self.send_command(0x07,[]) + time.sleep(0.01) + return self.x07 + + def get_temperature(self): + '''Get motor temperature''' + self.send_command(0x09,[]) + self.send_command(0x0b,[]) + self.send_command(0x0c,[]) + self.send_command(0x0d,[]) + + return self.x09+self.x0b+self.x0c+self.x0d + + def clear_faults(self): + '''Clear motor faults''' + self.send_command(0x07, [1, 1, 1, 1, 1]) + + def get_touch_type(self): + '''Get touch type''' + t = [] + for i in range(3): + self.send_command(0xb0,[],sleep=0.03) + if self.xb0 == [2]: + return 2 + elif self.xb0 == [1]: + return 1 + else: + self.send_command(0x20,[],sleep=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + + def get_touch(self): + '''Get touch data''' + self.send_command(0xb1,[],sleep=0.03) + self.send_command(0xb2,[],sleep=0.03) + self.send_command(0xb3,[],sleep=0.03) + self.send_command(0xb4,[],sleep=0.03) + self.send_command(0xb5,[],sleep=0.03) + return [self.xb1[1],self.xb2[1],self.xb3[1],self.xb4[1],self.xb5[1],0] # The last digit is palm, currently not available + + def get_matrix_touch(self): + self.send_command(0xb1,[0xc6],sleep=0.04) + self.send_command(0xb2,[0xc6],sleep=0.04) + self.send_command(0xb3,[0xc6],sleep=0.04) + self.send_command(0xb4,[0xc6],sleep=0.04) + self.send_command(0xb5,[0xc6],sleep=0.04) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + + def get_thumb_matrix_touch(self,sleep_time=0.009): + self.send_command(0xb1,[0xc6],sleep=sleep_time) + return self.thumb_matrix + + def get_index_matrix_touch(self,sleep_time=0.009): + self.send_command(0xb2,[0xc6],sleep=sleep_time) + return self.index_matrix + + def get_middle_matrix_touch(self,sleep_time=0.009): + self.send_command(0xb3,[0xc6],sleep=sleep_time) + return self.middle_matrix + + def get_ring_matrix_touch(self,sleep_time=0.009): + self.send_command(0xb4,[0xc6],sleep=sleep_time) + return self.ring_matrix + + def get_little_matrix_touch(self,sleep_time=0.009): + self.send_command(0xb5,[0xc6],sleep=sleep_time) + return self.little_matrix + + + def get_faults(self): + '''Get motor fault codes''' + self.send_command(0x07, []) + return self.x07 + def get_force(self): + '''Get pressure sensor data''' + return [self.normal_force,self.tangential_force,self.tangential_force_dir,self.approach_inc] + + def get_serial_number(self): + return [0] * 6 + + def show_fun_table(self): + pass + + def get_finger_order(self): + return [] + + def close_can_interface(self): + if self.bus: + self.bus.shutdown() # Close CAN bus diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l21_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l21_can.py new file mode 100644 index 0000000..66ba97a --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l21_can.py @@ -0,0 +1,822 @@ +#!/usr/bin/env python3 +import can +import time, sys, os +import threading +import numpy as np +from enum import Enum +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError +current_dir = os.path.dirname(os.path.abspath(__file__)) +target_dir = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(target_dir) + +class FrameProperty(Enum): + # Finger motion control - parallel control commands + ROLL_POS = 0x01 # Roll joint position + YAWPOS = 0x02 # Yaw joint position + ROOT1_POS = 0x03 # Root joint 1 position + ROOT2_POS = 0x04 # Root joint 2 position + ROOT3_POS = 0x05 # Root joint 3 position + TIP_POS = 0x06 # Fingertip joint position + # Finger motion control - serial control commands + THUMB_POS = 0x41 # Thumb joint position + INDEX_POS = 0x42 # Index finger joint position + MIDDLE_POS = 0x43 # Middle finger joint position + RING_POS = 0x44 # Ring finger joint position + LITTLE_POS = 0x45 # Little finger joint position + + # Finger motion control - speed + ROLL_SPEED = 0x09 # Roll joint speed + YAW_SPEED = 0x0A # Yaw joint speed + ROOT1_SPEED = 0x0B # Root joint 1 speed + ROOT2_SPEED = 0x0C # Root joint 2 speed + ROOT3_SPEED = 0x0D # Root joint 3 speed + TIP_SPEED = 0x0E # Fingertip joint speed + THUMB_SPEED = 0x49 # Thumb speed + INDEX_SPEED = 0x4A # Index finger speed + MIDDLE_SPEED = 0x4B # Middle finger speed + RING_SPEED = 0x4C # Ring finger speed + LITTLE_SPEED = 0x4D # Little finger speed + + # Finger motion control - torque + ROLL_TORQUE = 0x11 # Roll joint torque + YAW_TORQUE = 0x12 # Yaw joint torque + ROOT1_TORQUE = 0x13 # Root joint 1 torque + ROOT2_TORQUE = 0x14 # Root joint 2 torque + ROOT3_TORQUE = 0x15 # Root joint 3 torque + TIP_TORQUE = 0x16 # Fingertip joint torque + THUMB_TORQUE = 0x51 # Thumb torque + INDEX_TORQUE = 0x52 # Index finger torque + MIDDLE_TORQUE = 0x53 # Middle finger torque + RING_TORQUE = 0x54 # Ring finger torque + LITTLE_TORQUE = 0x55 # Little finger torque + + THUMB_FAULT = 0x59 # Thumb fault code | Returns this type of data + INDEX_FAULT = 0x5A # Index finger fault code | Returns this type of data + MIDDLE_FAULT = 0x5B # Middle finger fault code | Returns this type of data + RING_FAULT = 0x5C # Ring finger fault code | Returns this type of data + LITTLE_FAULT = 0x5D # Little finger fault code | Returns this type of data + + # Finger faults and temperature + ROLL_FAULT = 0x19 # Roll joint fault code + YAW_FAULT = 0x1A # Yaw joint fault code + ROOT1_FAULT = 0x1B # Root joint 1 fault code + ROOT2_FAULT = 0x1C # Root joint 2 fault code + ROOT3_FAULT = 0x1D # Root joint 3 fault code + TIP_FAULT = 0x1E # Fingertip joint fault code + ROLL_TEMPERATURE = 0x21 # Roll joint over-temperature protection threshold + YAW_TEMPERATURE = 0x22 # Yaw joint over-temperature protection threshold + ROOT1_TEMPERATURE = 0x23 # Root joint 1 over-temperature protection threshold + ROOT2_TEMPERATURE = 0x24 # Root joint 2 over-temperature protection threshold + ROOT3_TEMPERATURE = 0x25 # Root joint 3 over-temperature protection threshold + TIP_TEMPERATURE = 0x26 # Fingertip joint over-temperature protection threshold + THUMB_TEMPERATURE = 0x61 # Thumb over-temperature protection threshold + INDEX_TEMPERATURE = 0x62 # Index finger over-temperature protection threshold + MIDDLE_TEMPERATURE = 0x63 # Middle finger over-temperature protection threshold + RING_TEMPERATURE = 0x64 # Ring finger over-temperature protection threshold + LITTLE_TEMPERATURE = 0x65 # Little finger over-temperature protection threshold + + # Configuration and preset actions + HAND_UID = 0xC0 # Device unique identifier + HAND_HARDWARE_VERSION = 0xC1 # Hardware version + HAND_SOFTWARE_VERSION = 0xC2 # Software version + HAND_COMM_ID = 0xC3 # Device ID + HAND_FACTORY_RESET = 0xCE # Restore factory settings + HAND_SAVE_PARAMETER = 0xCF # Save parameters + + # Tactile sensor data + HAND_NORMAL_FORCE = 0x90 # Normal force of five fingers + HAND_TANGENTIAL_FORCE = 0x91 # Tangential force of five fingers + HAND_TANGENTIAL_FORCE_DIR = 0x92 # Tangential direction of five fingers + HAND_APPROACH_INC = 0x93 # Approach sensing of five fingers + + TOUCH_SENSOR_TYPE = 0xB0 # Sensor type + THUMB_TOUCH = 0xB1 # Thumb tactile sensing + INDEX_TOUCH = 0xB2 # Index finger tactile sensing + MIDDLE_TOUCH = 0xB3 # Middle finger tactile sensing + RING_TOUCH = 0xB4 # Ring finger tactile sensing + LITTLE_TOUCH = 0xB5 # Little finger tactile sensing + PALM_TOUCH = 0xB6 # Palm tactile sensing + + # Action control + ACTION_PLAY = 0xA0 # Action + + # Combined command area + FINGER_SPEED = 0x81 # Set maximum finger speed + FINGER_TORQUE = 0x82 # Set maximum finger torque + FINGER_FAULT = 0x83 # Clear finger faults and fault codes + FINGER_TEMPERATURE = 0x84 # Finger joint temperatures + +class LinkerHandL21Can: + def __init__(self, can_channel='can0', baudrate=1000000, can_id=0x28,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.running = True + self.last_thumb_pos, self.last_index_pos,self.last_ring_pos,self.last_middle_pos, self.last_little_pos = None,None,None,None,None + self.x01, self.x02, self.x03, self.x04,self.x05,self.x06,self.x07, self.x08,self.x09,self.x0A,self.x0B,self.x0C,self.x0D,self.x0E,self.speed = [],[],[],[],[],[],[],[],[],[],[],[],[],[],[] + self.last_root1,self.last_yaw,self.last_roll,self.last_root2,self.last_tip = None,None,None,None,None + # Speed + self.x49, self.x4a, self.x4b, self.x4c, self.x4d,self.xc1 = [],[],[],[],[],[] + self.x41,self.x42,self.x43,self.x44,self.x45 = [],[],[],[],[] + self.x83 = [-1] * 5 + # Torque + self.x51, self.x52, self.x53, self.x54,self.x55 = [],[],[],[],[] + # Fault codes + self.x59,self.x5a,self.x5b,self.x5c,self.x5d = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + # Temperature thresholds + self.x61,self.x62,self.x63,self.x64,self.x65 = [],[],[],[],[] + # Pressure sensors + self.x90,self.x91,self.x92,self.x93 = [],[],[],[] + # New pressure sensors + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5,self.xb6 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + # Initialize CAN bus according to operating system + # try: + # if sys.platform == "linux": + # self.open_can.open_can(self.can_channel) + # time.sleep(0.1) + # self.bus = can.interface.Bus( + # channel=can_channel, interface="socketcan", bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # elif sys.platform == "win32": + # self.bus = can.interface.Bus( + # channel=can_channel, interface='pcan', bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # else: + # raise EnvironmentError("Unsupported platform for CAN interface") + # except: + # print("Please insert CAN device") + self.bus = self.init_can_bus(channel=self.can_channel, baudrate=baudrate) + + # Start receive thread + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_command(self, frame_property, data_list,sleep_time=0.003): + """ + Send command to CAN bus + :param frame_property: Data frame property + :param data_list: Data payload + """ + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(sleep_time) + + def receive_response(self): + """ + Receive and process CAN bus response messages + """ + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving message: {e}") + + + def set_joint_positions(self, joint_ranges): + if len(joint_ranges) == 25: + l21_pose = self.joint_map(joint_ranges) + # Use list comprehension to split the list into subarrays of 6 elements each + chunks = [l21_pose[i:i+6] for i in range(0, 30, 6)] + for i in range(3): + self.send_command(FrameProperty.THUMB_POS, chunks[0]) + time.sleep(0.001) + self.send_command(FrameProperty.INDEX_POS, chunks[1]) + time.sleep(0.001) + self.send_command(FrameProperty.MIDDLE_POS, chunks[2]) + time.sleep(0.001) + self.send_command(FrameProperty.RING_POS, chunks[3]) + time.sleep(0.001) + self.send_command(FrameProperty.LITTLE_POS, chunks[4]) + time.sleep(0.001) + + def set_joint_positions_by_topic(self, joint_ranges): + if len(joint_ranges) == 25: + l21_pose = self.slice_list(joint_ranges,5) + if self._list_d_value(self.last_root1, l21_pose[0]): + self.set_root1_positions(l21_pose[0]) + self.last_root1 = l21_pose[0] + if self._list_d_value(self.last_yaw, l21_pose[1]): + self.set_yaw_positions(l21_pose[1]) + self.last_yaw = l21_pose[1] + if self._list_d_value(self.last_roll, l21_pose[2]): + self.set_roll_positions(l21_pose[2]) + self.last_roll = l21_pose[2] + if self._list_d_value(self.last_root2, l21_pose[3]): + self.set_root2_positions(l21_pose[3]) + self.last_root2 = l21_pose[3] + if self._list_d_value(self.last_tip, l21_pose[4]): + self.set_tip_positions(l21_pose[4]) + self.last_tip = l21_pose[4] + + + def slice_list(self, input_list, slice_size): + """ + Slice a list into pieces of specified size. + + Args: + input_list (list): The list to be sliced. + slice_size (int): Number of elements per slice. + + Returns: + list of lists: The sliced list. + """ + sliced_list = [input_list[i:i + slice_size] for i in range(0, len(input_list), slice_size)] + return sliced_list + + def _list_d_value(self,list1, list2): + if list1 == None: + return True + for a, b in zip(list1, list2): + if abs(b - a) > 2: + return True + break + return False + # Set all finger roll joint positions + def set_roll_positions(self, joint_ranges): + self.send_command(FrameProperty.ROLL_POS, joint_ranges) + # Set all finger yaw joint positions + def set_yaw_positions(self, joint_ranges): + self.send_command(FrameProperty.YAW_POS, joint_ranges) + # Set all finger root1 joint positions + def set_root1_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT1_POS, joint_ranges) + # Set all finger root2 joint positions + def set_root2_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT2_POS, joint_ranges) + # Set all finger root3 joint positions + def set_root3_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT3_POS, joint_ranges) + # Set all finger tip joint positions + def set_tip_positions(self, joint_ranges=[80]*5): + self.send_command(FrameProperty.TIP_POS, joint_ranges) + # Set thumb torque + def set_thumb_torque(self, j=[]): + self.send_command(FrameProperty.THUMB_TORQUE, j) + # Set index finger torque + def set_index_torque(self, j=[]): + self.send_command(FrameProperty.INDEX_TORQUE, j) + # Set middle finger torque + def set_middle_torque(self, j=[]): + self.send_command(FrameProperty.MIDDLE_TORQUE, j) + # Set ring finger torque + def set_ring_torque(self, j=[]): + self.send_command(FrameProperty.RING_TORQUE, j) + # Set little finger torque + def set_little_torque(self, j=[]): + self.send_command(FrameProperty.LITTLE_TORQUE, j) + + # Get thumb joint positions + def get_thumb_positions(self,j=[0]): + self.send_command(FrameProperty.THUMB_POS, j) + # Get index finger joint positions + def get_index_positions(self, j=[0]): + self.send_command(FrameProperty.INDEX_POS,j) + # Get middle finger joint positions + def get_middle_positions(self, j=[0]): + self.send_command(FrameProperty.MIDDLE_POS,j) + # Get ring finger joint positions + def get_ring_positions(self, j=[0]): + self.send_command(FrameProperty.RING_POS,j) + # Get little finger joint positions + def get_little_positions(self, j=[0]): + self.send_command(FrameProperty.LITTLE_POS, j) + # Get all thumb motor fault codes + def get_thumbn_fault(self,j=[]): + self.send_command(FrameProperty.THUMB_FAULT,j) + # Get all index finger motor fault codes + def get_index_fault(self,j=[]): + self.send_command(FrameProperty.INDEX_FAULT,j) + # Get all middle finger motor fault codes + def get_middle_fault(self,j=[]): + self.send_command(FrameProperty.MIDDLE_FAULT,j) + # Get all ring finger motor fault codes + def get_ring_fault(self,j=[]): + self.send_command(FrameProperty.RING_FAULT,j) + # Get all little finger motor fault codes + def get_little_fault(self,j=[]): + self.send_command(FrameProperty.LITTLE_FAULT,j) + # Get thumb temperature threshold + def get_thumb_threshold(self,j=[]): + self.send_command(FrameProperty.THUMB_TEMPERATURE, '') + # Get index finger temperature threshold + def get_index_threshold(self,j=[]): + self.send_command(FrameProperty.INDEX_TEMPERATURE, j) + # Get middle finger temperature threshold + def get_middle_threshold(self,j=[]): + self.send_command(FrameProperty.MIDDLE_TEMPERATURE, j) + # Get ring finger temperature threshold + def get_ring_threshold(self,j=[]): + self.send_command(FrameProperty.RING_TEMPERATURE, j) + # Get little finger temperature threshold + def get_little_threshold(self,j=[]): + self.send_command(FrameProperty.LITTLE_TEMPERATURE, j) + + # Disable mode 01 + def set_disability_mode(self, j=[1,1,1,1,1]): + self.send_command(0x85,j) + # Enable mode 00 + def set_enable_mode(self, j=[00,00,00,00,00]): + self.send_command(0x85,j) + + # Set all finger torques + def set_torque(self,torque=[250]*5): + t = torque[0] + i = torque[1] + m = torque[2] + r = torque[3] + l = torque[4] + self.set_thumb_torque(j=[t]*5) + self.set_index_torque(j=[i]*5) + self.set_middle_torque(j=[m]*5) + self.set_ring_torque(j=[r]*5) + self.set_little_torque(j=[l]*5) + + def set_speed(self, speed): + self.speed = speed + if len(speed) < 25: + thumb_speed = [self.speed[0]]*5 + index_speed = [self.speed[1]]*5 + middle_speed = [self.speed[2]]*5 + ring_speed = [self.speed[3]]*5 + little_speed = [self.speed[4]]*5 + else: + thumb_speed = [self.speed[0],self.speed[1],self.speed[2],self.speed[3],self.speed[4]] + index_speed = [self.speed[5],self.speed[6],self.speed[7],self.speed[8],self.speed[9]] + middle_speed = [self.speed[10],self.speed[11],self.speed[12],self.speed[13],self.speed[14]] + ring_speed = [self.speed[15],self.speed[16],self.speed[17],self.speed[18],self.speed[19]] + little_speed = [self.speed[20],self.speed[21],self.speed[22],self.speed[23],self.speed[24]] + self.send_command(FrameProperty.THUMB_SPEED, thumb_speed) + self.send_command(FrameProperty.INDEX_SPEED, index_speed) + self.send_command(FrameProperty.MIDDLE_SPEED, middle_speed) + self.send_command(FrameProperty.RING_SPEED, ring_speed) + self.send_command(FrameProperty.LITTLE_SPEED, little_speed) + + def set_finger_torque(self, torque): + self.send_command(0x42, torque) + + def request_device_info(self): + self.send_command(0xC0, [0]) + self.send_command(0xC1, [0]) + self.send_command(0xC2, [0]) + + def save_parameters(self): + self.send_command(0xCF, []) + def process_response(self, msg): + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == 0x01: + self.x01 = list(response_data) + elif frame_type == 0x02: + self.x02 = list(response_data) + elif frame_type == 0x03: + self.x03 = list(response_data) + elif frame_type == 0x04: + self.x04 = list(response_data) + elif frame_type == 0x05: + self.x05 = list(response_data) + elif frame_type == 0x06: + self.x06 = list(response_data) + elif frame_type == 0xC0: + print(f"Device ID info: {response_data}") + if self.can_id == 0x28: + self.right_hand_info = response_data + elif self.can_id == 0x27: + self.left_hand_info = response_data + elif frame_type == 0x08: + self.x08 = list(response_data) + elif frame_type == 0x09: + self.x09 = list(response_data) + elif frame_type == 0x0A: + self.x0A = list(response_data) + elif frame_type == 0x0B: + self.x0B = list(response_data) + elif frame_type == 0x0C: + self.x0C = list(response_data) + elif frame_type == 0x0D: + self.x0D = list(response_data) + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x41: + self.x41 = list(response_data) + elif frame_type == 0x42: + self.x42 = list(response_data) + elif frame_type == 0x43: + self.x43 = list(response_data) + elif frame_type == 0x44: + self.x44 = list(response_data) + elif frame_type == 0x45: + self.x45 = list(response_data) + elif frame_type == 0x49: + self.x49 = list(response_data) + elif frame_type == 0x4a: + self.x4a = list(response_data) + elif frame_type == 0x4b: + self.x4b = list(response_data) + elif frame_type == 0x4c: + self.x4c = list(response_data) + elif frame_type == 0x4d: + self.x4d = list(response_data) + elif frame_type == 0xc1: + self.xc1 = list(response_data) + elif frame_type == 0x51: + self.x51 = list(response_data) + elif frame_type == 0x52: + self.x52 = list(response_data) + elif frame_type == 0x53: + self.x53 = list(response_data) + elif frame_type == 0x54: + self.x54 = list(response_data) + elif frame_type == 0x55: + self.x55 = list(response_data) + elif frame_type == 0x59: + self.x59 = list(response_data) + elif frame_type == 0x5a: + self.x5a = list(response_data) + elif frame_type == 0x5b: + self.x5b = list(response_data) + elif frame_type == 0x5c: + self.x5c = list(response_data) + elif frame_type == 0x5d: + self.x5d = list(response_data) + elif frame_type == 0x61: + self.x61 = list(response_data) + elif frame_type == 0x62: + self.x62 = list(response_data) + elif frame_type == 0x63: + self.x63 = list(response_data) + elif frame_type == 0x64: + self.x64 = list(response_data) + elif frame_type == 0x65: + self.x65 = list(response_data) + elif frame_type == 0x83: + self.x83 = list(response_data) + elif frame_type == 0x90: + self.x90 = list(response_data) + elif frame_type == 0x91: + self.x91 = list(response_data) + elif frame_type == 0x92: + self.x92 = list(response_data) + elif frame_type == 0x93: + self.x93 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] + elif frame_type == 0xb6: + self.xb6 = list(response_data) + + def joint_map(self, pose): + # l21 CAN data by default receives 30 data + l21_pose = [0.0] * 30 + + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 4: None, 5: 20, + 6: None, 7: 6, 8: 1, 9: 16, 10: None, 11: 21, + 12: None, 13: 7, 14: 2, 15: 17, 16: None, 17: 22, + 18: None, 19: 8, 20: 3, 21: 18, 22: None, 23: 23, + 24: None, 25: 9, 26: 4, 27: 19, 28: None, 29: 24 + } + + for l21_idx, pose_idx in mapping.items(): + if pose_idx is not None: + l21_pose[l21_idx] = pose[pose_idx] + + return l21_pose + + def state_to_cmd(self, l21_state): + pose = [0.0] * 25 + + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 5: 20, 7: 6, + 8: 1, 9: 16, 11: 21, 13:7, 14: 2, 15: 17, 17: 22, + 19: 8, 20: 3, 21: 18, 23: 23, 25: 9, 26: 4, + 27: 19, 29: 24 + } + for l21_idx, pose_idx in mapping.items(): + pose[pose_idx] = l21_state[l21_idx] + return pose + def action_play(self): + self.send_command(0xA0,[]) + def get_current_status(self, j=''): + self.send_command(FrameProperty.THUMB_POS, j,sleep_time=0.001) + self.send_command(FrameProperty.INDEX_POS,j,sleep_time=0.001) + self.send_command(FrameProperty.MIDDLE_POS,j,sleep_time=0.001) + self.send_command(FrameProperty.RING_POS,j,sleep_time=0.001) + self.send_command(FrameProperty.LITTLE_POS, j,sleep_time=0.001) + state= self.x41+ self.x42+ self.x43+ self.x44+ self.x45 + if len(state) == 30: + l21_state = self.state_to_cmd(l21_state=state) + return l21_state + + def get_current_pub_status(self): + state= self.x41+ self.x42+ self.x43+ self.x44+ self.x45 + if len(state) == 30: + l21_state = self.state_to_cmd(l21_state=state) + return l21_state + + def get_current_state_topic(self): + self.send_command(0x01,[]) + self.send_command(0x02,[]) + self.send_command(0x03,[]) + self.send_command(0x04,[]) + self.send_command(0x06,[]) + state = self.x03+self.x02+self.x01+self.x04+self.x06 + return state + + def get_speed(self,j=''): + self.send_command(FrameProperty.THUMB_SPEED, j) + self.send_command(FrameProperty.INDEX_SPEED, j) + self.send_command(FrameProperty.MIDDLE_SPEED, j) + self.send_command(FrameProperty.RING_SPEED, j) + self.send_command(FrameProperty.LITTLE_SPEED, j) + speed = self.x49+ self.x4a+ self.x4b+ self.x4c+ self.x4d + if len(speed) == 30: + l21_speed = self.state_to_cmd(l21_state=speed) + return l21_speed + + # def get_finger_torque(self): + # return self.finger_torque() + def get_fault(self): + self.get_thumbn_fault() + self.get_index_fault() + self.get_middle_fault() + self.get_ring_fault() + self.get_little_fault() + return [self.x59]+[self.x5a]+[self.x5b]+[self.x5c]+[self.x5d] + def get_threshold(self): + self.get_thumb_threshold() + self.get_index_threshold() + self.get_middle_threshold() + self.get_ring_threshold() + self.get_little_threshold() + return [self.x61]+[self.x62]+[self.x63]+[self.x64]+[self.x65] + def get_version(self): + if self.xc1 == []: + self.send_command(FrameProperty.HAND_HARDWARE_VERSION,[]) + return self.xc1 + def get_normal_force(self): + self.send_command(FrameProperty.HAND_NORMAL_FORCE,[]) + return self.x90 + def get_tangential_force(self): + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE,[]) + return self.x91 + def get_tangential_force_dir(self): + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE_DIR,[]) + return self.x92 + def get_approach_inc(self): + self.send_command(FrameProperty.HAND_APPROACH_INC,[]) + return self.x93 + + def get_touch_type(self): + '''Get tactile sensor type data''' + self.send_command(FrameProperty.TOUCH_SENSOR_TYPE,[]) + try: + return self.xb0[0] + except: + pass + def get_finger_torque(self): + self.send_command(FrameProperty.THUMB_TORQUE,[]) + self.send_command(FrameProperty.INDEX_TORQUE,[]) + self.send_command(FrameProperty.MIDDLE_TORQUE,[]) + self.send_command(FrameProperty.RING_TORQUE,[]) + self.send_command(FrameProperty.LITTLE_TORQUE,[]) + return self.x51+self.x52+self.x53+self.x54+self.x55 + + def get_torque(self): + return self.get_finger_torque() + + def get_thumb_touch(self): + '''Get thumb tactile sensor data''' + self.send_command(FrameProperty.THUMB_TOUCH,[],sleep_time=0.015) + return self.xb1 + + def get_index_touch(self): + '''Get index finger tactile sensor data''' + self.send_command(FrameProperty.INDEX_TOUCH,[0xc6],sleep_time=0.015) + return self.xb2 + + def get_middle_touch(self): + '''Get middle finger tactile sensor data''' + self.send_command(FrameProperty.MIDDLE_TOUCH,[],sleep_time=0.015) + return self.xb3 + + def get_ring_touch(self): + '''Get ring finger tactile sensor data''' + self.send_command(FrameProperty.RING_TOUCH,[],sleep_time=0.015) + return self.xb4 + + def get_little_touch(self): + '''Get little finger tactile sensor data''' + self.send_command(FrameProperty.LITTLE_TOUCH,[],sleep_time=0.015) + return self.xb5 + + def get_palm_touch(self): + '''Get palm tactile sensor data''' + self.send_command(FrameProperty.PALM_TOUCH,[],sleep_time=0.015) + return self.xb6 + + def get_force(self): + '''Get pressure sensor data''' + return [self.x90,self.x91 , self.x92 , self.x93] + + def get_touch(self): + '''Get tactile sensor data''' + self.get_thumb_touch() + self.get_index_touch() + self.get_middle_touch() + self.get_ring_touch() + self.get_little_touch() + self.get_palm_touch() + try: + return [self.xb1[1],self.xb2[1] , self.xb3[1] , self.xb4[1],self.xb5[1],self.xb6[1]] + except: + pass + + def get_matrix_touch(self): + self.send_command(0xb1,[0xc6],sleep_time=0.04) + self.send_command(0xb2,[0xc6],sleep_time=0.04) + self.send_command(0xb3,[0xc6],sleep_time=0.04) + self.send_command(0xb4,[0xc6],sleep_time=0.04) + self.send_command(0xb5,[0xc6],sleep_time=0.04) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_current(self): + '''Not supported yet''' + return [0] * 21 + def get_temperature(self): + self.get_thumb_threshold() + self.get_index_threshold() + self.get_middle_threshold() + self.get_ring_threshold() + self.get_little_threshold() + return self.x61+self.x62+self.x63+self.x64+self.x65 + + def get_serial_number(self): + return [0] * 6 + + def get_finger_order(self): + return [ + "thumb_root", + "index_finger_root", + "middle_finger_root", + "ring_finger_root", + "little_finger_root", + "thumb_abduction", + "index_finger_abduction", + "middle_finger_abduction", + "ring_finger_abduction", + "little_finger_abduction", + "thumb_roll", + "reserved", + "reserved", + "reserved", + "reserved", + "thumb_middle_joint", + "reserved", + "reserved", + "reserved", + "reserved", + "thumb_tip", + "index_finger_tip", + "middle_finger_tip", + "ring_finger_tip", + "little_finger_tip" + ] + + def clear_faults(self): + '''Clear motor faults''' + self.send_command(0x83, [1, 1, 1, 1, 1],sleep_time=0.003) + return self.x83 + + def close_can_interface(self): + if self.bus: + self.bus.shutdown() # Close CAN bus diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l24_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l24_can.py new file mode 100644 index 0000000..d5192f6 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l24_can.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +import can +import time,sys,os +import threading +import numpy as np +from enum import Enum +current_dir = os.path.dirname(os.path.abspath(__file__)) +target_dir = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(target_dir) +from utils.color_msg import ColorMsg + +class FrameProperty(Enum): + INVALID_FRAME_PROPERTY = 0x00 # 无效的can帧属性 | 无返回 + # 并行指令区域 + ROLL_POS = 0x01 # 横滚关节位置 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + YAW_POS = 0x02 # 航向关节位置 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + ROOT1_POS = 0x03 # 指根1关节位置 | 最接近手掌的指根关节 + ROOT2_POS = 0x04 # 指根2关节位置 | 最接近手掌的指根关节 + ROOT3_POS = 0x05 # 指根3关节位置 | 最接近手掌的指根关节 + TIP_POS = 0x06 # 指尖关节位置 | 最接近手掌的指根关节 + + ROLL_SPEED = 0x09 # 横滚关节速度 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + YAW_SPEED = 0x0A # 航向关节速度 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + ROOT1_SPEED = 0x0B # 指根1关节速度 | 最接近手掌的指根关节 + ROOT2_SPEED = 0x0C # 指根2关节速度 | 最接近手掌的指根关节 + ROOT3_SPEED = 0x0D # 指根3关节速度 | 最接近手掌的指根关节 + TIP_SPEED = 0x0E # 指尖关节速度 | 最接近手掌的指根关节 + + ROLL_TORQUE = 0x11 # 横滚关节扭矩 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + YAW_TORQUE = 0x12 # 航向关节扭矩 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + ROOT1_TORQUE = 0x13 # 指根1关节扭矩 | 最接近手掌的指根关节 + ROOT2_TORQUE = 0x14 # 指根2关节扭矩 | 最接近手掌的指根关节 + ROOT3_TORQUE = 0x15 # 指根3关节扭矩 | 最接近手掌的指根关节 + TIP_TORQUE = 0x16 # 指尖关节扭矩 | 最接近手掌的指根关节 + + ROLL_FAULT = 0x19 # 横滚关节故障码 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + YAW_FAULT = 0x1A # 航向关节故障码 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + ROOT1_FAULT = 0x1B # 指根1关节故障码 | 最接近手掌的指根关节 + ROOT2_FAULT = 0x1C # 指根2关节故障码 | 最接近手掌的指根关节 + ROOT3_FAULT = 0x1D # 指根3关节故障码 | 最接近手掌的指根关节 + TIP_FAULT = 0x1E # 指尖关节故障码 | 最接近手掌的指根关节 + + ROLL_TEMPERATURE = 0x21 # 横滚关节温度 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + YAW_TEMPERATURE = 0x22 # 航向关节温度 | 坐标系建在每个手指的指根部位,按手指伸直的状态去定义旋转角度 + ROOT1_TEMPERATURE = 0x23 # 指根1关节温度 | 最接近手掌的指根关节 + ROOT2_TEMPERATURE = 0x24 # 指根2关节温度 | 最接近手掌的指根关节 + ROOT3_TEMPERATURE = 0x25 # 指根3关节温度 | 最接近手掌的指根关节 + TIP_TEMPERATURE = 0x26 # 指尖关节温度 | 最接近手掌的指根关节 + # 并行指令区域 + + # 串行指令区域 + THUMB_POS = 0x41 # 大拇指指关节位置 | 返回本类型数据 + INDEX_POS = 0x42 # 食指关节位置 | 返回本类型数据 + MIDDLE_POS = 0x43 # 中指关节位置 | 返回本类型数据 + RING_POS = 0x44 # 无名指关节位置 | 返回本类型数据 + LITTLE_POS = 0x45 # 小拇指关节位置 | 返回本类型数据 + + THUMB_SPEED = 0x49 # 大拇指速度 | 返回本类型数据 + INDEX_SPEED = 0x4A # 食指速度 | 返回本类型数据 + MIDDLE_SPEED = 0x4B # 中指速度 | 返回本类型数据 + RING_SPEED = 0x4C # 无名指速度 | 返回本类型数据 + LITTLE_SPEED = 0x4D # 小拇指速度 | 返回本类型数据 + + THUMB_TORQUE = 0x51 # 大拇指扭矩 | 返回本类型数据 + INDEX_TORQUE = 0x52 # 食指扭矩 | 返回本类型数据 + MIDDLE_TORQUE = 0x53 # 中指扭矩 | 返回本类型数据 + RING_TORQUE = 0x54 # 无名指扭矩 | 返回本类型数据 + LITTLE_TORQUE = 0x55 # 小拇指扭矩 | 返回本类型数据 + + THUMB_FAULT = 0x59 # 大拇指故障码 | 返回本类型数据 + INDEX_FAULT = 0x5A # 食指故障码 | 返回本类型数据 + MIDDLE_FAULT = 0x5B # 中指故障码 | 返回本类型数据 + RING_FAULT = 0x5C # 无名指故障码 | 返回本类型数据 + LITTLE_FAULT = 0x5D # 小拇指故障码 | 返回本类型数据 + + THUMB_TEMPERATURE = 0x61 # 大拇指温度 | 返回本类型数据 + INDEX_TEMPERATURE = 0x62 # 食指温度 | 返回本类型数据 + MIDDLE_TEMPERATURE = 0x63 # 中指温度 | 返回本类型数据 + RING_TEMPERATURE = 0x64 # 无名指温度 | 返回本类型数据 + LITTLE_TEMPERATURE = 0x65 # 小拇指温度 | 返回本类型数据 + # 串行指令区域 + + # 合并指令区域,同一手指非必要单控数据合并 + FINGER_SPEED = 0x81 # 手指速度 | 返回本类型数据 + FINGER_TORQUE = 0x82 # 转矩 | 返回本类型数据 + FINGER_FAULT = 0x83 # 手指故障码 | 返回本类型数据 + + # 指尖传感器数据组 + HAND_NORMAL_FORCE = 0x90 # 五指法向压力 + HAND_TANGENTIAL_FORCE = 0x91 # 五指切向压力 + HAND_TANGENTIAL_FORCE_DIR = 0x92 # 五指切向方向 + HAND_APPROACH_INC = 0x93 # 五指接近感应 + + THUMB_ALL_DATA = 0x98 # 大拇指所有数据 + INDEX_ALL_DATA = 0x99 # 食指所有数据 + MIDDLE_ALL_DATA = 0x9A # 中指所有数据 + RING_ALL_DATA = 0x9B # 无名指所有数据 + LITTLE_ALL_DATA = 0x9C # 小拇指所有数据 + # 动作指令 ·ACTION + ACTION_PLAY = 0xA0 # 动作 + + # 配置命令·CONFIG + HAND_UID = 0xC0 # 设备唯一标识码 + HAND_HARDWARE_VERSION = 0xC1 # 硬件版本 + HAND_SOFTWARE_VERSION = 0xC2 # 软件版本 + HAND_COMM_ID = 0xC3 # 设备id + HAND_FACTORY_RESET = 0xCE # 恢复出厂设置 + HAND_SAVE_PARAMETER = 0xCF # 保存参数 + + WHOLE_FRAME = 0xF0 # 整帧传输 | 返回一字节帧属性+整个结构体485及网络传输专属 + +class LinkerHandL24Can: + def __init__(self, config, can_channel='can0', baudrate=1000000, can_id=0x28): + self.config = config + self.can_id = can_id + self.running = True + self.x01, self.x02, self.x03, self.x04,self.x05,self.x06,self.x07, self.x08,self.x09,self.x0A,self.x0B,self.x0C,self.x0D,self.x0E,self.speed = [],[],[],[],[],[],[],[],[],[],[],[],[],[],[] + # 速度 + self.x49, self.x4a, self.x4b, self.x4c, self.x4d = [],[],[],[],[] + self.x41,self.x42,self.x43,self.x44,self.x45 = [],[],[],[],[] + # 根据操作系统初始化 CAN 总线 + if sys.platform == "linux": + self.bus = can.interface.Bus( + channel=can_channel, interface="socketcan", bitrate=baudrate, + can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + ) + elif sys.platform == "win32": + self.bus = can.interface.Bus( + channel=can_channel, interface='pcan', bitrate=baudrate, + can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + ) + else: + raise EnvironmentError("Unsupported platform for CAN interface") + + # 根据 can_id 初始化 publisher 和相关参数 + if can_id == 0x28: # 左手 + self.hand_exists = config['LINKER_HAND']['LEFT_HAND']['EXISTS'] + self.hand_joint = config['LINKER_HAND']['LEFT_HAND']['JOINT'] + self.hand_names = config['LINKER_HAND']['LEFT_HAND']['NAME'] + elif can_id == 0x27: # 右手 + + self.hand_exists = config['LINKER_HAND']['RIGHT_HAND']['EXISTS'] + self.hand_joint = config['LINKER_HAND']['RIGHT_HAND']['JOINT'] + self.hand_names = config['LINKER_HAND']['RIGHT_HAND']['NAME'] + + + # 启动接收线程 + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def send_command(self, frame_property, data_list): + """ + 发送命令到 CAN 总线 + :param frame_property: 数据帧属性 + :param data_list: 数据载荷 + """ + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + #print(f"Message sent: ID={hex(self.can_id)}, Data={data}") + except can.CanError as e: + print(f"Failed to send message: {e}") + time.sleep(0.002) + + def receive_response(self): + """ + 接收并处理 CAN 总线的响应消息 + """ + while self.running: + try: + msg = self.bus.recv(timeout=1.0) # 阻塞接收,1 秒超时 + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving message: {e}") + + + def set_joint_positions(self, joint_ranges): + if len(joint_ranges) == 25: + l24_pose = self.joint_map(joint_ranges) + # 使用列表推导式将列表每6个元素切成一个子数组 + chunks = [l24_pose[i:i+6] for i in range(0, 30, 6)] + self.send_command(FrameProperty.THUMB_POS, chunks[0]) + self.send_command(FrameProperty.INDEX_POS, chunks[1]) + self.send_command(FrameProperty.MIDDLE_POS, chunks[2]) + self.send_command(FrameProperty.RING_POS, chunks[3]) + self.send_command(FrameProperty.LITTLE_POS, chunks[4]) + #self.set_tip_positions(joint_ranges[:5]) + #print(l24_pose) + + # 设置所有手指横滚关节位置 + def set_roll_positions(self, joint_ranges): + self.send_command(FrameProperty.ROLL_POS, joint_ranges) + # 设置所有手指航向关节位置 + def set_yaw_positions(self, joint_ranges): + self.send_command(FrameProperty.YAW_POS, joint_ranges) + # 设置所有手指指根1关节位置 + def set_root1_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT1_POS, joint_ranges) + # 设置所有手指指根2关节位置 + def set_root2_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT2_POS, joint_ranges) + # 设置所有手指指根3关节位置 + def set_root3_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT3_POS, joint_ranges) + # 设置所有手指指尖关节位置 + def set_tip_positions(self, joint_ranges=[80]*5): + self.send_command(FrameProperty.TIP_POS, joint_ranges) + # 获取大拇指指关节位置 + def get_thumb_positions(self,j=[0]): + self.send_command(FrameProperty.THUMB_POS, j) + # 获取食指关节位置 + def get_index_positions(self, j=[0]): + self.send_command(FrameProperty.INDEX_POS,j) + # 获取中指关节位置 + def get_middle_positions(self, j=[0]): + self.send_command(FrameProperty.MIDDLE_POS,j) + # 获取无名指关节位置 + def get_ring_positions(self, j=[0]): + self.send_command(FrameProperty.RING_POS,j) + # 获取小拇指关节位置 + def get_little_positions(self, j=[0]): + self.send_command(FrameProperty.LITTLE_POS, j) + # 失能01模式 + def set_disability_mode(self, j=[1,1,1,1,1]): + self.send_command(0x85,j) + # 使能00模式 + def set_enable_mode(self, j=[00,00,00,00,00]): + self.send_command(0x85,j) + + + def set_speed(self, speed): + self.speed = [speed]*6 + ColorMsg(msg=f"L24设置速度为:{self.speed}", color="yellow") + self.send_command(FrameProperty.THUMB_SPEED, self.speed) + self.send_command(FrameProperty.INDEX_SPEED, self.speed) + self.send_command(FrameProperty.MIDDLE_SPEED, self.speed) + self.send_command(FrameProperty.RING_SPEED, self.speed) + self.send_command(FrameProperty.LITTLE_SPEED, self.speed) + + def set_finger_torque(self, torque): + self.send_command(0x42, torque) + + def request_device_info(self): + self.send_command(0xC0, [0]) + self.send_command(0xC1, [0]) + self.send_command(0xC2, [0]) + + def save_parameters(self): + self.send_command(0xCF, []) + def process_response(self, msg): + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if frame_type == 0x01: + self.x01 = list(response_data) + elif frame_type == 0x02: + self.x02 = list(response_data) + elif frame_type == 0x03: + self.x03 = list(response_data) + elif frame_type == 0x04: + self.x04 = list(response_data) + elif frame_type == 0x05: + self.x05 = list(response_data) + elif frame_type == 0x06: + self.x06 = list(response_data) + print("_-"*20) + print(self.x06) + elif frame_type == 0xC0: + print(f"Device ID info: {response_data}") + if self.can_id == 0x28: + self.right_hand_info = response_data + elif self.can_id == 0x27: + self.left_hand_info = response_data + elif frame_type == 0x08: + self.x08 = list(response_data) + elif frame_type == 0x09: + self.x09 = list(response_data) + elif frame_type == 0x0A: + self.x0A = list(response_data) + elif frame_type == 0x0B: + self.x0B = list(response_data) + elif frame_type == 0x0C: + self.x0C = list(response_data) + elif frame_type == 0x0D: + self.x0D = list(response_data) + elif frame_type == 0x22: + #ColorMsg(msg=f"五指切向压力方向:{list(response_data)}") + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + #ColorMsg(msg=f"五指接近度:{list(response_data)}") + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x41: # 拇指关节位置返回值 + self.x41 = list(response_data) + elif frame_type == 0x42: # 食指关节位置返回值 + self.x42 = list(response_data) + elif frame_type == 0x43: # 中指关节位置返回值 + self.x43 = list(response_data) + elif frame_type == 0x44: # 无名指关节位置返回值 + self.x44 = list(response_data) + elif frame_type == 0x45: # 小拇指关节位置返回值 + self.x45 = list(response_data) + elif frame_type == 0x49: # 拇指速度返回值 + self.x49 = list(response_data) + elif frame_type == 0x4a: # 食指速度返回值 + self.x4a = list(response_data) + elif frame_type == 0x4b: # 中指速度返回值 + self.x4b = list(response_data) + elif frame_type == 0x4c: # 无名指速度返回值 + self.x4c = list(response_data) + elif frame_type == 0x4d: # 小拇指速度返回值 + self.x4d = list(response_data) + + # topic映射L24 + def joint_map(self, pose): + # L24 CAN数据默认接收30个数据 + l24_pose = [0.0] * 30 # 初始化l24_pose为30个0.0 + + # 映射表,通过字典简化映射关系 + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 4: None, 5: 20, + 6: None, 7: 6, 8: 1, 9: 16, 10: None, 11: 21, + 12: None, 13: None, 14: 2, 15: 17, 16: None, 17: 22, + 18: None, 19: 8, 20: 3, 21: 18, 22: None, 23: 23, + 24: None, 25: 9, 26: 4, 27: 19, 28: None, 29: 24 + } + + # 遍历映射字典,进行值的映射 + for l24_idx, pose_idx in mapping.items(): + if pose_idx is not None: + l24_pose[l24_idx] = pose[pose_idx] + + return l24_pose + + # 将L24的状态值转换为CMD格式的状态值 + def state_to_cmd(self, l24_state): + # L24 CAN默认接收30个数据,初始化pose为25个0.0 + pose = [0.0] * 25 # 原来控制L24的指令数据为25个 + + # 映射关系,字典中存储l24_state索引和pose索引之间的映射关系 + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 5: 20, 7: 6, + 8: 1, 9: 16, 11: 21, 14: 2, 15: 17, 17: 22, + 19: 8, 20: 3, 21: 18, 23: 23, 25: 9, 26: 4, + 27: 19, 29: 24 + } + # 遍历映射字典,更新pose的值 + for l24_idx, pose_idx in mapping.items(): + pose[pose_idx] = l24_state[l24_idx] + return pose + + # 获取所有关节数据 + def get_current_status(self, j=''): + time.sleep(0.01) + self.send_command(FrameProperty.THUMB_POS, j) + self.send_command(FrameProperty.INDEX_POS,j) + self.send_command(FrameProperty.MIDDLE_POS,j) + self.send_command(FrameProperty.RING_POS,j) + self.send_command(FrameProperty.LITTLE_POS, j) + #return self.x41, self.x42, self.x43, self.x44, self.x45 + time.sleep(0.1) + state= self.x41+ self.x42+ self.x43+ self.x44+ self.x45 + if len(state) == 30: + l24_state = self.state_to_cmd(l24_state=state) + return l24_state + + def get_speed(self,j=''): + time.sleep(0.1) + self.send_command(FrameProperty.THUMB_SPEED, j) # 大拇指速度 + self.send_command(FrameProperty.INDEX_SPEED, j) # 食指速度 + self.send_command(FrameProperty.MIDDLE_SPEED, j) # 中指速度 + self.send_command(FrameProperty.RING_SPEED, j) # 无名指速度 + self.send_command(FrameProperty.LITTLE_SPEED, j) # 小拇指速度 + speed = self.x49+ self.x4a+ self.x4b+ self.x4c+ self.x4d + if len(speed) == 30: + l24_speed = self.state_to_cmd(l24_state=speed) + return l24_speed + + def get_finger_torque(self): + return self.finger_torque + # def get_current(self): + # return self.x06 + # def get_fault(self): + # return self.x07 + + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """L24 暂不支持清除故障码""" + pass + + def close_can_interface(self): + if self.bus: + self.bus.shutdown() # 关闭 CAN 总线 + + ''' + 这个方法只用于展示数据关系映射,使用的话最好使用上面的方法 + ''' + def joint_map_2(self, pose): + l24_pose = [0.0]*30 #L24 CAN默认接收30个数据 pose控制L24发送的指令数据默认25个,这里进行映射 + ''' + 需要进行映射 + # L24 CAN数据格式 + #["拇指横摆0-10", "拇指侧摆1-5", "拇指根部2-0", "拇指中部3-15", "预留4-", "拇指指尖5-20", "预留6-", "食指侧摆7-6", "食指根部8-1", "食指中部9-16", "预留10-", "食指指尖11-21", "预留12-", "预留13-", "中指根部14-2", "中指中部15-17", "预留16-", "中指指尖17-22", "预留18-", "无名指侧摆19-8", "无名指根部20-3", "无名指中部21-18", "预留22-", "无名指指尖23-23", "预留24-", "小指侧摆25-9", "小指根部26-4", "小指中部27-19", "预留28-", "小指指尖29-24"] + # CMD 接收到的数据格式 + #["拇指根部0", "食指根部1", "中指根部2", "无名指根部3","小指根部4","拇指侧摆5","食指侧摆6","中指侧摆","无名指侧摆8","小指侧摆9","拇指横摆10","预留","预留","预留","预留","拇指中部15","食指中部16","中指中部17","无名指中部18","小指中部19","拇指指尖20","食指指尖21","中指指尖22","无名指指尖23","小指指尖24"] + ''' + l24_pose[0] = pose[10] + l24_pose[1] = pose[5] + l24_pose[2] = pose[0] + l24_pose[3] = pose[15] + l24_pose[4] = 0.0 + l24_pose[5] = pose[20] + l24_pose[6] = 0.0 + l24_pose[7] = pose[6] + l24_pose[8] = pose[1] + l24_pose[9] = pose[16] + l24_pose[10] = 0.0 + l24_pose[11] = pose[21] + l24_pose[12] = 0.0 + l24_pose[13] = 0.0 + l24_pose[14] = pose[2] + l24_pose[15] = pose[17] + l24_pose[16] = 0.0 + l24_pose[17] = pose[22] + l24_pose[18] = 0.0 + l24_pose[19] = pose[8] + l24_pose[20] = pose[3] + l24_pose[21] = pose[18] + l24_pose[22] = 0.0 + l24_pose[23] = pose[23] + l24_pose[24] = 0.0 + l24_pose[25] = pose[9] + l24_pose[26] = pose[4] + l24_pose[27] = pose[19] + l24_pose[28] = 0.0 + l24_pose[29] = pose[24] + return l24_pose + + def get_finger_order(self): + return [] + def get_serial_number(self): + return [0] * 6 + def show_fun_table(self): + pass \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l25_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l25_can.py new file mode 100644 index 0000000..97f91b4 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l25_can.py @@ -0,0 +1,848 @@ +#!/usr/bin/env python3 +import can +import time,sys,os +import threading +import numpy as np +from enum import Enum +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError +current_dir = os.path.dirname(os.path.abspath(__file__)) +target_dir = os.path.abspath(os.path.join(current_dir, "..")) +sys.path.append(target_dir) + +class FrameProperty(Enum): + INVALID_FRAME_PROPERTY = 0x00 # Invalid CAN frame property | No response + # Parallel command area + ROLL_POS = 0x01 # Roll joint position | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger [10,11,12,13,14] + YAW_POS = 0x02 # Yaw joint position | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger [5,6,7,8,9] + ROOT1_POS = 0x03 # Root1 joint position | The root joint closest to the palm [0,1,2,3,4] + ROOT2_POS = 0x04 # Root2 joint position | The root joint closest to the palm [15, 16,17,18,19] + ROOT3_POS = 0x05 # Root3 joint position | The root joint closest to the palm Not available + TIP_POS = 0x06 # Fingertip joint position | The root joint closest to the palm [20,21,22,23,24] + + ROLL_SPEED = 0x09 # Roll joint speed | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + YAW_SPEED = 0x0A # Yaw joint speed | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + ROOT1_SPEED = 0x0B # Root1 joint speed | The root joint closest to the palm + ROOT2_SPEED = 0x0C # Root2 joint speed | The root joint closest to the palm + ROOT3_SPEED = 0x0D # Root3 joint speed | The root joint closest to the palm + TIP_SPEED = 0x0E # Fingertip joint speed | The root joint closest to the palm + + ROLL_TORQUE = 0x11 # Roll joint torque | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + YAW_TORQUE = 0x12 # Yaw joint torque | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + ROOT1_TORQUE = 0x13 # Root1 joint torque | The root joint closest to the palm + ROOT2_TORQUE = 0x14 # Root2 joint torque | The root joint closest to the palm + ROOT3_TORQUE = 0x15 # Root3 joint torque | The root joint closest to the palm + TIP_TORQUE = 0x16 # Fingertip joint torque | The root joint closest to the palm + + ROLL_FAULT = 0x19 # Roll joint fault code | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + YAW_FAULT = 0x1A # Yaw joint fault code | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + ROOT1_FAULT = 0x1B # Root1 joint fault code | The root joint closest to the palm + ROOT2_FAULT = 0x1C # Root2 joint fault code | The root joint closest to the palm + ROOT3_FAULT = 0x1D # Root3 joint fault code | The root joint closest to the palm + TIP_FAULT = 0x1E # Fingertip joint fault code | The root joint closest to the palm + + ROLL_TEMPERATURE = 0x21 # Roll joint temperature | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + YAW_TEMPERATURE = 0x22 # Yaw joint temperature | The coordinate system is built at the root of each finger, and the rotation angle is defined according to the straightened state of the finger + ROOT1_TEMPERATURE = 0x23 # Root1 joint temperature | The root joint closest to the palm + ROOT2_TEMPERATURE = 0x24 # Root2 joint temperature | The root joint closest to the palm + ROOT3_TEMPERATURE = 0x25 # Root3 joint temperature | The root joint closest to the palm + TIP_TEMPERATURE = 0x26 # Fingertip joint temperature | The root joint closest to the palm + # Parallel command area + + # Serial command area + THUMB_POS = 0x41 # Thumb joint position | Returns this type of data + INDEX_POS = 0x42 # Index finger joint position | Returns this type of data + MIDDLE_POS = 0x43 # Middle finger joint position | Returns this type of data + RING_POS = 0x44 # Ring finger joint position | Returns this type of data + LITTLE_POS = 0x45 # Little finger joint position | Returns this type of data + + THUMB_SPEED = 0x49 # Thumb speed | Returns this type of data + INDEX_SPEED = 0x4A # Index finger speed | Returns this type of data + MIDDLE_SPEED = 0x4B # Middle finger speed | Returns this type of data + RING_SPEED = 0x4C # Ring finger speed | Returns this type of data + LITTLE_SPEED = 0x4D # Little finger speed | Returns this type of data + + THUMB_TORQUE = 0x51 # Thumb torque | Returns this type of data + INDEX_TORQUE = 0x52 # Index finger torque | Returns this type of data + MIDDLE_TORQUE = 0x53 # Middle finger torque | Returns this type of data + RING_TORQUE = 0x54 # Ring finger torque | Returns this type of data + LITTLE_TORQUE = 0x55 # Little finger torque | Returns this type of data + + THUMB_FAULT = 0x59 # Thumb fault code | Returns this type of data + INDEX_FAULT = 0x5A # Index finger fault code | Returns this type of data + MIDDLE_FAULT = 0x5B # Middle finger fault code | Returns this type of data + RING_FAULT = 0x5C # Ring finger fault code | Returns this type of data + LITTLE_FAULT = 0x5D # Little finger fault code | Returns this type of data + + THUMB_TEMPERATURE = 0x61 # Thumb temperature | Returns this type of data + INDEX_TEMPERATURE = 0x62 # Index finger temperature | Returns this type of data + MIDDLE_TEMPERATURE = 0x63 # Middle finger temperature | Returns this type of data + RING_TEMPERATURE = 0x64 # Ring finger temperature | Returns this type of data + LITTLE_TEMPERATURE = 0x65 # Little finger temperature | Returns this type of data + # Serial command area + + # Merged command area, non-essential single control data of the same finger is merged + FINGER_SPEED = 0x81 # Finger speed | Returns this type of data + FINGER_TORQUE = 0x82 # Torque | Returns this type of data + FINGER_FAULT = 0x83 # Finger fault code | Returns this type of data + + # Fingertip sensor data group + HAND_NORMAL_FORCE = 0x90 # Normal force of five fingers + HAND_TANGENTIAL_FORCE = 0x91 # Tangential force of five fingers + HAND_TANGENTIAL_FORCE_DIR = 0x92 # Tangential direction of five fingers + HAND_APPROACH_INC = 0x93 # Proximity sensing of five fingers + + THUMB_ALL_DATA = 0x98 # All data of thumb + INDEX_ALL_DATA = 0x99 # All data of index finger + MIDDLE_ALL_DATA = 0x9A # All data of middle finger + RING_ALL_DATA = 0x9B # All data of ring finger + LITTLE_ALL_DATA = 0x9C # All data of little finger + # Action command ·ACTION + ACTION_PLAY = 0xA0 # Action + + # Configuration command ·CONFIG + HAND_UID = 0xC0 # Device unique identifier + HAND_HARDWARE_VERSION = 0xC1 # Hardware version + HAND_SOFTWARE_VERSION = 0xC2 # Software version + HAND_COMM_ID = 0xC3 # Device id + HAND_FACTORY_RESET = 0xCE # Restore factory settings + HAND_SAVE_PARAMETER = 0xCF # Save parameters + + WHOLE_FRAME = 0xF0 # Whole frame transmission | Returns one byte frame property + the entire structure for 485 and network transmission only + +class LinkerHandL25Can: + def __init__(self, can_channel='can0', baudrate=1000000, can_id=0x28,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.running = True + self.last_thumb_pos, self.last_index_pos,self.last_ring_pos,self.last_middle_pos, self.last_little_pos = None,None,None,None,None + self.x01, self.x02, self.x03, self.x04,self.x05,self.x06,self.x07, self.x08,self.x09,self.x0A,self.x0B,self.x0C,self.x0D,self.x0E,self.speed = [],[],[],[],[],[],[],[],[],[],[],[],[],[],[] + self.last_root1,self.last_yaw,self.last_roll,self.last_root2,self.last_tip = None,None,None,None,None + # 速度 + self.x49, self.x4a, self.x4b, self.x4c, self.x4d,self.xc1 = [],[],[],[],[],[] + self.x41,self.x42,self.x43,self.x44,self.x45 = [],[],[],[],[] + # 扭矩 + self.x51, self.x52, self.x53, self.x54,self.x55 = [],[],[],[],[] + # 故障码 + self.x59,self.x5a,self.x5b,self.x5c,self.x5d = [],[],[],[],[] + # 温度阈值 + self.x61,self.x62,self.x63,self.x64,self.x65 = [],[],[],[],[] + # 压感 + self.x90,self.x91,self.x92,self.x93 = [],[],[],[] + # 新压感 + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + # 根据操作系统初始化 CAN 总线 + # try: + # if sys.platform == "linux": + # self.open_can.open_can(self.can_channel) + # time.sleep(0.1) + # self.bus = can.interface.Bus( + # channel=can_channel, interface="socketcan", bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # elif sys.platform == "win32": + # self.bus = can.interface.Bus( + # channel=can_channel, interface='pcan', bitrate=baudrate, + # can_filters=[{"can_id": can_id, "can_mask": 0x7FF}] + # ) + # else: + # raise EnvironmentError("Unsupported platform for CAN interface") + # except: + # print("Please insert CAN device") + self.bus = self.init_can_bus(channel=self.can_channel, baudrate=baudrate) + # 启动接收线程 + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_command(self, frame_property, data_list): + """ + Send command to CAN bus + :param frame_property: Data frame properties + :param data_list: Data payload + """ + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + #print(f"Message sent: ID={hex(self.can_id)}, Data={data}") + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(0.001) + + def receive_response(self): + """ + Receive and process response messages from CAN bus + """ + while self.running: + try: + msg = self.bus.recv(timeout=1.0) # 阻塞接收,1 秒超时 + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving message: {e}") + + + def set_joint_positions(self, joint_ranges): + if len(joint_ranges) == 25: + l25_pose = self.joint_map(joint_ranges) + # 使用列表推导式将列表每6个元素切成一个子数组 + chunks = [l25_pose[i:i+6] for i in range(0, 30, 6)] + self.send_command(FrameProperty.THUMB_POS, chunks[0]) + #time.sleep(0.001) + self.send_command(FrameProperty.INDEX_POS, chunks[1]) + #time.sleep(0.001) + self.send_command(FrameProperty.MIDDLE_POS, chunks[2]) + #time.sleep(0.001) + self.send_command(FrameProperty.RING_POS, chunks[3]) + #time.sleep(0.001) + self.send_command(FrameProperty.LITTLE_POS, chunks[4]) + #time.sleep(0.001) + + def set_joint_positions_by_topic(self, joint_ranges): + if len(joint_ranges) == 25: + # Finger Joint Position Constants + #ROLL_POS = 0x01 # Roll joint position | Coordinate system based on finger base, rotation angle defined when finger is straight [10,11,12,13,14] + #YAW_POS = 0x02 # Yaw joint position | Coordinate system based on finger base, rotation angle defined when finger is straight [5,6,7,8,9] + #ROOT1_POS = 0x03 # Root1 joint position | Joint closest to the palm [0,1,2,3,4] + #ROOT2_POS = 0x04 # Root2 joint position | Joint closest to the palm [15,16,17,18,19] + #ROOT3_POS = 0x05 # Root3 joint position | Joint closest to the palm (currently unused) + #TIP_POS = 0x06 # Tip joint position | Joint closest to the palm [20,21,22,23,24] + + # Finger joint names mapping (Chinese to English translation): + # ["Thumb root", "Index root", "Middle root", "Ring root", "Pinky root", + # "Thumb yaw", "Index yaw", "Middle yaw", "Ring yaw", "Pinky yaw", + # "Thumb roll", "Reserved", "Reserved", "Reserved", "Reserved", + # "Thumb middle", "Index middle", "Middle middle", "Ring middle", "Pinky middle", + # "Thumb tip", "Index tip", "Middle tip", "Ring tip", "Pinky tip"] + + + l25_pose = self.slice_list(joint_ranges,5) + if self._list_d_value(self.last_root1, l25_pose[0]): + self.set_root1_positions(l25_pose[0]) + self.last_root1 = l25_pose[0] + if self._list_d_value(self.last_yaw, l25_pose[1]): + self.set_yaw_positions(l25_pose[1]) + self.last_yaw = l25_pose[1] + if self._list_d_value(self.last_roll, l25_pose[2]): + self.set_roll_positions(l25_pose[2]) + self.last_roll = l25_pose[2] + if self._list_d_value(self.last_root2, l25_pose[3]): + self.set_root2_positions(l25_pose[3]) + self.last_root2 = l25_pose[3] + if self._list_d_value(self.last_tip, l25_pose[4]): + self.set_tip_positions(l25_pose[4]) + self.last_tip = l25_pose[4] + + + def slice_list(self, input_list, slice_size): + """ + Split a list into chunks of specified size. + + Parameters: + input_list (list): The list to be chunked. + slice_size (int): Number of elements in each chunk. + + Returns: + list of lists: The chunked list. + """ + # Implementation using list comprehension + sliced_list = [input_list[i:i + slice_size] for i in range(0, len(input_list), slice_size)] + return sliced_list + + def _list_d_value(self,list1, list2): + if list1 == None: + return True + for a, b in zip(list1, list2): + if abs(b - a) > 2: + return True + break + return False + # Set roll joint positions for all fingers + def set_roll_positions(self, joint_ranges): + self.send_command(FrameProperty.ROLL_POS, joint_ranges) + # Set yaw joint positions for all fingers + def set_yaw_positions(self, joint_ranges): + print(joint_ranges) + self.send_command(FrameProperty.YAW_POS, joint_ranges) + # Set base joint 1 positions for all fingers + def set_root1_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT1_POS, joint_ranges) + # Set base joint 2 positions for all fingers + def set_root2_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT2_POS, joint_ranges) + # Set base joint 3 positions for all fingers + def set_root3_positions(self, joint_ranges): + self.send_command(FrameProperty.ROOT3_POS, joint_ranges) + # Set fingertip joint positions for all fingers + def set_tip_positions(self, joint_ranges=[80]*5): + self.send_command(FrameProperty.TIP_POS, joint_ranges) + # Set thumb torque parameters + def set_thumb_torque(self, j=[]): + self.send_command(FrameProperty.THUMB_TORQUE, j) + # Set index finger torque + def set_index_torque(self, j=[]): + self.send_command(FrameProperty.INDEX_TORQUE, j) + # Set middle finger torque + def set_middle_torque(self, j=[]): + self.send_command(FrameProperty.MIDDLE_TORQUE, j) + # Set ring finger torque + def set_ring_torque(self, j=[]): + self.send_command(FrameProperty.RING_TORQUE, j) + # Set little finger torque + def set_little_torque(self, j=[]): + self.send_command(FrameProperty.LITTLE_TORQUE, j) + + # Get thumb joint position + def get_thumb_positions(self,j=[0]): + self.send_command(FrameProperty.THUMB_POS, j) + # Get index finger joint positions + def get_index_positions(self, j=[0]): + self.send_command(FrameProperty.INDEX_POS,j) + # Get middle finger joint position + def get_middle_positions(self, j=[0]): + self.send_command(FrameProperty.MIDDLE_POS,j) + # Retrieve the position of the ring finger joint + def get_ring_positions(self, j=[0]): + self.send_command(FrameProperty.RING_POS,j) + # Retrieve the position of the little finger joint + def get_little_positions(self, j=[0]): + self.send_command(FrameProperty.LITTLE_POS, j) + # All fault codes of motors in the thumb + def get_thumbn_fault(self,j=[]): + self.send_command(FrameProperty.THUMB_FAULT,j) + # All motor fault codes for the index finger + def get_index_fault(self,j=[]): + self.send_command(FrameProperty.INDEX_FAULT,j) + # All motor fault codes for the middle finger + def get_middle_fault(self,j=[]): + self.send_command(FrameProperty.MIDDLE_FAULT,j) + # All motor fault codes for the ring finger + def get_ring_fault(self,j=[]): + self.send_command(FrameProperty.RING_FAULT,j) + # All motor fault codes for the little finger + def get_little_fault(self,j=[]): + self.send_command(FrameProperty.LITTLE_FAULT,j) + # Temperature threshold for the thumb motors + def get_thumb_threshold(self,j=[]): + self.send_command(FrameProperty.THUMB_TEMPERATURE, '') + # Temperature threshold for the index finger motors + def get_index_threshold(self,j=[]): + self.send_command(FrameProperty.INDEX_TEMPERATURE, j) + # Temperature threshold for the middle finger motors + def get_middle_threshold(self,j=[]): + self.send_command(FrameProperty.MIDDLE_TEMPERATURE, j) + # Temperature threshold for the ring finger motors + def get_ring_threshold(self,j=[]): + self.send_command(FrameProperty.RING_TEMPERATURE, j) + # Little finger temperature threshold + def get_little_threshold(self,j=[]): + self.send_command(FrameProperty.LITTLE_TEMPERATURE, j) + + + def set_disability_mode(self, j=[1,1,1,1,1]): + self.send_command(0x85,j) + + def set_enable_mode(self, j=[00,00,00,00,00]): + self.send_command(0x85,j) + + # Set torque for all fingers + def set_torque(self,torque=[250]*5): + t = torque[0] + i = torque[1] + m = torque[2] + r = torque[3] + l = torque[4] + self.set_thumb_torque(j=[t]*5) + self.set_index_torque(j=[i]*5) + self.set_middle_torque(j=[m]*5) + self.set_ring_torque(j=[r]*5) + self.set_little_torque(j=[l]*5) + + def set_speed(self, speed): + self.speed = speed + if len(speed) < 25: + thumb_speed = [self.speed[0]]*5 + index_speed = [self.speed[1]]*5 + middle_speed = [self.speed[2]]*5 + ring_speed = [self.speed[3]]*5 + little_speed = [self.speed[4]]*5 + else: + thumb_speed = [self.speed[0],self.speed[1],self.speed[2],self.speed[3],self.speed[4]] + index_speed = [self.speed[5],self.speed[6],self.speed[7],self.speed[8],self.speed[9]] + middle_speed = [self.speed[10],self.speed[11],self.speed[12],self.speed[13],self.speed[14]] + ring_speed = [self.speed[15],self.speed[16],self.speed[17],self.speed[18],self.speed[19]] + little_speed = [self.speed[20],self.speed[21],self.speed[22],self.speed[23],self.speed[24]] + self.send_command(FrameProperty.THUMB_SPEED, thumb_speed) + self.send_command(FrameProperty.INDEX_SPEED, index_speed) + self.send_command(FrameProperty.MIDDLE_SPEED, middle_speed) + self.send_command(FrameProperty.RING_SPEED, ring_speed) + self.send_command(FrameProperty.LITTLE_SPEED, little_speed) + + def set_finger_torque(self, torque): + self.send_command(0x42, torque) + + def request_device_info(self): + self.send_command(0xC0, [0]) + self.send_command(0xC1, [0]) + self.send_command(0xC2, [0]) + + def save_parameters(self): + self.send_command(0xCF, []) + def process_response(self, msg): + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == 0x01: + self.x01 = list(response_data) + elif frame_type == 0x02: + self.x02 = list(response_data) + elif frame_type == 0x03: + self.x03 = list(response_data) + elif frame_type == 0x04: + self.x04 = list(response_data) + elif frame_type == 0x05: + self.x05 = list(response_data) + elif frame_type == 0x06: + self.x06 = list(response_data) + elif frame_type == 0xC0: + print(f"Device ID info: {response_data}") + if self.can_id == 0x28: + self.right_hand_info = response_data + elif self.can_id == 0x27: + self.left_hand_info = response_data + elif frame_type == 0x08: + self.x08 = list(response_data) + elif frame_type == 0x09: + self.x09 = list(response_data) + elif frame_type == 0x0A: + self.x0A = list(response_data) + elif frame_type == 0x0B: + self.x0B = list(response_data) + elif frame_type == 0x0C: + self.x0C = list(response_data) + elif frame_type == 0x0D: + self.x0D = list(response_data) + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x41: + self.x41 = list(response_data) + elif frame_type == 0x42: + + self.x42 = list(response_data) + elif frame_type == 0x43: + self.x43 = list(response_data) + elif frame_type == 0x44: + + self.x44 = list(response_data) + elif frame_type == 0x45: + self.x45 = list(response_data) + elif frame_type == 0x49: + self.x49 = list(response_data) + elif frame_type == 0x4a: + self.x4a = list(response_data) + elif frame_type == 0x4b: + self.x4b = list(response_data) + elif frame_type == 0x4c: + self.x4c = list(response_data) + elif frame_type == 0x4d: + self.x4d = list(response_data) + elif frame_type == 0xc1: + self.xc1 = list(response_data) + elif frame_type == 0x51: + self.x51 = list(response_data) + elif frame_type == 0x52: + self.x52 = list(response_data) + elif frame_type == 0x53: + self.x53 = list(response_data) + elif frame_type == 0x54: + self.x54 = list(response_data) + elif frame_type == 0x55: + self.x55 = list(response_data) + elif frame_type == 0x59: + self.x59 = list(response_data) + elif frame_type == 0x5a: + self.x5a = list(response_data) + elif frame_type == 0x5b: + self.x5b = list(response_data) + elif frame_type == 0x5c: + self.x5c = list(response_data) + elif frame_type == 0x5d: + self.x5d = list(response_data) + elif frame_type == 0x61: + self.x61 = list(response_data) + elif frame_type == 0x62: + self.x62 = list(response_data) + elif frame_type == 0x63: + self.x63 = list(response_data) + elif frame_type == 0x64: + self.x64 = list(response_data) + elif frame_type == 0x65: + self.x65 = list(response_data) + elif frame_type == 0x90: + self.x90 = list(response_data) + elif frame_type == 0x91: + self.x91 = list(response_data) + elif frame_type == 0x92: + self.x92 = list(response_data) + elif frame_type == 0x93: + self.x93 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] + + + def joint_map(self, pose): + l25_pose = [0.0] * 30 + + # 映射表,通过字典简化映射关系 + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 4: None, 5: 20, + 6: None, 7: 6, 8: 1, 9: 16, 10: None, 11: 21, + 12: None, 13: 7, 14: 2, 15: 17, 16: None, 17: 22, + 18: None, 19: 8, 20: 3, 21: 18, 22: None, 23: 23, + 24: None, 25: 9, 26: 4, 27: 19, 28: None, 29: 24 + } + + # 遍历映射字典,进行值的映射 + for l25_idx, pose_idx in mapping.items(): + if pose_idx is not None: + l25_pose[l25_idx] = pose[pose_idx] + + return l25_pose + + + def state_to_cmd(self, l25_state): + + pose = [0.0] * 25 + mapping = { + 0: 10, 1: 5, 2: 0, 3: 15, 5: 20, 7: 6, + 8: 1, 9: 16, 11: 21, 13:7, 14: 2, 15: 17, 17: 22, + 19: 8, 20: 3, 21: 18, 23: 23, 25: 9, 26: 4, + 27: 19, 29: 24 + } + # 遍历映射字典,更新pose的值 + for l25_idx, pose_idx in mapping.items(): + pose[pose_idx] = l25_state[l25_idx] + return pose + def action_play(self): + self.send_command(0xA0,[]) + + def get_current_status(self, j=''): + self.send_command(FrameProperty.THUMB_POS, j) + #time.sleep(0.001) + self.send_command(FrameProperty.INDEX_POS,j) + #time.sleep(0.001) + self.send_command(FrameProperty.MIDDLE_POS,j) + #time.sleep(0.001) + self.send_command(FrameProperty.RING_POS,j) + #time.sleep(0.001) + self.send_command(FrameProperty.LITTLE_POS, j) + #time.sleep(0.001) + state= self.x41+ self.x42+ self.x43+ self.x44+ self.x45 + if len(state) == 30: + l25_state = self.state_to_cmd(l25_state=state) + return l25_state + + def get_current_pub_status(self): + state= self.x41+ self.x42+ self.x43+ self.x44+ self.x45 + if len(state) == 30: + l25_state = self.state_to_cmd(l25_state=state) + return l25_state + + def get_current_state_topic(self): + self.send_command(0x01,[]) + #time.sleep(0.001) + self.send_command(0x02,[]) + # time.sleep(0.001) + self.send_command(0x03,[]) + #time.sleep(0.001) + self.send_command(0x04,[]) + #time.sleep(0.001) + self.send_command(0x06,[]) + #time.sleep(0.001) + state = self.x03+self.x02+self.x01+self.x04+self.x06 + return state + def get_speed(self,j=''): + self.send_command(FrameProperty.THUMB_SPEED, j) + #time.sleep(0.01) + self.send_command(FrameProperty.INDEX_SPEED, j) + #time.sleep(0.01) + self.send_command(FrameProperty.MIDDLE_SPEED, j) + #time.sleep(0.01) + self.send_command(FrameProperty.RING_SPEED, j) + #time.sleep(0.01) + self.send_command(FrameProperty.LITTLE_SPEED, j) + #time.sleep(0.01) + speed = self.x49+ self.x4a+ self.x4b+ self.x4c+ self.x4d + if len(speed) == 30: + l25_speed = self.state_to_cmd(l25_state=speed) + return l25_speed + + def get_finger_torque(self): + self.send_command(FrameProperty.THUMB_TORQUE,[]) + self.send_command(FrameProperty.INDEX_TORQUE,[]) + self.send_command(FrameProperty.MIDDLE_TORQUE,[]) + self.send_command(FrameProperty.RING_TORQUE,[]) + self.send_command(FrameProperty.LITTLE_TORQUE,[]) + return self.x51+self.x52+self.x53+self.x54+self.x55 + + def get_torque(self): + return self.get_finger_torque() + def get_fault(self): + self.get_thumbn_fault() + #time.sleep(0.001) + self.get_index_fault() + #time.sleep(0.001) + self.get_middle_fault() + #time.sleep(0.001) + self.get_ring_fault() + #time.sleep(0.001) + self.get_little_fault() + #time.sleep(0.001) + return [self.x59]+[self.x5a]+[self.x5b]+[self.x5c]+[self.x5d] + def get_threshold(self): + self.get_thumb_threshold() + self.get_index_threshold() + self.get_middle_threshold() + self.get_ring_threshold() + self.get_little_threshold() + return [self.x61]+[self.x62]+[self.x63]+[self.x64]+[self.x65] + def get_version(self): + if self.xc1 == []: + self.send_command(FrameProperty.HAND_HARDWARE_VERSION,[]) + return self.xc1 + def get_normal_force(self): + self.send_command(FrameProperty.HAND_NORMAL_FORCE,[]) + return self.x90 + def get_tangential_force(self): + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE,[]) + return self.x91 + def get_tangential_force_dir(self): + self.send_command(FrameProperty.HAND_TANGENTIAL_FORCE_DIR,[]) + return self.x92 + def get_approach_inc(self): + self.send_command(FrameProperty.HAND_APPROACH_INC,[]) + return self.x93 + def get_force(self): + '''获取压感数据''' + return [self.x90,self.x91 , self.x92 , self.x93] + + def get_matrix_touch(self): + self.send_command(0xb1,[0xc6]) + time.sleep(0.03) + self.send_command(0xb2,[0xc6]) + time.sleep(0.03) + self.send_command(0xb3,[0xc6]) + time.sleep(0.03) + self.send_command(0xb4,[0xc6]) + time.sleep(0.03) + self.send_command(0xb5,[0xc6]) + time.sleep(0.03) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_touch_type(self): + '''Get touch type''' + self.send_command(0xb1,[]) + time.sleep(0.03) + if len(self.xb1) == 2: + return 2 + else: + return -1 + + def get_touch(self): + '''Get touch data (not supported yet)''' + return [-1] * 6 + + + def get_current(self): + return [0] * 21 + def get_temperature(self): + self.get_thumb_threshold() + self.get_index_threshold() + self.get_middle_threshold() + self.get_ring_threshold() + self.get_little_threshold() + return [self.x61]+[self.x62]+[self.x63]+[self.x64]+[self.x65] + + def get_finger_order(self): + return ["Thumb root", "Index root", "Middle root", "Ring root", "Little root", + "Thumb abduction", "Index abduction", "Middle abduction", "Ring abduction", "Little abduction", + "Thumb roll", "Reserved", "Reserved", "Reserved", "Reserved", + "Thumb middle", "Index middle", "Middle middle", "Ring middle", "Little middle", + "Thumb tip", "Index tip", "Middle tip", "Ring tip", "Little tip"] + + def close_can_interface(self): + if self.bus: + self.bus.shutdown() + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """L25 暂不支持清除故障码""" + pass + ''' + 这个方法只用于展示数据关系映射,使用的话最好使用上面的方法 + ''' + def joint_map_2(self, pose): + l25_pose = [0.0]*30 #L25 CAN默认接收30个数据 pose控制L25发送的指令数据默认25个,这里进行映射 + ''' + 需要进行映射 + # L25 CAN数据格式 + #["拇指横摆0-10", "拇指侧摆1-5", "拇指根部2-0", "拇指中部3-15", "预留4-", "拇指指尖5-20", "预留6-", "食指侧摆7-6", "食指根部8-1", "食指中部9-16", "预留10-", "食指指尖11-21", "预留12-", "预留13-", "中指根部14-2", "中指中部15-17", "预留16-", "中指指尖17-22", "预留18-", "无名指侧摆19-8", "无名指根部20-3", "无名指中部21-18", "预留22-", "无名指指尖23-23", "预留24-", "小指侧摆25-9", "小指根部26-4", "小指中部27-19", "预留28-", "小指指尖29-24"] + # CMD 接收到的数据格式 + #["拇指根部0", "食指根部1", "中指根部2", "无名指根部3","小指根部4","拇指侧摆5","食指侧摆6","中指侧摆","无名指侧摆8","小指侧摆9","拇指横摆10","预留","预留","预留","预留","拇指中部15","食指中部16","中指中部17","无名指中部18","小指中部19","拇指指尖20","食指指尖21","中指指尖22","无名指指尖23","小指指尖24"] + ''' + l25_pose[0] = pose[10] + l25_pose[1] = pose[5] + l25_pose[2] = pose[0] + l25_pose[3] = pose[15] + l25_pose[4] = 0.0 + l25_pose[5] = pose[20] + l25_pose[6] = 0.0 + l25_pose[7] = pose[6] + l25_pose[8] = pose[1] + l25_pose[9] = pose[16] + l25_pose[10] = 0.0 + l25_pose[11] = pose[21] + l25_pose[12] = 0.0 + l25_pose[13] = 0.0 + l25_pose[14] = pose[2] + l25_pose[15] = pose[17] + l25_pose[16] = 0.0 + l25_pose[17] = pose[22] + l25_pose[18] = 0.0 + l25_pose[19] = pose[8] + l25_pose[20] = pose[3] + l25_pose[21] = pose[18] + l25_pose[22] = 0.0 + l25_pose[23] = pose[23] + l25_pose[24] = 0.0 + l25_pose[25] = pose[9] + l25_pose[26] = pose[4] + l25_pose[27] = pose[19] + l25_pose[28] = 0.0 + l25_pose[29] = pose[24] + return l25_pose + + def get_serial_number(self): + return [0] * 6 + def show_fun_table(self): + pass \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l6_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l6_can.py new file mode 100644 index 0000000..d5cca5d --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l6_can.py @@ -0,0 +1,426 @@ +import can +import time, sys +import threading +import numpy as np +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError + + +class LinkerHandL6Can: + def __init__(self, can_id, can_channel='can0', baudrate=1000000,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.x01 = [0] * 6 # 关节位置 + self.x02 = [-1] * 6 # 转矩限制 + self.x05 = [0] * 6 # 速度 + self.x07 = [-1] * 6 # 加速度 + self.x33 = [0] * 6 # 温度 + self.x35 = [0] * 6 # 关节错误码 + self.x36 = [-1] * 6 # 电流 + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + self.serial_number = [] + self.serial_number_map = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + } + # Fault codes + + self.joint_angles = [0] * 6 + self.pressures = [200] * 6 # Default torque 200 + self.bus = self.init_can_bus(can_channel, baudrate) + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = [[-1] * 6 for _ in range(4)] + self.is_lock = False + self.version = None + # Start the receiving thread + self.running = True + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_frame(self, frame_property, data_list,sleep=0.003): + """Send a single CAN frame with specified properties and data.""" + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(sleep) + + def set_joint_positions(self, joint_angles): + """Set the positions of 10 joints (joint_angles: list of 10 values).""" + if len(joint_angles) > 6: + self.joint_angles = joint_angles[:6] + else: + self.joint_angles = joint_angles + # Send angle control in frames + self.send_frame(0x01, self.joint_angles, sleep=0.003) + + def set_max_torque_limits(self, pressures, type="get"): + """Set maximum torque limits.""" + if type == "get": + self.pressures = [0.0] + else: + self.pressures = pressures[:6] + + def set_torque(self, torque=[180] * 6): + """Set L6 maximum torque limits.""" + if len(torque) != 6: + raise ValueError("Torque list must have 6 elements.") + return + self.send_frame(0x02, torque) + + def set_speed(self, speed=[180] * 6): + """Set L6 speed.""" + if len(speed) != 6: + raise ValueError("Speed list must have 6 elements.") + return + self.x05 = speed + for i in range(2): + time.sleep(0.001) + self.send_frame(0x05, speed) + + ''' -------------------Pressure Sensors---------------------- ''' + def get_normal_force(self): + self.send_frame(0x20, [],sleep=0.01) + + def get_tangential_force(self): + self.send_frame(0x21, [],sleep=0.01) + + def get_tangential_force_dir(self): + self.send_frame(0x22, [],sleep=0.01) + + def get_approach_inc(self): + self.send_frame(0x23, [],sleep=0.01) + + ''' -------------------Motor Temperature---------------------- ''' + def get_motor_temperature(self): + self.send_frame(0x33, []) + + # Motor fault codes + def get_motor_fault_code(self): + self.send_frame(0x35, []) + + def receive_response(self): + """Receive CAN responses and process them.""" + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving CAN message: {e}") + + def process_response(self, msg): + """Process received CAN messages.""" + #if msg.arbitration_id == self.can_id: + if msg.arbitration_id in (self.can_id, self.can_id + 8): + try: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + except: + return + if frame_type == 0x01: # 0x01 + self.x01 = list(response_data) + elif frame_type == 0x02: # 0x02 + self.x02 = list(response_data) + elif frame_type == 0x05: # Set speed + self.x05 = list(response_data) + elif frame_type == 0x20: + d = list(response_data) + self.normal_force = [float(i) for i in d] + elif frame_type == 0x21: + d = list(response_data) + self.tangential_force = [float(i) for i in d] + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x33: # L6 temperature + self.x33 = list(response_data) + elif frame_type == 0x35: # L6 fault codes + self.x35 = list(response_data) + elif frame_type == 0x36: # L6 电流 + self.x36 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0x64: # L6 version number + self.version = list(response_data) + elif frame_type == 0xC2: # L6 version number + self.version = list(response_data) + elif frame_type == 0xC0: + d = list(response_data) + index = self.serial_number_map.get(d[0]) + if index is not None: + self.serial_number=self.serial_number + d[1:] + else: + self.serial_number=self.serial_number + [-1] * 6 + + + + def get_version(self): + self.send_frame(0x64, [],sleep=0.1) + time.sleep(0.1) + if self.version is None: + self.send_frame(0xC2, [],sleep=0.1) + time.sleep(0.1) + return self.version + + def get_current_status(self): + self.send_frame(0x01, [],sleep=0.005) + return self.x01 + + def get_current_pub_status(self): + return self.x01 + + def get_speed(self): + #self.send_frame(0x05, [],sleep=0.003) + #print("L6暂不支持读取实时速度") + return [0] * 6 + + def get_current(self): + '''Not supported yet.''' + self.send_frame(0x36, [],sleep=0.005) + return self.x36 + + + + def get_torque(self): + '''Not supported yet.''' + self.send_frame(0x2, [],sleep=0.01) + return self.x02 + + def get_touch_type(self): + '''Get touch type''' + self.send_frame(0xb1,[]) + t = [] + for i in range(3): + t = self.xb1 + time.sleep(0.01) + if len(t) == 2: + return 2 + else: + self.send_frame(0x20,[],sleep=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + else: + return 1 + + + def get_touch(self): + '''Get touch data''' + self.send_frame(0xb1,[],sleep=0.03) + self.send_frame(0xb2,[],sleep=0.03) + self.send_frame(0xb3,[],sleep=0.03) + self.send_frame(0xb4,[],sleep=0.03) + self.send_frame(0xb5,[],sleep=0.03) + return [self.xb1[1],self.xb2[1],self.xb3[1],self.xb4[1],self.xb5[1],0] # The last digit is palm, currently not available + + def get_matrix_touch(self): + self.send_frame(0xb1,[0xc6],sleep=0.01) + self.send_frame(0xb2,[0xc6],sleep=0.01) + self.send_frame(0xb3,[0xc6],sleep=0.01) + self.send_frame(0xb4,[0xc6],sleep=0.01) + self.send_frame(0xb5,[0xc6],sleep=0.01) + + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_matrix_touch_v2(self): + self.send_frame(0xb1,[0xc6],sleep=0.009) + self.send_frame(0xb2,[0xc6],sleep=0.009) + self.send_frame(0xb3,[0xc6],sleep=0.009) + self.send_frame(0xb4,[0xc6],sleep=0.009) + self.send_frame(0xb5,[0xc6],sleep=0.009) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_thumb_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb1,[0xc6],sleep=sleep_time) + return self.thumb_matrix + + def get_index_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb2,[0xc6],sleep=sleep_time) + return self.index_matrix + + def get_middle_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb3,[0xc6],sleep=sleep_time) + return self.middle_matrix + + def get_ring_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb4,[0xc6],sleep=sleep_time) + return self.ring_matrix + + def get_little_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb5,[0xc6],sleep=sleep_time) + return self.little_matrix + + def get_force(self): + '''Get pressure.''' + return [self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc] + + def get_temperature(self): + '''Get temperature.''' + self.get_motor_temperature() + return self.x33 + + def get_fault(self): + '''Get faults.''' + self.get_motor_fault_code() + return self.x35 + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + def show_fun_table(self): + pass + + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """O6 暂不支持清除故障码""" + pass + + def get_serial_number(self): + try: + self.send_frame(0xC0,[],sleep=0.005) + # 1. 使用 bytes() 函数将整数列表转换为字节对象 + # bytes() 接收一个由 0-255 之间的整数组成的列表。 + byte_data = bytes(self.serial_number) + # 2. 使用 .decode() 方法将字节对象解码为 ASCII 字符串 + result_string = byte_data.decode('ascii') + if result_string == "": + return "-1" + else: + # print(f"原始 ASCII 码列表: {self.serial_number}") + # print(f"解码后的字符串: {result_string}") + return result_string + except: + return "-1" + + def close_can_interface(self): + """Stop the CAN communication.""" + self.running = False + if self.receive_thread.is_alive(): + self.receive_thread.join() + if self.bus: + self.bus.shutdown() diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l7_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l7_can.py new file mode 100644 index 0000000..446d24a --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l7_can.py @@ -0,0 +1,419 @@ +import can +import time, sys +import threading +import numpy as np +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError + + +class LinkerHandL7Can: + def __init__(self, can_id, can_channel='can0', baudrate=1000000,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.x01 = [0] * 7 + self.x02 = [-1] * 7 + self.x05 = [0] * 7 + self.x33 = [0] * 7 + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + self.thumb_matrix = np.full((12, 6), -1) + self.index_matrix = np.full((12, 6), -1) + self.middle_matrix = np.full((12, 6), -1) + self.ring_matrix = np.full((12, 6), -1) + self.little_matrix = np.full((12, 6), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + self.serial_number = [] + self.serial_number_map = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + } + # Fault codes + self.x35 = [0] * 7, [0] * 7 + self.joint_angles = [0] * 10 + self.pressures = [200] * 7 # Default torque 200 + self.bus = self.init_can_bus(can_channel, baudrate) + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = [[-1] * 7 for _ in range(4)] + self.is_lock = False + self.version = None + # Start the receiving thread + self.running = True + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_frame(self, frame_property, data_list,sleep=0.005): + """Send a single CAN frame with specified properties and data.""" + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(sleep) + + def set_joint_positions(self, joint_angles): + """Set the positions of 10 joints (joint_angles: list of 10 values).""" + self.is_lock = True + if len(joint_angles) > 7: + self.joint_angles = joint_angles[:7] + else: + self.joint_angles = joint_angles + # Send angle control in frames + self.send_frame(0x01, self.joint_angles, sleep=0.003) + self.is_lock = False + + def set_max_torque_limits(self, pressures, type="get"): + """Set maximum torque limits.""" + if type == "get": + self.pressures = [0.0] + else: + self.pressures = pressures[:7] + + def set_torque(self, torque=[180] * 7): + """Set L7 maximum torque limits.""" + if len(torque) != 7: + raise ValueError("Torque list must have 7 elements.") + return + self.send_frame(0x02, torque) + + def set_speed(self, speed=[180] * 7): + """Set L7 speed.""" + if len(speed) != 7: + raise ValueError("Speed list must have 7 elements.") + return + self.x05 = speed + for i in range(2): + time.sleep(0.001) + self.send_frame(0x05, speed) + + ''' -------------------Pressure Sensors---------------------- ''' + def get_normal_force(self): + self.send_frame(0x20, [],sleep=0.004) + + def get_tangential_force(self): + self.send_frame(0x21, [],sleep=0.004) + + def get_tangential_force_dir(self): + self.send_frame(0x22, [],sleep=0.004) + + def get_approach_inc(self): + self.send_frame(0x23, [],sleep=0.004) + + ''' -------------------Motor Temperature---------------------- ''' + def get_motor_temperature(self): + self.send_frame(0x33, []) + + # Motor fault codes + def get_motor_fault_code(self): + self.send_frame(0x35, []) + + def receive_response(self): + """Receive CAN responses and process them.""" + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving CAN message: {e}") + + def process_response(self, msg): + """Process received CAN messages.""" + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == 0x01: # 0x01 + self.x01 = list(response_data) + elif frame_type == 0x02: # 0x02 + self.x02 = list(response_data) + elif frame_type == 0x05: # Set speed + self.x05 = list(response_data) + elif frame_type == 0x20: + d = list(response_data) + self.normal_force = [float(i) for i in d] + elif frame_type == 0x21: + d = list(response_data) + self.tangential_force = [float(i) for i in d] + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x33: # L7 temperature + self.x33 = list(response_data) + elif frame_type == 0x35: # L7 fault codes + self.x35 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 7: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0x64: # L7 version number + self.version = list(response_data) + elif frame_type == 0xC2: # O6 version number + self.version = list(response_data) + elif frame_type == 0xC0: + d = list(response_data) + index = self.serial_number_map.get(d[0]) + if index is not None: + self.serial_number=self.serial_number + d[1:] + else: + self.serial_number=self.serial_number + [-1] * 6 + + def get_version(self): + self.send_frame(0x64, [], sleep=0.1) + time.sleep(0.1) + if self.version is None: + self.send_frame(0xC2, [], sleep=0.1) + time.sleep(0.1) + return self.version + + def get_current_status(self): + if self.is_lock: + return self.x01 + elif self.is_lock == False: + self.send_frame(0x01, [],sleep=0.003) + return self.x01 + + def get_current_pub_status(self): + return self.x01 + + def get_speed(self): + self.send_frame(0x05, [],sleep=0.003) + return self.x05 + + def get_current(self): + '''Not supported yet.''' + self.send_frame(0x2, [],sleep=0.1) + return self.x02 + + + + def get_torque(self): + '''Not supported yet.''' + self.send_frame(0x2, [],sleep=0.01) + return self.x02 + + def get_touch_type(self): + '''Get touch type''' + self.send_frame(0xb1,[]) + t = [] + for i in range(3): + t = self.xb1 + time.sleep(0.01) + if len(t) == 2: + return 2 + else: + self.send_frame(0x20,[],sleep=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + else: + return 1 + + + def get_touch(self): + '''Get touch data''' + self.send_frame(0xb1,[],sleep=0.03) + self.send_frame(0xb2,[],sleep=0.03) + self.send_frame(0xb3,[],sleep=0.03) + self.send_frame(0xb4,[],sleep=0.03) + self.send_frame(0xb5,[],sleep=0.03) + return [self.xb1[1],self.xb2[1],self.xb3[1],self.xb4[1],self.xb5[1],0] # The last digit is palm, currently not available + + def get_matrix_touch(self): + self.send_frame(0xb1,[0xc6],sleep=0.01) + self.send_frame(0xb2,[0xc6],sleep=0.01) + self.send_frame(0xb3,[0xc6],sleep=0.01) + self.send_frame(0xb4,[0xc6],sleep=0.01) + self.send_frame(0xb5,[0xc6],sleep=0.01) + + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_matrix_touch_v2(self): + self.send_frame(0xb1,[0xc6],sleep=0.005) + self.send_frame(0xb2,[0xc6],sleep=0.005) + self.send_frame(0xb3,[0xc6],sleep=0.005) + self.send_frame(0xb4,[0xc6],sleep=0.005) + self.send_frame(0xb5,[0xc6],sleep=0.005) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + + def get_thumb_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb1,[0xc6],sleep=sleep_time) + return self.thumb_matrix + + def get_index_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb2,[0xc6],sleep=sleep_time) + return self.index_matrix + + def get_middle_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb3,[0xc6],sleep=sleep_time) + return self.middle_matrix + + def get_ring_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb4,[0xc6],sleep=sleep_time) + return self.ring_matrix + + def get_little_matrix_touch(self,sleep_time=0.005): + self.send_frame(0xb5,[0xc6],sleep=sleep_time) + return self.little_matrix + + def get_force(self): + '''Get pressure.''' + return [self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc] + + def get_temperature(self): + '''Get temperature.''' + self.get_motor_temperature() + return self.x33 + + def get_fault(self): + '''Get faults.''' + self.get_motor_fault_code() + return self.x35 + + def get_serial_number(self): + try: + self.send_frame(0xC0,[],sleep=0.005) + # 1. 使用 bytes() 函数将整数列表转换为字节对象 + # bytes() 接收一个由 0-255 之间的整数组成的列表。 + byte_data = bytes(self.serial_number) + # 2. 使用 .decode() 方法将字节对象解码为 ASCII 字符串 + result_string = byte_data.decode('ascii') + if result_string == "": + return "-1" + else: + # print(f"原始 ASCII 码列表: {self.serial_number}") + # print(f"解码后的字符串: {result_string}") + return result_string + except: + return "-1" + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch", "thumb_cmc_roll"] + + def show_fun_table(self): + pass + + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """L7 暂不支持清除故障码""" + pass + + def close_can_interface(self): + """Stop the CAN communication.""" + self.running = False + if self.receive_thread.is_alive(): + self.receive_thread.join() + if self.bus: + self.bus.shutdown() diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_o6_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_o6_can.py new file mode 100644 index 0000000..5c95ab8 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_o6_can.py @@ -0,0 +1,447 @@ +import can +import time, sys +import threading +import numpy as np +from utils.open_can import OpenCan +from utils.color_msg import ColorMsg +from can.exceptions import CanError + + +class LinkerHandO6Can: + def __init__(self, can_id, can_channel='can0', baudrate=1000000,yaml=""): + self.can_id = can_id + self.can_channel = can_channel + self.baudrate = baudrate + self.open_can = OpenCan(load_yaml=yaml) + + self.x01 = [0] * 6 # 关节位置 + self.x02 = [-1] * 6 # 转矩限制 + self.x05 = [0] * 6 # 速度 + self.x07 = [-1] * 6 # 加速度 + self.x33 = [0] * 6 # 温度 + self.x35 = [0] * 6 # 关节错误码 + self.x36 = [-1] * 6 # 电流 + self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 + + self.thumb_matrix = np.full((10, 4), -1) + self.index_matrix = np.full((10, 4), -1) + self.middle_matrix = np.full((10, 4), -1) + self.ring_matrix = np.full((10, 4), -1) + self.little_matrix = np.full((10, 4), -1) + self.matrix_map = { + 0: 0, + 16: 1, + 32: 2, + 48: 3, + 64: 4, + 80: 5, + 96: 6, + 112: 7, + 128: 8, + 144: 9, + 160: 10, + 176: 11, + } + self.serial_number = [] + self.serial_number_map = { + 0: 0, + 1: 1, + 2: 2, + 3: 3, + } + # Fault codes + + self.joint_angles = [0] * 6 + self.pressures = [200] * 6 # Default torque 200 + self.bus = self.init_can_bus(can_channel, baudrate) + self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = [[-1] * 6 for _ in range(4)] + self.is_lock = False + self.version = None + # Start the receiving thread + self.running = True + + self.receive_thread = threading.Thread(target=self.receive_response) + self.receive_thread.daemon = True + self.receive_thread.start() + time.sleep(0.1) + self._check_touch_type() + + def _check_touch_type(self): + '''根据SN编码判断压感类型''' + self.sn = self.get_serial_number() + time.sleep(0.1) + if self.sn != "-1": + parts = self.sn.split("-") + if parts[4] == "A": + self.touch_type = 1 + elif parts[4] == "B": + self.touch_type = 2 + self.touch_code = 0xA4 # 6*12 O6 一律0XA4 + elif parts[4] == "J": + self.touch_type = 3 + elif parts[4] == "F": + self.touch_type = 4 + self.touch_code = 0xA4 # 4*10 + elif parts[4] == "Z": + self.touch_type = -1 + else: + # 如果没有SN编码则根据返回数据进行判断 + self.touch_type = self.get_touch_type() + + + def init_can_bus(self, channel, baudrate): + """ + 尝试按优先级连接 CAN 总线,并实现回退机制。 + """ + # --- 统一异常处理块开始 --- + try: + if sys.platform == "linux": + # Linux 优先级:1. socketcan + try: + self.open_can.open_can(self.can_channel) + # 尝试 socketcan + bus = can.interface.Bus(channel=channel, interface="socketcan", bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='socketcan', channel='{channel}'", color="green") + return bus + except CanError as e: + # 如果 socketcan 失败,可以考虑在这里尝试其他 Linux 接口 (如 'pcan') + ColorMsg(msg=f"socketcan 接口连接失败: {e}", color="yellow") + raise # 重新抛出异常,让外层 try 捕获 + elif sys.platform == "win32": + # Windows 优先级:1. pcan + try: + bus = can.interface.Bus(channel=channel, interface='pcan', bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='pcan', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"pcan 接口连接失败,尝试回退到 'candle': {e}", color="yellow") + # Windows 优先级:2. candle (回退方法) + try: + bus = can.Bus(interface="candle", channel=channel, bitrate=baudrate) + ColorMsg(msg=f"成功连接: interface='candle', channel='{channel}'", color="green") + return bus + except CanError as e: + ColorMsg(msg=f"candle 接口连接失败: {e}", color="yellow") + raise # 两个接口都失败,抛出异常 + else: + raise EnvironmentError("Unsupported platform for CAN interface") + # --- 统一异常处理块结束 --- + except Exception as e: + # 如果任何一个接口尝试失败并抛出异常(包括 EnvironmentError) + ColorMsg(msg=f"致命错误:所有 CAN 接口连接尝试均失败或平台不受支持。请检查设备连接或驱动安装和配置文件中CAN参数的配置。\n错误详情: {e}", color="red") + # 保持 raise 动作,将错误信息传递给调用者,避免程序继续运行 + raise + + def send_frame(self, frame_property, data_list,sleep=0.005): + """Send a single CAN frame with specified properties and data.""" + frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property + data = [frame_property_value] + [int(val) for val in data_list] + msg = can.Message(arbitration_id=self.can_id, data=data, is_extended_id=False) + try: + self.bus.send(msg) + except can.CanError as e: + print(f"Failed to send message: {e}") + self.open_can.open_can(self.can_channel) + time.sleep(1) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can_channel) + time.sleep(1) + if self.is_can: + self.bus = can.interface.Bus(channel=self.can_channel, interface="socketcan", bitrate=self.baudrate) + else: + print("Reconnecting CAN devices ....") + time.sleep(sleep) + + def set_joint_positions(self, joint_angles): + """Set the positions of 10 joints (joint_angles: list of 10 values).""" + if len(joint_angles) > 6: + self.joint_angles = joint_angles[:6] + else: + self.joint_angles = joint_angles + # Send angle control in frames + self.send_frame(0x01, self.joint_angles, sleep=0.003) + + def set_max_torque_limits(self, pressures, type="get"): + """Set maximum torque limits.""" + if type == "get": + self.pressures = [0.0] + else: + self.pressures = pressures[:6] + + def set_torque(self, torque=[180] * 6): + """Set L6 maximum torque limits.""" + if len(torque) != 6: + raise ValueError("Torque list must have 6 elements.") + return + self.send_frame(0x02, torque) + + def set_speed(self, speed=[180] * 6): + """Set L6 speed.""" + if len(speed) != 6: + raise ValueError("Speed list must have 6 elements.") + return + self.x05 = speed + for i in range(2): + time.sleep(0.001) + self.send_frame(0x05, speed) + + ''' -------------------Pressure Sensors---------------------- ''' + def get_normal_force(self): + self.send_frame(0x20, [],sleep=0.01) + + def get_tangential_force(self): + self.send_frame(0x21, [],sleep=0.01) + + def get_tangential_force_dir(self): + self.send_frame(0x22, [],sleep=0.01) + + def get_approach_inc(self): + self.send_frame(0x23, [],sleep=0.01) + + ''' -------------------Motor Temperature---------------------- ''' + def get_motor_temperature(self): + self.send_frame(0x33, []) + + # Motor fault codes + def get_motor_fault_code(self): + self.send_frame(0x35, []) + + def receive_response(self): + """Receive CAN responses and process them.""" + while self.running: + try: + msg = self.bus.recv(timeout=1.0) + if msg: + self.process_response(msg) + except can.CanError as e: + print(f"Error receiving CAN message: {e}") + + def process_response(self, msg): + """Process received CAN messages.""" + if msg.arbitration_id == self.can_id: + frame_type = msg.data[0] + response_data = msg.data[1:] + if len(list(response_data)) == 0: + return + if frame_type == 0x01: # 0x01 + self.x01 = list(response_data) + elif frame_type == 0x02: # 0x02 + self.x02 = list(response_data) + elif frame_type == 0x05: # Set speed + self.x05 = list(response_data) + elif frame_type == 0x20: + d = list(response_data) + self.normal_force = [float(i) for i in d] + elif frame_type == 0x21: + d = list(response_data) + self.tangential_force = [float(i) for i in d] + elif frame_type == 0x22: + d = list(response_data) + self.tangential_force_dir = [float(i) for i in d] + elif frame_type == 0x23: + d = list(response_data) + self.approach_inc = [float(i) for i in d] + elif frame_type == 0x33: # O6 temperature + self.x33 = list(response_data) + elif frame_type == 0x35: # O6 fault codes + self.x35 = list(response_data) + elif frame_type == 0x36: # O6 电流 + self.x36 = list(response_data) + elif frame_type == 0xb0: + self.xb0 = list(response_data) + elif frame_type == 0xb1: + d = list(response_data) + if len(d) == 2: + self.xb1 = d + elif len(d) == 5: + index = self.matrix_map.get(d[0]) + if index is not None: + self.thumb_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb2: + d = list(response_data) + if len(d) == 2: + self.xb2 = d + elif len(d) == 5: + index = self.matrix_map.get(d[0]) + if index is not None: + self.index_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb3: + d = list(response_data) + if len(d) == 2: + self.xb3 = d + elif len(d) == 5: + index = self.matrix_map.get(d[0]) + if index is not None: + self.middle_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb4: + d = list(response_data) + if len(d) == 2: + self.xb4 = d + elif len(d) == 5: + index = self.matrix_map.get(d[0]) + if index is not None: + self.ring_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0xb5: + d = list(response_data) + if len(d) == 2: + self.xb5 = d + elif len(d) == 5: + index = self.matrix_map.get(d[0]) + if index is not None: + self.little_matrix[index] = d[1:] # Remove the first flag bit + elif frame_type == 0x64: # O6 version number + self.version = list(response_data) + elif frame_type == 0xC2: # O6 version number + self.version = list(response_data) + elif frame_type == 0xC0: + d = list(response_data) + index = self.serial_number_map.get(d[0]) + if index is not None: + self.serial_number += d[1:] + else: + self.serial_number=self.serial_number + [-1] * 6 + + + + def get_version(self): + self.send_frame(0x64, [],sleep=0.1) + time.sleep(0.1) + if self.version is None: + self.send_frame(0xC2, [],sleep=0.1) + time.sleep(0.1) + return self.version + + def get_current_status(self): + self.send_frame(0x01, [],sleep=0.005) + return self.x01 + + def get_current_pub_status(self): + return self.x01 + + def get_speed(self): + self.send_frame(0x05, [],sleep=0.002) + #print("L6暂不支持读取实时速度") + return self.x05 + + def get_current(self): + '''Not supported yet.''' + self.send_frame(0x36, [],sleep=0.005) + return self.x36 + + + + def get_torque(self): + '''Not supported yet.''' + self.send_frame(0x2, [],sleep=0.01) + return self.x02 + + def get_touch_type(self): + '''Get touch type''' + self.send_frame(0xb1,[]) + t = [] + for i in range(3): + t = self.xb1 + time.sleep(0.01) + if len(t) == 2: + return 2 + else: + self.send_frame(0x20,[],sleep=0.03) + time.sleep(0.01) + if self.normal_force[0] == -1: + return -1 + else: + return 1 + + + def get_touch(self): + '''Get touch data''' + self.send_frame(0xb1,[],sleep=0.03) + self.send_frame(0xb2,[],sleep=0.03) + self.send_frame(0xb3,[],sleep=0.03) + self.send_frame(0xb4,[],sleep=0.03) + self.send_frame(0xb5,[],sleep=0.03) + return [self.xb1[1],self.xb2[1],self.xb3[1],self.xb4[1],self.xb5[1],0] # The last digit is palm, currently not available + + def get_matrix_touch(self): + self.send_frame(0xb1,[self.touch_code],sleep=0.01) + self.send_frame(0xb2,[self.touch_code],sleep=0.01) + self.send_frame(0xb3,[self.touch_code],sleep=0.01) + self.send_frame(0xb4,[self.touch_code],sleep=0.01) + self.send_frame(0xb5,[self.touch_code],sleep=0.01) + + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_matrix_touch_v2(self): + self.send_frame(0xb1,[self.touch_code],sleep=0.009) + self.send_frame(0xb2,[self.touch_code],sleep=0.009) + self.send_frame(0xb3,[self.touch_code],sleep=0.009) + self.send_frame(0xb4,[self.touch_code],sleep=0.009) + self.send_frame(0xb5,[self.touch_code],sleep=0.009) + return self.thumb_matrix , self.index_matrix , self.middle_matrix , self.ring_matrix , self.little_matrix + + def get_thumb_matrix_touch(self,sleep_time=0.002): + self.send_frame(0xb1,[self.touch_code],sleep=sleep_time) + return self.thumb_matrix + + def get_index_matrix_touch(self,sleep_time=0.002): + self.send_frame(0xb2,[self.touch_code],sleep=sleep_time) + return self.index_matrix + + def get_middle_matrix_touch(self,sleep_time=0.002): + self.send_frame(0xb3,[self.touch_code],sleep=sleep_time) + return self.middle_matrix + + def get_ring_matrix_touch(self,sleep_time=0.002): + self.send_frame(0xb4,[self.touch_code],sleep=sleep_time) + return self.ring_matrix + + def get_little_matrix_touch(self,sleep_time=0.002): + self.send_frame(0xb5,[self.touch_code],sleep=sleep_time) + return self.little_matrix + + def get_force(self): + '''Get pressure.''' + return [self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc] + + def get_temperature(self): + '''Get temperature.''' + self.get_motor_temperature() + return self.x33 + + def get_fault(self): + '''Get faults.''' + self.get_motor_fault_code() + return self.x35 + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + def clear_faults(self, finger_mask=[1, 1, 1, 1, 1]): + """O6 暂不支持清除故障码""" + pass + def show_fun_table(self): + pass + + def get_serial_number(self): + try: + self.send_frame(0xC0,[],sleep=0.005) + # 1. 使用 bytes() 函数将整数列表转换为字节对象 + # bytes() 接收一个由 0-255 之间的整数组成的列表。 + byte_data = bytes(self.serial_number) + # 2. 使用 .decode() 方法将字节对象解码为 ASCII 字符串 + result_string = byte_data.decode('ascii') + if result_string == "": + return "-1" + else: + # print(f"原始 ASCII 码列表: {self.serial_number}") + # print(f"解码后的字符串: {result_string}") + return result_string + except: + return "-1" + + def close_can_interface(self): + """Stop the CAN communication.""" + self.running = False + if self.receive_thread.is_alive(): + self.receive_thread.join() + if self.bus: + self.bus.shutdown() diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l10_rs485.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l10_rs485.py new file mode 100644 index 0000000..8db78ab --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l10_rs485.py @@ -0,0 +1,345 @@ +#!/usr/bin/env python3 +import os +import time +import struct +from typing import Dict, List +import numpy as np +from pymodbus.client import ModbusSerialClient +_INTERVAL = 0.005 # 8 ms +class LinkerHandL10RS485: + KEYS = ["thumb_cmc_pitch", "thumb_cmc_roll", "index_mcp_pitch", "middle_mcp_pitch", + "ring_mcp_pitch", "pinky_mcp_pitch", "index_mcp_roll", "ring_mcp_roll", + "pinky_mcp_roll", "thumb_cmc_yaw"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + self.slave = hand_id + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=0.05, # 50 ms 超时 + retries=3, # 重试次数 + retry_on_empty=True, + handle_local_echo=False + ) + # 在 pymodbus 3.5.1 中,连接需要显式调用 connect() + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485 connect fail to {modbus_port}") + + # -------------------------------------------------- + # 批量读取接口 + # -------------------------------------------------- + def read_angles(self) -> List[int]: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=0, count=10, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_angles failed: {rsp}") + return rsp.registers + + def read_torques(self) -> List[int]: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=10, count=10, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_torques failed: {rsp}") + return rsp.registers + + def read_speeds(self) -> List[int]: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=20, count=10, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_speeds failed: {rsp}") + return rsp.registers + + def read_temperatures(self) -> List[int]: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=40, count=10, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_temperatures failed: {rsp}") + return rsp.registers + + def read_error_codes(self) -> List[int]: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=50, count=10, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_error_codes failed: {rsp}") + return rsp.registers + + def read_versions(self) -> dict: + time.sleep(_INTERVAL) + rsp = self.cli.read_input_registers(address=158, count=6, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"read_versions failed: {rsp}") + keys = ["hand_freedom", "hand_version", "hand_number", + "hand_direction", "software_version", "hardware_version"] + #return dict(zip(keys, rsp.registers)) + return rsp.registers + + # -------------------------------------------------- + # 5 个压力传感器 + # -------------------------------------------------- + def read_pressure_thumb(self) -> np.ndarray: + return np.array(self._pressure(1), dtype=np.uint8) + + def read_pressure_index(self) -> np.ndarray: + return np.array(self._pressure(2), dtype=np.uint8) + + def read_pressure_middle(self) -> np.ndarray: + return np.array(self._pressure(3), dtype=np.uint8) + + def read_pressure_ring(self) -> np.ndarray: + return np.array(self._pressure(4), dtype=np.uint8) + + def read_pressure_pinky(self) -> np.ndarray: + return np.array(self._pressure(5), dtype=np.uint8) + + # def _pressure(self, finger: int) -> List[int]: + # time.sleep(_INTERVAL) + # # 先选择手指 + # wrsp = self.cli.write_register(address=60, value=finger, slave=self.slave) + # if wrsp.isError(): + # raise RuntimeError(f"write finger select {finger} failed: {wrsp}") + + # time.sleep(_INTERVAL) + # # 读取压力传感器数据 (96个寄存器) + # rrsp = self.cli.read_input_registers(address=62, count=96, slave=self.slave) + # if rrsp.isError(): + # raise RuntimeError(f"read pressure finger={finger} failed: {rrsp}") + # return np.array(rrsp.registers, dtype=np.uint8) + def _pressure(self, finger: int) -> np.ndarray: + """ + 6x12 (72点) 矩阵尺寸。 + Modbus 地址 60/62。 + """ + rows = 12 # 12 行 + cols = 6 # 6 列 + finger_size = rows * cols # 72 个数据点 + + # modbus 地址和计数 + write_address = 70 # 写入手指选择 + read_address = 72 # 读取压力数据 + read_count = 96 # 读取 96 个寄存器 + skip_count = 10 # 跳过前 10 个校验点 + + # 0. 参数校验和手指写入值确定 + if finger < 1 or finger > 5: + raise ValueError(f"无效的手指编号: {finger}。手指编号应在 1 到 5 之间。") + + finger_write_value = finger + + # 1. 写入手指选择寄存器 (地址 60) + time.sleep(0.008) + wrsp = self.cli.write_register(address=write_address, value=finger_write_value, slave=self.slave) + if wrsp.isError(): + raise RuntimeError(f"写入手指选择 {finger} 到地址 {write_address} 失败: {wrsp}") + + # 写入后等待片刻 + time.sleep(0.008) + + # 2. 读取地址 62 的数据 + rrsp = self.cli.read_input_registers(address=read_address, count=read_count, slave=self.slave) + + if rrsp.isError(): + raise RuntimeError(f"读取地址 {read_address} 压力数据失败: {rrsp}") + + registers_16bit: List[int] = rrsp.registers + + # 3. 核心数据处理 + # a. 提取低 8 位数据 (得到 96 个 8 位数据点) + final_data_96 = [reg_value & 255 for reg_value in registers_16bit] + + # b. 跳过前 10 个校验/头部数据点 (得到 86 个有效数据点) + effective_data = np.array(final_data_96[skip_count:], dtype=np.uint8) + # c. 截取当前手指的矩阵数据 (从 86 个有效点中截取 72 个点) + start_idx = 0 + end_idx = finger_size # 72 + + finger_data_flat = effective_data[start_idx:end_idx] + + # d. 验证数据长度 + if finger_data_flat.size != finger_size: + raise ValueError( + f"数据提取失败。期望 {finger_size} 点 ({rows}x{cols})," + f"但仅截取到 {finger_data_flat.size} 点。请检查协议,确认地址 62 是否一次性返回了所有手指数据。" + ) + + # e. 重塑为二维矩阵 (12 行 6 列) + finger_matrix = finger_data_flat.reshape((rows, cols)) + + return finger_matrix + + + + + # -------------------------------------------------- + # 批量写入接口 + # -------------------------------------------------- + def write_angles(self, vals: List[int]): + vals = [int(x) for x in vals] + if not self.is_valid_10xuint8(vals): + raise ValueError("需要 10 个 0-255 整数") + + time.sleep(_INTERVAL) + rsp = self.cli.write_registers(address=0, values=vals, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"write_angles failed: {rsp}") + + def write_speeds(self, vals: List[int]): + vals = [int(x) for x in vals] + if not self.is_valid_10xuint8(vals): + raise ValueError("需要 10 个 0-255 整数") + + time.sleep(_INTERVAL) + rsp = self.cli.write_registers(address=20, values=vals, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"write_speeds failed: {rsp}") + + def write_torques(self, vals: List[int]): + vals = [int(x) for x in vals] + if not self.is_valid_10xuint8(vals): + raise ValueError("需要 10 个 0-255 整数") + + time.sleep(_INTERVAL) + rsp = self.cli.write_registers(address=10, values=vals, slave=self.slave) + if rsp.isError(): + raise RuntimeError(f"write_torques failed: {rsp}") + + # -------------------------------------------------- + # 上下文管理 + # -------------------------------------------------- + def close(self): + if self.connected: + self.cli.close() + self.connected = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # -------------------------------------------------- + # 工具函数 + # -------------------------------------------------- + def is_valid_10xuint8(self, lst) -> bool: + if len(lst) != 10: + return False + return all(isinstance(x, int) and 0 <= x <= 255 for x in lst) + + # -------------------------------------------------- + # 固定 API 接口 + # -------------------------------------------------- + def set_joint_positions(self, joint_angles=None): + joint_angles = joint_angles or [0] * 10 + self.write_angles(joint_angles) + + def set_speed(self, speed=None): + speed = speed or [200] * 10 + self.write_speeds(speed) + + def set_torque(self, torque=None): + torque = torque or [200] * 10 + self.write_torques(torque) + + def set_current(self, current=None): + print("当前L10不支持设置电流", flush=True) + + def get_version(self) -> dict: + return self.read_versions() + + def get_current(self): + print("当前L10不支持获取电流", flush=True) + + def get_state(self) -> List[int]: + return self.read_angles() + + def get_state_for_pub(self) -> List[int]: + return self.get_state() + + def get_current_status(self) -> List[int]: + return self.get_state() + + def get_speed(self) -> List[int]: + return self.read_speeds() + + def get_joint_speed(self) -> List[int]: + return self.get_speed() + + def get_touch_type(self) -> int: + return 2 + + def get_normal_force(self) -> List[int]: + return [-1] * 5 + + def get_tangential_force(self) -> List[int]: + return [-1] * 5 + + def get_approach_inc(self) -> List[int]: + return [-1] * 5 + + def get_touch(self) -> List[int]: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return self._pressure(1) + + def get_index_matrix_touch(self,sleep_time=0): + return self._pressure(2) + + def get_middle_matrix_touch(self,sleep_time=0): + return self._pressure(3) + + def get_ring_matrix_touch(self,sleep_time=0): + return self._pressure(4) + + def get_little_matrix_touch(self,sleep_time=0): + return self._pressure(5) + + def get_matrix_touch(self) -> List[List[int]]: + return self.get_thumb_matrix_touch(),self.get_index_matrix_touch(), self.get_middle_matrix_touch(), self.get_ring_matrix_touch(), self.get_little_matrix_touch() + + def get_matrix_touch_v2(self) -> List[List[int]]: + return self.get_matrix_touch() + + def get_torque(self) -> List[int]: + return self.read_torques() + + def get_temperature(self) -> List[int]: + return self.read_temperatures() + + def get_fault(self) -> List[int]: + return self.read_error_codes() + + def get_serial_number(self): + return [0] * 6 + + def clear_faults(self): + pass + +# ------------------- demo ------------------- +if __name__ == "__main__": + try: + with LinkerHandL10RS485(hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200) as hand: + print("连接成功!") + + # 测试读取角度 + angles = hand.read_angles() + print("角度:", dict(zip(LinkerHandL10RS485.KEYS, angles))) + + # 测试读取版本信息 + ver = hand.get_version() + print("版本信息:", ver) + + # 测试压力传感器 + print("拇指压力传感器数据长度:", len(hand.read_pressure_thumb())) + + # 测试其他读取功能 + print("电流:", hand.read_torques()) + print("速度:", hand.read_speeds()) + print("温度:", hand.read_temperatures()) + print("错误码:", hand.read_error_codes()) + + except Exception as e: + print(f"错误: {e}") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py new file mode 100644 index 0000000..81db0e2 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py @@ -0,0 +1,460 @@ +#!/usr/bin/env python3 +import os +import time +from pymodbus.client import ModbusSerialClient +from typing import List, Dict +import numpy as np + +_INTERVAL = 0.006 # 8 ms + +class LinkerHandL6RS485: + """L6机械手 Modbus-RTU 控制类""" + + # 6个关节名称 + JOINT_NAMES = ["thumb_pitch", "thumb_yaw", "index_pitch", + "middle_pitch", "ring_pitch", "little_pitch"] + + # 手指名称 + FINGER_NAMES = ["thumb", "index", "middle", "ring", "little"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + """ + 初始化L6机械手 + hand_id: 右手0x27(39), 左手0x28(40) + modbus_port: 串口设备路径 + baudrate: 波特率,固定115200 + """ + self.slave = hand_id + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=0.05 + ) + # pymodbus 3.5.1 需要显式连接 + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485连接失败,端口: {modbus_port}") + + def _read_input_registers(self, address: int, count: int) -> List[int]: + """读取输入寄存器""" + time.sleep(_INTERVAL) + result = self.cli.read_input_registers(address=address, count=count, slave=self.slave) + if result.isError(): + raise RuntimeError(f"读取输入寄存器失败: address={address}, count={count}") + return result.registers + + def _write_register(self, address: int, value: int): + """写入单个寄存器""" + time.sleep(_INTERVAL) + result = self.cli.write_register(address=address, value=value, slave=self.slave) + if result.isError(): + raise RuntimeError(f"写入寄存器失败: address={address}, value={value}") + + def _write_registers(self, address: int, values: List[int]): + """写入多个寄存器""" + time.sleep(_INTERVAL) + result = self.cli.write_registers(address=address, values=values, slave=self.slave) + if result.isError(): + raise RuntimeError(f"写入多个寄存器失败: address={address}, values={values}") + + # -------------------------------------------------- + # 基础读取接口 + # -------------------------------------------------- + + def read_angles(self) -> List[int]: + """读取6个关节角度 (输入寄存器 0-5)""" + return self._read_input_registers(0, 6) + + def read_torques(self) -> List[int]: + """读取6个关节转矩 (输入寄存器 6-11)""" + return self._read_input_registers(6, 6) + + def read_speeds(self) -> List[int]: + """读取6个关节速度 (输入寄存器 12-17)""" + return self._read_input_registers(12, 6) + + def read_temperatures(self) -> List[int]: + """读取6个关节温度 (输入寄存器 18-23)""" + return self._read_input_registers(18, 6) + + def read_error_codes(self) -> List[int]: + """读取6个关节错误码 (输入寄存器 24-29)""" + return self._read_input_registers(24, 6) + + # -------------------------------------------------- + # 压力传感器接口 + # -------------------------------------------------- + + # def _pressure(self, finger: int) -> List[int]: + # """内部:选手指 → 读压力数据""" + # # 选择手指 (保持寄存器 36) + # self._write_register(36, finger) + # time.sleep(_INTERVAL) + # # 读取压力数据 (输入寄存器 52-122) + # return np.array(self._read_input_registers(52, 71)) + def _pressure(self, finger: int) -> np.ndarray: + """ + 6x12 (72点) 矩阵尺寸。 + Modbus 地址 60/62。 + """ + rows = 12 # 12 行 + cols = 6 # 6 列 + finger_size = rows * cols # 72 个数据点 + + # modbus 地址和计数 (按协议文档) + write_address = 36 # 写入手指选择 (保持寄存器) + read_address = 52 # 读取压力数据 (输入寄存器) + read_count = 71 # 读取 71 个寄存器 + skip_count = 0 # 不跳过数据点 + + # 0. 参数校验和手指写入值确定 + if finger < 1 or finger > 5: + raise ValueError(f"无效的手指编号: {finger}。手指编号应在 1 到 5 之间。") + + finger_write_value = finger + + # 1. 写入手指选择寄存器 (地址 36) + time.sleep(0.08) + wrsp = self.cli.write_register(address=write_address, value=finger_write_value, slave=self.slave) + if wrsp.isError(): + raise RuntimeError(f"写入手指选择 {finger} 到地址 {write_address} 失败: {wrsp}") + + # 写入后等待片刻 + time.sleep(0.08) + + # 2. 读取地址 52 的数据 + rrsp = self.cli.read_input_registers(address=read_address, count=read_count, slave=self.slave) + + if rrsp.isError(): + raise RuntimeError(f"读取地址 {read_address} 压力数据失败: {rrsp}") + + registers_16bit: List[int] = rrsp.registers + + # 3. 核心数据处理 + # a. 提取低 8 位数据 (得到 71 个 8 位数据点) + final_data_71 = [reg_value & 255 for reg_value in registers_16bit] + + # b. 不跳过数据点 (按协议) + effective_data = np.array(final_data_71, dtype=np.uint8) + + # c. 截取当前手指的矩阵数据 (71 个有效点中截取 72 个点,可能需要多读) + # 注: 协议返回 71 个点,手指数 1-5,每个手指需要 72 点 + # 这里取全部数据 + start_idx = 0 + end_idx = min(len(effective_data), finger_size) # 取较小值 + + finger_data_flat = effective_data[start_idx:end_idx] + + # d. 验证数据长度 + if finger_data_flat.size < finger_size: + # 如果数据不足,尝试多读一些 + rrsp2 = self.cli.read_input_registers(address=read_address + read_count, count=10, slave=self.slave) + if not rrsp2.isError(): + extra_data = [reg_value & 255 for reg_value in rrsp2.registers] + finger_data_flat = np.concatenate([finger_data_flat, np.array(extra_data, dtype=np.uint8)]) + + # 最终确保有足够数据 + if finger_data_flat.size >= finger_size: + finger_data_flat = finger_data_flat[:finger_size] + else: + raise ValueError( + f"数据提取失败。期望 {finger_size} 点 ({rows}x{cols})," + f"但仅获取到 {finger_data_flat.size} 点。请检查协议。" + ) + + # e. 重塑为二维矩阵 (12 行 6 列) + finger_matrix = finger_data_flat.reshape((rows, cols)) + + return finger_matrix + + def read_pressure_thumb(self) -> np.ndarray: + """读取大拇指压力数据""" + return np.array(self._pressure(1), dtype=np.uint8) + + def read_pressure_index(self) -> np.ndarray: + """读取食指压力数据""" + return np.array(self._pressure(2), dtype=np.uint8) + + def read_pressure_middle(self) -> np.ndarray: + """读取中指压力数据""" + return np.array(self._pressure(3), dtype=np.uint8) + + def read_pressure_ring(self) -> np.ndarray: + """读取无名指压力数据""" + return np.array(self._pressure(4), dtype=np.uint8) + + def read_pressure_little(self) -> np.ndarray: + """读取小拇指压力数据""" + return np.array(self._pressure(5), dtype=np.uint8) + + # -------------------------------------------------- + # 版本信息接口 + # -------------------------------------------------- + + def read_versions(self) -> Dict[str, int]: + """读取版本信息 (输入寄存器 148-155)""" + result = self._read_input_registers(148, 8) + + return { + "hand_freedom": result[0], + "hand_version": result[1], + "hand_number": result[2], + "hand_direction": result[3], + "software_version_major": result[4], + "software_version_minor": result[5] if len(result) > 5 else 0, + "software_version_revision": result[6] if len(result) > 6 else 0, + "hardware_version": result[7] if len(result) > 7 else 0 + } + + # -------------------------------------------------- + # 写入接口 + # -------------------------------------------------- + + def write_angles(self, vals: List[int]): + """设置6个关节角度 (保持寄存器 0-5)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(0, vals) + + def write_torques(self, vals: List[int]): + """设置6个关节转矩 (保持寄存器 6-11)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(6, vals) + + def write_speeds(self, vals: List[int]): + """设置6个关节速度 (保持寄存器 12-17)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(12, vals) + + # -------------------------------------------------- + # 上下文管理 + # -------------------------------------------------- + + def close(self): + """关闭连接""" + if self.connected: + self.cli.close() + self.connected = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # -------------------------------------------------- + # API固定接口函数 + # -------------------------------------------------- + + def is_valid_6xuint8(self, lst) -> bool: + """验证6个0-255的整数列表""" + if len(lst) != 6: + return False + return all(isinstance(x, int) and 0 <= x <= 255 for x in lst) + + def set_joint_positions(self, joint_angles=None): + """设置关节位置""" + joint_angles = joint_angles or [0] * 6 + self.write_angles(joint_angles) + + def set_speed(self, speed=None): + """设置速度""" + speed = speed or [200] * 6 + self.write_speeds(speed) + + def set_torque(self, torque=None): + """设置扭矩""" + torque = torque or [200] * 6 + self.write_torques(torque) + + def set_current(self, current=None): + """设置电流 (L6不支持)""" + print("当前L6不支持设置电流", flush=True) + + def get_version(self) -> list: + """获取版本信息""" + versions = self.read_versions() + return [ + versions.get("hand_freedom", 0), + versions.get("hand_version", 0), + versions.get("hand_number", 0), + versions.get("hand_direction", 0), + versions.get("software_version_major", 0), + versions.get("hardware_version", 0) + ] + + def get_current(self): + """获取电流 (L6不支持)""" + print("当前L6不支持获取电流", flush=True) + return [] + + def get_state(self) -> list: + """获取关节状态""" + return self.read_angles() + + def get_state_for_pub(self) -> list: + return self.get_state() + + def get_current_status(self) -> list: + return self.get_state() + + def get_speed(self) -> list: + """获取当前速度""" + return self.read_speeds() + + def get_joint_speed(self) -> list: + return self.get_speed() + + def get_touch_type(self) -> int: + """获取压感类型 (2=矩阵式)""" + return 2 + + def get_normal_force(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_tangential_force(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_approach_inc(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_touch(self) -> list: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return self._pressure(1) + + def get_index_matrix_touch(self,sleep_time=0): + return self._pressure(2) + + def get_middle_matrix_touch(self,sleep_time=0): + return self._pressure(3) + + def get_ring_matrix_touch(self,sleep_time=0): + return self._pressure(4) + + def get_little_matrix_touch(self,sleep_time=0): + return self._pressure(5) + + def get_matrix_touch(self) -> list: + """获取压感数据:矩阵式""" + return [self._pressure(1), self._pressure(2), self._pressure(3), + self._pressure(4), self._pressure(5)] + + def get_matrix_touch_v2(self) -> list: + """获取压感数据:矩阵式""" + return self.get_matrix_touch() + + def get_torque(self) -> list: + """获取当前扭矩""" + return self.read_torques() + + def get_temperature(self) -> list: + """获取当前电机温度""" + return self.read_temperatures() + + def get_fault(self) -> list: + """获取当前电机故障码""" + return self.read_error_codes() + + def get_serial_number(self): + return [0] * 6 + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + # -------------------------------------------------- + # 便捷方法 + # -------------------------------------------------- + + def relax(self): + """所有手指伸直""" + self.set_joint_positions([255] * 6) + + def fist(self): + """所有手指握拳""" + self.set_joint_positions([0] * 6) + + def dump_status(self): + """打印状态信息""" + print("=" * 50) + print("L6机械手状态信息") + print("=" * 50) + + try: + # 关节状态 + angles = self.read_angles() + torques = self.read_torques() + speeds = self.read_speeds() + temps = self.read_temperatures() + errors = self.read_error_codes() + + print("关节状态:") + for i, name in enumerate(self.JOINT_NAMES): + print(f" {name:15s}: 角度={angles[i]:3d}, 扭矩={torques[i]:3d}, " + f"速度={speeds[i]:3d}, 温度={temps[i]:2d}℃, 错误={errors[i]:2d}") + + # 版本信息 + versions = self.read_versions() + print("\n版本信息:") + for key, value in versions.items(): + print(f" {key:20s}: {value}") + + # 压力传感器测试 + print("\n压力传感器测试:") + thumb_pressure = self.read_pressure_thumb() + print(f"大拇指压力数据长度: {len(thumb_pressure)}") + + except Exception as e: + print(f"读取状态时出错: {e}") + + print("=" * 50) + + +# ------------------- 演示程序 ------------------- +if __name__ == "__main__": + # 使用示例 + try: + with LinkerHandL6RS485(hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200) as hand: + print("连接成功!") + + # 打印状态信息 + hand.dump_status() + + # 测试基本控制 + print("\n测试控制功能...") + print("伸直手指...") + hand.relax() + time.sleep(2) + + print("握拳...") + hand.fist() + time.sleep(2) + + print("恢复伸直...") + hand.relax() + + # 测试压力传感器 + print("\n测试压力传感器...") + thumb_matrix = hand.get_thumb_matrix_touch() + print(f"大拇指压力数据: {len(thumb_matrix)}个点") + + # 获取所有手指压力数据 + all_matrices = hand.get_matrix_touch() + for i, name in enumerate(hand.FINGER_NAMES): + matrix = all_matrices[i] + print(f"{name}手指压力数据长度: {len(matrix)}") + + except Exception as e: + print(f"错误: {e}") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py.bak b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py.bak new file mode 100644 index 0000000..d68b621 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l6_rs485.py.bak @@ -0,0 +1,444 @@ +#!/usr/bin/env python3 +import os +import time +from pymodbus.client import ModbusSerialClient +from typing import List, Dict +import numpy as np + +_INTERVAL = 0.006 # 8 ms + +class LinkerHandL6RS485: + """L6机械手 Modbus-RTU 控制类""" + + # 6个关节名称 + JOINT_NAMES = ["thumb_pitch", "thumb_yaw", "index_pitch", + "middle_pitch", "ring_pitch", "little_pitch"] + + # 手指名称 + FINGER_NAMES = ["thumb", "index", "middle", "ring", "little"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + """ + 初始化L6机械手 + hand_id: 右手0x27(39), 左手0x28(40) + modbus_port: 串口设备路径 + baudrate: 波特率,固定115200 + """ + self.slave = hand_id + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=0.05 + ) + # pymodbus 3.5.1 需要显式连接 + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485连接失败,端口: {modbus_port}") + + def _read_input_registers(self, address: int, count: int) -> List[int]: + """读取输入寄存器""" + time.sleep(_INTERVAL) + result = self.cli.read_input_registers(address=address, count=count, slave=self.slave) + if result.isError(): + raise RuntimeError(f"读取输入寄存器失败: address={address}, count={count}") + return result.registers + + def _write_register(self, address: int, value: int): + """写入单个寄存器""" + time.sleep(_INTERVAL) + result = self.cli.write_register(address=address, value=value, slave=self.slave) + if result.isError(): + raise RuntimeError(f"写入寄存器失败: address={address}, value={value}") + + def _write_registers(self, address: int, values: List[int]): + """写入多个寄存器""" + time.sleep(_INTERVAL) + result = self.cli.write_registers(address=address, values=values, slave=self.slave) + if result.isError(): + raise RuntimeError(f"写入多个寄存器失败: address={address}, values={values}") + + # -------------------------------------------------- + # 基础读取接口 + # -------------------------------------------------- + + def read_angles(self) -> List[int]: + """读取6个关节角度 (输入寄存器 0-5)""" + return self._read_input_registers(0, 6) + + def read_torques(self) -> List[int]: + """读取6个关节转矩 (输入寄存器 6-11)""" + return self._read_input_registers(6, 6) + + def read_speeds(self) -> List[int]: + """读取6个关节速度 (输入寄存器 12-17)""" + return self._read_input_registers(12, 6) + + def read_temperatures(self) -> List[int]: + """读取6个关节温度 (输入寄存器 18-23)""" + return self._read_input_registers(18, 6) + + def read_error_codes(self) -> List[int]: + """读取6个关节错误码 (输入寄存器 24-29)""" + return self._read_input_registers(24, 6) + + # -------------------------------------------------- + # 压力传感器接口 + # -------------------------------------------------- + + # def _pressure(self, finger: int) -> List[int]: + # """内部:选手指 → 读压力数据""" + # # 选择手指 (保持寄存器 36) + # self._write_register(36, finger) + # time.sleep(_INTERVAL) + # # 读取压力数据 (输入寄存器 52-122) + # return np.array(self._read_input_registers(52, 71)) + def _pressure(self, finger: int) -> np.ndarray: + """ + 6x12 (72点) 矩阵尺寸。 + Modbus 地址 60/62。 + """ + rows = 12 # 12 行 + cols = 6 # 6 列 + finger_size = rows * cols # 72 个数据点 + + # modbus 地址和计数 + write_address = 60 # 写入手指选择 + read_address = 62 # 读取压力数据 + read_count = 96 # 读取 96 个寄存器 + skip_count = 10 # 跳过前 10 个校验点 + + # 0. 参数校验和手指写入值确定 + if finger < 1 or finger > 5: + raise ValueError(f"无效的手指编号: {finger}。手指编号应在 1 到 5 之间。") + + finger_write_value = finger + + # 1. 写入手指选择寄存器 (地址 60) + time.sleep(0.008) + wrsp = self.cli.write_register(address=write_address, value=finger_write_value, slave=self.slave) + if wrsp.isError(): + raise RuntimeError(f"写入手指选择 {finger} 到地址 {write_address} 失败: {wrsp}") + + # 写入后等待片刻 + time.sleep(0.008) + + # 2. 读取地址 62 的数据 + rrsp = self.cli.read_input_registers(address=read_address, count=read_count, slave=self.slave) + + if rrsp.isError(): + raise RuntimeError(f"读取地址 {read_address} 压力数据失败: {rrsp}") + + registers_16bit: List[int] = rrsp.registers + + # 3. 核心数据处理 + # a. 提取低 8 位数据 (得到 96 个 8 位数据点) + final_data_96 = [reg_value & 255 for reg_value in registers_16bit] + + # b. 跳过前 10 个校验/头部数据点 (得到 86 个有效数据点) + effective_data = np.array(final_data_96[skip_count:], dtype=np.uint8) + # c. 截取当前手指的矩阵数据 (从 86 个有效点中截取 72 个点) + start_idx = 0 + end_idx = finger_size # 72 + + finger_data_flat = effective_data[start_idx:end_idx] + + # d. 验证数据长度 + if finger_data_flat.size != finger_size: + raise ValueError( + f"数据提取失败。期望 {finger_size} 点 ({rows}x{cols})," + f"但仅截取到 {finger_data_flat.size} 点。请检查协议,确认地址 62 是否一次性返回了所有手指数据。" + ) + + # e. 重塑为二维矩阵 (12 行 6 列) + finger_matrix = finger_data_flat.reshape((rows, cols)) + + return finger_matrix + + def read_pressure_thumb(self) -> np.ndarray: + """读取大拇指压力数据""" + return np.array(self._pressure(1), dtype=np.uint8) + + def read_pressure_index(self) -> np.ndarray: + """读取食指压力数据""" + return np.array(self._pressure(2), dtype=np.uint8) + + def read_pressure_middle(self) -> np.ndarray: + """读取中指压力数据""" + return np.array(self._pressure(3), dtype=np.uint8) + + def read_pressure_ring(self) -> np.ndarray: + """读取无名指压力数据""" + return np.array(self._pressure(4), dtype=np.uint8) + + def read_pressure_little(self) -> np.ndarray: + """读取小拇指压力数据""" + return np.array(self._pressure(5), dtype=np.uint8) + + # -------------------------------------------------- + # 版本信息接口 + # -------------------------------------------------- + + def read_versions(self) -> Dict[str, int]: + """读取版本信息 (输入寄存器 148-155)""" + result = self._read_input_registers(148, 8) + + return { + "hand_freedom": result[0], + "hand_version": result[1], + "hand_number": result[2], + "hand_direction": result[3], + "software_version_major": result[4], + "software_version_minor": result[5] if len(result) > 5 else 0, + "software_version_revision": result[6] if len(result) > 6 else 0, + "hardware_version": result[7] if len(result) > 7 else 0 + } + + # -------------------------------------------------- + # 写入接口 + # -------------------------------------------------- + + def write_angles(self, vals: List[int]): + """设置6个关节角度 (保持寄存器 0-5)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(0, vals) + + def write_torques(self, vals: List[int]): + """设置6个关节转矩 (保持寄存器 6-11)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(6, vals) + + def write_speeds(self, vals: List[int]): + """设置6个关节速度 (保持寄存器 12-17)""" + vals = [int(x) for x in vals] + if not self.is_valid_6xuint8(vals): + raise ValueError("需要6个0-255的整数") + self._write_registers(12, vals) + + # -------------------------------------------------- + # 上下文管理 + # -------------------------------------------------- + + def close(self): + """关闭连接""" + if self.connected: + self.cli.close() + self.connected = False + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # -------------------------------------------------- + # API固定接口函数 + # -------------------------------------------------- + + def is_valid_6xuint8(self, lst) -> bool: + """验证6个0-255的整数列表""" + if len(lst) != 6: + return False + return all(isinstance(x, int) and 0 <= x <= 255 for x in lst) + + def set_joint_positions(self, joint_angles=None): + """设置关节位置""" + joint_angles = joint_angles or [0] * 6 + self.write_angles(joint_angles) + + def set_speed(self, speed=None): + """设置速度""" + speed = speed or [200] * 6 + self.write_speeds(speed) + + def set_torque(self, torque=None): + """设置扭矩""" + torque = torque or [200] * 6 + self.write_torques(torque) + + def set_current(self, current=None): + """设置电流 (L6不支持)""" + print("当前L6不支持设置电流", flush=True) + + def get_version(self) -> list: + """获取版本信息""" + versions = self.read_versions() + return [ + versions.get("hand_freedom", 0), + versions.get("hand_version", 0), + versions.get("hand_number", 0), + versions.get("hand_direction", 0), + versions.get("software_version_major", 0), + versions.get("hardware_version", 0) + ] + + def get_current(self): + """获取电流 (L6不支持)""" + print("当前L6不支持获取电流", flush=True) + return [] + + def get_state(self) -> list: + """获取关节状态""" + return self.read_angles() + + def get_state_for_pub(self) -> list: + return self.get_state() + + def get_current_status(self) -> list: + return self.get_state() + + def get_speed(self) -> list: + """获取当前速度""" + return self.read_speeds() + + def get_joint_speed(self) -> list: + return self.get_speed() + + def get_touch_type(self) -> int: + """获取压感类型 (2=矩阵式)""" + return 2 + + def get_normal_force(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_tangential_force(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_approach_inc(self) -> list: + """获取压感数据:点式""" + return [-1] * 5 + + def get_touch(self) -> list: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return self._pressure(1) + + def get_index_matrix_touch(self,sleep_time=0): + return self._pressure(2) + + def get_middle_matrix_touch(self,sleep_time=0): + return self._pressure(3) + + def get_ring_matrix_touch(self,sleep_time=0): + return self._pressure(4) + + def get_little_matrix_touch(self,sleep_time=0): + return self._pressure(5) + + def get_matrix_touch(self) -> list: + """获取压感数据:矩阵式""" + return [self._pressure(1), self._pressure(2), self._pressure(3), + self._pressure(4), self._pressure(5)] + + def get_matrix_touch_v2(self) -> list: + """获取压感数据:矩阵式""" + return self.get_matrix_touch() + + def get_torque(self) -> list: + """获取当前扭矩""" + return self.read_torques() + + def get_temperature(self) -> list: + """获取当前电机温度""" + return self.read_temperatures() + + def get_fault(self) -> list: + """获取当前电机故障码""" + return self.read_error_codes() + + def get_serial_number(self): + return [0] * 6 + + # -------------------------------------------------- + # 便捷方法 + # -------------------------------------------------- + + def relax(self): + """所有手指伸直""" + self.set_joint_positions([255] * 6) + + def fist(self): + """所有手指握拳""" + self.set_joint_positions([0] * 6) + + def dump_status(self): + """打印状态信息""" + print("=" * 50) + print("L6机械手状态信息") + print("=" * 50) + + try: + # 关节状态 + angles = self.read_angles() + torques = self.read_torques() + speeds = self.read_speeds() + temps = self.read_temperatures() + errors = self.read_error_codes() + + print("关节状态:") + for i, name in enumerate(self.JOINT_NAMES): + print(f" {name:15s}: 角度={angles[i]:3d}, 扭矩={torques[i]:3d}, " + f"速度={speeds[i]:3d}, 温度={temps[i]:2d}℃, 错误={errors[i]:2d}") + + # 版本信息 + versions = self.read_versions() + print("\n版本信息:") + for key, value in versions.items(): + print(f" {key:20s}: {value}") + + # 压力传感器测试 + print("\n压力传感器测试:") + thumb_pressure = self.read_pressure_thumb() + print(f"大拇指压力数据长度: {len(thumb_pressure)}") + + except Exception as e: + print(f"读取状态时出错: {e}") + + print("=" * 50) + + +# ------------------- 演示程序 ------------------- +if __name__ == "__main__": + # 使用示例 + try: + with LinkerHandL6RS485(hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200) as hand: + print("连接成功!") + + # 打印状态信息 + hand.dump_status() + + # 测试基本控制 + print("\n测试控制功能...") + print("伸直手指...") + hand.relax() + time.sleep(2) + + print("握拳...") + hand.fist() + time.sleep(2) + + print("恢复伸直...") + hand.relax() + + # 测试压力传感器 + print("\n测试压力传感器...") + thumb_matrix = hand.get_thumb_matrix_touch() + print(f"大拇指压力数据: {len(thumb_matrix)}个点") + + # 获取所有手指压力数据 + all_matrices = hand.get_matrix_touch() + for i, name in enumerate(hand.FINGER_NAMES): + matrix = all_matrices[i] + print(f"{name}手指压力数据长度: {len(matrix)}") + + except Exception as e: + print(f"错误: {e}") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l7_rs485.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l7_rs485.py new file mode 100644 index 0000000..274a1ae --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_l7_rs485.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +import time +from typing import List, Dict, Union +import numpy as np +from pymodbus.client import ModbusSerialClient +from pymodbus.exceptions import ModbusException + +# --- 协议常量和寄存器地址定义 (根据 O7 协议文件) --- + +# RS485 通信设置 +DEFAULT_BAUDRATE = 115200 + +# O7机械手七个可控关节的键名 (根据保持寄存器和输入寄存器地址 0-6) +O7_JOINT_KEYS = [ + "Thumb_Pitch", "Thumb_Yaw", "Index_Pitch", "Middle_Pitch", + "Ring_Pitch", "Little_Pitch", "Thumb_Roll" +] + +# 保持寄存器地址 (写操作 FC 16) +HR_ADDR = { + "Position_Start": 0, # 关节目标位置 (7 个寄存器: 0-6) + "Torque_Start": 7, # 关节目标转矩 (7 个寄存器: 7-13) + "Speed_Start": 14, # 关节目标速度 (7 个寄存器: 14-20) + "Pressure_Select": 42 # 压力传感器数据选择 (1 个寄存器) + # 21-41 为堵转保护阈值、时间和扭矩,暂未实现 +} + +# 输入寄存器地址 (读操作 FC 04) +IR_ADDR = { + "Current_Position_Start": 0, # 当前关节位置 (7 个寄存器: 0-6) + "Current_Torque_Start": 7, # 当前关节转矩 (7 个寄存器: 7-13) + "Current_Speed_Start": 14, # 当前关节速度 (7 个寄存器: 14-20) + "Current_Temperature_Start": 21, # 当前关节温度 (7 个寄存器: 21-27) + "Error_Code_Start": 28, # 当前关节错误码 (7 个寄存器: 28-34) + "Tip_Force_Start": 35, # 指尖力数据 (20 个寄存器: 35-54) + "Pressure_Data_Start": 57, # 压力传感器数据起始 (96 个寄存器: 57-152) + "Version_Start": 153 # 版本信息 (6 个寄存器: 153-158) +} + +# 辅助常量 +_JOINT_COUNT = 7 +_VERSION_COUNT = 6 +_TIP_FORCE_COUNT = 20 +_PRESSURE_REG_COUNT = 96 +_PRESSURE_ROWS = 12 # 从 IR 56 (0xC6) 推断 +_PRESSURE_COLS = 6 # 从 IR 56 (0xC6) 推断 +_PRESSURE_DATA_SIZE = _PRESSURE_ROWS * _PRESSURE_COLS # 72 +_PRESSURE_HEADER_SKIP = 10 # 假设跳过 10 个头部/校验字节 + +# 通信间隔时间 (使用 L10 参考中的 5ms) +_INTERVAL = 0.005 + + +class LinkerHandL7RS485: + """ + O7机械手 Modbus RTU (RS485) 控制类。 + 使用 pymodbus 3.5.1 版本和 O7 机械手协议。 + """ + def __init__(self, + hand_id: int = 0x27, + modbus_port: str = "/dev/ttyUSB0", + baudrate: int = DEFAULT_BAUDRATE, + timeout: float = 0.05): + """ + 初始化 Modbus 客户端。 + + :param hand_id: Modbus 从站地址 (0x27: 右手, 0x28: 左手) + :param modbus_port: 串口名称 + :param baudrate: 波特率 (默认为 115200) + :param timeout: 通信超时时间 (秒) + """ + self.slave = hand_id + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, # 确保与 pymodbus 3.x 兼容的写法 + timeout=timeout, + retries=3, # 重试次数 + retry_on_empty=True, + handle_local_echo=False, + method='rtu' + ) + + # 尝试连接 + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485 connect fail to {modbus_port} with ID {hex(hand_id)}.") + + print(f"O7机械手 Modbus ID {hex(hand_id)} 连接成功到 {modbus_port}。") + + + # -------------------------------------------------- + # 核心读写函数 (基于 pymodbus 3.5.1) + # -------------------------------------------------- + + def _read_input_registers(self, address: int, count: int) -> List[int]: + """封装 Modbus 读取输入寄存器 (FC 04) 操作。""" + time.sleep(_INTERVAL) + try: + rsp = self.cli.read_input_registers( + address=address, + count=count, + slave=self.slave + ) + + # 使用 L10 参考中验证过的 3.x 兼容错误检查 + if rsp.isError(): + raise RuntimeError(f"Modbus FC04 读取失败。地址: {address}, 错误: {rsp}") + + return rsp.registers + + except ModbusException as e: + # 捕获通信超时、CRC 错误等 Modbus 异常 + raise RuntimeError(f"Modbus 通信异常。地址: {address}, 错误: {e}") + except Exception as e: + raise RuntimeError(f"未知读取异常。地址: {address}, 错误: {e}") + + def _write_holding_registers(self, address: int, values: List[int]): + """封装 Modbus 写入保持寄存器 (FC 16) 操作。""" + time.sleep(_INTERVAL) + + # 批量写入 (FC 16) + if len(values) > 1: + write_func = self.cli.write_registers + # 单个写入 (FC 06) + elif len(values) == 1: + write_func = lambda address, values, slave: self.cli.write_register(address, values[0], slave) + else: + raise ValueError("写入值列表不能为空。") + + try: + rsp = write_func( + address=address, + values=values, + slave=self.slave + ) + + if rsp.isError(): + raise RuntimeError(f"Modbus FC16 写入失败。地址: {address}, 错误: {rsp}") + + except ModbusException as e: + raise RuntimeError(f"Modbus 通信异常。地址: {address}, 错误: {e}") + except Exception as e: + raise RuntimeError(f"未知写入异常。地址: {address}, 错误: {e}") + + # -------------------------------------------------- + # 读操作 (Read API) + # -------------------------------------------------- + + def get_joint_positions(self) -> Dict[str, int]: + """读取当前关节位置 (地址 0-6)。""" + registers = self._read_input_registers(IR_ADDR["Current_Position_Start"], _JOINT_COUNT) + #return dict(zip(O7_JOINT_KEYS, registers)) + return registers + + def get_current_torques(self) -> Dict[str, int]: + """读取当前关节转矩 (地址 7-13)。""" + registers = self._read_input_registers(IR_ADDR["Current_Torque_Start"], _JOINT_COUNT) + #return dict(zip(O7_JOINT_KEYS, registers)) + return registers + + def get_current_speeds(self) -> Dict[str, int]: + """读取当前关节速度 (地址 14-20)。""" + registers = self._read_input_registers(IR_ADDR["Current_Speed_Start"], _JOINT_COUNT) + #return dict(zip(O7_JOINT_KEYS, registers)) + return registers + + def get_temperatures(self) -> Dict[str, int]: + """读取当前关节温度 (地址 21-27)。""" + registers = self._read_input_registers(IR_ADDR["Current_Temperature_Start"], _JOINT_COUNT) + #return dict(zip(O7_JOINT_KEYS, registers)) + return registers + + def get_error_codes(self) -> Dict[str, int]: + """读取当前关节错误码 (地址 28-34)。""" + registers = self._read_input_registers(IR_ADDR["Error_Code_Start"], _JOINT_COUNT) + #return dict(zip(O7_JOINT_KEYS, registers)) + return registers + + def get_tip_forces(self) -> List[int]: + """读取指尖法向力、切向力等数据 (地址 35-54)。""" + return self._read_input_registers(IR_ADDR["Tip_Force_Start"], _TIP_FORCE_COUNT) + + def get_version(self) -> List[int]: + """读取版本信息 (地址 153-158)。""" + return self._read_input_registers(IR_ADDR["Version_Start"], _VERSION_COUNT) + + def get_pressure_matrix(self, finger_id: int) -> np.ndarray: + """ + 读取特定手指的压力传感器数据矩阵。 + + :param finger_id: 手指编号 (1: 大拇指, 2: 食指, 3: 中指, 4: 无名指, 5: 小拇指) + :return: 12x6 的压力数据矩阵 (np.ndarray) + """ + if not (1 <= finger_id <= 5): + raise ValueError(f"无效的手指编号: {finger_id}。应在 1 到 5 之间。") + + # 1. 写入手指选择寄存器 (HR 42) + # 使用单个写入 (FC 06) + self._write_holding_registers(HR_ADDR["Pressure_Select"], [finger_id]) + + # 2. 读取压力传感器数据 (IR 57, 96 个寄存器) + time.sleep(_INTERVAL) # 等待数据更新 + registers_16bit: List[int] = self._read_input_registers( + IR_ADDR["Pressure_Data_Start"], + _PRESSURE_REG_COUNT + ) + + # 3. 数据解析 (假设与 L10 类似的数据格式: 低 8 位有效,有头部数据) + + # a. 提取低 8 位数据 (得到 96 个 8 位数据点) + final_data_96 = [reg_value & 255 for reg_value in registers_16bit] + + # b. 跳过头部数据点 + effective_data = np.array(final_data_96[_PRESSURE_HEADER_SKIP:], dtype=np.uint8) + + # c. 截取当前手指的矩阵数据 (72 个点) + finger_data_flat = effective_data[:_PRESSURE_DATA_SIZE] + + if finger_data_flat.size != _PRESSURE_DATA_SIZE: + raise ValueError( + f"压力数据提取失败。期望 {_PRESSURE_DATA_SIZE} 点," + f"但仅截取到 {finger_data_flat.size} 点。请检查协议解析逻辑。" + ) + + # d. 重塑为二维矩阵 (12 行 6 列) + finger_matrix = finger_data_flat.reshape((_PRESSURE_ROWS, _PRESSURE_COLS)) + return finger_matrix + + + # -------------------------------------------------- + # 写操作 (Write API) + # -------------------------------------------------- + + def set_joint_positions(self, joint_angles: List[int]): + """ + 设置所有 7 个关节的目标位置 (地址 0-6)。 + :param joint_angles: 7 个 0-255 的整数值列表 + """ + if len(joint_angles) != _JOINT_COUNT: + raise ValueError(f"需要 {_JOINT_COUNT} 个关节位置值,提供了 {len(joint_angles)} 个。") + self._write_holding_registers(HR_ADDR["Position_Start"], joint_angles) + + def set_torques(self, torques: List[int]): + """ + 设置所有 7 个关节的目标转矩 (地址 7-13)。 + :param torques: 7 个 0-255 的整数值列表 + """ + if len(torques) != _JOINT_COUNT: + raise ValueError(f"需要 {_JOINT_COUNT} 个关节转矩值,提供了 {len(torques)} 个。") + self._write_holding_registers(HR_ADDR["Torque_Start"], torques) + + + def set_speeds(self, speeds: List[int]): + """ + 设置所有 7 个关节的目标速度 (地址 14-20)。 + :param speeds: 7 个 0-255 的整数值列表 + """ + if len(speeds) != _JOINT_COUNT: + raise ValueError(f"需要 {_JOINT_COUNT} 个关节速度值,提供了 {len(speeds)} 个。") + self._write_holding_registers(HR_ADDR["Speed_Start"], speeds) + + def set_speed(self, speed:List[int] = [200] * 7): + self.set_speeds(speed) + + def set_torque(self, torque: List[int] = [250] * 7): + self.set_torques(torque) + + def set_current(self, current=None): + print("当前L7不支持设置电流", flush=True) + + def get_current(self): + #print("当前L7不支持获取电流", flush=True) + return [-1] * 7 + + def get_state(self) -> List[int]: + return self.get_joint_positions() + + + def get_state_for_pub(self) -> List[int]: + return self.get_joint_positions() + + def get_current_status(self) -> List[int]: + return self.get_joint_positions() + + def get_speed(self) -> List[int]: + return self.get_current_speeds() + + def get_joint_speed(self) -> List[int]: + return self.get_speed() + + def get_touch_type(self) -> int: + return 2 + + def get_normal_force(self) -> List[int]: + return [-1] * 5 + + def get_tangential_force(self) -> List[int]: + return [-1] * 5 + + def get_approach_inc(self) -> List[int]: + return [-1] * 5 + + def get_touch(self) -> List[int]: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return self.get_pressure_matrix(finger_id=1) + + def get_index_matrix_touch(self,sleep_time=0): + return self.get_pressure_matrix(finger_id=2) + + def get_middle_matrix_touch(self,sleep_time=0): + return self.get_pressure_matrix(finger_id=3) + + def get_ring_matrix_touch(self,sleep_time=0): + return self.get_pressure_matrix(finger_id=4) + + def get_little_matrix_touch(self,sleep_time=0): + return self.get_pressure_matrix(finger_id=5) + + def get_matrix_touch(self) -> List[List[int]]: + return self.get_thumb_matrix_touch(),self.get_index_matrix_touch(), self.get_middle_matrix_touch(), self.get_ring_matrix_touch(), self.get_little_matrix_touch() + + def get_matrix_touch_v2(self) -> List[List[int]]: + return self.get_matrix_touch() + + def get_torque(self) -> List[int]: + return self.get_current_torques() + + def get_temperature(self) -> List[int]: + return self.get_temperatures() + + def get_fault(self) -> List[int]: + return self.get_error_codes() + + def get_serial_number(self): + return [0] * 6 + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch", "thumb_cmc_roll"] + + def show_fun_table(self): + pass + + def clear_faults(self): + pass + # -------------------------------------------------- + # 上下文管理 + # -------------------------------------------------- + + def close(self): + """断开 Modbus 连接。""" + if self.connected: + self.cli.close() + self.connected = False + print("Modbus 连接已断开。") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + +# ------------------- Demo/使用示例 ------------------- +if __name__ == "__main__": + # --- 配置区域 --- + # 右手 Modbus ID: 0x27 (39) + # 左手 Modbus ID: 0x28 (40) + TARGET_HAND_ID = 0x28 # <--- 请根据需要修改为 0x27 或 0x28 + PORT = "/dev/ttyUSB0" # <--- 请修改为您的实际串口,例如 'COM3' + + try: + # 使用上下文管理器,确保连接自动关闭 + with LinkerHandL7RS485(hand_id=TARGET_HAND_ID, modbus_port=PORT) as hand: + print("\n--- 1. 读取当前状态 ---") + + # 读取当前关节位置、速度、转矩 + angles = hand.get_joint_positions() + print(f"当前关节位置 (7DOF): {angles}") + + speeds = hand.get_current_speeds() + print(f"当前关节速度: {speeds}") + + # 读取传感器和错误信息 + temps = hand.get_temperatures() + print(f"关节温度: {temps}") + + errors = hand.get_error_codes() + print(f"关节错误码: {errors}") + + # 读取版本 + version_info = hand.get_version() + print(f"版本信息 (Hand_freedom, ..., hardware_version): {version_info}") + + # --- 2. 写入指令示例 --- + print("\n--- 2. 写入指令示例 (设置所有关节到 128) ---") + + # 假设要将所有关节位置设置到中间值 128 + target_angles = [128] * _JOINT_COUNT + hand.set_joint_positions(target_angles) + print(f"写入目标角度: {target_angles}") + + # 假设要设置所有关节的速度到 100 + target_speeds = [100] * _JOINT_COUNT + hand.set_speeds(target_speeds) + print(f"写入目标速度: {target_speeds}") + + # --- 3. 压力传感器读取示例 --- + print("\n--- 3. 压力传感器读取 (大拇指 1) ---") + thumb_matrix = hand.get_pressure_matrix(finger_id=1) + print(f"大拇指压力矩阵 (12x6):") + print(thumb_matrix) + + except ConnectionError as e: + print(f"致命错误: 连接失败。{e}") + except RuntimeError as e: + print(f"致命错误: Modbus 操作失败。{e}") + except Exception as e: + print(f"捕获到未知异常: {e}") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py new file mode 100644 index 0000000..98240fd --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py @@ -0,0 +1,671 @@ +#!/usr/bin/env python3 +""" +O6 机械手 Modbus-RTU 控制类 (基于 pymodbus 3.5.1) +""" + +import os +import time +from typing import List, Dict, Any # 引入 Any 来表示灵活的输入类型 +import numpy as np +import logging +from threading import Lock # 用于线程安全和总线仲裁 + +# 导入 pymodbus 客户端 +from pymodbus.client import ModbusSerialClient +from struct import error as StructError + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s] %(levelname)-8s %(message)s", + datefmt="%H:%M:%S" +) + +# ------------------------------------------------------------------ +# 读输入寄存器地址枚举(功能码 04,只读)- 按照 O6 协议文档定义 +# ------------------------------------------------------------------ +REG_RD_CURRENT_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255,小=弯,大=伸) +REG_RD_CURRENT_THUMB_YAW = 1 # 大拇指横摆角度(0-255,小=靠掌心,大=远离) +REG_RD_CURRENT_INDEX_PITCH = 2 # 食指弯曲角度 +REG_RD_CURRENT_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_RD_CURRENT_RING_PITCH = 4 # 无名指弯曲角度 +REG_RD_CURRENT_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_RD_CURRENT_THUMB_TORQUE = 6 # 大拇指弯曲转矩(0-255) +REG_RD_CURRENT_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_RD_CURRENT_INDEX_TORQUE = 8 # 食指转矩 +REG_RD_CURRENT_MIDDLE_TORQUE = 9 # 中指转矩 +REG_RD_CURRENT_RING_TORQUE = 10 # 无名指转矩 +REG_RD_CURRENT_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_RD_CURRENT_THUMB_SPEED = 12 # 大拇指弯曲速度(0-255) +REG_RD_CURRENT_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_RD_CURRENT_INDEX_SPEED = 14 # 食指速度 +REG_RD_CURRENT_MIDDLE_SPEED = 15 # 中指速度 +REG_RD_CURRENT_RING_SPEED = 16 # 无名指速度 +REG_RD_CURRENT_LITTLE_SPEED = 17 # 小拇指速度 +REG_RD_THUMB_TEMP = 18 # 大拇指弯曲温度(0-70℃) +REG_RD_THUMB_YAW_TEMP = 19 # 大拇指横摆温度 +REG_RD_INDEX_TEMP = 20 # 食指温度 +REG_RD_MIDDLE_TEMP = 21 # 中指温度 +REG_RD_RING_TEMP = 22 # 无名指温度 +REG_RD_LITTLE_TEMP = 23 # 小拇指温度 +REG_RD_THUMB_ERROR = 24 # 大拇指错误码 +REG_RD_THUMB_YAW_ERROR = 25 # 大拇指横摆错误码 +REG_RD_INDEX_ERROR = 26 # 食指错误码 +REG_RD_MIDDLE_ERROR = 27 # 中指错误码 +REG_RD_RING_ERROR = 28 # 无名指错误码 +REG_RD_LITTLE_ERROR = 29 # 小拇指错误码 + +# 版本号/设备编号寄存器(地址 30-44,共15个寄存器) +REG_RD_HAND_FREEDOM = 30 # Hand_freedom - 设备编号 / 自由度(与机械手上标签相同) +REG_RD_HAND_VERSION = 31 # hand_version - 手版本 +REG_RD_HAND_NUMBER_HIGH = 32 # hand_number_高位 - 设备编号(高字节) +REG_RD_HAND_NUMBER_MID = 33 # hand_number_中位 - 设备编号(中字节) +REG_RD_HAND_NUMBER_LOW = 34 # hand_number_低位 - 设备编号(低字节) +REG_RD_HAND_DIRECTION = 35 # hand_direction - 手方向(左/右) +REG_RD_HARDWARE_VERSION_HIGH = 36 # hardware_version_高位 - 硬件版本(高字节) +REG_RD_HARDWARE_VERSION_MID = 37 # hardware_version_中位 - 硬件版本(中字节) +REG_RD_HARDWARE_VERSION_LOW = 38 # hardware_version_低位 - 硬件版本(低字节) +REG_RD_SOFTWARE_VERSION_HIGH = 39 # software_version_高位 - 软件版本(高字节) +REG_RD_SOFTWARE_VERSION_MID = 40 # software_version_中位 - 软件版本(中字节) +REG_RD_SOFTWARE_VERSION_LOW = 41 # software_version_低位 - 软件版本(低字节) +REG_RD_MECHANICAL_VERSION_HIGH = 42 # mechanical_version_高位 - 机械版本(高字节) +REG_RD_MECHANICAL_VERSION_MID = 43 # mechanical_version_中位 - 机械版本(中字节) +REG_RD_MECHANICAL_VERSION_LOW = 44 # mechanical_version_低位 - 机械版本(低字节) + +# 力传感器寄存器(地址 45-87+,动态范围) +REG_RD_PRESSURE_SENSING_ID = 45 # Pressure_Sensing_ID - 压力传感器ID (0-5) +REG_RD_PRESSURE_SENSING_SPEC = 46 # Pressure_Sensing_Specifications - 传感器数据规格 + + +# ------------------------------------------------------------------ +# 写保持寄存器地址枚举(功能码 16,读写)- 保持原样 +# ------------------------------------------------------------------ +REG_WR_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255) +REG_WR_THUMB_YAW = 1 # 大拇指横摆角度 +REG_WR_INDEX_PITCH = 2 # 食指弯曲角度 +REG_WR_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_WR_RING_PITCH = 4 # 无名指弯曲角度 +REG_WR_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_WR_THUMB_TORQUE = 6 # 大拇指弯曲转矩 +REG_WR_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_WR_INDEX_TORQUE = 8 # 食指转矩 +REG_WR_MIDDLE_TORQUE = 9 # 中指转矩 +REG_WR_RING_TORQUE = 10 # 无名指转矩 +REG_WR_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_WR_THUMB_SPEED = 12 # 大拇指弯曲速度 +REG_WR_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_WR_INDEX_SPEED = 14 # 食指速度 +REG_WR_MIDDLE_SPEED = 15 # 中指速度 +REG_WR_RING_SPEED = 16 # 无名指速度 +REG_WR_LITTLE_SPEED = 17 # 小拇指速度 + + +class LinkerHandO6RS485: + """O6 机械手 Modbus-RTU 控制类,使用 pymodbus 3.5.1""" + + TTL_TIMEOUT = 0.15 # 串口超时 + FRAME_GAP = 0.030 # 30 ms + + # KEYS for easy indexing + JOINT_KEYS = ["thumb_pitch", "thumb_yaw", "index_pitch", + "middle_pitch", "ring_pitch", "little_pitch"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + self._id = hand_id + self._last_ts = 0.0 # 上一次帧结束时间 + self._lock = Lock() # 总线访问锁 + + # 使用 pymodbus 3.x 客户端 + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=self.TTL_TIMEOUT, + handle_local_echo=False + ) + + try: + logging.info(f"Connecting to Modbus RTU on {modbus_port}...") + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485 connect fail to {modbus_port}") + logging.info("Connection successful.") + except Exception as e: + logging.error(f"Initialization failed: {e}") + raise + + # ---------------------------------------------------------- + # 辅助方法 + # ---------------------------------------------------------- + def _bus_free(self): + """保证距离上一帧 ≥ 30 ms""" + with self._lock: + elapse = time.perf_counter() - self._last_ts + if elapse < self.FRAME_GAP: + time.sleep(self.FRAME_GAP - elapse) + + def _execute_read(self, address: int, count: int) -> List[int]: + """执行 Modbus 读取操作 (功能码 04), 带总线仲裁。""" + self._bus_free() + + rsp = self.cli.read_input_registers( + address=address, + count=count, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Read Failed (Addr={address}, Count={count}): {rsp}") + + # 确保返回的值是 Python 原生整数 + return [int(reg) for reg in rsp.registers] + + def _execute_write(self, address: int, values: List[int]): + """执行 Modbus 批量写入操作 (功能码 16), 带总线仲裁。""" + self._bus_free() + + # values 必须是 Python 原生整数列表 + rsp = self.cli.write_registers( + address=address, + values=values, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Write Failed (Addr={address}, Values={values}): {rsp}") + + # ---------------------------------------------------------- + # 批量读取和数据封装(优化通信效率) + # ---------------------------------------------------------- + def read_all_angles(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_PITCH, 6) + + def read_all_torques(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_TORQUE, 6) + + def read_all_speeds(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_SPEED, 6) + + def read_all_temperatures(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_TEMP, 6) + + def read_all_errors(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_ERROR, 6) + + # ---------------------------------------------------------- + # 版本号/设备编号读取(按照协议文档:地址30-44,共15个寄存器) + # ---------------------------------------------------------- + def read_all_versions(self) -> str: + """一次性读取全部15个寄存器 (地址30-44),返回以 '.' 连接的字符串。 + + 按 O6 协议文档的版本号格式返回: + hand_freedom.hand_version.hand_number_high.hand_number_mid.hand_number_low + .hand_direction.hardware_ver_hardware_ver_m.hardware_ver_l + .software_ver_h.software_ver_m.software_ver_l + .mechanical_ver_h.mechanical_ver_m.mechanical_ver_l + + 例如: "6.1.001.002.003.0.1.2.3.4.5.6.7.8.9" + """ + raw = self._execute_read(REG_RD_HAND_FREEDOM, 15) + return ".".join(str(v) for v in raw) + + # ---------------------------------------------------------- + # 基于 read_all_versions() 的设备编号解析方法 + # ---------------------------------------------------------- + def _parse_versions(self): + """解析 read_all_versions() 返回的字符串为字典""" + parts = self.read_all_versions().split(".") + if len(parts) < 15: + return {} + return { + "hand_freedom": int(parts[0]), + "hand_version": int(parts[1]), + "hand_number_high": int(parts[2]), + "hand_number_mid": int(parts[3]), + "hand_number_low": int(parts[4]), + "hand_direction": int(parts[5]), + "hw_ver_high": int(parts[6]), + "hw_ver_mid": int(parts[7]), + "hw_ver_low": int(parts[8]), + "sw_ver_high": int(parts[9]), + "sw_ver_mid": int(parts[10]), + "sw_ver_low": int(parts[11]), + "mech_ver_high": int(parts[12]), + "mech_ver_mid": int(parts[13]), + "mech_ver_low": int(parts[14]), + } + + def get_device_number(self) -> str: + """获取设备编号(与机械手上标签相同)。格式:高位+中位+低位 拼接的字符串。""" + v = self._parse_versions() + if not v: + return "0" + high, mid, low = v["hand_number_high"], v["hand_number_mid"], v["hand_number_low"] + # 将每个字节格式化为无前导零的整数(与标签显示一致) + return f"{high}{mid}{low}" + + def get_device_number_value(self) -> int: + """获取设备编号数值""" + v = self._parse_versions() + if not v: + return 0 + high, mid, low = v["hand_number_high"], v["hand_number_mid"], v["hand_number_low"] + return high * 65536 + mid * 256 + low + + def get_hardware_version(self) -> str: + """获取硬件版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['hw_ver_high']}.{v['hw_ver_mid']}.{v['hw_ver_low']}" + + def get_software_version(self) -> str: + """获取软件版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['sw_ver_high']}.{v['sw_ver_mid']}.{v['sw_ver_low']}" + + def get_mechanical_version(self) -> str: + """获取机械版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['mech_ver_high']}.{v['mech_ver_mid']}.{v['mech_ver_low']}" + + def get_hand_freedom(self) -> int: + """获取自由度(与机械手上标签相同)""" + v = self._parse_versions() + return v.get("hand_freedom", 0) + + def get_hand_version_raw(self) -> int: + """获取手版本原始值""" + v = self._parse_versions() + return v.get("hand_version", 0) + + def get_hand_direction(self) -> str: + """获取手方向,转换为字符:76→'L', 82→'R'""" + v = self._parse_versions() + val = v.get("hand_direction", 0) + if val in (76, 82): + return chr(val) + # fallback: 直接转为字符(如果值在可打印范围内) + return chr(val) if 32 < val < 128 else f'Unknown({val})' + + + # ---------------------------------------------------------- + # 只读属性(单个寄存器读取) + # ---------------------------------------------------------- + def _read_reg(self, addr: int) -> int: + """读单个输入寄存器(功能码 04),带 30 ms 帧间隔""" + return self._execute_read(addr, 1)[0] + + def get_thumb_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_PITCH) + def get_thumb_yaw(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW) + def get_index_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_PITCH) + def get_middle_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_PITCH) + def get_ring_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_PITCH) + def get_little_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_PITCH) + + def get_thumb_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_TORQUE) + def get_thumb_yaw_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_TORQUE) + def get_index_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_TORQUE) + def get_middle_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_TORQUE) + def get_ring_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_TORQUE) + def get_little_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_TORQUE) + + def get_thumb_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_SPEED) + def get_thumb_yaw_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_SPEED) + def get_index_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_SPEED) + def get_middle_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_SPEED) + def get_ring_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_SPEED) + def get_little_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_SPEED) + + def get_thumb_temp(self) -> int: return self._read_reg(REG_RD_THUMB_TEMP) + def get_thumb_yaw_temp(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_TEMP) + def get_index_temp(self) -> int: return self._read_reg(REG_RD_INDEX_TEMP) + def get_middle_temp(self) -> int: return self._read_reg(REG_RD_MIDDLE_TEMP) + def get_ring_temp(self) -> int: return self._read_reg(REG_RD_RING_TEMP) + def get_little_temp(self) -> int: return self._read_reg(REG_RD_LITTLE_TEMP) + + def get_thumb_error(self) -> int: return self._read_reg(REG_RD_THUMB_ERROR) + def get_thumb_yaw_error(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_ERROR) + def get_index_error(self) -> int: return self._read_reg(REG_RD_INDEX_ERROR) + def get_middle_error(self) -> int: return self._read_reg(REG_RD_MIDDLE_ERROR) + def get_ring_error(self) -> int: return self._read_reg(REG_RD_RING_ERROR) + def get_little_error(self) -> int: return self._read_reg(REG_RD_LITTLE_ERROR) + + + # ---------------------------------------------------------- + # 批量 Getter (使用 read_all_... 方法) + # ---------------------------------------------------------- + def get_state(self) -> List[int]: + """获取手指电机状态 (角度)""" + return self.read_all_angles() + + def get_torque(self) -> List[int]: + """获取当前扭矩""" + return self.read_all_torques() + + def get_speed(self) -> List[int]: + """获取当前速度""" + return self.read_all_speeds() + + def get_temperature(self) -> List[int]: + """获取当前电机温度""" + return self.read_all_temperatures() + + def get_fault(self) -> List[int]: + """获取当前电机故障码""" + return self.read_all_errors() + + def get_version(self) -> str: + """获取当前固件版本号(已转换为字符串格式)""" + return self.read_all_versions() + + + # ---------------------------------------------------------- + # 写保持寄存器 (单个寄存器写入) + # ---------------------------------------------------------- + def _write_reg(self, addr: int, value: int): + """写单个保持寄存器(功能码 16),带 30 ms 帧间隔""" + if not 0 <= value <= 255: + raise ValueError("value must be 0-255") + + # 确保 value 是 Python 原生 int + self._execute_write(addr, [int(value)]) + + def _write_regs(self, addr: int, values: List[int]): + """写多个保持寄存器(功能码 16),带 30 ms 帧间隔""" + # 此时 values 应该已经是经过 is_valid_6xuint8 验证并转换的 Python int 列表 + if not all(0 <= v <= 255 for v in values): + # 这行理论上不应触发,因为上层调用已校验 + raise ValueError("All values must be 0-255") + self._execute_write(addr, values) + + + def set_thumb_pitch(self, v: int): self._write_reg(REG_WR_THUMB_PITCH, v) + def set_thumb_yaw(self, v: int): self._write_reg(REG_WR_THUMB_YAW, v) + def set_index_pitch(self, v: int): self._write_reg(REG_WR_INDEX_PITCH, v) + def set_middle_pitch(self, v: int): self._write_reg(REG_WR_MIDDLE_PITCH, v) + def set_ring_pitch(self, v: int): self._write_reg(REG_WR_RING_PITCH, v) + def set_little_pitch(self, v: int): self._write_reg(REG_WR_LITTLE_PITCH, v) + + def set_thumb_torque(self, v: int): self._write_reg(REG_WR_THUMB_TORQUE, v) + def set_thumb_yaw_torque(self, v: int): self._write_reg(REG_WR_THUMB_YAW_TORQUE, v) + def set_index_torque(self, v: int): self._write_reg(REG_WR_INDEX_TORQUE, v) + def set_middle_torque(self, v: int): self._write_reg(REG_WR_MIDDLE_TORQUE, v) + def set_ring_torque(self, v: int): self._write_reg(REG_WR_RING_TORQUE, v) + def set_little_torque(self, v: int): self._write_reg(REG_WR_LITTLE_TORQUE, v) + + def set_thumb_speed(self, v: int): self._write_reg(REG_WR_THUMB_SPEED, v) + def set_thumb_yaw_speed(self, v: int): self._write_reg(REG_WR_THUMB_YAW_SPEED, v) + def set_index_speed(self, v: int): self._write_reg(REG_WR_INDEX_SPEED, v) + def set_middle_speed(self, v: int): self._write_reg(REG_WR_MIDDLE_SPEED, v) + def set_ring_speed(self, v: int): self._write_reg(REG_WR_RING_SPEED, v) + def set_little_speed(self, v: int): self._write_reg(REG_WR_LITTLE_SPEED, v) + + # ---------------------------------------------------------- + # 固定函数 (采用批量写入优化) + # ---------------------------------------------------------- + def is_valid_6xuint8(self, lst: List[Any]) -> bool: + """ + 验证6个0-255的整数列表。 + 允许输入包含浮点数、NumPy整数等可转换为 int 的类型,并进行范围校验。 + """ + if not (isinstance(lst, list) and len(lst) == 6): + return False + + try: + # 关键:尝试将所有元素转换为 Python 原生 int + int_values = [int(v) for v in lst] + except (ValueError, TypeError): + # 转换失败,列表中包含不可转换的元素 + return False + + # 校验转换后的整数列表是否在 0-255 范围内 + return all(0 <= x <= 255 for x in int_values) + + def set_joint_positions(self, joint_angles: List[Any] = None): + joint_angles = joint_angles or [0] * 6 + + if not self.is_valid_6xuint8(joint_angles): + logging.error(f"Invalid joint angles received: {joint_angles}") + raise ValueError("Joint angles must be a list of 6 values between 0 and 255 (convertible to int).") + + # 强制转换为 Modbus 兼容的 Python 原生 int 列表 + int_angles = [int(v) for v in joint_angles] + + # 批量写入 6 个角度寄存器 (从 REG_WR_THUMB_PITCH 地址 0 开始, count=6) + self._write_regs(REG_WR_THUMB_PITCH, int_angles) + + def set_speed(self, speed: List[Any] = None): + speed = speed or [200] * 6 + if not self.is_valid_6xuint8(speed): + logging.error(f"Invalid speed values received: {speed}") + raise ValueError("Speed values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_speed = [int(v) for v in speed] + self._write_regs(REG_WR_THUMB_SPEED, int_speed) + + def set_torque(self, torque: List[Any] = None): + torque = torque or [200] * 6 + if not self.is_valid_6xuint8(torque): + logging.error(f"Invalid torque values received: {torque}") + raise ValueError("Torque values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_torque = [int(v) for v in torque] + self._write_regs(REG_WR_THUMB_TORQUE, int_torque) + + # ... (其他固定函数保持不变) ... + + def set_current(self, current: List[int] = None): + print("当前O6不支持设置电流", flush=True) + pass + + def get_state_for_pub(self) -> list: + return self.get_state() + + def get_current_status(self) -> list: + return self.get_state() + + def get_joint_speed(self) -> list: + return self.get_speed() + + def get_touch_type(self) -> list: + return -1 + + def get_normal_force(self) -> list: + return [-1] * 5 + + def get_tangential_force(self) -> list: + return [-1] * 5 + + def get_approach_inc(self) -> list: + return [-1] * 5 + + def get_touch(self) -> list: + return [-1] * 5 + + def _pressure(self, finger: int) -> np.ndarray: + """ + 读取压力传感器数据 (10x4矩阵) + """ + rows = 10 # 10行 + cols = 4 # 4列 + finger_size = rows * cols # 40个数据点 + + # modbus 地址 (按O6协议文档) + write_address = 18 # 写入手指选择 (保持寄存器) + read_address = 47 # 读取压力数据 (输入寄存器) + read_count = 40 # 读取40个寄存器 + + # 0. 参数校验 + if finger < 1 or finger > 5: + raise ValueError(f"无效的手指编号: {finger}。手指编号应在 1 到 5 之间。") + + # 1. 写入手指选择寄存器 (地址18) + time.sleep(0.01) + self._write_reg(write_address, finger) + + # 2. 读取压力数据 + data = self._execute_read(read_address, read_count) + + # 3. 转换为numpy数组并重塑为10x4矩阵 + finger_matrix = np.array(data, dtype=np.uint8).reshape((rows, cols)) + + return finger_matrix + + def get_thumb_matrix_touch(self,sleep_time=0): + return np.array(self._pressure(1), dtype=np.uint8) + + + def get_index_matrix_touch(self,sleep_time=0): + return np.array(self._pressure(2), dtype=np.uint8) + + def get_middle_matrix_touch(self,sleep_time=0): + return np.array(self._pressure(3), dtype=np.uint8) + + def get_ring_matrix_touch(self,sleep_time=0): + return np.array(self._pressure(4), dtype=np.uint8) + + def get_little_matrix_touch(self,sleep_time=0): + return np.array(self._pressure(5), dtype=np.uint8) + + def get_matrix_touch(self) -> list: + thumb_matrix = np.full((12, 6), -1) + index_matrix = np.full((12, 6), -1) + middle_matrix = np.full((12, 6), -1) + ring_matrix = np.full((12, 6), -1) + little_matrix = np.full((12, 6), -1) + return thumb_matrix , index_matrix , middle_matrix , ring_matrix , little_matrix + + def get_serial_number(self): + + return "["+str(self.get_hand_freedom())+"-"+str(self.get_mechanical_version())+"-"+str(self.get_device_number())+"-"+str(self.get_hand_direction())+"]" + + def get_matrix_touch_v2(self) -> list: + return self.get_matrix_touch() + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + def clear_faults(self): + pass + + def close(self): + if hasattr(self, 'connected') and self.connected: + self.cli.close() + self.connected = False + logging.info("Modbus connection closed.") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # ---------------------------------------------------------- + # 便捷函数 + # ---------------------------------------------------------- + def set_all_fingers(self, pitch: int): + """同时设置五指弯曲角度(0-255),使用批量写入""" + # 允许传入 float/numpy int 等可转换为 int 的类型 + try: + pitch_int = int(pitch) + except (ValueError, TypeError): + raise ValueError("Pitch value must be a number convertible to int (0-255)") + + if not 0 <= pitch_int <= 255: + raise ValueError("Pitch value must be 0-255") + + # 批量设置所有 6 个关节的角度 + self.set_joint_positions([pitch_int] * 6) + + def relax(self): + """全部手指伸直(255)""" + self.set_all_fingers(255) + + def fist(self): + """全部手指弯曲(0)""" + self.set_all_fingers(0) + + def dump_status(self): + """打印当前所有可读状态 (使用批量读取优化)""" + print("--------- O6 Hand Status ---------") + + angles = self.get_state() + temps = self.get_temperature() + errors = self.get_fault() + + # 解析版本号字符串 + v = self._parse_versions() + + if v: + device_num_str = f"{v['hand_number_high']}{v['hand_number_mid']}{v['hand_number_low']}" + hw_ver = f"{v['hw_ver_high']}.{v['hw_ver_mid']}.{v['hw_ver_low']}" + sw_ver = f"{v['sw_ver_high']}.{v['sw_ver_mid']}.{v['sw_ver_low']}" + mech_ver = f"{v['mech_ver_high']}.{v['mech_ver_mid']}.{v['mech_ver_low']}" + + print(f"Device Number: {device_num_str}") + print(f"HWSW Version: HW={hw_ver} SW={sw_ver}") + print(f"Mechanical Ver: {mech_ver}") + print(f"Hand Freedom: {v['hand_freedom']}") + print(f"Full Version: {self.read_all_versions()}") + + print(f"Joint Angles: {angles}") + print(f"Temperature: {temps}℃") + print(f"Error Codes: {errors}") + print("----------------------------------") + + +# ------------------------------------------------------------------ +# 命令行快速测试 +# ------------------------------------------------------------------ +if __name__ == "__main__": + import argparse + + # 假设默认站号是 0x27 (39) + DEFAULT_HAND_ID = 0x27 + + parser = argparse.ArgumentParser(description="O6 Hand Modbus tester (using pymodbus 3.5.1)") + parser.add_argument("-p", "--port", required=True, help="串口, 如 /dev/ttyUSB0") + parser.add_argument("-l", "--left", action="store_const", const=0x28, default=DEFAULT_HAND_ID, dest='hand_id', help="左手 (0x28),默认右手 (0x27)") + + args = parser.parse_args() + + try: + # 使用 with 语句确保连接关闭,这是 pymodbus 的推荐用法 + with LinkerHandO6RS485(hand_id=args.hand_id, modbus_port=args.port, baudrate=115200) as hand: + hand.dump_status() + + # 测试新增的设备编号读取方法 + print("\n--- 设备信息 ---") + print(f"设备编号(字符串): {hand.get_device_number()}") + print(f"设备编号(数值): {hand.get_device_number_value()}") + print(f"硬件版本号: {hand.get_hardware_version()}") + print(f"软件版本号: {hand.get_software_version()}") + print(f"机械版本号: {hand.get_mechanical_version()}") + + print("\n执行 relax → 伸直") + hand.relax() + time.sleep(1) + print("执行 fist → 握拳") + hand.fist() + time.sleep(1) + hand.relax() + print("演示完成") + + except ConnectionError as e: + print(f"连接错误: {e}") + except RuntimeError as e: + print(f"Modbus 运行时错误: {e}") + except StructError as e: + print(f"数据结构错误 (请检查输入数据类型是否为原生int): {e}") + except Exception as e: + print(f"发生其他错误: {e}") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py.bak b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py.bak new file mode 100644 index 0000000..cb08dd9 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/rs485/linker_hand_o6_rs485.py.bak @@ -0,0 +1,1157 @@ +#!/usr/bin/env python3 +""" +O6 机械手 Modbus-RTU 控制类 (基于 pymodbus 3.5.1) +""" + +import os +import time +from typing import List, Dict, Any # 引入 Any 来表示灵活的输入类型 +import numpy as np +import logging +from threading import Lock # 用于线程安全和总线仲裁 + +# 导入 pymodbus 客户端 +from pymodbus.client import ModbusSerialClient +from struct import error as StructError + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s] %(levelname)-8s %(message)s", + datefmt="%H:%M:%S" +) + +# ------------------------------------------------------------------ +# 读输入寄存器地址枚举(功能码 04,只读)- 保持原样 +# ------------------------------------------------------------------ +REG_RD_CURRENT_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255,小=弯,大=伸) +REG_RD_CURRENT_THUMB_YAW = 1 # 大拇指横摆角度(0-255,小=靠掌心,大=远离) +REG_RD_CURRENT_INDEX_PITCH = 2 # 食指弯曲角度 +REG_RD_CURRENT_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_RD_CURRENT_RING_PITCH = 4 # 无名指弯曲角度 +REG_RD_CURRENT_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_RD_CURRENT_THUMB_TORQUE = 6 # 大拇指弯曲转矩(0-255) +REG_RD_CURRENT_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_RD_CURRENT_INDEX_TORQUE = 8 # 食指转矩 +REG_RD_CURRENT_MIDDLE_TORQUE = 9 # 中指转矩 +REG_RD_CURRENT_RING_TORQUE = 10 # 无名指转矩 +REG_RD_CURRENT_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_RD_CURRENT_THUMB_SPEED = 12 # 大拇指弯曲速度(0-255) +REG_RD_CURRENT_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_RD_CURRENT_INDEX_SPEED = 14 # 食指速度 +REG_RD_CURRENT_MIDDLE_SPEED = 15 # 中指速度 +REG_RD_CURRENT_RING_SPEED = 16 # 无名指速度 +REG_RD_CURRENT_LITTLE_SPEED = 17 # 小拇指速度 +REG_RD_THUMB_TEMP = 18 # 大拇指弯曲温度(0-70℃) +REG_RD_THUMB_YAW_TEMP = 19 # 大拇指横摆温度 +REG_RD_INDEX_TEMP = 20 # 食指温度 +REG_RD_MIDDLE_TEMP = 21 # 中指温度 +REG_RD_RING_TEMP = 22 # 无名指温度 +REG_RD_LITTLE_TEMP = 23 # 小拇指温度 +REG_RD_THUMB_ERROR = 24 # 大拇指错误码 +REG_RD_THUMB_YAW_ERROR = 25 # 大拇指横摆错误码 +REG_RD_INDEX_ERROR = 26 # 食指错误码 +REG_RD_MIDDLE_ERROR = 27 # 中指错误码 +REG_RD_RING_ERROR = 28 # 无名指错误码 +REG_RD_LITTLE_ERROR = 29 # 小拇指错误码 +REG_RD_HAND_FREEDOM = 30 # 版本号(与机械手标签相同) +REG_RD_HAND_VERSION = 31 # 手版本 +REG_RD_HAND_NUMBER = 32 # 手编号 +REG_RD_HAND_DIRECTION = 33 # 手方向(左/右) +REG_RD_SOFTWARE_VERSION = 34 # 软件版本 +REG_RD_HARDWARE_VERSION = 35 # 硬件版本 + + +# ------------------------------------------------------------------ +# 写保持寄存器地址枚举(功能码 16,读写)- 保持原样 +# ------------------------------------------------------------------ +REG_WR_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255) +REG_WR_THUMB_YAW = 1 # 大拇指横摆角度 +REG_WR_INDEX_PITCH = 2 # 食指弯曲角度 +REG_WR_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_WR_RING_PITCH = 4 # 无名指弯曲角度 +REG_WR_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_WR_THUMB_TORQUE = 6 # 大拇指弯曲转矩 +REG_WR_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_WR_INDEX_TORQUE = 8 # 食指转矩 +REG_WR_MIDDLE_TORQUE = 9 # 中指转矩 +REG_WR_RING_TORQUE = 10 # 无名指转矩 +REG_WR_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_WR_THUMB_SPEED = 12 # 大拇指弯曲速度 +REG_WR_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_WR_INDEX_SPEED = 14 # 食指速度 +REG_WR_MIDDLE_SPEED = 15 # 中指速度 +REG_WR_RING_SPEED = 16 # 无名指速度 +REG_WR_LITTLE_SPEED = 17 # 小拇指速度 + + +class LinkerHandO6RS485: + """O6 机械手 Modbus-RTU 控制类,使用 pymodbus 3.5.1""" + + TTL_TIMEOUT = 0.15 # 串口超时 + FRAME_GAP = 0.030 # 30 ms + + # KEYS for easy indexing + JOINT_KEYS = ["thumb_pitch", "thumb_yaw", "index_pitch", + "middle_pitch", "ring_pitch", "little_pitch"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + self._id = hand_id + self._last_ts = 0.0 # 上一次帧结束时间 + self._lock = Lock() # 总线访问锁 + + # 使用 pymodbus 3.x 客户端 + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=self.TTL_TIMEOUT, + handle_local_echo=False + ) + + try: + logging.info(f"Connecting to Modbus RTU on {modbus_port}...") + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485 connect fail to {modbus_port}") + logging.info("Connection successful.") + except Exception as e: + logging.error(f"Initialization failed: {e}") + raise + + # ---------------------------------------------------------- + # 辅助方法 + # ---------------------------------------------------------- + def _bus_free(self): + """保证距离上一帧 ≥ 30 ms""" + with self._lock: + elapse = time.perf_counter() - self._last_ts + if elapse < self.FRAME_GAP: + time.sleep(self.FRAME_GAP - elapse) + + def _execute_read(self, address: int, count: int) -> List[int]: + """执行 Modbus 读取操作 (功能码 04), 带总线仲裁。""" + self._bus_free() + + rsp = self.cli.read_input_registers( + address=address, + count=count, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Read Failed (Addr={address}, Count={count}): {rsp}") + + # 确保返回的值是 Python 原生整数 + return [int(reg) for reg in rsp.registers] + + def _execute_write(self, address: int, values: List[int]): + """执行 Modbus 批量写入操作 (功能码 16), 带总线仲裁。""" + self._bus_free() + + # values 必须是 Python 原生整数列表 + rsp = self.cli.write_registers( + address=address, + values=values, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Write Failed (Addr={address}, Values={values}): {rsp}") + + # ---------------------------------------------------------- + # 批量读取和数据封装(优化通信效率) + # ---------------------------------------------------------- + def read_all_angles(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_PITCH, 6) + + def read_all_torques(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_TORQUE, 6) + + def read_all_speeds(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_SPEED, 6) + + def read_all_temperatures(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_TEMP, 6) + + def read_all_errors(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_ERROR, 6) + + def read_all_versions(self) -> List[int]: + return self._execute_read(REG_RD_HAND_FREEDOM, 6) + + + # ---------------------------------------------------------- + # 只读属性(单个寄存器读取) + # ---------------------------------------------------------- + def _read_reg(self, addr: int) -> int: + """读单个输入寄存器(功能码 04),带 30 ms 帧间隔""" + return self._execute_read(addr, 1)[0] + + def get_thumb_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_PITCH) + def get_thumb_yaw(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW) + def get_index_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_PITCH) + def get_middle_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_PITCH) + def get_ring_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_PITCH) + def get_little_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_PITCH) + + def get_thumb_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_TORQUE) + def get_thumb_yaw_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_TORQUE) + def get_index_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_TORQUE) + def get_middle_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_TORQUE) + def get_ring_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_TORQUE) + def get_little_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_TORQUE) + + def get_thumb_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_SPEED) + def get_thumb_yaw_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_SPEED) + def get_index_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_SPEED) + def get_middle_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_SPEED) + def get_ring_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_SPEED) + def get_little_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_SPEED) + + def get_thumb_temp(self) -> int: return self._read_reg(REG_RD_THUMB_TEMP) + def get_thumb_yaw_temp(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_TEMP) + def get_index_temp(self) -> int: return self._read_reg(REG_RD_INDEX_TEMP) + def get_middle_temp(self) -> int: return self._read_reg(REG_RD_MIDDLE_TEMP) + def get_ring_temp(self) -> int: return self._read_reg(REG_RD_RING_TEMP) + def get_little_temp(self) -> int: return self._read_reg(REG_RD_LITTLE_TEMP) + + def get_thumb_error(self) -> int: return self._read_reg(REG_RD_THUMB_ERROR) + def get_thumb_yaw_error(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_ERROR) + def get_index_error(self) -> int: return self._read_reg(REG_RD_INDEX_ERROR) + def get_middle_error(self) -> int: return self._read_reg(REG_RD_MIDDLE_ERROR) + def get_ring_error(self) -> int: return self._read_reg(REG_RD_RING_ERROR) + def get_little_error(self) -> int: return self._read_reg(REG_RD_LITTLE_ERROR) + + def get_hand_freedom(self) -> int: return self._read_reg(REG_RD_HAND_FREEDOM) + def get_hand_version(self) -> int: return self._read_reg(REG_RD_HAND_VERSION) + def get_hand_number(self) -> int: return self._read_reg(REG_RD_HAND_NUMBER) + def get_hand_direction(self) -> int: return self._read_reg(REG_RD_HAND_DIRECTION) + def get_software_version(self) -> int: return self._read_reg(REG_RD_SOFTWARE_VERSION) + def get_hardware_version(self) -> int: return self._read_reg(REG_RD_HARDWARE_VERSION) + + # ---------------------------------------------------------- + # 批量 Getter (使用 read_all_... 方法) + # ---------------------------------------------------------- + def get_state(self) -> List[int]: + """获取手指电机状态 (角度)""" + return self.read_all_angles() + + def get_torque(self) -> List[int]: + """获取当前扭矩""" + return self.read_all_torques() + + def get_speed(self) -> List[int]: + """获取当前速度""" + return self.read_all_speeds() + + def get_temperature(self) -> List[int]: + """获取当前电机温度""" + return self.read_all_temperatures() + + def get_fault(self) -> List[int]: + """获取当前电机故障码""" + return self.read_all_errors() + + def get_version(self) -> list: + """获取当前固件版本号""" + return self.read_all_versions() + + + # ---------------------------------------------------------- + # 写保持寄存器 (单个寄存器写入) + # ---------------------------------------------------------- + def _write_reg(self, addr: int, value: int): + """写单个保持寄存器(功能码 16),带 30 ms 帧间隔""" + if not 0 <= value <= 255: + raise ValueError("value must be 0-255") + + # 确保 value 是 Python 原生 int + self._execute_write(addr, [int(value)]) + + def _write_regs(self, addr: int, values: List[int]): + """写多个保持寄存器(功能码 16),带 30 ms 帧间隔""" + # 此时 values 应该已经是经过 is_valid_6xuint8 验证并转换的 Python int 列表 + if not all(0 <= v <= 255 for v in values): + # 这行理论上不应触发,因为上层调用已校验 + raise ValueError("All values must be 0-255") + self._execute_write(addr, values) + + + def set_thumb_pitch(self, v: int): self._write_reg(REG_WR_THUMB_PITCH, v) + def set_thumb_yaw(self, v: int): self._write_reg(REG_WR_THUMB_YAW, v) + def set_index_pitch(self, v: int): self._write_reg(REG_WR_INDEX_PITCH, v) + def set_middle_pitch(self, v: int): self._write_reg(REG_WR_MIDDLE_PITCH, v) + def set_ring_pitch(self, v: int): self._write_reg(REG_WR_RING_PITCH, v) + def set_little_pitch(self, v: int): self._write_reg(REG_WR_LITTLE_PITCH, v) + + def set_thumb_torque(self, v: int): self._write_reg(REG_WR_THUMB_TORQUE, v) + def set_thumb_yaw_torque(self, v: int): self._write_reg(REG_WR_THUMB_YAW_TORQUE, v) + def set_index_torque(self, v: int): self._write_reg(REG_WR_INDEX_TORQUE, v) + def set_middle_torque(self, v: int): self._write_reg(REG_WR_MIDDLE_TORQUE, v) + def set_ring_torque(self, v: int): self._write_reg(REG_WR_RING_TORQUE, v) + def set_little_torque(self, v: int): self._write_reg(REG_WR_LITTLE_TORQUE, v) + + def set_thumb_speed(self, v: int): self._write_reg(REG_WR_THUMB_SPEED, v) + def set_thumb_yaw_speed(self, v: int): self._write_reg(REG_WR_THUMB_YAW_SPEED, v) + def set_index_speed(self, v: int): self._write_reg(REG_WR_INDEX_SPEED, v) + def set_middle_speed(self, v: int): self._write_reg(REG_WR_MIDDLE_SPEED, v) + def set_ring_speed(self, v: int): self._write_reg(REG_WR_RING_SPEED, v) + def set_little_speed(self, v: int): self._write_reg(REG_WR_LITTLE_SPEED, v) + + # ---------------------------------------------------------- + # 固定函数 (采用批量写入优化) + # ---------------------------------------------------------- + def is_valid_6xuint8(self, lst: List[Any]) -> bool: + """ + 验证6个0-255的整数列表。 + 允许输入包含浮点数、NumPy整数等可转换为 int 的类型,并进行范围校验。 + """ + if not (isinstance(lst, list) and len(lst) == 6): + return False + + try: + # 关键:尝试将所有元素转换为 Python 原生 int + int_values = [int(v) for v in lst] + except (ValueError, TypeError): + # 转换失败,列表中包含不可转换的元素 + return False + + # 校验转换后的整数列表是否在 0-255 范围内 + return all(0 <= x <= 255 for x in int_values) + + def set_joint_positions(self, joint_angles: List[Any] = None): + joint_angles = joint_angles or [0] * 6 + + if not self.is_valid_6xuint8(joint_angles): + logging.error(f"Invalid joint angles received: {joint_angles}") + raise ValueError("Joint angles must be a list of 6 values between 0 and 255 (convertible to int).") + + # 强制转换为 Modbus 兼容的 Python 原生 int 列表 + int_angles = [int(v) for v in joint_angles] + + # 批量写入 6 个角度寄存器 (从 REG_WR_THUMB_PITCH 地址 0 开始, count=6) + self._write_regs(REG_WR_THUMB_PITCH, int_angles) + + def set_speed(self, speed: List[Any] = None): + speed = speed or [200] * 6 + if not self.is_valid_6xuint8(speed): + logging.error(f"Invalid speed values received: {speed}") + raise ValueError("Speed values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_speed = [int(v) for v in speed] + self._write_regs(REG_WR_THUMB_SPEED, int_speed) + + def set_torque(self, torque: List[Any] = None): + torque = torque or [200] * 6 + if not self.is_valid_6xuint8(torque): + logging.error(f"Invalid torque values received: {torque}") + raise ValueError("Torque values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_torque = [int(v) for v in torque] + self._write_regs(REG_WR_THUMB_TORQUE, int_torque) + + # ... (其他固定函数保持不变) ... + + def set_current(self, current: List[int] = None): + print("当前O6不支持设置电流", flush=True) + pass + + def get_state_for_pub(self) -> list: + return self.get_state() + + def get_current_status(self) -> list: + return self.get_state() + + def get_joint_speed(self) -> list: + return self.get_speed() + + def get_touch_type(self) -> list: + return -1 + + def get_normal_force(self) -> list: + return [-1] * 5 + + def get_tangential_force(self) -> list: + return [-1] * 5 + + def get_approach_inc(self) -> list: + return [-1] * 5 + + def get_touch(self) -> list: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_index_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_middle_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_ring_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_little_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_matrix_touch(self) -> list: + thumb_matrix = np.full((12, 6), -1) + index_matrix = np.full((12, 6), -1) + middle_matrix = np.full((12, 6), -1) + ring_matrix = np.full((12, 6), -1) + little_matrix = np.full((12, 6), -1) + return thumb_matrix , index_matrix , middle_matrix , ring_matrix , little_matrix + + def get_serial_number(self): + return [0] * 6 + + def get_matrix_touch_v2(self) -> list: + return self.get_matrix_touch() + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + # ---------------------------------------------------------- + # 上下文管理 + # ---------------------------------------------------------- + def close(self): + if hasattr(self, 'connected') and self.connected: + self.cli.close() + self.connected = False + logging.info("Modbus connection closed.") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # ---------------------------------------------------------- + # 便捷函数 + # ---------------------------------------------------------- + def set_all_fingers(self, pitch: int): + """同时设置五指弯曲角度(0-255),使用批量写入""" + # 允许传入 float/numpy int 等可转换为 int 的类型 + try: + pitch_int = int(pitch) + except (ValueError, TypeError): + raise ValueError("Pitch value must be a number convertible to int (0-255)") + + if not 0 <= pitch_int <= 255: + raise ValueError("Pitch value must be 0-255") + + # 批量设置所有 6 个关节的角度 + self.set_joint_positions([pitch_int] * 6) + + def relax(self): + """全部手指伸直(255)""" + self.set_all_fingers(255) + + def fist(self): + """全部手指弯曲(0)""" + self.set_all_fingers(0) + + def dump_status(self): + """打印当前所有可读状态 (使用批量读取优化)""" + print("--------- O6 Hand Status ---------") + + angles = self.get_state() + temps = self.get_temperature() + errors = self.get_fault() + versions = self.get_version() + + print(f"Joint Angles: {angles}") + print(f"Temperature: {temps}℃") + print(f"Error Codes: {errors}") + print(f"Versions: {versions}") + print("----------------------------------") + +# ------------------------------------------------------------------ +# 命令行快速测试 +# ------------------------------------------------------------------ +if __name__ == "__main__": + import argparse + + # 假设默认站号是 0x27 (39) + DEFAULT_HAND_ID = 0x27 + + parser = argparse.ArgumentParser(description="O6 Hand Modbus tester (using pymodbus 3.5.1)") + parser.add_argument("-p", "--port", required=True, help="串口, 如 /dev/ttyUSB0") + parser.add_argument("-l", "--left", action="store_const", const=0x28, default=DEFAULT_HAND_ID, dest='hand_id', help="左手 (0x28),默认右手 (0x27)") + + args = parser.parse_args() + + try: + # 使用 with 语句确保连接关闭,这是 pymodbus 的推荐用法 + with LinkerHandO6RS485(hand_id=args.hand_id, modbus_port=args.port, baudrate=115200) as hand: + hand.dump_status() + print("执行 relax → 伸直") + hand.relax() + time.sleep(1) + print("执行 fist → 握拳") + hand.fist() + time.sleep(1) + hand.relax() + print("演示完成") + + except ConnectionError as e: + print(f"连接错误: {e}") + except RuntimeError as e: + print(f"Modbus 运行时错误: {e}") + except StructError as e: + print(f"数据结构错误 (请检查输入数据类型是否为原生int): {e}") + except Exception as e: + print(f"发生其他错误: {e}") + + +========================================================================================================================== + + +#!/usr/bin/env python3 +""" +O6 机械手 Modbus-RTU 控制类 (基于 pymodbus 3.5.1) +""" + +import os +import time +from typing import List, Dict, Any # 引入 Any 来表示灵活的输入类型 +import numpy as np +import logging +from threading import Lock # 用于线程安全和总线仲裁 + +# 导入 pymodbus 客户端 +from pymodbus.client import ModbusSerialClient +from struct import error as StructError + +logging.basicConfig( + level=logging.INFO, + format="[%(asctime)s] %(levelname)-8s %(message)s", + datefmt="%H:%M:%S" +) + +# ------------------------------------------------------------------ +# 读输入寄存器地址枚举(功能码 04,只读)- 按照 O6 协议文档定义 +# ------------------------------------------------------------------ +REG_RD_CURRENT_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255,小=弯,大=伸) +REG_RD_CURRENT_THUMB_YAW = 1 # 大拇指横摆角度(0-255,小=靠掌心,大=远离) +REG_RD_CURRENT_INDEX_PITCH = 2 # 食指弯曲角度 +REG_RD_CURRENT_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_RD_CURRENT_RING_PITCH = 4 # 无名指弯曲角度 +REG_RD_CURRENT_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_RD_CURRENT_THUMB_TORQUE = 6 # 大拇指弯曲转矩(0-255) +REG_RD_CURRENT_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_RD_CURRENT_INDEX_TORQUE = 8 # 食指转矩 +REG_RD_CURRENT_MIDDLE_TORQUE = 9 # 中指转矩 +REG_RD_CURRENT_RING_TORQUE = 10 # 无名指转矩 +REG_RD_CURRENT_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_RD_CURRENT_THUMB_SPEED = 12 # 大拇指弯曲速度(0-255) +REG_RD_CURRENT_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_RD_CURRENT_INDEX_SPEED = 14 # 食指速度 +REG_RD_CURRENT_MIDDLE_SPEED = 15 # 中指速度 +REG_RD_CURRENT_RING_SPEED = 16 # 无名指速度 +REG_RD_CURRENT_LITTLE_SPEED = 17 # 小拇指速度 +REG_RD_THUMB_TEMP = 18 # 大拇指弯曲温度(0-70℃) +REG_RD_THUMB_YAW_TEMP = 19 # 大拇指横摆温度 +REG_RD_INDEX_TEMP = 20 # 食指温度 +REG_RD_MIDDLE_TEMP = 21 # 中指温度 +REG_RD_RING_TEMP = 22 # 无名指温度 +REG_RD_LITTLE_TEMP = 23 # 小拇指温度 +REG_RD_THUMB_ERROR = 24 # 大拇指错误码 +REG_RD_THUMB_YAW_ERROR = 25 # 大拇指横摆错误码 +REG_RD_INDEX_ERROR = 26 # 食指错误码 +REG_RD_MIDDLE_ERROR = 27 # 中指错误码 +REG_RD_RING_ERROR = 28 # 无名指错误码 +REG_RD_LITTLE_ERROR = 29 # 小拇指错误码 + +# 版本号/设备编号寄存器(地址 30-44,共15个寄存器) +REG_RD_HAND_FREEDOM = 30 # Hand_freedom - 设备编号 / 自由度(与机械手上标签相同) +REG_RD_HAND_VERSION = 31 # hand_version - 手版本 +REG_RD_HAND_NUMBER_HIGH = 32 # hand_number_高位 - 设备编号(高字节) +REG_RD_HAND_NUMBER_MID = 33 # hand_number_中位 - 设备编号(中字节) +REG_RD_HAND_NUMBER_LOW = 34 # hand_number_低位 - 设备编号(低字节) +REG_RD_HAND_DIRECTION = 35 # hand_direction - 手方向(左/右) +REG_RD_HARDWARE_VERSION_HIGH = 36 # hardware_version_高位 - 硬件版本(高字节) +REG_RD_HARDWARE_VERSION_MID = 37 # hardware_version_中位 - 硬件版本(中字节) +REG_RD_HARDWARE_VERSION_LOW = 38 # hardware_version_低位 - 硬件版本(低字节) +REG_RD_SOFTWARE_VERSION_HIGH = 39 # software_version_高位 - 软件版本(高字节) +REG_RD_SOFTWARE_VERSION_MID = 40 # software_version_中位 - 软件版本(中字节) +REG_RD_SOFTWARE_VERSION_LOW = 41 # software_version_低位 - 软件版本(低字节) +REG_RD_MECHANICAL_VERSION_HIGH = 42 # mechanical_version_高位 - 机械版本(高字节) +REG_RD_MECHANICAL_VERSION_MID = 43 # mechanical_version_中位 - 机械版本(中字节) +REG_RD_MECHANICAL_VERSION_LOW = 44 # mechanical_version_低位 - 机械版本(低字节) + +# 力传感器寄存器(地址 45-87+,动态范围) +REG_RD_PRESSURE_SENSING_ID = 45 # Pressure_Sensing_ID - 压力传感器ID (0-5) +REG_RD_PRESSURE_SENSING_SPEC = 46 # Pressure_Sensing_Specifications - 传感器数据规格 + + +# ------------------------------------------------------------------ +# 写保持寄存器地址枚举(功能码 16,读写)- 保持原样 +# ------------------------------------------------------------------ +REG_WR_THUMB_PITCH = 0 # 大拇指弯曲角度(0-255) +REG_WR_THUMB_YAW = 1 # 大拇指横摆角度 +REG_WR_INDEX_PITCH = 2 # 食指弯曲角度 +REG_WR_MIDDLE_PITCH = 3 # 中指弯曲角度 +REG_WR_RING_PITCH = 4 # 无名指弯曲角度 +REG_WR_LITTLE_PITCH = 5 # 小拇指弯曲角度 +REG_WR_THUMB_TORQUE = 6 # 大拇指弯曲转矩 +REG_WR_THUMB_YAW_TORQUE = 7 # 大拇指横摆转矩 +REG_WR_INDEX_TORQUE = 8 # 食指转矩 +REG_WR_MIDDLE_TORQUE = 9 # 中指转矩 +REG_WR_RING_TORQUE = 10 # 无名指转矩 +REG_WR_LITTLE_TORQUE = 11 # 小拇指转矩 +REG_WR_THUMB_SPEED = 12 # 大拇指弯曲速度 +REG_WR_THUMB_YAW_SPEED = 13 # 大拇指横摆速度 +REG_WR_INDEX_SPEED = 14 # 食指速度 +REG_WR_MIDDLE_SPEED = 15 # 中指速度 +REG_WR_RING_SPEED = 16 # 无名指速度 +REG_WR_LITTLE_SPEED = 17 # 小拇指速度 + + +class LinkerHandO6RS485: + """O6 机械手 Modbus-RTU 控制类,使用 pymodbus 3.5.1""" + + TTL_TIMEOUT = 0.15 # 串口超时 + FRAME_GAP = 0.030 # 30 ms + + # KEYS for easy indexing + JOINT_KEYS = ["thumb_pitch", "thumb_yaw", "index_pitch", + "middle_pitch", "ring_pitch", "little_pitch"] + + def __init__(self, hand_id=0x27, modbus_port="/dev/ttyUSB0", baudrate=115200): + self._id = hand_id + self._last_ts = 0.0 # 上一次帧结束时间 + self._lock = Lock() # 总线访问锁 + + # 使用 pymodbus 3.x 客户端 + self.cli = ModbusSerialClient( + port=modbus_port, + baudrate=baudrate, + bytesize=8, + parity="N", + stopbits=1, + timeout=self.TTL_TIMEOUT, + handle_local_echo=False + ) + + try: + logging.info(f"Connecting to Modbus RTU on {modbus_port}...") + self.connected = self.cli.connect() + if not self.connected: + raise ConnectionError(f"RS485 connect fail to {modbus_port}") + logging.info("Connection successful.") + except Exception as e: + logging.error(f"Initialization failed: {e}") + raise + + # ---------------------------------------------------------- + # 辅助方法 + # ---------------------------------------------------------- + def _bus_free(self): + """保证距离上一帧 ≥ 30 ms""" + with self._lock: + elapse = time.perf_counter() - self._last_ts + if elapse < self.FRAME_GAP: + time.sleep(self.FRAME_GAP - elapse) + + def _execute_read(self, address: int, count: int) -> List[int]: + """执行 Modbus 读取操作 (功能码 04), 带总线仲裁。""" + self._bus_free() + + rsp = self.cli.read_input_registers( + address=address, + count=count, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Read Failed (Addr={address}, Count={count}): {rsp}") + + # 确保返回的值是 Python 原生整数 + return [int(reg) for reg in rsp.registers] + + def _execute_write(self, address: int, values: List[int]): + """执行 Modbus 批量写入操作 (功能码 16), 带总线仲裁。""" + self._bus_free() + + # values 必须是 Python 原生整数列表 + rsp = self.cli.write_registers( + address=address, + values=values, + slave=self._id + ) + + self._last_ts = time.perf_counter() + + if rsp.isError(): + raise RuntimeError(f"Modbus Write Failed (Addr={address}, Values={values}): {rsp}") + + # ---------------------------------------------------------- + # 批量读取和数据封装(优化通信效率) + # ---------------------------------------------------------- + def read_all_angles(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_PITCH, 6) + + def read_all_torques(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_TORQUE, 6) + + def read_all_speeds(self) -> List[int]: + return self._execute_read(REG_RD_CURRENT_THUMB_SPEED, 6) + + def read_all_temperatures(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_TEMP, 6) + + def read_all_errors(self) -> List[int]: + return self._execute_read(REG_RD_THUMB_ERROR, 6) + + # ---------------------------------------------------------- + # 版本号/设备编号读取(按照协议文档:地址30-44,共15个寄存器) + # ---------------------------------------------------------- + def read_all_versions(self) -> str: + """一次性读取全部15个寄存器 (地址30-44),返回以 '.' 连接的字符串。 + + 按 O6 协议文档的版本号格式返回: + hand_freedom.hand_version.hand_number_high.hand_number_mid.hand_number_low + .hand_direction.hardware_ver_hardware_ver_m.hardware_ver_l + .software_ver_h.software_ver_m.software_ver_l + .mechanical_ver_h.mechanical_ver_m.mechanical_ver_l + + 例如: "6.1.001.002.003.0.1.2.3.4.5.6.7.8.9" + """ + raw = self._execute_read(REG_RD_HAND_FREEDOM, 15) + return ".".join(str(v) for v in raw) + + # ---------------------------------------------------------- + # 基于 read_all_versions() 的设备编号解析方法 + # ---------------------------------------------------------- + def _parse_versions(self): + """解析 read_all_versions() 返回的字符串为字典""" + parts = self.read_all_versions().split(".") + if len(parts) < 15: + return {} + return { + "hand_freedom": int(parts[0]), + "hand_version": int(parts[1]), + "hand_number_high": int(parts[2]), + "hand_number_mid": int(parts[3]), + "hand_number_low": int(parts[4]), + "hand_direction": int(parts[5]), + "hw_ver_high": int(parts[6]), + "hw_ver_mid": int(parts[7]), + "hw_ver_low": int(parts[8]), + "sw_ver_high": int(parts[9]), + "sw_ver_mid": int(parts[10]), + "sw_ver_low": int(parts[11]), + "mech_ver_high": int(parts[12]), + "mech_ver_mid": int(parts[13]), + "mech_ver_low": int(parts[14]), + } + + def get_device_number(self) -> str: + """获取设备编号(与机械手上标签相同)。格式:高位+中位+低位 拼接的字符串。""" + v = self._parse_versions() + if not v: + return "0" + high, mid, low = v["hand_number_high"], v["hand_number_mid"], v["hand_number_low"] + # 将每个字节格式化为无前导零的整数(与标签显示一致) + return f"{high}{mid}{low}" + + def get_device_number_value(self) -> int: + """获取设备编号数值""" + v = self._parse_versions() + if not v: + return 0 + high, mid, low = v["hand_number_high"], v["hand_number_mid"], v["hand_number_low"] + return high * 65536 + mid * 256 + low + + def get_hardware_version(self) -> str: + """获取硬件版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['hw_ver_high']}.{v['hw_ver_mid']}.{v['hw_ver_low']}" + + def get_software_version(self) -> str: + """获取软件版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['sw_ver_high']}.{v['sw_ver_mid']}.{v['sw_ver_low']}" + + def get_mechanical_version(self) -> str: + """获取机械版本号。格式:高.中.低""" + v = self._parse_versions() + if not v: + return "0.0.0" + return f"{v['mech_ver_high']}.{v['mech_ver_mid']}.{v['mech_ver_low']}" + + def get_hand_freedom(self) -> int: + """获取自由度(与机械手上标签相同)""" + v = self._parse_versions() + return v.get("hand_freedom", 0) + + def get_hand_version_raw(self) -> int: + """获取手版本原始值""" + v = self._parse_versions() + return v.get("hand_version", 0) + + def get_hand_direction(self) -> str: + """获取手方向,转换为字符:76→'L', 82→'R'""" + v = self._parse_versions() + val = v.get("hand_direction", 0) + if val in (76, 82): + return chr(val) + # fallback: 直接转为字符(如果值在可打印范围内) + return chr(val) if 32 < val < 128 else f'Unknown({val})' + + + # ---------------------------------------------------------- + # 只读属性(单个寄存器读取) + # ---------------------------------------------------------- + def _read_reg(self, addr: int) -> int: + """读单个输入寄存器(功能码 04),带 30 ms 帧间隔""" + return self._execute_read(addr, 1)[0] + + def get_thumb_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_PITCH) + def get_thumb_yaw(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW) + def get_index_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_PITCH) + def get_middle_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_PITCH) + def get_ring_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_PITCH) + def get_little_pitch(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_PITCH) + + def get_thumb_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_TORQUE) + def get_thumb_yaw_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_TORQUE) + def get_index_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_TORQUE) + def get_middle_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_TORQUE) + def get_ring_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_TORQUE) + def get_little_torque(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_TORQUE) + + def get_thumb_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_SPEED) + def get_thumb_yaw_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_THUMB_YAW_SPEED) + def get_index_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_INDEX_SPEED) + def get_middle_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_MIDDLE_SPEED) + def get_ring_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_RING_SPEED) + def get_little_speed(self) -> int: return self._read_reg(REG_RD_CURRENT_LITTLE_SPEED) + + def get_thumb_temp(self) -> int: return self._read_reg(REG_RD_THUMB_TEMP) + def get_thumb_yaw_temp(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_TEMP) + def get_index_temp(self) -> int: return self._read_reg(REG_RD_INDEX_TEMP) + def get_middle_temp(self) -> int: return self._read_reg(REG_RD_MIDDLE_TEMP) + def get_ring_temp(self) -> int: return self._read_reg(REG_RD_RING_TEMP) + def get_little_temp(self) -> int: return self._read_reg(REG_RD_LITTLE_TEMP) + + def get_thumb_error(self) -> int: return self._read_reg(REG_RD_THUMB_ERROR) + def get_thumb_yaw_error(self) -> int: return self._read_reg(REG_RD_THUMB_YAW_ERROR) + def get_index_error(self) -> int: return self._read_reg(REG_RD_INDEX_ERROR) + def get_middle_error(self) -> int: return self._read_reg(REG_RD_MIDDLE_ERROR) + def get_ring_error(self) -> int: return self._read_reg(REG_RD_RING_ERROR) + def get_little_error(self) -> int: return self._read_reg(REG_RD_LITTLE_ERROR) + + + # ---------------------------------------------------------- + # 批量 Getter (使用 read_all_... 方法) + # ---------------------------------------------------------- + def get_state(self) -> List[int]: + """获取手指电机状态 (角度)""" + return self.read_all_angles() + + def get_torque(self) -> List[int]: + """获取当前扭矩""" + return self.read_all_torques() + + def get_speed(self) -> List[int]: + """获取当前速度""" + return self.read_all_speeds() + + def get_temperature(self) -> List[int]: + """获取当前电机温度""" + return self.read_all_temperatures() + + def get_fault(self) -> List[int]: + """获取当前电机故障码""" + return self.read_all_errors() + + def get_version(self) -> str: + """获取当前固件版本号(已转换为字符串格式)""" + return self.read_all_versions() + + + # ---------------------------------------------------------- + # 写保持寄存器 (单个寄存器写入) + # ---------------------------------------------------------- + def _write_reg(self, addr: int, value: int): + """写单个保持寄存器(功能码 16),带 30 ms 帧间隔""" + if not 0 <= value <= 255: + raise ValueError("value must be 0-255") + + # 确保 value 是 Python 原生 int + self._execute_write(addr, [int(value)]) + + def _write_regs(self, addr: int, values: List[int]): + """写多个保持寄存器(功能码 16),带 30 ms 帧间隔""" + # 此时 values 应该已经是经过 is_valid_6xuint8 验证并转换的 Python int 列表 + if not all(0 <= v <= 255 for v in values): + # 这行理论上不应触发,因为上层调用已校验 + raise ValueError("All values must be 0-255") + self._execute_write(addr, values) + + + def set_thumb_pitch(self, v: int): self._write_reg(REG_WR_THUMB_PITCH, v) + def set_thumb_yaw(self, v: int): self._write_reg(REG_WR_THUMB_YAW, v) + def set_index_pitch(self, v: int): self._write_reg(REG_WR_INDEX_PITCH, v) + def set_middle_pitch(self, v: int): self._write_reg(REG_WR_MIDDLE_PITCH, v) + def set_ring_pitch(self, v: int): self._write_reg(REG_WR_RING_PITCH, v) + def set_little_pitch(self, v: int): self._write_reg(REG_WR_LITTLE_PITCH, v) + + def set_thumb_torque(self, v: int): self._write_reg(REG_WR_THUMB_TORQUE, v) + def set_thumb_yaw_torque(self, v: int): self._write_reg(REG_WR_THUMB_YAW_TORQUE, v) + def set_index_torque(self, v: int): self._write_reg(REG_WR_INDEX_TORQUE, v) + def set_middle_torque(self, v: int): self._write_reg(REG_WR_MIDDLE_TORQUE, v) + def set_ring_torque(self, v: int): self._write_reg(REG_WR_RING_TORQUE, v) + def set_little_torque(self, v: int): self._write_reg(REG_WR_LITTLE_TORQUE, v) + + def set_thumb_speed(self, v: int): self._write_reg(REG_WR_THUMB_SPEED, v) + def set_thumb_yaw_speed(self, v: int): self._write_reg(REG_WR_THUMB_YAW_SPEED, v) + def set_index_speed(self, v: int): self._write_reg(REG_WR_INDEX_SPEED, v) + def set_middle_speed(self, v: int): self._write_reg(REG_WR_MIDDLE_SPEED, v) + def set_ring_speed(self, v: int): self._write_reg(REG_WR_RING_SPEED, v) + def set_little_speed(self, v: int): self._write_reg(REG_WR_LITTLE_SPEED, v) + + # ---------------------------------------------------------- + # 固定函数 (采用批量写入优化) + # ---------------------------------------------------------- + def is_valid_6xuint8(self, lst: List[Any]) -> bool: + """ + 验证6个0-255的整数列表。 + 允许输入包含浮点数、NumPy整数等可转换为 int 的类型,并进行范围校验。 + """ + if not (isinstance(lst, list) and len(lst) == 6): + return False + + try: + # 关键:尝试将所有元素转换为 Python 原生 int + int_values = [int(v) for v in lst] + except (ValueError, TypeError): + # 转换失败,列表中包含不可转换的元素 + return False + + # 校验转换后的整数列表是否在 0-255 范围内 + return all(0 <= x <= 255 for x in int_values) + + def set_joint_positions(self, joint_angles: List[Any] = None): + joint_angles = joint_angles or [0] * 6 + + if not self.is_valid_6xuint8(joint_angles): + logging.error(f"Invalid joint angles received: {joint_angles}") + raise ValueError("Joint angles must be a list of 6 values between 0 and 255 (convertible to int).") + + # 强制转换为 Modbus 兼容的 Python 原生 int 列表 + int_angles = [int(v) for v in joint_angles] + + # 批量写入 6 个角度寄存器 (从 REG_WR_THUMB_PITCH 地址 0 开始, count=6) + self._write_regs(REG_WR_THUMB_PITCH, int_angles) + + def set_speed(self, speed: List[Any] = None): + speed = speed or [200] * 6 + if not self.is_valid_6xuint8(speed): + logging.error(f"Invalid speed values received: {speed}") + raise ValueError("Speed values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_speed = [int(v) for v in speed] + self._write_regs(REG_WR_THUMB_SPEED, int_speed) + + def set_torque(self, torque: List[Any] = None): + torque = torque or [200] * 6 + if not self.is_valid_6xuint8(torque): + logging.error(f"Invalid torque values received: {torque}") + raise ValueError("Torque values must be a list of 6 values between 0 and 255 (convertible to int).") + + int_torque = [int(v) for v in torque] + self._write_regs(REG_WR_THUMB_TORQUE, int_torque) + + # ... (其他固定函数保持不变) ... + + def set_current(self, current: List[int] = None): + print("当前O6不支持设置电流", flush=True) + pass + + def get_state_for_pub(self) -> list: + return self.get_state() + + def get_current_status(self) -> list: + return self.get_state() + + def get_joint_speed(self) -> list: + return self.get_speed() + + def get_touch_type(self) -> list: + return -1 + + def get_normal_force(self) -> list: + return [-1] * 5 + + def get_tangential_force(self) -> list: + return [-1] * 5 + + def get_approach_inc(self) -> list: + return [-1] * 5 + + def get_touch(self) -> list: + return [-1] * 5 + + def get_thumb_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_index_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_middle_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_ring_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_little_matrix_touch(self,sleep_time=0): + return np.full((12, 6), -1) + + def get_matrix_touch(self) -> list: + thumb_matrix = np.full((12, 6), -1) + index_matrix = np.full((12, 6), -1) + middle_matrix = np.full((12, 6), -1) + ring_matrix = np.full((12, 6), -1) + little_matrix = np.full((12, 6), -1) + return thumb_matrix , index_matrix , middle_matrix , ring_matrix , little_matrix + + def get_serial_number(self): + + return "["+str(self.get_hand_freedom())+"-"+str(self.get_mechanical_version())+"-"+str(self.get_device_number())+"-"+str(self.get_hand_direction())+"]" + + def get_matrix_touch_v2(self) -> list: + return self.get_matrix_touch() + + def get_finger_order(self): + return ["thumb_cmc_pitch", "thumb_cmc_yaw", "index_mcp_pitch", "middle_mcp_pitch", "ring_mcp_pitch", "pinky_mcp_pitch"] + + def clear_faults(self): + pass + + def close(self): + if hasattr(self, 'connected') and self.connected: + self.cli.close() + self.connected = False + logging.info("Modbus connection closed.") + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + + # ---------------------------------------------------------- + # 便捷函数 + # ---------------------------------------------------------- + def set_all_fingers(self, pitch: int): + """同时设置五指弯曲角度(0-255),使用批量写入""" + # 允许传入 float/numpy int 等可转换为 int 的类型 + try: + pitch_int = int(pitch) + except (ValueError, TypeError): + raise ValueError("Pitch value must be a number convertible to int (0-255)") + + if not 0 <= pitch_int <= 255: + raise ValueError("Pitch value must be 0-255") + + # 批量设置所有 6 个关节的角度 + self.set_joint_positions([pitch_int] * 6) + + def relax(self): + """全部手指伸直(255)""" + self.set_all_fingers(255) + + def fist(self): + """全部手指弯曲(0)""" + self.set_all_fingers(0) + + def dump_status(self): + """打印当前所有可读状态 (使用批量读取优化)""" + print("--------- O6 Hand Status ---------") + + angles = self.get_state() + temps = self.get_temperature() + errors = self.get_fault() + + # 解析版本号字符串 + v = self._parse_versions() + + if v: + device_num_str = f"{v['hand_number_high']}{v['hand_number_mid']}{v['hand_number_low']}" + hw_ver = f"{v['hw_ver_high']}.{v['hw_ver_mid']}.{v['hw_ver_low']}" + sw_ver = f"{v['sw_ver_high']}.{v['sw_ver_mid']}.{v['sw_ver_low']}" + mech_ver = f"{v['mech_ver_high']}.{v['mech_ver_mid']}.{v['mech_ver_low']}" + + print(f"Device Number: {device_num_str}") + print(f"HWSW Version: HW={hw_ver} SW={sw_ver}") + print(f"Mechanical Ver: {mech_ver}") + print(f"Hand Freedom: {v['hand_freedom']}") + print(f"Full Version: {self.read_all_versions()}") + + print(f"Joint Angles: {angles}") + print(f"Temperature: {temps}℃") + print(f"Error Codes: {errors}") + print("----------------------------------") + + +# ------------------------------------------------------------------ +# 命令行快速测试 +# ------------------------------------------------------------------ +if __name__ == "__main__": + import argparse + + # 假设默认站号是 0x27 (39) + DEFAULT_HAND_ID = 0x27 + + parser = argparse.ArgumentParser(description="O6 Hand Modbus tester (using pymodbus 3.5.1)") + parser.add_argument("-p", "--port", required=True, help="串口, 如 /dev/ttyUSB0") + parser.add_argument("-l", "--left", action="store_const", const=0x28, default=DEFAULT_HAND_ID, dest='hand_id', help="左手 (0x28),默认右手 (0x27)") + + args = parser.parse_args() + + try: + # 使用 with 语句确保连接关闭,这是 pymodbus 的推荐用法 + with LinkerHandO6RS485(hand_id=args.hand_id, modbus_port=args.port, baudrate=115200) as hand: + hand.dump_status() + + # 测试新增的设备编号读取方法 + print("\n--- 设备信息 ---") + print(f"设备编号(字符串): {hand.get_device_number()}") + print(f"设备编号(数值): {hand.get_device_number_value()}") + print(f"硬件版本号: {hand.get_hardware_version()}") + print(f"软件版本号: {hand.get_software_version()}") + print(f"机械版本号: {hand.get_mechanical_version()}") + + print("\n执行 relax → 伸直") + hand.relax() + time.sleep(1) + print("执行 fist → 握拳") + hand.fist() + time.sleep(1) + hand.relax() + print("演示完成") + + except ConnectionError as e: + print(f"连接错误: {e}") + except RuntimeError as e: + print(f"Modbus 运行时错误: {e}") + except StructError as e: + print(f"数据结构错误 (请检查输入数据类型是否为原生int): {e}") + except Exception as e: + print(f"发生其他错误: {e}") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/linker_hand_api.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/linker_hand_api.py new file mode 100644 index 0000000..631a7c9 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/linker_hand_api.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys, os, time,threading +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from utils.mapping import * +from utils.color_msg import ColorMsg +from utils.load_write_yaml import LoadWriteYaml +from utils.open_can import OpenCan + +class LinkerHandApi: + def __init__(self, hand_type="left", hand_joint="L10", modbus = "None",can="can0"): # Ubuntu:can0 win:PCAN_USBBUS1 + self.last_position = [] + self.yaml = LoadWriteYaml() + self.config = self.yaml.load_setting_yaml() + self.version = self.config["VERSION"] + self.can = can + ColorMsg(msg=f"Current SDK version: {self.version}", color="green") + self.hand_joint = hand_joint + self.hand_type = hand_type + self.is_palm_touch = -1 # 是否为全掌压力传感器 + if self.hand_type == "left": + self.hand_id = 0x28 # Left hand + if self.hand_type == "right": + self.hand_id = 0x27 # Right hand + if self.hand_joint.upper() == "O6": + if modbus != "None": + from core.rs485.linker_hand_o6_rs485 import LinkerHandO6RS485 + self.hand = LinkerHandO6RS485(hand_id=self.hand_id,modbus_port=modbus,baudrate=115200) + else: + from core.can.linker_hand_o6_can import LinkerHandO6Can + self.hand = LinkerHandO6Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "L6": + if modbus != "None": + from core.rs485.linker_hand_l6_rs485 import LinkerHandL6RS485 + self.hand = LinkerHandL6RS485(hand_id=self.hand_id,modbus_port=modbus,baudrate=115200) + else: + from core.can.linker_hand_l6_can import LinkerHandL6Can + self.hand = LinkerHandL6Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "L7": + if modbus != "None": + from core.rs485.linker_hand_l7_rs485 import LinkerHandL7RS485 + self.hand = LinkerHandL7RS485(hand_id=self.hand_id,modbus_port=modbus,baudrate=115200) + else: + from core.can.linker_hand_l7_can import LinkerHandL7Can + self.hand = LinkerHandL7Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "L10": + if modbus != "None": + from core.rs485.linker_hand_l10_rs485 import LinkerHandL10RS485 + self.hand = LinkerHandL10RS485(hand_id=self.hand_id,modbus_port=modbus,baudrate=115200) + else: + from core.can.linker_hand_l10_can import LinkerHandL10Can + self.hand = LinkerHandL10Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "L20": + from core.can.linker_hand_l20_can import LinkerHandL20Can + self.hand = LinkerHandL20Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "G20": + from core.can.linker_hand_g20_can import LinkerHandG20Can + self.hand = LinkerHandG20Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + time.sleep(0.01) + self.is_palm_touch = self.hand.get_touch_sensor_type() + ColorMsg(msg=f"传感器类型:{self.is_palm_touch}") + if self.hand_joint == "L21": + from core.can.linker_hand_l21_can import LinkerHandL21Can + self.hand = LinkerHandL21Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + if self.hand_joint == "L25": + from core.can.linker_hand_l25_can import LinkerHandL25Can + self.hand = LinkerHandL25Can(can_id=self.hand_id,can_channel=self.can, yaml=self.yaml) + # Open can0 + if sys.platform == "linux" and modbus=="None": + self.open_can = OpenCan(load_yaml=self.yaml) + self.open_can.open_can(self.can) + self.is_can = self.open_can.is_can_up_sysfs(interface=self.can) + if not self.is_can: + ColorMsg(msg=f"{self.can} interface is not open", color="red") + sys.exit(1) + version = self.get_embedded_version() + self.serial_number = self.get_serial_number() + if version == None or len(version) == 0: + ColorMsg(msg="Warning: Hardware version number not recognized, it is recommended to terminate the program and re insert USB to CAN conversion", color="yellow") + else: + ColorMsg(msg=f"Embedded:{version}", color="green") + ColorMsg(msg=f"Linker Hand Serial Number: {self.serial_number}", color="green") + + + # Five-finger movement + def finger_move(self, pose=[]): + ''' + Five-finger movement + @params: pose list L7 len(7) | L10 len(10) | L20 len(20) | L25 len(25) 0~255 + ''' + + if len(pose) == 0: + return + pose = [int(v) for v in pose] + if any(not isinstance(x, (int, float)) or x < 0 or x > 255 for x in pose): + ColorMsg(msg=f"The numerical range cannot be less than 0 or greater than 255",color="red") + return + if (self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6") and len(pose) == 6: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "L7" and len(pose) == 7: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "L10" and len(pose) == 10: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "L20" and len(pose) == 20: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "G20" and len(pose) == 20: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "L21" and len(pose) == 25: + self.hand.set_joint_positions(pose) + elif self.hand_joint == "L25" and len(pose) == 25: + self.hand.set_joint_positions(pose) + else: + ColorMsg(msg=f"Current LinkerHand is {self.hand_type}{self.hand_joint}, action sequence is {pose}, does not match", color="red") + self.last_position = pose + + def _get_normal_force(self): + '''# Get normal force''' + self.hand.get_normal_force() + + def _get_tangential_force(self): + '''# Get tangential force''' + self.hand.get_tangential_force() + + def _get_tangential_force_dir(self): + '''# Get tangential force direction''' + self.hand.get_tangential_force_dir() + + def _get_approach_inc(self): + '''# Get approach increment''' + self.hand.get_approach_inc() + + + def set_speed(self, speed=[100]*5): + '''# Set speed''' + has_non_int = any(not isinstance(x, (int, float)) or x < 0 or x > 255 for x in speed) + if has_non_int: + print("Set Speed The numerical range can only be positive integers or floating-point numbers between 0 and 255", flush=True) + return + if len(speed) < 5: + print("数据长度不够,至少5个元素", flush=True) + return + if self.hand_joint == "L7" and len(speed) < 7: + print("数据长度不够,至少7个元素", flush=True) + return + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} set speed to {speed}", color="green") + self.hand.set_speed(speed=speed) + + def set_joint_speed(self, speed=[100]*5): + '''Set speed by topic''' + if len(speed) == 0: + return + if any(not isinstance(x, (int, float)) or x < 10 or x > 255 for x in speed): + ColorMsg(msg=f"The numerical range cannot be less than 10 or greater than 255",color="red") + return + self.hand.set_speed(speed=speed) + + def set_torque(self, torque=[180] * 5): + '''Set maximum torque''' + has_non_int = any(not isinstance(x, (int, float)) or x < 0 or x > 255 for x in torque) + if has_non_int: + print("Set Torque The numerical range can only be positive integers or floating-point numbers between 0 and 255", flush=True) + return + if len(torque) < 5: + print("数据长度不够,至少5个元素", flush=True) + return + if self.hand_joint == "L7" and len(torque) < 7: + print("数据长度不够,至少7个元素", flush=True) + return + if (self.hand_joint == "L6" or self.hand_joint == "O6") and len(torque) != 6: + print("L6 or O6数据长度错误,至少6个元素", flush=True) + return + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} set maximum torque to {torque}", color="green") + return self.hand.set_torque(torque=torque) + + + def set_current(self, current=[250] * 5): + '''Set current L7/L10/L25 not supported''' + if any(not isinstance(x, (int, float)) or x < 0 or x > 255 for x in current): + print("Set Current The numerical range can only be positive integers or floating-point numbers between 0 and 255", flush=True) + return + if self.hand_joint == "L20": + return self.hand.set_current(current=current) + else: + pass + + def get_embedded_version(self): + '''Get embedded version''' + return self.hand.get_version() + + def get_serial_number(self): + '''Get serial number''' + try: + return self.hand.sn + except: + return self.hand.get_serial_number() + + def get_current(self): + '''Get current''' + return self.hand.get_current() + + def get_state(self): + '''Get current joint state''' + return self.hand.get_current_status() + + + def get_state_for_pub(self): + return self.hand.get_current_pub_status() + + def get_speed(self): + '''Get speed''' + return self.hand.get_speed() + + + def get_joint_speed(self): + speed = [] + if self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6": + return self.hand.get_speed() + elif self.hand_joint == "L7": + return self.hand.get_speed() + elif self.hand_joint == "L10": + speed = self.hand.get_speed() + return speed + elif self.hand_joint == "G20": + return self.hand.get_speed() + elif self.hand_joint == "L20": + speed = self.hand.get_speed() + return [255, speed[1], speed[2], speed[3], speed[4], 255, 255, 255, 255, 255, speed[0], 255, 255, 255, 255, 255, 255, 255, 255, 255] + elif self.hand_joint == "L21": + return self.hand.get_speed() + elif self.hand_joint == "L25": + return self.hand.get_speed() + + def get_touch_type(self): + '''Get touch type''' + try: + return self.hand.touch_type + except: + return self.hand.get_touch_type() + + def get_force(self): + '''Get normal force, tangential force, tangential force direction, approach sensing data''' + self._get_normal_force() + self._get_tangential_force() + self._get_tangential_force_dir() + self._get_approach_inc() + return self.hand.get_force() + + def get_touch(self): + '''Get touch data''' + return self.hand.get_touch() + + def get_matrix_touch(self): + return self.hand.get_matrix_touch() + + def get_matrix_touch_v2(self): + return self.hand.get_matrix_touch_v2() + + + def get_thumb_matrix_touch(self,sleep_time=0): + if sleep_time > 0: + return self.hand.get_thumb_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_thumb_matrix_touch() + + def get_index_matrix_touch(self,sleep_time=0): + if sleep_time > 0: + return self.hand.get_index_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_index_matrix_touch() + + def get_middle_matrix_touch(self,sleep_time=0): + if sleep_time > 0: + return self.hand.get_middle_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_middle_matrix_touch() + + def get_ring_matrix_touch(self,sleep_time=0): + if sleep_time > 0: + return self.hand.get_ring_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_ring_matrix_touch() + + def get_little_matrix_touch(self,sleep_time=0): + if sleep_time > 0: + return self.hand.get_little_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_little_matrix_touch() + + def get_palm_matrix_touch(self,sleep_time=0): + if self.is_palm_touch == 5: + if sleep_time > 0: + return self.hand.get_palm_matrix_touch(sleep_time=sleep_time) + else: + return self.hand.get_palm_matrix_touch() + + def get_torque(self): + '''Get current maximum torque''' + return self.hand.get_torque() + + def get_temperature(self): + '''Get current motor temperature''' + return self.hand.get_temperature() + + def get_fault(self): + '''Get motor fault code''' + return self.hand.get_fault() + + def clear_faults(self): + '''Clear motor fault codes Not supported yet, currently only supports L20''' + self.hand.clear_faults() + return [0] * 5 + + def set_enable(self): + '''Set motor enable Only supports L25''' + if self.hand_joint == "L25": + self.hand.set_enable_mode() + else: + pass + + def set_disable(self): + '''Set motor disable Only supports L25''' + if self.hand_joint == "L25": + self.hand.set_disability_mode() + else: + pass + + def get_finger_order(self): + '''Get finger motor order''' + # if self.hand_joint == "L21" or self.hand_joint == "L25" or self.hand_joint == "G20": + # return self.hand.get_finger_order() + # else: + # return [] + return self.hand.get_finger_order() + + def range_to_arc_left(self, state, hand_joint): + return range_to_arc_left(left_range=state, hand_joint=hand_joint) + + def range_to_arc_right(self, state, hand_joint): + return range_to_arc_right(right_range=state, hand_joint=hand_joint) + + def arc_to_range_left(self,state,hand_joint): + return arc_to_range_left(hand_arc_l=state,hand_joint=hand_joint) + + def arc_to_range_right(self,state,hand_joint): + return arc_to_range_right(right_arc=state,hand_joint=hand_joint) + + def show_fun_table(self): + self.hand.show_fun_table() + + def close_can(self): + if sys.platform == "linux" and modbus=="None": + self.open_can.close_can(can=self.can) + +if __name__ == "__main__": + hand = LinkerHandApi(hand_type="right", hand_joint="L10") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/color_msg.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/color_msg.py new file mode 100644 index 0000000..9ba06ac --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/color_msg.py @@ -0,0 +1,27 @@ +#! /usr/bin/env python3 + +import time + +class ColorMsg(): + def __init__(self,msg: str,color: str = '', timestamp: bool = True) -> None: + self.msg = msg + self.color = color + self.timestamp = timestamp + self.colorMsg(msg=self.msg, color=self.color, timestamp=self.timestamp) + + def colorMsg(self,msg: str, color: str = '', timestamp: bool = True): + str = "" + if timestamp: + str += time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime(time.time())) + " " + if color == "red": + str += "\033[1;31;40m" + elif color == "green": + str += "\033[1;32;40m" + elif color == "yellow": + str += "\033[1;33;40m" + else: + print(str + msg, flush=True) + return + str += msg + "\033[0m" + print(str, flush=True) \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/init_linker_hand.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/init_linker_hand.py new file mode 100644 index 0000000..6bfcae7 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/init_linker_hand.py @@ -0,0 +1,81 @@ +''' +Author: HJX +Date: 2025-04-01 14:09:21 +LastEditors: Please set LastEditors +LastEditTime: 2025-04-08 11:18:23 +FilePath: /Linker_Hand_SDK_ROS/src/linker_hand_sdk_ros/scripts/LinkerHand/utils/init_linker_hand.py +Description: +symbol_custom_string_obkorol_copyright: +''' +import yaml, os, sys +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from load_write_yaml import LoadWriteYaml + +class InitLinkerHand(): + def __init__(self): + self.yaml = LoadWriteYaml() + self.setting = self.yaml.load_setting_yaml() + + def current_hand(self): + ''' + 初始化灵巧手 + return: hand_joint str L7/L10/L20/L21/L25, hand_type str left or right + ''' + # 左手是否配置 + self.left_hand = None + self.left_hand_joint = None + self.left_hand_type = None + self.left_hand_force = None + self.left_hand_pose = None + self.left_hand_torque = [200, 200, 200, 200, 200] + self.left_hand_speed = [80, 200, 200, 200, 200] + # 右手是否配置 + self.right_hand = None + self.right_hand_joint = None + self.right_hand_type = None + self.right_hand_force = None + self.right_hand_pose = None + self.right_hand_torque = [200, 200, 200, 200, 200] + self.right_hand_speed = [80, 200, 200, 200, 200] + if self.setting['LINKER_HAND']['LEFT_HAND']['EXISTS'] == True: + self.left_hand = True + self.left_hand_joint = self.setting['LINKER_HAND']['LEFT_HAND']['JOINT'] + self.left_hand_type = "left" + self.left_hand_force = self.setting['LINKER_HAND']['LEFT_HAND']['TOUCH'] + if self.left_hand_joint == "L7": + # The data length of L7 is 7, reinitialize here + self.left_hand_pose = [255, 200, 255, 255, 255, 255, 180] + self.left_hand_torque = [250, 250, 250, 250, 250, 250, 250] + self.left_hand_speed = [120, 180, 180, 180, 180, 180, 180] + elif self.left_hand_joint == "L10": + self.left_hand_pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] + elif self.left_hand_joint == "L20": + self.left_hand_pose = [255,255,255,255,255,255,10,100,180,240,245,255,255,255,255,255,255,255,255,255] + elif self.left_hand_joint == "L21": + self.left_hand_pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + elif self.left_hand_joint == "L25": + self.left_hand_pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + # 判断右手是否配置 + if self.setting['LINKER_HAND']['RIGHT_HAND']['EXISTS'] == True: + self.right_hand = True + self.right_hand_joint = self.setting['LINKER_HAND']['RIGHT_HAND']['JOINT'] + self.right_hand_type = "right" + self.right_hand_force = self.setting['LINKER_HAND']['RIGHT_HAND']['TOUCH'] + if self.right_hand_joint == "L7": + # The data length of L7 is 7, reinitialize here + self.right_hand_pose = [255, 200, 255, 255, 255, 255, 180] + self.right_hand_torque = [250, 250, 250, 250, 250, 250, 250] + self.right_hand_speed = [120, 250, 250, 250, 250, 250, 250] + elif self.right_hand_joint == "L10": + self.right_hand_pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] + elif self.right_hand_joint == "L20": + self.right_hand_pose = [255,255,255,255,255,255,10,100,180,240,245,255,255,255,255,255,255,255,255,255] + elif self.right_hand_joint == "L21": + self.right_hand_pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + elif self.right_hand_joint == "L25": + self.right_hand_pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + + + return self.left_hand ,self.left_hand_joint ,self.left_hand_type ,self.left_hand_force,self.left_hand_pose, self.left_hand_torque, self.left_hand_speed ,self.right_hand ,self.right_hand_joint ,self.right_hand_type ,self.right_hand_force,self.right_hand_pose, self.right_hand_torque, self.right_hand_speed,self.setting + + \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/load_write_yaml.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/load_write_yaml.py new file mode 100644 index 0000000..160674c --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/load_write_yaml.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +Author: HJX +Date: 2025-04-01 14:09:21 +LastEditors: Please set LastEditors +LastEditTime: 2025-04-11 10:19:01 +FilePath: /LinkerHand_Python_SDK/LinkerHand/utils/load_write_yaml.py +Description: +symbol_custom_string_obkorol_copyright: +''' +import yaml, os, sys +class LoadWriteYaml(): + def __init__(self): + # 由于是API形式,这里要给配置文件目录绝对路径 + #yaml_path = "/home/linkerhand/ROS2/linker_hand_ros2_sdk/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand" + yaml_path = os.path.dirname(os.path.abspath(__file__)) + "/../../LinkerHand" + self.setting_path = yaml_path+"/config/setting.yaml" + self.l7_positions = yaml_path+"/config/L7_positions.yaml" + self.l10_positions = yaml_path+"/config/L10_positions.yaml" + self.l20_positions = yaml_path+"/config/L20_positions.yaml" + self.l21_positions = yaml_path+"/config/L21_positions.yaml" + self.l25_positions = yaml_path+"/config/L25_positions.yaml" + + + def load_setting_yaml(self): + try: + with open(self.setting_path, 'r', encoding='utf-8') as file: + setting = yaml.safe_load(file) + self.sdk_version = setting["VERSION"] + self.left_hand_exists = setting['LINKER_HAND']['LEFT_HAND']['EXISTS'] + self.left_hand_names = setting['LINKER_HAND']['LEFT_HAND']['NAME'] + self.left_hand_joint = setting['LINKER_HAND']['LEFT_HAND']['JOINT'] + self.left_hand_force = setting['LINKER_HAND']['LEFT_HAND']['TOUCH'] + self.right_hand_exists = setting['LINKER_HAND']['RIGHT_HAND']['EXISTS'] + self.right_hand_names = setting['LINKER_HAND']['RIGHT_HAND']['NAME'] + self.right_hand_joint = setting['LINKER_HAND']['RIGHT_HAND']['JOINT'] + self.right_hand_force = setting['LINKER_HAND']['RIGHT_HAND']['TOUCH'] + self.password = setting['PASSWORD'] + except Exception as e: + setting = None + print(f"Error reading setting.yaml: {e}") + self.setting = setting + return self.setting + + def load_action_yaml(self,hand_joint="",hand_type=""): + if hand_joint == "L20": + action_path = self.l20_positions + elif hand_joint == "L10": + action_path = self.l10_positions + elif hand_joint == "L25": + action_path = self.l25_positions + elif hand_joint == "L21": + action_path = self.l21_positions + elif hand_joint == "L7": + action_path = self.l7_positions + print(action_path) + try: + with open(action_path, 'r', encoding='utf-8') as file: + yaml_data = yaml.safe_load(file) + if hand_type == "left": + self.action_yaml = yaml_data["LEFT_HAND"] + else: + self.action_yaml = yaml_data["RIGHT_HAND"] + except Exception as e: + self.action_yaml = None + print(f"yaml配置文件不存在: {e}") + return self.action_yaml + + def write_to_yaml(self, action_name, action_pos,hand_joint="",hand_type=""): + a = False + if hand_joint == "L20": + action_path = self.l20_positions + elif hand_joint == "L10": + action_path = self.l10_positions + elif hand_joint == "L7": + action_path = self.l7_positions + elif hand_joint == "L21": + action_path = self.l21_positions + elif hand_joint == "L25": + action_path = self.l25_positions + try: + with open(action_path, 'r', encoding='utf-8') as file: + yaml_data = yaml.safe_load(file) + print(yaml_data) + if hand_type == "left": + if yaml_data["LEFT_HAND"] == None: + yaml_data["LEFT_HAND"] = [] + yaml_data["LEFT_HAND"].append({"ACTION_NAME": action_name, "POSITION": action_pos}) + elif hand_type == "right": + if yaml_data["RIGHT_HAND"] == None: + yaml_data["RIGHT_HAND"] = [] + yaml_data["RIGHT_HAND"].append({"ACTION_NAME": action_name, "POSITION": action_pos}) + with open(action_path, 'w', encoding='utf-8') as file: + yaml.safe_dump(yaml_data, file, allow_unicode=True) + a = True + except Exception as e: + a = False + print(f"Error writing to yaml file: {e}") + return a + \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/mapping.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/mapping.py new file mode 100644 index 0000000..deb6a19 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/mapping.py @@ -0,0 +1,383 @@ +#--------------------------------------------------------------------------------------------------- +# L6 L +l6_l_min = [0, 0, 0, 0, 0, 0] +l6_l_max = [0.99, 1.39, 1.26, 1.26, 1.26, 1.26] +l6_l_derict = [-1, -1, -1, -1, -1, -1] +# L6 R +l6_r_min = [0, 0, 0, 0, 0, 0] +l6_r_max = [0.99, 1.39, 1.26, 1.26, 1.26, 1.26] +l6_r_derict = [-1, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- +# O6 L +o6_l_min = [0, 0, 0, 0, 0, 0] +o6_l_max = [0.58, 1.36, 1.6, 1.6, 1.6, 1.6] +o6_l_derict = [-1, -1, -1, -1, -1, -1] +# O6 R +o6_r_min = [0, 0, 0, 0, 0, 0] +o6_r_max = [0.58, 1.36, 1.6, 1.6, 1.6, 1.6] +o6_r_derict = [-1, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- +# L7 L OK +l7_l_min = [0, 0, 0, 0, 0, 0, 0] +l7_l_max = [0.44, 1.43, 1.62, 1.62, 1.62, 1.62, 1.01] +l7_l_derict = [-1, -1, -1, -1, -1, -1, -1] +# L7 R OK (urdf后续会更改!!!) +l7_r_min = [0, -1.43, 0, 0, 0, 0, 0] +l7_r_max = [0.75, 0, 1.62, 1.62, 1.62, 1.62, 1.54] +l7_r_derict = [-1, 0, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- +# L10 L OK +l10_l_min = [0, 0, 0, 0, 0, 0, 0, -0.26, -0.26, -0.52] +l10_l_max = [1.45, 1.43, 1.62, 1.62, 1.62, 1.62, 0.26, 0, 0, 1.01] +l10_l_derict = [-1, -1, -1, -1, -1, -1, 0, -1, -1, -1] +# L10 R OK +l10_r_min = [0, 0, 0, 0, 0, 0, -0.26, 0, 0, -0.52] +l10_r_max = [0.75, 1.43, 1.62, 1.62, 1.62, 1.62, 0.21, 0.21, 0.34, 1.01] +l10_r_derict = [-1, -1, -1, -1, -1, -1, 0, 0, 0, -1] +#--------------------------------------------------------------------------------------------------- +# L20 L OK +l20_l_min = [0, 0, 0, 0, 0, -0.297, -0.26, -0.26, -0.26, -0.26, 0.122, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l20_l_max = [0.87, 1.4, 1.4, 1.4, 1.4, 0.683, 0.26, 0.26, 0.26, 0.26, 1.78, 0, 0, 0, 0, 1.29, 1.08, 1.08, 1.08, 1.08] +l20_l_derict = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1] +# L20 R OK +l20_r_min = [0, 0, 0, 0, 0, -0.297, -0.26, -0.26, -0.26, -0.26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l20_r_max = [0.87, 1.4, 1.4, 1.4, 1.4, 0.683, 0.26, 0.26, 0.26, 0.26, 1.78, 0, 0, 0, 0, 1.29, 1.08, 1.08, 1.08, 1.08] +l20_r_derict = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- +# L21 L OK +l21_l_min = [0, 0, 0, 0, 0, 0, 0, -0.18, -0.18, 0, -0.6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l21_l_max = [1, 1.57, 1.57, 1.57, 1.57, 1.6, 0.18, 0.18, 0.18, 0.18, 0.6, 0, 0, 0, 0, 1.57, 0, 0, 0, 0, 1.57, 1.57, 1.57, 1.57, 1.57] +l21_l_derict = [-1, -1, -1, -1, -1, -1, -1, -1, -1, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1] +# L21 R OK +l21_r_min = [0, 0, 0, 0, 0, 0, -0.18, -0.18, -0.18, -0.18, -0.6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l21_r_max = [1, 1.57, 1.57, 1.57, 1.57, 1.6, 0.18, 0.18, 0.18, 0.18, 0.6, 0, 0, 0, 0, 1.57, 0, 0, 0, 0, 1.57, 1.57, 1.57, 1.57, 1.57] +l21_r_derict = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- +#--------------------------------------------------------------------------------------------------- +# L25 L OK +l25_l_min = [0, 0, 0, 0, 0, 0, -0.26, -0.26, -0.26, -0.26, -0.26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l25_l_max = [0.9, 1.57, 1.57, 1.57, 1.57, 1.3, 0.26, 0.26, 0.26, 0.26, 0.61, 0, 0, 0, 0, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57] +l25_l_derict = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] +# L25 R OK +l25_r_min = [0, 0, 0, 0, 0, 0, -0.26, -0.26, -0.26, -0.26, -0.26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] +l25_r_max = [0.9, 1.57, 1.57, 1.57, 1.57, 1.3, 0.26, 0.26, 0.26, 0.26, 0.61, 0, 0, 0, 0, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57, 1.57] +l25_r_derict = [-1, -1, -1, -1, -1, -1, 0, 0, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1] +#--------------------------------------------------------------------------------------------------- + +def range_to_arc_left(left_range,hand_joint): + num=0 + if hand_joint == "L6": + num = 6 + l_min = l6_l_min + l_max = l6_l_max + l_derict = l6_l_derict + elif hand_joint == "O6": + num = 6 + l_min = o6_l_min + l_max = o6_l_max + l_derict = o6_l_derict + elif hand_joint == "L7": + num = 7 + l_min = l7_l_min + l_max = l7_l_max + l_derict = l7_l_derict + elif hand_joint == "L10": + num = 10 + l_min = l10_l_min + l_max = l10_l_max + l_derict = l10_l_derict + elif hand_joint == "L20": + num = 20 + l_min = l20_l_min + l_max = l20_l_max + l_derict = l20_l_derict + elif hand_joint == "L21": + num = 25 + l_min = l21_l_min + l_max = l21_l_max + l_derict = l21_l_derict + hand_arc = [0] * num + for i in range(num): + if hand_joint == "L20": + if 11 <= i <= 14: continue + if hand_joint == "L21": + if 11 <= i <= 14: continue + if 16 <= i <= 19: continue + val_l = is_within_range(left_range[i], 0, 255) + if l_derict[i] == -1: + hand_arc[i] = scale_value(val_l, 0, 255, l_max[i], l_min[i]) + else: + hand_arc[i] = scale_value(val_l, 0, 255, l_min[i], l_max[i]) + return hand_arc + +def range_to_arc_right(right_range,hand_joint): + num=0 + if hand_joint == "L6": + num = 6 + r_min = l6_r_min + r_max = l6_r_max + r_derict = l6_r_derict + elif hand_joint == "O6": + num = 6 + r_min = o6_r_min + r_max = o6_r_max + r_derict = o6_r_derict + elif hand_joint == "L7": + num = 7 + r_min = l7_r_min + r_max = l7_r_max + r_derict = l7_r_derict + elif hand_joint == "L10": + num = 10 + r_min = l10_r_min + r_max = l10_r_max + r_derict = l10_r_derict + elif hand_joint == "L20": + num = 20 + r_min = l20_r_min + r_max = l20_r_max + r_derict = l20_r_derict + elif hand_joint == "L21": + num = 25 + r_min = l21_r_min + r_max = l21_r_max + r_derict = l21_r_derict + hand_arc = [0] * num + for i in range(num): + if hand_joint == "L20": + if 11 <= i <= 14: continue + if hand_joint == "L21": + if 11 <= i <= 14: continue + if 16 <= i <= 19: continue + val_r = is_within_range(right_range[i], 0, 255) + if r_derict[i] == -1: + hand_arc[i] = scale_value(val_r, 0, 255, r_max[i], r_min[i]) + else: + hand_arc[i] = scale_value(val_r, 0, 255, r_min[i], r_max[i]) + return hand_arc + +''' +def arc_to_range_left(left_arc,hand_joint): + num=0 + if hand_joint == "L7": + num = 7 + l_min = l7_l_min + l_max = l7_l_max + l_derict = l7_l_derict + elif hand_joint == "L10": + num = 10 + l_min = l10_l_min + l_max = l10_l_max + l_derict = l10_l_derict + elif hand_joint == "L20": + num = 20 + l_min = l20_l_min + l_max = l20_l_max + l_derict = l20_l_derict + elif hand_joint == "L21": + num = 25 + l_min = l21_l_min + l_max = l21_l_max + l_derict = l21_l_derict + hand_range = [0] * num + for i in range(num): + if hand_joint == "L20": + if 11 <= i <= 14: continue + if hand_joint == "L21": + if 11 <= i <= 14: continue + if 16 <= i <= 19: continue + val_l = is_within_range(left_arc[i], 0, 255) + if l_derict[i] == -1: + hand_range[i] = scale_value(val_l, 0, 255, l_max[i], l_min[i]) + else: + hand_range[i] = scale_value(val_l, 0, 255, l_min[i], l_max[i]) + return hand_range + ''' +def arc_to_range_left(hand_arc_l,hand_joint): + num=0 + if hand_joint == "O6": + num = 6 + l_min = o6_l_min + l_max = o6_l_max + l_derict = o6_l_derict + elif hand_joint == "L7": + num = 7 + l_min = l7_l_min + l_max = l7_l_max + l_derict = l7_l_derict + elif hand_joint == "L10": + num = 10 + l_min = l10_l_min + l_max = l10_l_max + l_derict = l10_l_derict + elif hand_joint == "L20": + num = 20 + l_min = l20_l_min + l_max = l20_l_max + l_derict = l20_l_derict + elif hand_joint == "L21": + num = 25 + l_min = l21_l_min + l_max = l21_l_max + l_derict = l21_l_derict + hand_range = [0] * num + #hand_range_l = [0] * 7 + for i in range(num): + if hand_joint == "L20": + if 11 <= i <= 14: continue + if hand_joint == "L21": + if 11 <= i <= 14: continue + if 16 <= i <= 19: continue + val_l = is_within_range(hand_arc_l[i], l_min[i], l_max[i]) + if l_derict[i] == -1: + hand_range[i] = scale_value(val_l, l_min[i], l_max[i], 255, 0) + else: + hand_range[i] = scale_value(val_l, l_min[i], l_max[i], 0, 255) + + return hand_range + +def arc_to_range_right(right_arc,hand_joint): + num=0 + if hand_joint == "O6": + num = 6 + r_min = o6_r_min + r_max = o6_r_max + r_derict = o6_r_derict + elif hand_joint == "L7": + num = 7 + r_min = l7_r_min + r_max = l7_r_max + r_derict = l7_r_derict + elif hand_joint == "L10": + num = 10 + r_min = l10_r_min + r_max = l10_r_max + r_derict = l10_r_derict + elif hand_joint == "L20": + num = 20 + r_min = l20_r_min + r_max = l20_r_max + r_derict = l20_r_derict + elif hand_joint == "L21": + num = 25 + r_min = l21_r_min + r_max = l21_r_max + r_derict = l21_r_derict + hand_range = [0] * num + for i in range(num): + if hand_joint == "L20": + if 11 <= i <= 14: continue + if hand_joint == "L21": + if 11 <= i <= 14: continue + if 16 <= i <= 19: continue + val_r = is_within_range(right_arc[i], r_min[i], r_max[i]) + if r_derict[i] == -1: + hand_range[i] = scale_value(val_r, r_min[i], r_max[i], 255, 0) + else: + hand_range[i] = scale_value(val_r, r_min[i], r_max[i], 0, 255) + return hand_range + + + + +def range_to_arc_right_l20(hand_range_r): + hand_arc_r = [0] * 20 + for i in range(20): + if 11 <= i <= 14: continue + val_r = is_within_range(hand_range_r[i], 0, 255) + if l20_r_derict[i] == -1: + hand_arc_r[i] = scale_value(val_r, 0, 255, l20_r_max[i], l20_r_min[i]) + else: + hand_arc_r[i] = scale_value(val_r, 0, 255, l20_r_min[i], l20_r_max[i]) + return hand_arc_r + + +def range_to_arc_left_l20(hand_range_l): + hand_arc_l = [0] * 20 + for i in range(20): + if 11 <= i <= 14: continue + val_l = is_within_range(hand_range_l[i], 0, 255) + if l20_l_derict[i] == -1: + hand_arc_l[i] = scale_value(val_l, 0, 255, l20_l_max[i], l20_l_min[i]) + else: + hand_arc_l[i] = scale_value(val_l, 0, 255, l20_l_min[i], l20_l_max[i]) + return hand_arc_l + + +def arc_to_range_right_l20(hand_arc_r): + hand_range_r = [0] * 20 + for i in range(20): + if 11 <= i <= 14: continue + val_r = is_within_range(hand_arc_r[i], l20_r_min[i], l20_r_max[i]) + if l20_r_derict[i] == -1: + hand_range_r[i] = scale_value(val_r, l20_r_min[i], l20_r_max[i], 255, 0) + else: + hand_range_r[i] = scale_value(val_r, l20_r_min[i], l20_r_max[i], 0, 255) + return hand_range_r + + +def arc_to_range_left_l20(hand_arc_l): + hand_range_l = [0] * 20 + for i in range(20): + if 11 <= i <= 14: continue + val_l = is_within_range(hand_arc_l[i], l20_l_min[i], l20_l_max[i]) + if l20_l_derict[i] == -1: + hand_range_l[i] = scale_value(val_l, l20_l_min[i], l20_l_max[i], 255, 0) + else: + hand_range_l[i] = scale_value(val_l, l20_l_min[i], l20_l_max[i], 0, 255) + + return hand_range_l + + +def range_to_arc_right_10(hand_range_r): + hand_arc_r = [0] * 10 + for i in range(10): + val_r = is_within_range(hand_range_r[i], 0, 255) + if l10_r_derict[i] == -1: + hand_arc_r[i] = scale_value(val_r, 0, 255, l10_r_max[i], l10_r_min[i]) + else: + hand_arc_r[i] = scale_value(val_r, 0, 255, l10_r_min[i], l10_r_max[i]) + + return hand_arc_r + + +def range_to_arc_left_10(hand_range_l): + hand_arc_l = [0] * 10 + for i in range(10): + val_l = is_within_range(hand_range_l[i], 0, 255) + if l10_l_derict[i] == -1: + hand_arc_l[i] = scale_value(val_l, 0, 255, l10_l_max[i], l10_l_min[i]) + else: + hand_arc_l[i] = scale_value(val_l, 0, 255, l10_l_min[i], l10_l_max[i]) + return hand_arc_l + + +def arc_to_range_right_10(hand_arc_r): + hand_range_r = [0] * 10 + for i in range(10): + val_r = is_within_range(hand_arc_r[i], l10_r_min[i], l10_r_max[i]) + if l10_r_derict[i] == -1: + hand_range_r[i] = scale_value(val_r, l10_r_min[i], l10_r_max[i], 255, 0) + else: + hand_range_r[i] = scale_value(val_r, l10_r_min[i], l10_r_max[i], 0, 255) + return hand_range_r + + +def arc_to_range_left_10(hand_arc_l): + hand_range_l = [0] * 10 + for i in range(10): + val_l = is_within_range(hand_arc_l[i], l10_l_min[i], l10_l_max[i]) + if l10_l_derict[i] == -1: + hand_range_l[i] = scale_value(val_l, l10_l_min[i], l10_l_max[i], 255, 0) + else: + hand_range_l[i] = scale_value(val_l, l10_l_min[i], l10_l_max[i], 0, 255) + + return hand_range_l + + +def scale_value(original_value, a_min, a_max, b_min, b_max): + return (original_value - a_min) * (b_max - b_min) / (a_max - a_min) + b_min + + +def is_within_range(value, min_value, max_value): + return min(max_value, max(min_value, value)) diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/open_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/open_can.py new file mode 100644 index 0000000..96aa932 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/utils/open_can.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +Author: HJX +Date: 2025-04-01 14:09:21 +LastEditors: Please set LastEditors +LastEditTime: 2025-04-11 09:15:31 +FilePath: /Linker_Hand_SDK_ROS/src/linker_hand_sdk_ros/scripts/LinkerHand/utils/open_can.py +Description: +symbol_custom_string_obkorol_copyright: +''' +import sys,os,time,subprocess +sys.path.append(os.path.dirname(os.path.abspath(__file__))) +from color_msg import ColorMsg +from load_write_yaml import LoadWriteYaml +# from ament_index_python.packages import get_package_share_directory +import os + + +class OpenCan: + def __init__(self,load_yaml=None): + self.yaml = LoadWriteYaml() + self.password = self.yaml.load_setting_yaml()["PASSWORD"] + + def open_can0(self): + try: + # 检查 can0 接口是否已存在并处于 up 状态 + result = subprocess.run( + ["ip", "link", "show", "can0"], + check=True, + text=True, + capture_output=True + ) + if "state UP" in result.stdout: + return + # 如果没有处于 UP 状态,则配置接口 + subprocess.run( + ["sudo", "-S", "ip", "link", "set", "can0", "up", "type", "can", "bitrate", "1000000"], + input=f"{self.password}\n", + check=True, + text=True, + capture_output=True + ) + + except subprocess.CalledProcessError as e: + pass + except Exception as e: + pass + def open_can(self,can="can0"): + try: + # 检查 can0 接口是否已存在并处于 up 状态 + result = subprocess.run( + ["ip", "link", "show", can], + check=True, + text=True, + capture_output=True + ) + if "state UP" in result.stdout: + return + # 如果没有处于 UP 状态,则配置接口 + subprocess.run( + ["sudo", "-S", "ip", "link", "set", can, "up", "type", "can", "bitrate", "1000000"], + input=f"{self.password}\n", + check=True, + text=True, + capture_output=True + ) + except subprocess.CalledProcessError as e: + pass + except Exception as e: + pass + + + def is_can_up_sysfs(self, interface="can0"): + # 检查接口目录是否存在 + if not os.path.exists(f"/sys/class/net/{interface}"): + return False + # 读取接口状态 + try: + with open(f"/sys/class/net/{interface}/operstate", "r") as f: + state = f.read().strip() + if state == "up": + return True + except Exception as e: + print(f"Error reading CAN interface state: {e}") + return False + + def close_can0(self): + try: + # 检查 can0 接口是否存在 + result = subprocess.run( + ["ip", "link", "show", "can0"], + check=True, + text=True, + capture_output=True + ) + + # 如果接口存在且处于 UP 状态,则关闭它 + if "state UP" in result.stdout: + subprocess.run( + ["sudo", "-S", "ip", "link", "set", "can0", "down"], + input=f"{self.password}\n", + check=True, + text=True, + capture_output=True + ) + return True + return False + + except subprocess.CalledProcessError as e: + print(f"Error closing CAN interface: {e}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + + def close_can(self,can="can0"): + try: + # 检查 can0 接口是否存在 + result = subprocess.run( + ["ip", "link", "show", can], + check=True, + text=True, + capture_output=True + ) + + # 如果接口存在且处于 UP 状态,则关闭它 + if "state UP" in result.stdout: + subprocess.run( + ["sudo", "-S", "ip", "link", "set", can, "down"], + input=f"{self.password}\n", + check=True, + text=True, + capture_output=True + ) + return True + return False + + except subprocess.CalledProcessError as e: + print(f"Error closing CAN interface: {e}") + return False + except Exception as e: + print(f"Unexpected error: {e}") + return False + \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__init__.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py new file mode 100644 index 0000000..6e7d22f --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py @@ -0,0 +1,562 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +编译: colcon build --symlink-install +启动命令:ros2 run linker_hand_ros2_sdk linker_hand_sdk +''' +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from rclpy.parameter import Parameter +from rclpy.qos import HistoryPolicy, QoSProfile, ReliabilityPolicy +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + + +COMMAND_QOS = QoSProfile( + history=HistoryPolicy.KEEP_LAST, + depth=1, + reliability=ReliabilityPolicy.RELIABLE, +) + + +def command_changed(previous, current): + """Return whether a non-empty command differs from the applied command.""" + values = list(current) + if not values: + return False + if previous is None or len(previous) != len(values): + return True + return any(float(old) != float(new) for old, new in zip(previous, values)) + + +def state_poll_due(last_poll_time, now, poll_period): + """Keep slow CAN state reads off the latency-sensitive command path.""" + return last_poll_time is None or now >= last_poll_time + poll_period + + +class LinkerHand(Node): + def __init__(self, name): + super().__init__(name) + # 声明参数(带默认值) + self.declare_parameter('hand_type', 'left') + self.declare_parameter('hand_joint', 'L6') + self.declare_parameter('is_touch', False) + self.declare_parameter('can', 'can0') + self.declare_parameter('modbus', "None") + # -1 keeps the model's original startup speed. Camera teleoperation can + # set this to a conservative value before the startup pose is sent. + self.declare_parameter('startup_speed', -1) + # Empty keeps the legacy absolute topics/startup pose. A prefix lets + # two same-side hands coexist without receiving each other's commands. + self.declare_parameter('topic_prefix', '') + self.declare_parameter('startup_pose', Parameter.Type.INTEGER_ARRAY) + # Hardware state reads are synchronous CAN transactions. Keeping them + # below the command rate prevents G20's five-frame reads from starving + # incoming position commands. + self.declare_parameter('state_poll_rate', 60.0) + self.declare_parameter('velocity_poll_rate', 60.0) + + # ros时间获取 + self.stamp_clock = Clock() + # 获取参数值 + self.hand_type = self.get_parameter('hand_type').value + self.hand_joint = self.get_parameter('hand_joint').value + self.is_touch = self.get_parameter('is_touch').value + self.can = self.get_parameter('can').value + self.modbus = self.get_parameter('modbus').value + self.startup_speed = int(self.get_parameter('startup_speed').value) + if self.startup_speed < -1 or self.startup_speed > 255: + raise ValueError('startup_speed must be -1 or in the range [0, 255]') + self.topic_prefix = self.normalize_topic_prefix( + self.get_parameter('topic_prefix').value + ) + self.state_poll_rate = float( + self.get_parameter('state_poll_rate').value + ) + if self.state_poll_rate <= 0.0: + raise ValueError('state_poll_rate must be greater than zero') + self.state_poll_period = 1.0 / self.state_poll_rate + self.last_state_poll_time = None + self.velocity_poll_rate = float( + self.get_parameter('velocity_poll_rate').value + ) + if self.velocity_poll_rate <= 0.0: + raise ValueError('velocity_poll_rate must be greater than zero') + self.velocity_poll_period = 1.0 / self.velocity_poll_rate + self.last_velocity_poll_time = None + configured_startup_pose = self.get_parameter_or( + 'startup_pose', + Parameter('startup_pose', Parameter.Type.INTEGER_ARRAY, []), + ).value + self.startup_pose = [int(value) for value in configured_startup_pose] + if any(value < 0 or value > 255 for value in self.startup_pose): + raise ValueError('startup_pose values must be in the range [0, 255]') + self.sdk_v = 2 + self.sleep_time = 0.005 + self.cmd_lock = False + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.applied_hand_post_cmd = None + self.applied_hand_vel_cmd = None + + self.last_hand_state = [-1] * 10 + self.last_hand_vel = [-1] * 10 + self.force = [[-1] * 5] * 4 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.last_hand_info = { + "version": [-1], # Dexterous hand version number + "hand_joint": self.hand_joint, # Dexterous hand joint type + "speed": [-1] * 10, # Current speed threshold of the dexterous hand + "current": [-1] * 10, # Current of the dexterous hand + "fault": [-1] * 10, # Current fault of the dexterous hand + "motor_temperature": [-1] * 10, # Current motor temperature of the dexterous hand + "torque": [-1] * 10, # Current torque of the dexterous hand + "is_touch":self.is_touch, + "touch_type": -1, + "finger_order": None # Finger motor order + } + self.version = [] + self.touch_type = -1 + self.hz = 1.0/60.0 + + self.hand_setting_sub = self.create_subscription( + String, self.topic('/cb_hand_setting_cmd'), self.hand_setting_cb, 10 + ) + self._init_hand() + time.sleep(1) + self.run_count = 0 # 计数器,用于记录运行次数 + self.timer = self.create_timer(0.01, self.run) # 100 Hz + self.thread_pub_state = threading.Thread(target=self.pub_state) + self.thread_pub_state.daemon = True + self.thread_pub_state.start() + + @staticmethod + def normalize_topic_prefix(prefix): + prefix = str(prefix).strip() + if not prefix or prefix == '/': + return '' + if not prefix.startswith('/'): + prefix = '/' + prefix + return prefix.rstrip('/') + + def topic(self, absolute_topic): + if not absolute_topic.startswith('/'): + raise ValueError('base topic must be absolute') + return self.topic_prefix + absolute_topic + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription( + JointState, + self.topic(f'/cb_{self.hand_type}_hand_control_cmd'), + self.hand_control_cb, + COMMAND_QOS, + ) + self.hand_state_pub = self.create_publisher(JointState, self.topic(f'/cb_{self.hand_type}_hand_state'),10) + self.hand_info_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_info'), 10) + if self.is_touch == True: + if self.modbus != "None": + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + elif self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + elif self.touch_type != -1 and self.modbus == "None": + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, self.topic(f'/cb_{self.hand_type}_hand_force'), 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + + self.embedded_version = self.api.get_embedded_version() + pose = None + torque = [200, 200, 200, 200, 200] + speed = [200, 250, 250, 250, 250] + if self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6" or self.hand_joint.upper() == "L6P": + pose = [200, 255, 255, 255, 255, 180] + torque = [250, 250, 250, 250, 250, 250] + # O6 最大速度阈值 + speed = [200, 250, 250, 250, 250, 250] + elif self.hand_joint == "L7": + # The data length of L7 is 7, reinitialize here + pose = [255, 200, 255, 255, 255, 255, 180] + torque = [250, 250, 250, 250, 250, 250, 250] + speed = [120, 250, 250, 250, 250, 250, 250] + elif self.hand_joint == "L10": + torque = [255] * 10 + pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] + speed = [200, 250, 250, 250, 250, 250, 250, 250, 250, 250] + elif self.hand_joint == "L20": + pose = [255,255,255,255,255,255,10,100,180,240,245,255,255,255,255,255,255,255,255,255] + elif self.hand_joint == "G20": + # G20 uses a different CAN protocol from L20 even though both + # expose a 20-value ROS command. This is the calibrated startup + # pose also used by linker_hand_advanced_g20. + pose = [255,255,255,255,255,255,193,148,105,42,245,255,255,255,255,255,255,255,255,255] + torque = [255] * 5 + speed = [255] * 5 + elif self.hand_joint == "L21": + pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + elif self.hand_joint == "L25": + pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + if self.startup_pose: + if pose is None or len(self.startup_pose) != len(pose): + expected = 0 if pose is None else len(pose) + raise ValueError( + f'startup_pose for {self.hand_joint} must contain {expected} values' + ) + pose = list(self.startup_pose) + if self.startup_speed >= 0: + speed = [self.startup_speed] * len(speed) + if pose is not None: + for i in range(1): + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + + def hand_control_cb(self, msg): + # The hardware can be slower than the camera. Always replace a + # pending command with the newest sample and never replay an already + # applied sample; this prevents latency from accumulating in software. + position = list(msg.position) + if position: + self.last_hand_post_cmd = ( + position + if command_changed(self.applied_hand_post_cmd, position) + else None + ) + + velocity = list(msg.velocity) + if velocity: + self.last_hand_vel_cmd = ( + velocity + if command_changed(self.applied_hand_vel_cmd, velocity) + else None + ) + + effort = list(msg.effort) + if effort: + self.last_hand_eff_cmd = effort + + def _apply_pending_commands(self): + if self.cmd_lock: + return + + if self.last_hand_post_cmd is not None: + pose = list(self.last_hand_post_cmd) + self.api.finger_move(pose=pose) + self.applied_hand_post_cmd = pose + self.last_hand_post_cmd = None + + if self.last_hand_vel_cmd is not None: + vel = list(self.last_hand_vel_cmd) + if not all(x == 0 for x in vel): + if (str(self.hand_joint).upper() == "O6" or str(self.hand_joint).upper() == "L6" or str(self.hand_joint).upper() == "L6P") and len(vel) == 6: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L7" and len(vel) == 7: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L10" and len(vel) == 10: + speed = [vel[0],vel[2],vel[3],vel[4],vel[5]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L20" and len(vel) == 20: + speed = [vel[10],vel[1],vel[2],vel[3],vel[4]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "G20" and len(vel) == 20: + # G20 speed is configured per finger, not per one of + # the 20 exposed position slots. + speed = [vel[0],vel[1],vel[2],vel[3],vel[4]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L21" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L25" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + self.applied_hand_vel_cmd = vel + self.last_hand_vel_cmd = None + + def _poll_state_if_due(self): + if self.hand_state_pub.get_subscription_count() < 1: + return + now = time.monotonic() + if not state_poll_due( + self.last_state_poll_time, now, self.state_poll_period + ): + return + # Record the start time so a slow G20 read cannot immediately trigger + # another read on the following timer callback. + self.last_state_poll_time = now + self.last_hand_state = self.api.get_state() + time.sleep(0.003) + if state_poll_due( + self.last_velocity_poll_time, now, self.velocity_poll_period + ): + self.last_velocity_poll_time = now + self.last_hand_vel = self.api.get_joint_speed() + time.sleep(0.002) + + def run(self): + if self.sdk_v == 1: + self.sleep_time = 0.009 + # Position commands have priority over synchronous state reads. + self._apply_pending_commands() + self._poll_state_if_due() + if self.cmd_lock == False: + time.sleep(0.003) + if self.run_count == 3 and self.is_touch == True and self.touch_type == 1 and self.modbus == "None" and self.touch_pub.get_subscription_count() > 0: + """单点式压力传感器""" + self.force = self.api.get_force() + if self.is_touch == True and (self.touch_type > 1 or self.modbus != "None") and (self.matrix_touch_pub.get_subscription_count() > 0 or self.matrix_touch_mass_pub.get_subscription_count() > 0 or self.matrix_touch_pub_pc.get_subscription_count() > 0): + """矩阵式压力传感器""" + if self.run_count == 3: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 4: + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 5: + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 6: + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 7: + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=self.sleep_time).tolist() + time.sleep(0.005) + if self.run_count == 8 and self.hand_info_pub.get_subscription_count() > 0: + """手部信息""" + self.last_hand_info = { + "version": self.embedded_version, # Dexterous hand version number + "hand_joint": self.hand_joint, # Dexterous hand joint type + "speed": self.api.get_speed(), # Current speed threshold of the dexterous hand + "current": self.api.get_current(), # Current of the dexterous hand + "fault": self.api.get_fault(), # Current fault of the dexterous hand + "motor_temperature": self.api.get_temperature(), # Current motor temperature of the dexterous hand + "torque": self.api.get_torque(), # Current torque of the dexterous hand + "is_touch":self.is_touch, + "touch_type": self.touch_type, + "finger_order": self.api.get_finger_order() # Finger motor order + } + + if self.run_count == 9: + self.api.clear_faults() # 自动清除错误编码 + self.run_count = 0 + self.run_count += 1 + time.sleep(0.003) + + + def pub_state(self): + while True: + if self.hand_state_pub.get_subscription_count() > 0: + msg = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg) + if self.is_touch == True and self.touch_type == 1 and self.modbus == "None" and self.touch_pub.get_subscription_count() > 0: + msg = Float32MultiArray() + msg.data = [float(val) for sublist in self.force for val in sublist] + self.touch_pub.publish(msg) + if self.is_touch == True and (self.touch_type > 1 or self.modbus != "None") and (self.matrix_touch_pub.get_subscription_count() > 0 or self.matrix_touch_mass_pub.get_subscription_count() > 0 or self.matrix_touch_pub_pc.get_subscription_count() > 0): + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + if self.hand_info_pub.get_subscription_count() > 0: + msg = String() + msg.data = json.dumps(self.last_hand_info) + self.hand_info_pub.publish(msg) + time.sleep(self.hz) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + """发布矩阵数据点云格式""" + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 + # 摊平到一维:360 个 float + flat_list = [v for frame in all_matrices for v in frame] # 360 + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField( + name='val', + offset=0, + datatype=PointField.UINT8, + count=1 + )] + pc = PointCloud2() + pc.header.stamp = self.stamp_clock.now().to_msg() + pc.header.frame_id = '' + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + + + + + def hand_setting_cb(self,msg): + '''控制命令回调''' + data = json.loads(msg.data) + print(f"Received setting command: {data['setting_cmd']}",flush=True) + try: + if data["params"]["hand_type"] == "left": + hand = self.api + hand_left = True + elif data["params"]["hand_type"] == "right": + hand = self.api + hand_right = True + else: + print("Please specify the hand part to be set",flush=True) + return + self.cmd_lock = True + # Set maximum torque + if data["setting_cmd"] == "set_max_torque_limits": # Set maximum torque + torque = list(data["params"]["torque"]) + hand.set_torque(torque=torque) + + if data["setting_cmd"] == "set_speed": # Set speed + if isinstance(data["params"]["speed"], list) == True: + speed = data["params"]["speed"] + hand.set_speed(speed=speed) + else: + ColorMsg(msg=f"Speed parameter error, speed must be a list", color="red") + if data["setting_cmd"] == "clear_faults": # Clear faults + if hand_left == True and self.hand_joint == "L10" : + ColorMsg(msg=f"L10 left hand cannot clear faults") + elif hand_right == True and self.hand_joint == "L10" : + ColorMsg(msg=f"L10 right hand cannot clear faults") + else: + hand.clear_faults() + if data["setting_cmd"] == "get_faults": # Get faults + f = hand.get_fault() + ColorMsg(msg=f"Get faults: {f}") + if data["setting_cmd"] == "electric_current": # Get current + ColorMsg(msg=f"Get current: {hand.get_current()}") + if data["setting_cmd"] == "set_electric_current": # Set current + if isinstance(data["params"]["current"], list) == True: + hand.set_current(data["params"]["current"]) + if data["setting_cmd"] == "show_fun_table": # Get faults + f = hand.show_fun_table() + except: + print("命令参数错误") + self.cmd_lock = False + finally: + self.cmd_lock = False + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + try: + rclpy.init(args=args) + node = LinkerHand("linker_hand_sdk") + embedded_version = node.embedded_version or [] + hand_joint = node.hand_joint.upper() + if len(embedded_version) == 3 or hand_joint in {"O6", "L6", "G20"}: + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + elif len(embedded_version) == 6 and hand_joint == "L10": + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + elif len(embedded_version) > 4 and ((embedded_version[0]==10 and embedded_version[4]>35) or (embedded_version[0]==7 and embedded_version[4]>50) or (embedded_version[0] == 6)): + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + else: + ColorMsg(msg=f"SDK V1", color="green") + node.sdk_v = 1 + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py.bak b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py.bak new file mode 100644 index 0000000..81c31a9 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py.bak @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +编译: colcon build --symlink-install +启动命令:ros2 run linker_hand_ros2_sdk linker_hand_sdk +''' +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + + +class LinkerHand(Node): + def __init__(self, name): + super().__init__(name) + # 声明参数(带默认值) + self.declare_parameter('hand_type', 'left') + self.declare_parameter('hand_joint', 'L6') + self.declare_parameter('is_touch', False) + self.declare_parameter('can', 'can0') + self.declare_parameter('modbus', "None") + + # ros时间获取 + self.stamp_clock = Clock() + # 获取参数值 + self.hand_type = self.get_parameter('hand_type').value + self.hand_joint = self.get_parameter('hand_joint').value + self.is_touch = self.get_parameter('is_touch').value + self.can = self.get_parameter('can').value + self.modbus = self.get_parameter('modbus').value + self.sdk_v = 2 + self.sleep_time = 0.005 + self.cmd_lock = False + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + + self.last_hand_state = [-1] * 10 + self.last_hand_vel = [-1] * 10 + self.force = [[-1] * 5] * 4 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.last_hand_info = { + "version": [-1], # Dexterous hand version number + "hand_joint": self.hand_joint, # Dexterous hand joint type + "speed": [-1] * 10, # Current speed threshold of the dexterous hand + "current": [-1] * 10, # Current of the dexterous hand + "fault": [-1] * 10, # Current fault of the dexterous hand + "motor_temperature": [-1] * 10, # Current motor temperature of the dexterous hand + "torque": [-1] * 10, # Current torque of the dexterous hand + "is_touch":self.is_touch, + "touch_type": -1, + "finger_order": None # Finger motor order + } + self.version = [] + self.touch_type = -1 + self.hz = 1.0/60.0 + + self.hand_setting_sub = self.create_subscription(String,'/cb_hand_setting_cmd', self.hand_setting_cb, 10) + self._init_hand() + time.sleep(1) + self.run_count = 0 # 计数器,用于记录运行次数 + self.timer = self.create_timer(0.01, self.run) # 100 Hz + self.thread_pub_state = threading.Thread(target=self.pub_state) + self.thread_pub_state.daemon = True + self.thread_pub_state.start() + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_info_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_info', 10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + pose = None + torque = [200, 200, 200, 200, 200] + speed = [200, 250, 250, 250, 250] + if self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6" or self.hand_joint.upper() == "L6P": + pose = [200, 255, 255, 255, 255, 180] + torque = [250, 250, 250, 250, 250, 250] + # O6 最大速度阈值 + speed = [200, 250, 250, 250, 250, 250] + elif self.hand_joint == "L7": + # The data length of L7 is 7, reinitialize here + pose = [255, 200, 255, 255, 255, 255, 180] + torque = [250, 250, 250, 250, 250, 250, 250] + speed = [120, 250, 250, 250, 250, 250, 250] + elif self.hand_joint == "L10": + torque = [255] * 10 + pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] + speed = [200, 250, 250, 250, 250, 250, 250, 250, 250, 250] + elif self.hand_joint == "L20": + pose = [255,255,255,255,255,255,10,100,180,240,245,255,255,255,255,255,255,255,255,255] + elif self.hand_joint == "L21": + pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + elif self.hand_joint == "L25": + pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + if pose is not None: + for i in range(1): + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def run(self): + if self.sdk_v == 1: + self.sleep_time = 0.009 + if self.hand_state_pub.get_subscription_count() > 0: + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + time.sleep(0.003) + self.last_hand_vel = self.api.get_joint_speed() + time.sleep(0.002) + if self.cmd_lock == False: + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + if self.last_hand_vel_cmd != None: + vel = list(self.last_hand_vel_cmd) + if all(x == 0 for x in vel): + pass + else: + if (str(self.hand_joint).upper() == "O6" or str(self.hand_joint).upper() == "L6" or str(self.hand_joint).upper() == "L6P") and len(vel) == 6: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L7" and len(vel) == 7: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L10" and len(vel) == 10: + speed = [vel[0],vel[2],vel[3],vel[4],vel[5]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L20" and len(vel) == 20: + speed = [vel[10],vel[1],vel[2],vel[3],vel[4]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L21" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L25" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + self.last_hand_vel_cmd = None + time.sleep(0.003) + if self.run_count == 3 and self.is_touch == True and self.touch_type == 1 and self.touch_pub.get_subscription_count() > 0: + """单点式压力传感器""" + self.force = self.api.get_force() + if self.is_touch == True and self.touch_type > 1 and (self.matrix_touch_pub.get_subscription_count() > 0 or self.matrix_touch_mass_pub.get_subscription_count() > 0 or self.matrix_touch_pub_pc.get_subscription_count() > 0): + """矩阵式压力传感器""" + if self.run_count == 3: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 4: + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 5: + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 6: + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=self.sleep_time).tolist() + if self.run_count == 7: + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=self.sleep_time).tolist() + time.sleep(0.005) + if self.run_count == 8 and self.hand_info_pub.get_subscription_count() > 0: + """手部信息""" + self.last_hand_info = { + "version": self.embedded_version, # Dexterous hand version number + "hand_joint": self.hand_joint, # Dexterous hand joint type + "speed": self.api.get_speed(), # Current speed threshold of the dexterous hand + "current": self.api.get_current(), # Current of the dexterous hand + "fault": self.api.get_fault(), # Current fault of the dexterous hand + "motor_temperature": self.api.get_temperature(), # Current motor temperature of the dexterous hand + "torque": self.api.get_torque(), # Current torque of the dexterous hand + "is_touch":self.is_touch, + "touch_type": self.touch_type, + "finger_order": self.api.get_finger_order() # Finger motor order + } + if self.run_count == 9: + self.run_count = 0 + self.run_count += 1 + time.sleep(0.003) + + + def pub_state(self): + while True: + if self.hand_state_pub.get_subscription_count() > 0: + msg = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg) + if self.is_touch == True and self.touch_type == 1 and self.touch_pub.get_subscription_count() > 0: + msg = Float32MultiArray() + msg.data = [float(val) for sublist in self.force for val in sublist] + self.touch_pub.publish(msg) + if self.is_touch == True and self.touch_type > 1 and (self.matrix_touch_pub.get_subscription_count() > 0 or self.matrix_touch_mass_pub.get_subscription_count() > 0 or self.matrix_touch_pub_pc.get_subscription_count() > 0): + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + if self.hand_info_pub.get_subscription_count() > 0: + msg = String() + msg.data = json.dumps(self.last_hand_info) + self.hand_info_pub.publish(msg) + time.sleep(self.hz) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + """发布矩阵数据点云格式""" + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 + # 摊平到一维:360 个 float + flat_list = [v for frame in all_matrices for v in frame] # 360 + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField( + name='val', + offset=0, + datatype=PointField.UINT8, + count=1 + )] + pc = PointCloud2() + pc.header.stamp = self.stamp_clock.now().to_msg() + pc.header.frame_id = '' + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + + + + + def hand_setting_cb(self,msg): + '''控制命令回调''' + data = json.loads(msg.data) + print(f"Received setting command: {data['setting_cmd']}",flush=True) + try: + if data["params"]["hand_type"] == "left": + hand = self.api + hand_left = True + elif data["params"]["hand_type"] == "right": + hand = self.api + hand_right = True + else: + print("Please specify the hand part to be set",flush=True) + return + self.cmd_lock = True + # Set maximum torque + if data["setting_cmd"] == "set_max_torque_limits": # Set maximum torque + torque = list(data["params"]["torque"]) + hand.set_torque(torque=torque) + + if data["setting_cmd"] == "set_speed": # Set speed + if isinstance(data["params"]["speed"], list) == True: + speed = data["params"]["speed"] + hand.set_speed(speed=speed) + else: + ColorMsg(msg=f"Speed parameter error, speed must be a list", color="red") + if data["setting_cmd"] == "clear_faults": # Clear faults + if hand_left == True and self.hand_joint == "L10" : + ColorMsg(msg=f"L10 left hand cannot clear faults") + elif hand_right == True and self.hand_joint == "L10" : + ColorMsg(msg=f"L10 right hand cannot clear faults") + else: + hand.clear_faults() + if data["setting_cmd"] == "get_faults": # Get faults + f = hand.get_fault() + ColorMsg(msg=f"Get faults: {f}") + if data["setting_cmd"] == "electric_current": # Get current + ColorMsg(msg=f"Get current: {hand.get_current()}") + if data["setting_cmd"] == "set_electric_current": # Set current + if isinstance(data["params"]["current"], list) == True: + hand.set_current(data["params"]["current"]) + if data["setting_cmd"] == "show_fun_table": # Get faults + f = hand.show_fun_table() + except: + print("命令参数错误") + self.cmd_lock = False + finally: + self.cmd_lock = False + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + try: + rclpy.init(args=args) + node = LinkerHand("linker_hand_sdk") + embedded_version = node.embedded_version + if len(embedded_version) == 3 or node.hand_joint.upper() == "O6" or node.hand_joint.upper() == "L6" or node.hand_joint.upper() == "G20": + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + elif len(embedded_version) == 6 and node.hand_joint == "L10": + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + elif len(embedded_version) > 4 and ((embedded_version[0]==10 and embedded_version[4]>35) or (embedded_version[0]==7 and embedded_version[4]>50) or (embedded_version[0] == 6)): + ColorMsg(msg=f"New Matrix Touch For SDK V2", color="green") + node.sdk_v = 2 + else: + ColorMsg(msg=f"SDK V1", color="green") + node.sdk_v = 1 + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py new file mode 100644 index 0000000..2bf1f83 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import argparse +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +# Linker Hand 型号 +HAND_JOINT = "G20" +# 默认手指关节位置 +DEFAULT_POSITION = [255, 255, 255, 255, 255, 255, 193, 148, 105, 42, 245, 255, 255, 255, 255, 255, 255, 255, 255, 255] +# 默认手指关节速度 +DEFAULT_SPEED=[255, 255, 255, 255, 255] +# 默认手指关节力矩 +DEFAULT_TORQUE = [255, 255, 255, 255, 255] +# 压感传感器延迟时间 +TOUCH_SLEEP_TIME = 0.003 + + +class LinkerHandAdvancedG20(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = HAND_JOINT + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.count = 0 + self.timer = self.create_timer(self.hz, self.run) # 100 Hz + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "G20": + ColorMsg(msg=f"Linker Hand hand_joint参数错误", color="red") + sys.exit(0) + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + self.api.set_speed(speed=DEFAULT_SPEED) + time.sleep(0.1) + self.api.set_torque(torque=DEFAULT_TORQUE) + time.sleep(0.1) + self.api.finger_move(pose=DEFAULT_POSITION) + time.sleep(0.1) + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = [0.0] * len(self.last_hand_state) + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg_state) + + if self.is_touch == True: + # 获取压感数据 + if self.count == 2: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() + if self.count == 4: + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() + if self.count == 6: + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() + if self.count == 8: + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() + if self.count == 10: + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + self.count += 1 + if self.count == 11: + self.count = 0 + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # 摊平到一维 + flat_list = [v for frame in all_matrices for v in frame] + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + pc = PointCloud2() + pc.header.stamp = self.get_clock().now().to_msg() + pc.header.frame_id = '' # 可改成你需要的坐标系 + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_advanced_g20 --hand_type left --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedG20(name="linker_hand_advanced_g20",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py new file mode 100644 index 0000000..37a7be3 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import argparse +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +class LinkerHandAdvancedL10(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = "L10" + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.count = 0 # 循环计数器 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.timer = self.create_timer(self.hz, self.run) # 100 Hz + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "L10": + ColorMsg(msg=f"L10以外其他Linker Hand暂不支持", color="red") + sys.exit(0) + + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + if self.hand_joint.upper() == "L10": + pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] + torque = [255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + speed = [255, 255, 255, 255, 255, 255, 255, 255, 255, 255] + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + self.serial_number = self.api.get_serial_number() + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = [0.0] * len(self.last_hand_state) + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state) + self.hand_state_pub.publish(msg_state) + # 获取压感数据 + if self.is_touch == True: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=0.004).tolist() + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=0.004).tolist() + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.004).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # 摊平到一维 + flat_list = [v for frame in all_matrices for v in frame] + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + pc = PointCloud2() + pc.header.stamp = self.get_clock().now().to_msg() + pc.header.frame_id = '' # 可改成你需要的坐标系 + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + '/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_advanced_l10 --hand_type left --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedL10(name="linker_hand_advanced_l10",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py new file mode 100644 index 0000000..2672283 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py @@ -0,0 +1,251 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +编译: colcon build --symlink-install +启动命令:ros2 run linker_hand_ros2_sdk linker_hand_sdk +''' +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import numpy as np +import argparse +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +class LinkerHandAdvancedL6(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = "L6" + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.count = 0 # 循环计数器 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.timer = self.create_timer(self.hz, self.run) # 60 Hz + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "L6": + ColorMsg(msg=f"L6以外其他Linker Hand暂不支持", color="red") + sys.exit(0) + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + if self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6": + pose = [200, 255, 255, 255, 255, 180] + torque = [255, 255, 255, 255, 255, 255] + # O6 最大速度阈值 + speed = [255, 255, 255, 255, 255, 255] + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + #time.sleep(0.002) + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = self.api.get_joint_speed() + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg_state) + time.sleep(0.002) + # 获取压感数据 + if self.is_touch == True: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=0.003).tolist() + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.003).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # 摊平到一维 + flat_list = [v for frame in all_matrices for v in frame] + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + pc = PointCloud2() + pc.header.stamp = self.get_clock().now().to_msg() + pc.header.frame_id = '' # 可改成你需要的坐标系 + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_advanced_l6 --hand_type left --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedL6(name="linker_hand_advanced_l6",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + if embedded_version[2] < 8 and len(embedded_version) != 3: + ColorMsg(msg=f"固件版本过低,请升级固件到V{embedded_version[0]}.{embedded_version[1]}.8及以上版本", color="red") + sys.exit(0) + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py new file mode 100644 index 0000000..7ed58bd --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +编译: colcon build --symlink-install +启动命令:ros2 run linker_hand_ros2_sdk linker_hand_sdk +''' +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import argparse +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +class LinkerHandAdvancedL7(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = "L7" + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.count = 0 # 循环计数器 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.timer = self.create_timer(self.hz, self.run) # 100 Hz + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "L7": + ColorMsg(msg=f"L6以外其他Linker Hand暂不支持", color="red") + sys.exit(0) + + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + if self.hand_joint.upper() == "L7": + pose = [255, 200, 255, 255, 255, 255, 180] + torque = [255] * 7 + speed = [255] * 7 + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + self.serial_number = self.api.get_serial_number() + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = self.api.get_joint_speed() + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg_state) + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + time.sleep(0.003) + self.last_hand_post_cmd = None + if self.last_hand_vel_cmd != None: + vel = list(self.last_hand_vel_cmd) + if all(x == 0 for x in vel): + pass + else: + if (str(self.hand_joint).upper() == "O6" or str(self.hand_joint).upper() == "L6" or str(self.hand_joint).upper() == "L6P") and len(vel) == 6: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L7" and len(vel) == 7: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L10" and len(vel) == 10: + speed = [vel[0],vel[2],vel[3],vel[4],vel[5]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L20" and len(vel) == 20: + speed = [vel[10],vel[1],vel[2],vel[3],vel[4]] + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L21" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + elif self.hand_joint == "L25" and len(vel) == 25: + speed = vel + self.api.set_joint_speed(speed=speed) + self.last_hand_vel_cmd = None + time.sleep(0.005) + # 获取压感数据 + if self.is_touch == True: + if self.count == 3: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=0.006).tolist() + if self.count == 4: + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=0.006).tolist() + if self.count == 5: + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=0.006).tolist() + if self.count == 6: + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=0.006).tolist() + if self.count == 7: + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.006).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + self.count += 1 + if self.count == 8: + self.count = 0 + time.sleep(0.006) + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # 摊平到一维 + flat_list = [v for frame in all_matrices for v in frame] + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + pc = PointCloud2() + pc.header.stamp = self.get_clock().now().to_msg() + pc.header.frame_id = '' # 可改成你需要的坐标系 + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_advanced_l7 --hand_type left --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedL7(name="linker_hand_collect_l7",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py new file mode 100644 index 0000000..42638c8 --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +''' +编译: colcon build --symlink-install +启动命令:ros2 run linker_hand_ros2_sdk linker_hand_sdk +''' +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import argparse +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +class LinkerHandAdvancedO6(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = "O6" + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.timer = self.create_timer(self.hz, self.run) # 100 Hz + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "O6": + ColorMsg(msg=f"O6以外其他Linker Hand暂不支持", color="red") + sys.exit(0) + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + if self.hand_joint.upper() == "O6" or self.hand_joint.upper() == "L6": + pose = [200, 255, 255, 255, 255, 180] + torque = [255, 255, 255, 255, 255, 255] + # O6 最大速度阈值 + speed = [255, 255, 255, 255, 255, 255] + self.api.set_speed(speed=speed) + time.sleep(0.1) + self.api.set_torque(torque=torque) + time.sleep(0.1) + self.api.finger_move(pose=pose) + time.sleep(0.1) + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + if self.last_hand_vel_cmd != None: + vel = list(self.last_hand_vel_cmd) + if all(x == 0 for x in vel): + pass + else: + speed = vel + self.api.set_joint_speed(speed=speed) + self.last_hand_vel_cmd = None + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = self.api.get_joint_speed() + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg_state) + if self.is_touch == True: + # 获取压感数据 + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=0.002).tolist() + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=0.002).tolist() + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=0.002).tolist() + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=0.002).tolist() + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.002).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + self.pub_matrix_point_cloud() + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) + self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) + self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) + self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) + self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + def pub_matrix_point_cloud(self): + tmp_dic = self.matrix_dic.copy() + del tmp_dic['stamp'] # 去掉时间戳字段 + all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # 摊平到一维 + flat_list = [v for frame in all_matrices for v in frame] + flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + pc = PointCloud2() + pc.header.stamp = self.get_clock().now().to_msg() + pc.header.frame_id = '' # 可改成你需要的坐标系 + pc.height = 1 + pc.width = flat.size # 360 + pc.fields = fields + pc.is_bigendian = False + pc.point_step = 1 # 1 个 float32 + pc.row_step = pc.point_step * pc.width + pc.data = flat.tobytes() # 1440 字节 + self.matrix_touch_pub_pc.publish(pc) + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 50Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 50Hz + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_advanced_o6 --hand_type right --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedO6(name="linker_hand_advanced_o6",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + # node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py new file mode 100644 index 0000000..eda447c --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +from re import A +import rclpy,sys # ROS2 Python接口库 +import time +import argparse +import numpy as np +from rclpy.node import Node # ROS2 节点类 +from rclpy.clock import Clock +from std_msgs.msg import String, Header, Float32MultiArray +from sensor_msgs.msg import JointState, PointCloud2, PointField +import time, json, threading +from linker_hand_ros2_sdk.LinkerHand.linker_hand_api import LinkerHandApi +from linker_hand_ros2_sdk.LinkerHand.utils.color_msg import ColorMsg +from linker_hand_ros2_sdk.LinkerHand.utils.open_can import OpenCan + +# Linker Hand 型号 +HAND_JOINT = "G20" +# 默认手指关节位置 +DEFAULT_POSITION = [255, 255, 255, 255, 255, 255, 193, 148, 105, 42, 245, 255, 255, 255, 255, 255, 255, 255, 255, 255] +# 默认手指关节速度 +DEFAULT_SPEED=[255, 255, 255, 255, 255] +# 默认手指关节力矩 +DEFAULT_TORQUE = [255, 255, 255, 255, 255] +# 压感传感器延迟时间 +TOUCH_SLEEP_TIME = 0.003 + + + +class LinkerHandAdvancedG20(Node): + def __init__(self, name, hand_type, can, is_touch): + super().__init__(name) + self.hand_type = hand_type + self.hand_joint = HAND_JOINT + if is_touch == "true": + self.is_touch = True + else: + self.is_touch = False + self.can = can + self.modbus = "None" + time.sleep(0.1) + self._check_linker_hand_type() + self.last_hand_post_cmd = None # 最新手指位置命令 + self.last_hand_vel_cmd = None # 最新手指速度命令 + self.last_hand_eff_cmd = None # 最新手指力矩命令 + self.matrix_dic = { + "stamp":{ + "sec": 0, + "nanosec": 0, + }, + "thumb_matrix":[[-1] * 6 for _ in range(12)], + "index_matrix":[[-1] * 6 for _ in range(12)], + "middle_matrix":[[-1] * 6 for _ in range(12)], + "ring_matrix":[[-1] * 6 for _ in range(12)], + "little_matrix":[[-1] * 6 for _ in range(12)] + } + # 压感矩阵合值,单位g 克 + self.matrix_mass_dic = { + "stamp":{ + "secs": 0, + "nsecs": 0, + }, + "thumb_mass":[-1], + "index_mass":[-1], + "middle_mass":[-1], + "ring_mass":[-1], + "little_mass":[-1] + } + self.hz = 1.0/60.0 + # ros时间获取 + self.stamp_clock = Clock() + self._init_hand() + time.sleep(2) + self.count = 0 + self.timer = self.create_timer(self.hz, self.run) + + + def _check_linker_hand_type(self): + if self.modbus != "None": + ColorMsg(msg=f"Modbus暂不支持", color="red") + sys.exit(0) + if self.hand_joint.upper() != "G20": + ColorMsg(msg=f"Linker Hand hand_joint参数错误", color="red") + sys.exit(0) + + def _init_hand(self): + self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) + time.sleep(0.1) + self.touch_type = self.api.get_touch_type() + self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + if self.is_touch == True: + if self.touch_type > 1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') + self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) + #self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + elif self.touch_type != -1: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") + self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + else: + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") + self.is_touch = False + self.embedded_version = self.api.get_embedded_version() + self.api.set_speed(speed=DEFAULT_SPEED) + time.sleep(0.1) + self.api.set_torque(torque=DEFAULT_TORQUE) + time.sleep(0.1) + self.api.finger_move(pose=DEFAULT_POSITION) + time.sleep(0.1) + self.palm_touch = self.api.is_palm_touch + if self.palm_touch == 5: + self.touch_sleep_time = 0.03 + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} 全掌压感版", color="green") + else: + self.touch_sleep_time = 0.003 + + def hand_control_cb(self, msg): + if self.last_hand_post_cmd == None or self.list_check(msg.position) == True: + self.last_hand_post_cmd = msg.position + if self.last_hand_vel_cmd == None or self.list_check(msg.velocity) == True: + self.last_hand_vel_cmd = msg.velocity + if self.last_hand_eff_cmd == None or self.list_check(msg.effort) == True: + self.last_hand_eff_cmd = msg.effort + + def list_check(self,pose): + if isinstance(pose, list) == False: + return False + if len(self.last_hand_post_cmd) != len(pose): + return False + return any(abs(self.last_hand_post_cmd - pose) >= 3 for self.last_hand_post_cmd, pose in zip(self.last_hand_post_cmd, pose)) + + def joint_state_msg(self, pose,vel=[]): + joint_state = JointState() + joint_state.header = Header() + joint_state.header.stamp = self.get_clock().now().to_msg() + joint_state.name = self.api.get_finger_order() + joint_state.position = [float(x) for x in pose] + if len(vel) > 1: + joint_state.velocity = [float(x) for x in vel] + else: + joint_state.velocity = [0.0] * len(pose) + joint_state.effort = [0.0] * len(pose) + return joint_state + + def run(self): + # 执行手控制指令 + if self.last_hand_post_cmd != None: + self.api.finger_move(pose=self.last_hand_post_cmd) + self.last_hand_post_cmd = None + # 优先获取手指状态并且发布 + self.last_hand_state = self.api.get_state() + self.last_hand_vel = [0.0] * len(self.last_hand_state) + # 发布手状态 + msg_state = self.joint_state_msg(self.last_hand_state, self.last_hand_vel) + self.hand_state_pub.publish(msg_state) + + if self.is_touch == True: + # 获取压感数据 + if self.count == 2: + self.matrix_dic["thumb_matrix"] = self.api.get_thumb_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + if self.count == 4: + self.matrix_dic["index_matrix"] = self.api.get_index_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + if self.count == 6: + self.matrix_dic["middle_matrix"] = self.api.get_middle_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + if self.count == 8: + self.matrix_dic["ring_matrix"] = self.api.get_ring_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + if self.count == 10: + self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + if self.count == 14 and self.palm_touch == 5: + self.matrix_dic["palm_matrix"] = self.api.get_palm_matrix_touch(sleep_time=self.touch_sleep_time).tolist() + # 发布矩阵压感数据JSON格式 + self.pub_matrix_dic() + # 发布矩阵压感和值JSON格式 + self.pub_matrix_mass(dic=self.matrix_dic) + # 发布矩阵压感点云格式 + #self.pub_matrix_point_cloud() + self.count += 1 + if self.count == 15: + self.count = 0 + + def pub_matrix_dic(self): + """发布矩阵数据JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_dic["stamp"]["secs"] = t_secs + self.matrix_dic["stamp"]["nsecs"] = t_nsecs + msg.data = json.dumps(self.matrix_dic) + self.matrix_touch_pub.publish(msg) + + def pub_matrix_mass(self, dic): + """发布矩阵数据合值 单位g 克 JSON格式""" + msg = String() + # 获取当前的 ROS 时间 + current_time = self.stamp_clock.now() + # 提取 secs 和 nsecs + t_secs = current_time.to_msg().sec + t_nsecs = current_time.to_msg().nanosec + self.matrix_mass_dic["stamp"]["secs"] = t_secs + self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs + self.matrix_mass_dic["unit"] = "g" + self.matrix_mass_dic["thumb_mass"] = self.api.hand.thumb_matrix_palm_mass + self.matrix_mass_dic["index_mass"] = self.api.hand.index_matrix_palm_mass + self.matrix_mass_dic["middle_mass"] = self.api.hand.middle_matrix_palm_mass + self.matrix_mass_dic["ring_mass"] = self.api.hand.ring_matrix_palm_mass + self.matrix_mass_dic["little_mass"] = self.api.hand.little_matrix_palm_mass + self.matrix_mass_dic["palm_mass"] = self.api.hand.palm_matrix_palm_mass + msg.data = json.dumps(self.matrix_mass_dic) + self.matrix_touch_mass_pub.publish(msg) + + # def pub_matrix_point_cloud(self): + # tmp_dic = self.matrix_dic.copy() + # del tmp_dic['stamp'] # 去掉时间戳字段 + # all_matrices = list(tmp_dic.values()) # 5 帧,每帧 6×12=72 个数 or 5 帧,每帧 4×10=40 个数 列x行 + # # 摊平到一维 + # flat_list = [v for frame in all_matrices for v in frame] + # flat = np.concatenate([np.asarray(np.clip(c, 0, 255), dtype=np.uint8) for c in flat_list]) + # fields = [PointField(name='val', offset=0, datatype=PointField.UINT8, count=1)] + # pc = PointCloud2() + # pc.header.stamp = self.get_clock().now().to_msg() + # pc.header.frame_id = '' # 可改成你需要的坐标系 + # pc.height = 1 + # pc.width = flat.size # 360 + # pc.fields = fields + # pc.is_bigendian = False + # pc.point_step = 1 # 1 个 float32 + # pc.row_step = pc.point_step * pc.width + # pc.data = flat.tobytes() # 1440 字节 + # self.matrix_touch_pub_pc.publish(pc) + + + def close_can(self): + self.api.open_can.close_can(can=self.can) + sys.exit(0) + + +def main(args=None): + ''' + 本节点用于收集手指状态和压感数据。 + '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + 启动命令: + ros2 run linker_hand_ros2_sdk linker_hand_g20_palm_touch --hand_type left --can can0 --is_touch true + ''' + try: + rclpy.init(args=args) + parser = argparse.ArgumentParser() + parser.add_argument('--hand_type', required=True) + parser.add_argument('--can', required=True) + parser.add_argument('--is_touch', choices=['true','false'], required=True) + + args = parser.parse_args() + node = LinkerHandAdvancedG20(name="linker_hand_g20_palm_touch",hand_type=args.hand_type,can=args.can,is_touch=args.is_touch) + embedded_version = node.embedded_version + rclpy.spin(node) # 主循环,监听 ROS 回调 + except KeyboardInterrupt: + print("收到 Ctrl+C,准备退出...") + finally: + node.close_can() # 关闭 CAN 或其他硬件资源 + # node.destroy_node() # 销毁 ROS 节点 + # rclpy.shutdown() # 关闭 ROS + print("程序已退出。") \ No newline at end of file diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py new file mode 100644 index 0000000..2a06b5a --- /dev/null +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Record O6 joint positions and plot angle/derived-velocity curves. + +The O6 ``0x05`` value exposed by the SDK is a configured speed, not a measured +joint velocity. This recorder therefore always derives velocity from the +position samples and their monotonic timestamps. +""" + +import argparse +import csv +from datetime import datetime +import json +import math +from pathlib import Path +import sys +import time + +import numpy as np +import rclpy +from rclpy.node import Node +from rclpy.utilities import remove_ros_args +from sensor_msgs.msg import JointState + + +O6_JOINT_NAMES = ( + "thumb_cmc_pitch", + "thumb_cmc_yaw", + "index_mcp_pitch", + "middle_mcp_pitch", + "ring_mcp_pitch", + "pinky_mcp_pitch", +) + +# Nominal SDK ranges. A hand-specific lookup JSON should be used for formal +# measurements because individual hands can differ from these values. +O6_NOMINAL_MAX_DEG = dict( + zip(O6_JOINT_NAMES, np.rad2deg([0.58, 1.36, 1.6, 1.6, 1.6, 1.6])) +) + + +class AngleMapper: + """Convert O6 uint8 positions to degrees using lookup tables or nominal ranges.""" + + def __init__(self, mapping_json=None): + self.tables = {} + self.source = "SDK nominal range" + if mapping_json: + self._load(Path(mapping_json).expanduser()) + + def _load(self, path): + with path.open("r", encoding="utf-8") as stream: + data = json.load(stream) + + joints = data.get("joints", data) + if not isinstance(joints, dict): + raise ValueError("mapping JSON must contain an object named 'joints'") + + for joint_name, values in joints.items(): + if not isinstance(values, dict): + continue + joint_u8 = values.get("joint_u8") + joint_real_deg = values.get("joint_real_deg") + if joint_u8 is None or joint_real_deg is None: + continue + if len(joint_u8) != len(joint_real_deg) or len(joint_u8) < 2: + raise ValueError( + f"{joint_name}: joint_u8 and joint_real_deg need equal lengths >= 2" + ) + + x = np.asarray(joint_u8, dtype=float) + y = np.asarray(joint_real_deg, dtype=float) + if not np.all(np.isfinite(x)) or not np.all(np.isfinite(y)): + raise ValueError(f"{joint_name}: mapping contains a non-finite value") + order = np.argsort(x) + x = x[order] + y = y[order] + if np.any(np.diff(x) <= 0): + raise ValueError(f"{joint_name}: joint_u8 values must be unique") + self.tables[joint_name] = (x, y) + + if not self.tables: + raise ValueError( + "mapping JSON has no joint with joint_u8 and joint_real_deg arrays" + ) + self.source = str(path) + + def to_degrees(self, joint_name, position_u8): + if joint_name in self.tables: + x, y = self.tables[joint_name] + return float(np.interp(position_u8, x, y)) + + if joint_name not in O6_NOMINAL_MAX_DEG: + raise KeyError(f"no O6 nominal range or lookup table for {joint_name}") + position_u8 = float(np.clip(position_u8, 0.0, 255.0)) + return (255.0 - position_u8) * O6_NOMINAL_MAX_DEG[joint_name] / 255.0 + + +def derive_velocity(time_s, angle_deg, cutoff_hz=5.0): + """Differentiate angle samples and apply a first-order low-pass filter.""" + time_s = np.asarray(time_s, dtype=float) + angle_deg = np.asarray(angle_deg, dtype=float) + if len(time_s) != len(angle_deg): + raise ValueError("time and angle arrays must have the same length") + if len(time_s) < 2: + return np.zeros_like(angle_deg) + if np.any(np.diff(time_s) <= 0): + raise ValueError("timestamps must be strictly increasing") + + velocity = np.gradient(angle_deg, time_s) + if cutoff_hz <= 0.0: + return velocity + + filtered = np.empty_like(velocity) + filtered[0] = velocity[0] + rc = 1.0 / (2.0 * math.pi * cutoff_hz) + for index in range(1, len(velocity)): + dt = time_s[index] - time_s[index - 1] + alpha = dt / (rc + dt) + filtered[index] = filtered[index - 1] + alpha * ( + velocity[index] - filtered[index - 1] + ) + return filtered + + +class O6JointCurveRecorder(Node): + def __init__( + self, + hand_type, + duration, + output_dir, + joint_name, + mapping_json, + filter_hz, + ): + super().__init__("o6_joint_curve") + self.hand_type = hand_type + self.duration = duration + self.output_dir = Path(output_dir).expanduser() + self.joint_name = joint_name + self.filter_hz = filter_hz + self.mapper = AngleMapper(mapping_json) + self.samples = [] + self.start_time = None + self.saved_paths = None + self.finished = False + + topic = f"/cb_{hand_type}_hand_state" + self.subscription = self.create_subscription( + JointState, topic, self._state_callback, 100 + ) + self.stop_timer = self.create_timer(0.05, self._check_duration) + self.get_logger().info( + f"Recording {topic}; duration={duration:.3f}s; angle map={self.mapper.source}" + ) + + def _state_callback(self, msg): + if self.finished or not msg.position: + return + + names = tuple(msg.name) if len(msg.name) == len(msg.position) else O6_JOINT_NAMES + if len(msg.position) != len(names): + self.get_logger().warning( + f"Ignoring JointState with {len(msg.position)} positions and " + f"{len(msg.name)} names" + ) + return + + now = time.perf_counter() + if self.start_time is None: + self.start_time = now + relative_time = now - self.start_time + for name, position in zip(names, msg.position): + if self.joint_name != "all" and name != self.joint_name: + continue + try: + angle_deg = self.mapper.to_degrees(name, position) + except KeyError as error: + self.get_logger().warning(str(error)) + continue + self.samples.append((relative_time, name, float(position), angle_deg)) + + def _check_duration(self): + if self.finished or self.start_time is None or self.duration <= 0.0: + return + if time.perf_counter() - self.start_time >= self.duration: + self.finish() + rclpy.shutdown() + + def finish(self): + if self.finished: + return self.saved_paths + self.finished = True + if not self.samples: + self.get_logger().warning("No joint-state sample received; no files were written") + return None + self.saved_paths = export_curves( + samples=self.samples, + output_dir=self.output_dir, + hand_type=self.hand_type, + mapping_source=self.mapper.source, + cutoff_hz=self.filter_hz, + ) + csv_path, plot_path = self.saved_paths + self.get_logger().info(f"Saved samples: {csv_path}") + self.get_logger().info(f"Saved curves: {plot_path}") + return self.saved_paths + + +def export_curves(samples, output_dir, hand_type, mapping_source, cutoff_hz): + """Export long-form CSV and a two-panel PNG for all recorded joints.""" + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + output_dir = Path(output_dir).expanduser() + output_dir.mkdir(parents=True, exist_ok=True) + stem = f"o6_{hand_type}_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + csv_path = output_dir / f"{stem}.csv" + plot_path = output_dir / f"{stem}.png" + + joint_samples = {} + for sample in samples: + joint_samples.setdefault(sample[1], []).append(sample) + + processed = {} + for joint_name, values in joint_samples.items(): + time_s = np.asarray([value[0] for value in values], dtype=float) + position_u8 = np.asarray([value[2] for value in values], dtype=float) + angle_deg = np.asarray([value[3] for value in values], dtype=float) + velocity_deg_s = derive_velocity(time_s, angle_deg, cutoff_hz) + processed[joint_name] = (time_s, position_u8, angle_deg, velocity_deg_s) + + with csv_path.open("w", encoding="utf-8", newline="") as stream: + writer = csv.writer(stream) + writer.writerow( + ["t_s", "joint_name", "position_u8", "angle_deg", "velocity_deg_s"] + ) + for joint_name, arrays in processed.items(): + for row in zip(*arrays): + writer.writerow( + [ + f"{row[0]:.9f}", + joint_name, + f"{row[1]:.6f}", + f"{row[2]:.6f}", + f"{row[3]:.6f}", + ] + ) + + figure, axes = plt.subplots(2, 1, sharex=True, figsize=(12, 8)) + for joint_name, (time_s, _position_u8, angle_deg, velocity_deg_s) in processed.items(): + axes[0].plot(time_s, angle_deg, label=joint_name) + axes[1].plot(time_s, velocity_deg_s, label=joint_name) + + axes[0].set_ylabel("Angle (deg)") + axes[0].set_title(f"O6 {hand_type} joint curves\nangle map: {mapping_source}") + axes[1].set_ylabel("Velocity (deg/s)") + axes[1].set_xlabel("Time (s)") + axes[1].set_title(f"Derived velocity, first-order low-pass {cutoff_hz:g} Hz") + for axis in axes: + axis.grid(True, alpha=0.3) + axis.legend(loc="best", fontsize="small") + figure.tight_layout() + figure.savefig(plot_path, dpi=160) + plt.close(figure) + return csv_path, plot_path + + +def _parse_args(argv): + parser = argparse.ArgumentParser( + description="Record O6 angle and angle-derived velocity curves" + ) + parser.add_argument("--hand-type", choices=("left", "right"), default="right") + parser.add_argument( + "--duration", + type=float, + default=10.0, + help="recording seconds; <= 0 records until Ctrl+C (default: 10)", + ) + parser.add_argument( + "--output-dir", default="logs/O6/curves", help="CSV/PNG output directory" + ) + parser.add_argument( + "--joint", + default="all", + choices=("all",) + O6_JOINT_NAMES, + help="plot one joint or all six (default: all)", + ) + parser.add_argument( + "--mapping-json", + help="optional hand_.json containing joints..joint_u8 and joint_real_deg", + ) + parser.add_argument( + "--filter-hz", + type=float, + default=5.0, + help="velocity low-pass cutoff; <= 0 disables filtering (default: 5)", + ) + parsed = parser.parse_args(remove_ros_args(args=argv)[1:]) + if parsed.duration == 0.0: + parsed.duration = -1.0 + return parsed + + +def main(args=None): + argv = sys.argv if args is None else [sys.argv[0], *args] + parsed = _parse_args(argv) + rclpy.init(args=args) + node = O6JointCurveRecorder( + hand_type=parsed.hand_type, + duration=parsed.duration, + output_dir=parsed.output_dir, + joint_name=parsed.joint, + mapping_json=parsed.mapping_json, + filter_hz=parsed.filter_hz, + ) + try: + rclpy.spin(node) + except KeyboardInterrupt: + node.get_logger().info("Stopping on Ctrl+C") + finally: + node.finish() + node.destroy_node() + if rclpy.ok(): + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/src/linker_hand_ros2_sdk/package.xml b/src/linker_hand_ros2_sdk/package.xml new file mode 100644 index 0000000..943c8c2 --- /dev/null +++ b/src/linker_hand_ros2_sdk/package.xml @@ -0,0 +1,24 @@ + + + + linker_hand_ros2_sdk + 0.0.0 + TODO: Package description + linker-robot + TODO: License declaration + + ament_copyright + ament_flake8 + ament_pep257 + python3-pytest + + rclpy + launch + sensor_msgs + python3-numpy + python3-matplotlib + + + ament_python + + diff --git a/src/linker_hand_ros2_sdk/pyproject.toml b/src/linker_hand_ros2_sdk/pyproject.toml new file mode 100644 index 0000000..638dd9c --- /dev/null +++ b/src/linker_hand_ros2_sdk/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/src/linker_hand_ros2_sdk/resource/linker_hand_ros2_sdk b/src/linker_hand_ros2_sdk/resource/linker_hand_ros2_sdk new file mode 100644 index 0000000..e69de29 diff --git a/src/linker_hand_ros2_sdk/setup.cfg b/src/linker_hand_ros2_sdk/setup.cfg new file mode 100644 index 0000000..eb74b8f --- /dev/null +++ b/src/linker_hand_ros2_sdk/setup.cfg @@ -0,0 +1,4 @@ +[develop] +script_dir=$base/lib/linker_hand_ros2_sdk +[install] +install_scripts=$base/lib/linker_hand_ros2_sdk diff --git a/src/linker_hand_ros2_sdk/setup.py b/src/linker_hand_ros2_sdk/setup.py new file mode 100644 index 0000000..5a1479f --- /dev/null +++ b/src/linker_hand_ros2_sdk/setup.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os +from glob import glob +from setuptools import find_packages, setup + +package_name = 'linker_hand_ros2_sdk' + +this_dir = os.path.abspath(os.path.dirname(__file__)) +custom_dir = os.path.join(this_dir, package_name, "LinkerHand") + +data_files = [ + ('share/ament_index/resource_index/packages', + ['resource/' + package_name]), + ('share/' + package_name, ['package.xml']), + (os.path.join('share', package_name, 'launch'), glob('launch/*.launch.py')), +] + +# for root, dirs, files in os.walk(custom_dir): +# if files: +# relative_path = os.path.relpath(root, os.path.join(this_dir, package_name)) +# target_path = os.path.join('share', package_name, relative_path) +# # 修复这里:路径必须是相对路径 +# files_full_path = [os.path.relpath(os.path.join(root, f), start=os.getcwd()) for f in files] +# data_files.append((target_path, files_full_path)) + + +setup( + name=package_name, + version='0.0.0', + packages=find_packages(include=[package_name, f"{package_name}.*"]), + data_files=data_files, + install_requires=['setuptools'], + zip_safe=True, + maintainer='linker-robot', + maintainer_email='linker-robot@todo.todo', + description='ROS2 SDK for Linker Hand', + license='TODO: License declaration', + entry_points={ + 'console_scripts': [ + 'linker_hand_sdk = linker_hand_ros2_sdk.linker_hand:main', + 'linker_hand_advanced_o6 = linker_hand_ros2_sdk.linker_hand_advanced_o6:main', + 'o6_joint_curve = linker_hand_ros2_sdk.o6_joint_curve:main', + 'linker_hand_advanced_l6 = linker_hand_ros2_sdk.linker_hand_advanced_l6:main', + 'linker_hand_advanced_l7 = linker_hand_ros2_sdk.linker_hand_advanced_l7:main', + 'linker_hand_advanced_l10 = linker_hand_ros2_sdk.linker_hand_advanced_l10:main', + 'linker_hand_advanced_g20 = linker_hand_ros2_sdk.linker_hand_advanced_g20:main', + 'linker_hand_g20_palm_touch = linker_hand_ros2_sdk.linker_hand_g20_palm_touch:main', + ], + }, +) diff --git a/src/linker_hand_ros2_sdk/test/test_copyright.py b/src/linker_hand_ros2_sdk/test/test_copyright.py new file mode 100644 index 0000000..97a3919 --- /dev/null +++ b/src/linker_hand_ros2_sdk/test/test_copyright.py @@ -0,0 +1,25 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_copyright.main import main +import pytest + + +# Remove the `skip` decorator once the source file(s) have a copyright header +@pytest.mark.skip(reason='No copyright header has been placed in the generated source file.') +@pytest.mark.copyright +@pytest.mark.linter +def test_copyright(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found errors' diff --git a/src/linker_hand_ros2_sdk/test/test_flake8.py b/src/linker_hand_ros2_sdk/test/test_flake8.py new file mode 100644 index 0000000..27ee107 --- /dev/null +++ b/src/linker_hand_ros2_sdk/test/test_flake8.py @@ -0,0 +1,25 @@ +# Copyright 2017 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_flake8.main import main_with_errors +import pytest + + +@pytest.mark.flake8 +@pytest.mark.linter +def test_flake8(): + rc, errors = main_with_errors(argv=[]) + assert rc == 0, \ + 'Found %d code style errors / warnings:\n' % len(errors) + \ + '\n'.join(errors) diff --git a/src/linker_hand_ros2_sdk/test/test_pep257.py b/src/linker_hand_ros2_sdk/test/test_pep257.py new file mode 100644 index 0000000..b234a38 --- /dev/null +++ b/src/linker_hand_ros2_sdk/test/test_pep257.py @@ -0,0 +1,23 @@ +# Copyright 2015 Open Source Robotics Foundation, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ament_pep257.main import main +import pytest + + +@pytest.mark.linter +@pytest.mark.pep257 +def test_pep257(): + rc = main(argv=['.', 'test']) + assert rc == 0, 'Found code style errors / warnings' diff --git a/src/linker_hand_ros2_sdk/test/test_topic_prefix.py b/src/linker_hand_ros2_sdk/test/test_topic_prefix.py new file mode 100644 index 0000000..f3f7931 --- /dev/null +++ b/src/linker_hand_ros2_sdk/test/test_topic_prefix.py @@ -0,0 +1,36 @@ +from rclpy.qos import HistoryPolicy + +from linker_hand_ros2_sdk.linker_hand import ( + COMMAND_QOS, + LinkerHand, + command_changed, + state_poll_due, +) + + +def test_empty_topic_prefix_preserves_legacy_topics(): + assert LinkerHand.normalize_topic_prefix("") == "" + assert LinkerHand.normalize_topic_prefix("/") == "" + + +def test_topic_prefix_is_absolute_and_has_no_trailing_slash(): + assert LinkerHand.normalize_topic_prefix("o6") == "/o6" + assert LinkerHand.normalize_topic_prefix("/g20/") == "/g20" + + +def test_command_qos_keeps_only_the_latest_sample(): + assert COMMAND_QOS.history == HistoryPolicy.KEEP_LAST + assert COMMAND_QOS.depth == 1 + + +def test_identical_commands_are_not_reapplied(): + assert command_changed(None, [60, 60]) + assert not command_changed([60, 60], [60, 60]) + assert command_changed([60, 60], [60, 61]) + assert not command_changed([60, 60], []) + + +def test_state_polling_is_throttled_without_missing_deadline(): + assert state_poll_due(None, 10.0, 0.1) + assert not state_poll_due(10.0, 10.09, 0.1) + assert state_poll_due(10.0, 10.1, 0.1) -- 2.52.0 From fb682422d32cfbd91c040f9252fb6a8d775a36bc Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 12:58:38 +0800 Subject: [PATCH 11/18] =?UTF-8?q?=E7=81=B5=E5=B7=A7=E6=89=8B=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E9=A9=B1=E5=8A=A8=E5=B7=B2=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../launch/linker_hand_double.launch.py | 4 ++-- src/linker_hand_ros2_sdk/setup.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py index 4d81d46..f5c283a 100644 --- a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py +++ b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py @@ -12,7 +12,7 @@ def generate_launch_description(): output='screen', parameters=[{ 'hand_type': 'left', # 配置Linker Hand灵巧手类型 left | right 字母为小写 - 'hand_joint': "G20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 + 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* @@ -26,7 +26,7 @@ def generate_launch_description(): output='screen', parameters=[{ 'hand_type': 'right', # 配置Linker Hand灵巧手类型 left | right 字母为小写 - 'hand_joint': "G20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 + 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* diff --git a/src/linker_hand_ros2_sdk/setup.py b/src/linker_hand_ros2_sdk/setup.py index 5a1479f..13b71d4 100644 --- a/src/linker_hand_ros2_sdk/setup.py +++ b/src/linker_hand_ros2_sdk/setup.py @@ -29,9 +29,13 @@ setup( name=package_name, version='0.0.0', packages=find_packages(include=[package_name, f"{package_name}.*"]), + package_data={ + package_name: ['LinkerHand/config/*.yaml'], + }, + include_package_data=True, data_files=data_files, install_requires=['setuptools'], - zip_safe=True, + zip_safe=False, maintainer='linker-robot', maintainer_email='linker-robot@todo.todo', description='ROS2 SDK for Linker Hand', -- 2.52.0 From 37c426f994d6f13e229434aa6f0360d9ea8f6dbf Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 13:14:27 +0800 Subject: [PATCH 12/18] =?UTF-8?q?L20=E7=81=B5=E5=B7=A7=E6=89=8B=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/linker_hand_control.sh | 177 +++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100755 scripts/linker_hand_control.sh diff --git a/scripts/linker_hand_control.sh b/scripts/linker_hand_control.sh new file mode 100755 index 0000000..1a3f44a --- /dev/null +++ b/scripts/linker_hand_control.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WS_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +STATE_DIR="/tmp/linker_hand_control" +PID_FILE="${STATE_DIR}/linker_hand.pid" +PGID_FILE="${STATE_DIR}/linker_hand.pgid" +LOG_FILE="${WS_DIR}/log/linker_hand_control.log" +CAN_INTERFACES=(can0 can1) +CAN_BITRATE=1000000 + +usage() { + echo "Usage: $0 {start|stop|status}" +} + +is_running() { + [[ -f "${PID_FILE}" ]] || return 1 + local pid + pid="$(cat "${PID_FILE}")" + [[ -n "${pid}" ]] || return 1 + kill -0 "${pid}" 2>/dev/null +} + +source_workspace() { + set +u + if [[ -n "${ROS_DISTRO:-}" && -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then + # shellcheck disable=SC1090 + source "/opt/ros/${ROS_DISTRO}/setup.bash" + elif [[ -f "/opt/ros/jazzy/setup.bash" ]]; then + # shellcheck disable=SC1091 + source "/opt/ros/jazzy/setup.bash" + fi + set -u + + if [[ ! -f "${WS_DIR}/install/setup.bash" ]]; then + echo "Workspace is not built: ${WS_DIR}/install/setup.bash not found" + echo "Run: cd ${WS_DIR} && colcon build --packages-select linker_hand_ros2_sdk" + exit 1 + fi + + set +u + # shellcheck disable=SC1091 + source "${WS_DIR}/install/setup.bash" + set -u +} + +start_can() { + for can_if in "${CAN_INTERFACES[@]}"; do + if ! ip link show "${can_if}" >/dev/null 2>&1; then + echo "CAN interface not found: ${can_if}" + exit 1 + fi + + echo "Starting ${can_if} at ${CAN_BITRATE} bps..." + sudo ip link set "${can_if}" down 2>/dev/null || true + sudo ip link set "${can_if}" type can bitrate "${CAN_BITRATE}" + sudo ip link set "${can_if}" up + done +} + +stop_can() { + for can_if in "${CAN_INTERFACES[@]}"; do + if ip link show "${can_if}" >/dev/null 2>&1; then + echo "Stopping ${can_if}..." + sudo ip link set "${can_if}" down 2>/dev/null || true + fi + done +} + +start_driver() { + mkdir -p "${STATE_DIR}" "$(dirname "${LOG_FILE}")" + + if is_running; then + echo "linker_hand service is already running, pid=$(cat "${PID_FILE}")" + return 0 + fi + + source_workspace + start_can + + echo "Starting linker_hand service..." + echo "Log: ${LOG_FILE}" + + : > "${LOG_FILE}" + setsid bash -lc "set +u; source '${WS_DIR}/install/setup.bash'; set -u; exec ros2 launch linker_hand_ros2_sdk linker_hand_double.launch.py" \ + >> "${LOG_FILE}" 2>&1 & + + local pid=$! + local pgid + pgid="$(ps -o pgid= -p "${pid}" | tr -d ' ')" + echo "${pid}" > "${PID_FILE}" + echo "${pgid:-${pid}}" > "${PGID_FILE}" + + sleep 1 + if is_running; then + echo "linker_hand service started, pid=${pid}, pgid=$(cat "${PGID_FILE}")" + else + echo "linker_hand service failed to start. Check log: ${LOG_FILE}" + rm -f "${PID_FILE}" "${PGID_FILE}" + stop_can + exit 1 + fi +} + +stop_driver() { + if ! [[ -f "${PID_FILE}" ]]; then + echo "linker_hand service is not running" + stop_can + return 0 + fi + + local pid pgid + pid="$(cat "${PID_FILE}")" + pgid="${pid}" + if [[ -f "${PGID_FILE}" ]]; then + pgid="$(cat "${PGID_FILE}")" + fi + + if ! kill -0 "${pid}" 2>/dev/null; then + echo "linker_hand process is not alive, cleaning state" + rm -f "${PID_FILE}" "${PGID_FILE}" + stop_can + return 0 + fi + + echo "Stopping linker_hand service, pid=${pid}, pgid=${pgid}..." + kill -TERM -- "-${pgid}" 2>/dev/null || kill -TERM "${pid}" 2>/dev/null || true + + for _ in {1..30}; do + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${PID_FILE}" "${PGID_FILE}" + echo "linker_hand service stopped" + stop_can + return 0 + fi + sleep 0.2 + done + + echo "linker_hand service did not exit after SIGTERM, forcing stop..." + kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true + rm -f "${PID_FILE}" "${PGID_FILE}" + echo "linker_hand service stopped" + stop_can +} + +status_driver() { + if is_running; then + echo "linker_hand service is running, pid=$(cat "${PID_FILE}"), pgid=$(cat "${PGID_FILE}" 2>/dev/null || cat "${PID_FILE}")" + else + echo "linker_hand service is not running" + fi + + for can_if in "${CAN_INTERFACES[@]}"; do + if ip link show "${can_if}" >/dev/null 2>&1; then + ip -details link show "${can_if}" | sed -n '1,2p' + else + echo "${can_if}: not found" + fi + done +} + +case "${1:-}" in + start) + start_driver + ;; + stop) + stop_driver + ;; + status) + status_driver + ;; + *) + usage + exit 1 + ;; +esac -- 2.52.0 From b4d6c9876c4f20ad9c2c84e5cfb14bc159ab265a Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 14:55:07 +0800 Subject: [PATCH 13/18] =?UTF-8?q?=E5=B7=B2=E7=BB=8F=E5=8F=AF=E4=BB=A5?= =?UTF-8?q?=E6=AD=A3=E5=B8=B8=E9=A9=B1=E5=8A=A8=E7=81=B5=E5=B7=A7=E6=89=8B?= =?UTF-8?q?=E5=B9=B6=E8=8E=B7=E5=8F=96=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../launch/linker_hand_double.launch.py | 4 +- .../LinkerHand/config/setting.yaml | 10 +- .../linker_hand_l20_can.cpython-312.pyc | Bin 0 -> 29695 bytes .../core/can/linker_hand_l20_can.py | 206 ++++++++++++------ .../__pycache__/linker_hand.cpython-312.pyc | Bin 0 -> 36093 bytes .../linker_hand_ros2_sdk/linker_hand.py | 24 +- 6 files changed, 164 insertions(+), 80 deletions(-) create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__pycache__/linker_hand_l20_can.cpython-312.pyc create mode 100644 src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__pycache__/linker_hand.cpython-312.pyc diff --git a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py index f5c283a..4f50f53 100644 --- a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py +++ b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py @@ -14,7 +14,7 @@ def generate_launch_description(): 'hand_type': 'left', # 配置Linker Hand灵巧手类型 left | right 字母为小写 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False - 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* }], ), @@ -28,7 +28,7 @@ def generate_launch_description(): 'hand_type': 'right', # 配置Linker Hand灵巧手类型 left | right 字母为小写 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False - 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* }], ), diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml index f3a7511..793e043 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/config/setting.yaml @@ -3,9 +3,9 @@ LINKER_HAND: # 手部配置信息 LEFT_HAND: EXISTS: True # 是否存在左手 TOUCH: True # 是否有压力传感器 - CAN: "can0" # 配置CAN端口 默认can0 如果MODUBS不为"None",则CAN配置失效。 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 + CAN: "can1" # 配置CAN端口 默认can0 如果MODUBS不为"None",则CAN配置失效。 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 MODBUS: "None" # 通讯协议是否为485 默认None 如果启动485,则是设备端口 /dev/ttyUSB* CAN配置失效 当前只支持O6/L6.后续版本正在努力增加中 - JOINT: L10 # 左手型号 O6/L6/L7/L10/L20/G20/L21/L25/ + JOINT: L20 # 左手型号 O6/L6/L7/L10/L20/G20/L21/L25/ NAME: # 默认值,不用修改 - joint41 - joint42 @@ -29,11 +29,11 @@ LINKER_HAND: # 手部配置信息 - joint60 RIGHT_HAND: - EXISTS: False # 是否存在右手 - TOUCH: False # 是否有压力传感器 + EXISTS: True # 是否存在右手 + TOUCH: True # 是否有压力传感器 CAN: "can0" # 配置CAN端口 默认can0 如果MODUBS不为"None",则CAN配置失效。 如果是win系统则类似于 PCAN_USBBUS1。注:蓝色盒子为Linux下can0,WIN下位PCAN_USBBUS1。透明盒子Linux下为can0,WIN下为0 MODBUS: "None" # 通讯协议是否为485 默认None 如果启动485,则是设备端口 /dev/ttyUSB* CAN配置失效 当前只支持O6/L6.后续版本正在努力增加中 - JOINT: L10 # 右手型号 O6/L6/L7/L10/L20/G20/L21/L25/ + JOINT: L20 # 右手型号 O6/L6/L7/L10/L20/G20/L21/L25/ NAME: # 默认值,不用修改 - joint71 - joint72 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__pycache__/linker_hand_l20_can.cpython-312.pyc b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/__pycache__/linker_hand_l20_can.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c92aaee89a056bfdab747ce920255ae1dca185b1 GIT binary patch literal 29695 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 Qwwrdv(C~R&gk(_tUo_u%?f?J) literal 0 HcmV?d00001 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py index 5b5f36e..18410d9 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/LinkerHand/core/can/linker_hand_l20_can.py @@ -10,21 +10,40 @@ from can.exceptions import CanError class FrameProperty(Enum): INVALID_FRAME_PROPERTY = 0x00 # Invalid CAN frame property | No return - JOINT_PITCH_R = 0x01 # Short frame pitch angle - finger base flexion | Returns this type of data - JOINT_YAW_R = 0x02 # Short frame yaw angle - finger abduction/adduction | Returns this type of data - JOINT_ROLL_R = 0x03 # Short frame roll angle - only used for thumb | Returns this type of data - JOINT_TIP_R = 0x04 # Short frame fingertip angle control | Returns this type of data - JOINT_SPEED_R = 0x05 # Short frame speed - motor running speed control | Returns this type of data - JOINT_CURRENT_R = 0x06 # Short frame current - motor running current feedback | Returns this type of data - JOINT_FAULT_R = 0x07 # Short frame fault - motor running fault feedback | Returns this type of data - REQUEST_DATA_RETURN = 0x09 # Request data return | Returns all data - JOINT_PITCH_NR = 0x11 # Pitch angle - finger base flexion | No return for this type of data - JOINT_YAW_NR = 0x12 # Yaw angle - finger abduction/adduction | No return for this type of data - JOINT_ROLL_NR = 0x13 # Roll angle - only used for thumb | No return for this type of data - JOINT_TIP_NR = 0x14 # Fingertip angle control | No return for this type of data - JOINT_SPEED_NR = 0x15 # Speed - motor running speed control | No return for this type of data - JOINT_CURRENT_NR = 0x16 # Current - motor running current feedback | No return for this type of data - JOINT_FAULT_NR = 0x17 # Fault - motor running fault feedback | No return for this type of data + ROLL_POS = 0x01 # All fingers roll joint position + YAW_POS = 0x02 # All fingers yaw joint position + ROOT1_POS = 0x03 # All fingers root1 joint position + ROOT2_POS = 0x04 # All fingers root2 joint position + ROOT3_POS = 0x05 # All fingers root3 joint position + TIP_POS = 0x06 # All fingers tip joint position + ROLL_SPEED = 0x09 # All fingers roll joint speed + YAW_SPEED = 0x0A # All fingers yaw joint speed + ROOT1_SPEED = 0x0B # All fingers root1 joint speed + ROOT2_SPEED = 0x0C # All fingers root2 joint speed + ROOT3_SPEED = 0x0D # All fingers root3 joint speed + TIP_SPEED = 0x0E # All fingers tip joint speed + ROLL_TORQUE = 0x11 # All fingers roll joint load/current threshold + YAW_TORQUE = 0x12 # All fingers yaw joint load/current threshold + ROOT1_TORQUE = 0x13 # All fingers root1 joint load/current threshold + ROOT2_TORQUE = 0x14 # All fingers root2 joint load/current threshold + ROOT3_TORQUE = 0x15 # All fingers root3 joint load/current threshold + TIP_TORQUE = 0x16 # All fingers tip joint load/current threshold + ROLL_FAULT = 0x19 # All fingers roll joint fault + YAW_FAULT = 0x1A # All fingers yaw joint fault + ROOT1_FAULT = 0x1B # All fingers root1 joint fault + ROOT2_FAULT = 0x1C # All fingers root2 joint fault + ROOT3_FAULT = 0x1D # All fingers root3 joint fault + TIP_FAULT = 0x1E # All fingers tip joint fault + ROLL_TEMPERATURE = 0x21 # All fingers roll joint temperature + YAW_TEMPERATURE = 0x22 # All fingers yaw joint temperature + ROOT1_TEMPERATURE = 0x23 # All fingers root1 joint temperature + ROOT2_TEMPERATURE = 0x24 # All fingers root2 joint temperature + ROOT3_TEMPERATURE = 0x25 # All fingers root3 joint temperature + TIP_TEMPERATURE = 0x26 # All fingers tip joint temperature + FINGER_SPEED = 0x81 # Per-finger speed + FINGER_TORQUE = 0x82 # Per-finger torque/current threshold + FINGER_FAULT = 0x83 # Per-finger fault + FINGER_TEMPERATURE = 0x84 # Per-finger temperature HAND_UID = 0xC0 # Device unique identifier Read only -------- HAND_HARDWARE_VERSION = 0xC1 # Hardware version Read only -------- HAND_SOFTWARE_VERSION = 0xC2 # Software version Read only -------- @@ -42,9 +61,13 @@ class LinkerHandL20Can: self.running = True self.x05 = [255] * 5 self.x06, self.x07 = [],[] + self.x09, self.x0a, self.x0b, self.x0c, self.x0d, self.x0e = [[-1] * 5 for _ in range(6)] + self.x11, self.x12, self.x13, self.x14, self.x15, self.x16 = [[-1] * 5 for _ in range(6)] + self.x19, self.x1a, self.x1b, self.x1c, self.x1d, self.x1e = [[-1] * 5 for _ in range(6)] + self.x21, self.x22, self.x23, self.x24, self.x25, self.x26 = [[-1] * 5 for _ in range(6)] + self.x81, self.x82, self.x83, self.x84 = [[-1] * 5 for _ in range(4)] # New pressure sensors self.xb0,self.xb1,self.xb2,self.xb3,self.xb4,self.xb5 = [-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5,[-1] * 5 - self.x09 = self.x0b = self.x0c = self.x0d = [-1] * 5 self.thumb_matrix = np.full((12, 6), -1) self.index_matrix = np.full((12, 6), -1) self.middle_matrix = np.full((12, 6), -1) @@ -89,12 +112,12 @@ class LinkerHandL20Can: self.normal_force, self.tangential_force, self.tangential_force_dir, self.approach_inc = \ [[-1] * 5 for _ in range(4)] - # Start receive thread - self.get_touch_type() - time.sleep(0.1) + # Start receive thread before querying touch type so replies can be processed. self.receive_thread = threading.Thread(target=self.receive_response) self.receive_thread.daemon = True self.receive_thread.start() + self.get_touch_type() + time.sleep(0.1) def init_can_bus(self, channel, baudrate): """ @@ -168,16 +191,16 @@ class LinkerHandL20Can: def set_finger_base(self, angles): - self.send_command(FrameProperty.JOINT_PITCH_NR, angles) + self.send_command(FrameProperty.ROOT1_POS, angles) def set_finger_tip(self, angles): - self.send_command(FrameProperty.JOINT_TIP_NR, angles) + self.send_command(FrameProperty.TIP_POS, angles) def set_finger_middle(self, angles): - self.send_command(FrameProperty.JOINT_YAW_NR, angles) + self.send_command(FrameProperty.YAW_POS, angles) def set_thumb_roll(self, angle): - self.send_command(FrameProperty.JOINT_ROLL_NR, angle) + self.send_command(FrameProperty.ROLL_POS, angle) def send_command(self, frame_property, data_list,sleep=0.002): frame_property_value = int(frame_property.value) if hasattr(frame_property, 'value') else frame_property @@ -202,35 +225,36 @@ class LinkerHandL20Can: self.send_command(frame, angles) def set_joint_yaw(self, angles): - self.send_command(0x02, angles) + self.send_command(FrameProperty.YAW_POS, angles) def set_joint_roll(self, thumb_roll): - self.send_command(0x03, [thumb_roll, 0, 0, 0, 0]) + self.send_command(FrameProperty.ROLL_POS, [thumb_roll, 0, 0, 0, 0]) def set_joint_speed(self, speed): - self.x05 = speed - self.send_command(0x05, speed) + self.x81 = speed + self.send_command(FrameProperty.FINGER_SPEED, speed) def set_electric_current(self, e_c=[]): - self.send_command(0x06, e_c) + self.x82 = e_c + self.send_command(FrameProperty.FINGER_TORQUE, e_c) def get_normal_force(self): - self.send_command(0x20,[]) + self.send_command(0x90, []) def get_tangential_force(self): - self.send_command(0x21,[]) + self.send_command(0x91, []) def get_tangential_force_dir(self): - self.send_command(0x22,[]) + self.send_command(0x92, []) def get_approach_inc(self): - self.send_command(0x23,[]) + self.send_command(0x93, []) def get_electric_current(self, e_c=[]): - self.send_command(0x06, e_c) + self.send_command(FrameProperty.FINGER_TORQUE, e_c) def request_device_info(self): self.send_command(0xC0, [0]) @@ -263,28 +287,73 @@ class LinkerHandL20Can: self.x05 = list(response_data) elif frame_type == 0x06: self.x06 = list(response_data) - elif frame_type == 0x07: - self.x07 = list(response_data) elif frame_type == 0x09: self.x09 = list(response_data) + elif frame_type == 0x0A: + self.x0a = list(response_data) elif frame_type == 0x0B: self.x0b = list(response_data) elif frame_type == 0x0C: self.x0c = list(response_data) elif frame_type == 0x0D: self.x0d = list(response_data) + elif frame_type == 0x0E: + self.x0e = list(response_data) + elif frame_type == 0x11: + self.x11 = list(response_data) + elif frame_type == 0x12: + self.x12 = list(response_data) + elif frame_type == 0x13: + self.x13 = list(response_data) + elif frame_type == 0x14: + self.x14 = list(response_data) + elif frame_type == 0x15: + self.x15 = list(response_data) + elif frame_type == 0x16: + self.x16 = list(response_data) + elif frame_type == 0x19: + self.x19 = list(response_data) + elif frame_type == 0x1A: + self.x1a = list(response_data) + elif frame_type == 0x1B: + self.x1b = list(response_data) + elif frame_type == 0x1C: + self.x1c = list(response_data) + elif frame_type == 0x1D: + self.x1d = list(response_data) + elif frame_type == 0x1E: + self.x1e = list(response_data) elif frame_type == 0x20: d = list(response_data) self.normal_force = [float(i) for i in d] elif frame_type == 0x21: - d = list(response_data) - self.tangential_force = [float(i) for i in d] + self.x21 = list(response_data) elif frame_type == 0x22: - d = list(response_data) - self.tangential_force_dir = [float(i) for i in d] + self.x22 = list(response_data) elif frame_type == 0x23: - d = list(response_data) - self.approach_inc = [float(i) for i in d] + self.x23 = list(response_data) + elif frame_type == 0x24: + self.x24 = list(response_data) + elif frame_type == 0x25: + self.x25 = list(response_data) + elif frame_type == 0x26: + self.x26 = list(response_data) + elif frame_type == 0x81: + self.x81 = list(response_data) + elif frame_type == 0x82: + self.x82 = list(response_data) + elif frame_type == 0x83: + self.x83 = list(response_data) + elif frame_type == 0x84: + self.x84 = list(response_data) + elif frame_type == 0x90: + self.normal_force = [float(i) for i in list(response_data)] + elif frame_type == 0x91: + self.tangential_force = [float(i) for i in list(response_data)] + elif frame_type == 0x92: + self.tangential_force_dir = [float(i) for i in list(response_data)] + elif frame_type == 0x93: + self.approach_inc = [float(i) for i in list(response_data)] elif frame_type == 0xb0: self.xb0 = list(response_data) elif frame_type == 0xb1: @@ -350,57 +419,56 @@ class LinkerHandL20Can: if len(speed) != 5: raise ValueError("Speed list must have 5 elements.") return - self.send_command(0x05,speed) + self.x81 = speed + self.send_command(FrameProperty.FINGER_SPEED, speed) def set_torque(self, torque=[]): '''Set torque, not supported for L20''' print("Set torque, not supported for L20") def set_current(self, current=[]): - '''Set current''' - self.set_electric_current(e_c=current) + '''Set per-finger torque/current threshold.''' + self.x82 = current + self.send_command(FrameProperty.FINGER_TORQUE, current) def get_version(self): '''Get version, currently not supported''' return [0] * 5 def get_current_status(self): '''Get current finger joint status''' - self.send_command(0x01,[],sleep=0.01) - self.send_command(0x02,[],sleep=0.01) - self.send_command(0x03,[],sleep=0.01) - self.send_command(0x04,[],sleep=0.01) - return self.x01 + self.x02 + self.x03 + self.x04 + self.send_command(FrameProperty.ROOT1_POS, [], sleep=0.01) + self.send_command(FrameProperty.YAW_POS, [], sleep=0.01) + self.send_command(FrameProperty.ROLL_POS, [], sleep=0.01) + self.send_command(FrameProperty.TIP_POS, [], sleep=0.01) + return self.x03 + self.x02 + self.x01 + self.x06 def get_current_pub_status(self): time.sleep(0.01) - return self.x01 + self.x02 + self.x03 + self.x04 + return self.x03 + self.x02 + self.x01 + self.x06 def get_speed(self): '''Get current motor speed''' - self.send_command(0x05, [0]) + self.send_command(FrameProperty.FINGER_SPEED, []) time.sleep(0.001) - return self.x05 + return self.x81 def get_current(self): - '''Get current threshold''' - self.send_command(0x06, [0]) - return self.x06 + '''Get per-finger torque/current threshold.''' + self.send_command(FrameProperty.FINGER_TORQUE, []) + return self.x82 def get_torque(self): - '''Get current motor torque, not supported for L20''' - return [0] * 5 + '''Get current motor torque/current threshold.''' + self.send_command(FrameProperty.FINGER_TORQUE, []) + return self.x82 def get_fault(self): - self.send_command(0x07,[]) + self.send_command(FrameProperty.FINGER_FAULT, []) time.sleep(0.01) - return self.x07 + return self.x83 def get_temperature(self): '''Get motor temperature''' - self.send_command(0x09,[]) - self.send_command(0x0b,[]) - self.send_command(0x0c,[]) - self.send_command(0x0d,[]) - - return self.x09+self.x0b+self.x0c+self.x0d + self.send_command(FrameProperty.FINGER_TEMPERATURE, []) + return self.x84 def clear_faults(self): '''Clear motor faults''' - self.send_command(0x07, [1, 1, 1, 1, 1]) + self.send_command(FrameProperty.FINGER_FAULT, [1, 1, 1, 1, 1]) def get_touch_type(self): '''Get touch type''' @@ -412,7 +480,7 @@ class LinkerHandL20Can: elif self.xb0 == [1]: return 1 else: - self.send_command(0x20,[],sleep=0.03) + self.send_command(0x90, [], sleep=0.03) time.sleep(0.01) if self.normal_force[0] == -1: return -1 @@ -458,8 +526,8 @@ class LinkerHandL20Can: def get_faults(self): '''Get motor fault codes''' - self.send_command(0x07, []) - return self.x07 + self.send_command(FrameProperty.FINGER_FAULT, []) + return self.x83 def get_force(self): '''Get pressure sensor data''' return [self.normal_force,self.tangential_force,self.tangential_force_dir,self.approach_inc] diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__pycache__/linker_hand.cpython-312.pyc b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/__pycache__/linker_hand.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..408d74998a3a1917b805ae8c8c4a2f6744916b11 GIT binary patch literal 36093 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 Ph0l-9=rUwQq>}#+k&}M$ literal 0 HcmV?d00001 diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py index 6e7d22f..34d4e1d 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py @@ -192,6 +192,12 @@ class LinkerHand(Node): self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + elif self.hand_joint == "L20": + ColorMsg(msg=f"{self.hand_type} {self.hand_joint} matrix pressure sensing enabled", color='green') + self.touch_type = 2 + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) elif self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) @@ -223,7 +229,7 @@ class LinkerHand(Node): pose = [255, 200, 255, 255, 255, 255, 180, 180, 180, 41] speed = [200, 250, 250, 250, 250, 250, 250, 250, 250, 250] elif self.hand_joint == "L20": - pose = [255,255,255,255,255,255,10,100,180,240,245,255,255,255,255,255,255,255,255,255] + pose = None elif self.hand_joint == "G20": # G20 uses a different CAN protocol from L20 even though both # expose a 20-value ROS command. This is the calibrated startup @@ -236,8 +242,19 @@ class LinkerHand(Node): elif self.hand_joint == "L25": pose = [75, 255, 255, 255, 255, 176, 97, 81, 114, 147, 202, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] if self.startup_pose: - if pose is None or len(self.startup_pose) != len(pose): - expected = 0 if pose is None else len(pose) + expected_lengths = { + "O6": 6, + "L6": 6, + "L6P": 6, + "L7": 7, + "L10": 10, + "L20": 20, + "G20": 20, + "L21": 25, + "L25": 25, + } + expected = expected_lengths.get(str(self.hand_joint).upper(), 0) + if len(self.startup_pose) != expected: raise ValueError( f'startup_pose for {self.hand_joint} must contain {expected} values' ) @@ -376,7 +393,6 @@ class LinkerHand(Node): } if self.run_count == 9: - self.api.clear_faults() # 自动清除错误编码 self.run_count = 0 self.run_count += 1 time.sleep(0.003) -- 2.52.0 From 579aa958e5ea00349aaa6a3ce765bd52913fa9e1 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 18:27:40 +0800 Subject: [PATCH 14/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=9C=AC=E6=9C=BA?= =?UTF-8?q?=E8=AE=A2=E9=98=85=E8=AF=9D=E9=A2=98=E7=9A=84=E9=9A=94=E7=A6=BB?= =?UTF-8?q?=E6=9C=BA=E5=88=B6,=E4=BF=AE=E6=94=B9=E8=AF=9D=E9=A2=98?= =?UTF-8?q?=E5=90=8D=E5=AD=97=E9=81=BF=E5=85=8D=E8=A2=AB=E5=86=85=E7=BD=91?= =?UTF-8?q?=E9=80=8F=E4=BC=A0=E6=B6=88=E6=81=AF=E5=BC=95=E5=8F=91=E5=86=B2?= =?UTF-8?q?=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/linker_hand_control.sh | 15 ++++- .../linker_hand_ros2_sdk/linker_hand.py | 55 ++++++++----------- .../linker_hand_advanced_g20.py | 45 +++++++-------- .../linker_hand_advanced_l10.py | 49 +++++++---------- .../linker_hand_advanced_l6.py | 45 +++++++-------- .../linker_hand_advanced_l7.py | 45 +++++++-------- .../linker_hand_advanced_o6.py | 45 +++++++-------- .../linker_hand_g20_palm_touch.py | 48 +++++++--------- .../linker_hand_ros2_sdk/o6_joint_curve.py | 2 +- 9 files changed, 156 insertions(+), 193 deletions(-) diff --git a/scripts/linker_hand_control.sh b/scripts/linker_hand_control.sh index 1a3f44a..0e55d2f 100755 --- a/scripts/linker_hand_control.sh +++ b/scripts/linker_hand_control.sh @@ -10,6 +10,11 @@ LOG_FILE="${WS_DIR}/log/linker_hand_control.log" CAN_INTERFACES=(can0 can1) CAN_BITRATE=1000000 +# Keep this driver isolated from ROS 2 nodes on other machines in the same LAN. +# Without this, DDS discovery can pick up stale linker_hand_sdk nodes from the +# subnet and make the same topic appear to have multiple publishers. +export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST + usage() { echo "Usage: $0 {start|stop|status}" } @@ -45,6 +50,11 @@ source_workspace() { set -u } +restart_ros_daemon_localhost() { + ros2 daemon stop >/dev/null 2>&1 || true + ros2 daemon start >/dev/null 2>&1 || true +} + start_can() { for can_if in "${CAN_INTERFACES[@]}"; do if ! ip link show "${can_if}" >/dev/null 2>&1; then @@ -77,13 +87,14 @@ start_driver() { fi source_workspace + restart_ros_daemon_localhost start_can echo "Starting linker_hand service..." echo "Log: ${LOG_FILE}" : > "${LOG_FILE}" - setsid bash -lc "set +u; source '${WS_DIR}/install/setup.bash'; set -u; exec ros2 launch linker_hand_ros2_sdk linker_hand_double.launch.py" \ + setsid bash -lc "export ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST; set +u; source '${WS_DIR}/install/setup.bash'; set -u; exec ros2 launch linker_hand_ros2_sdk linker_hand_double.launch.py" \ >> "${LOG_FILE}" 2>&1 & local pid=$! @@ -151,6 +162,8 @@ status_driver() { echo "linker_hand service is not running" fi + echo "ROS_AUTOMATIC_DISCOVERY_RANGE=${ROS_AUTOMATIC_DISCOVERY_RANGE}" + for can_if in "${CAN_INTERFACES[@]}"; do if ip link show "${can_if}" >/dev/null 2>&1; then ip -details link show "${can_if}" | sed -n '1,2p' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py index 34d4e1d..3f0542d 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py @@ -151,7 +151,7 @@ class LinkerHand(Node): self.hz = 1.0/60.0 self.hand_setting_sub = self.create_subscription( - String, self.topic('/cb_hand_setting_cmd'), self.hand_setting_cb, 10 + String, self.topic('/l20_hand_setting_cmd'), self.hand_setting_cb, 10 ) self._init_hand() time.sleep(1) @@ -181,31 +181,31 @@ class LinkerHand(Node): self.touch_type = self.api.get_touch_type() self.hand_cmd_sub = self.create_subscription( JointState, - self.topic(f'/cb_{self.hand_type}_hand_control_cmd'), + self.topic(f'/l20_{self.hand_type}_hand_control_cmd'), self.hand_control_cb, COMMAND_QOS, ) - self.hand_state_pub = self.create_publisher(JointState, self.topic(f'/cb_{self.hand_type}_hand_state'),10) - self.hand_info_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_info'), 10) + self.hand_state_pub = self.create_publisher(JointState, self.topic(f'/l20_{self.hand_type}_hand_state'),10) + self.hand_info_pub = self.create_publisher(String, self.topic(f'/l20_{self.hand_type}_hand_info'), 10) if self.is_touch == True: if self.modbus != "None": - self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) - self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_mass'), 10) elif self.hand_joint == "L20": ColorMsg(msg=f"{self.hand_type} {self.hand_joint} matrix pressure sensing enabled", color='green') self.touch_type = 2 - self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) - self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_mass'), 10) elif self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch'), 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_pc'), 10) - self.matrix_touch_mass_pub = self.create_publisher(String, self.topic(f'/cb_{self.hand_type}_hand_matrix_touch_mass'), 10) + self.matrix_touch_pub = self.create_publisher(String, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch'), 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_pc'), 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, self.topic(f'/l20_{self.hand_type}_hand_matrix_touch_mass'), 10) elif self.touch_type != -1 and self.modbus == "None": ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, self.topic(f'/cb_{self.hand_type}_hand_force'), 10) + self.touch_pub = self.create_publisher(Float32MultiArray, self.topic(f'/l20_{self.hand_type}_hand_force'), 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -410,7 +410,7 @@ class LinkerHand(Node): if self.is_touch == True and (self.touch_type > 1 or self.modbus != "None") and (self.matrix_touch_pub.get_subscription_count() > 0 or self.matrix_touch_mass_pub.get_subscription_count() > 0 or self.matrix_touch_pub_pc.get_subscription_count() > 0): # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -421,22 +421,15 @@ class LinkerHand(Node): time.sleep(self.hz) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py index 2bf1f83..f4b7d75 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_g20.py @@ -87,17 +87,17 @@ class LinkerHandAdvancedG20(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -163,7 +163,7 @@ class LinkerHandAdvancedG20(Node): self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=TOUCH_SLEEP_TIME).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -185,22 +185,15 @@ class LinkerHandAdvancedG20(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): @@ -232,9 +225,9 @@ class LinkerHandAdvancedG20(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_advanced_g20 --hand_type left --can can0 --is_touch true ''' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py index 37a7be3..4026aef 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l10.py @@ -76,17 +76,17 @@ class LinkerHandAdvancedL10(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -151,7 +151,7 @@ class LinkerHandAdvancedL10(Node): self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.004).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -171,22 +171,15 @@ class LinkerHandAdvancedL10(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): @@ -218,11 +211,11 @@ class LinkerHandAdvancedL10(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz - '/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + '/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_advanced_l10 --hand_type left --can can0 --is_touch true ''' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py index 2672283..81c6222 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l6.py @@ -78,17 +78,17 @@ class LinkerHandAdvancedL6(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -155,7 +155,7 @@ class LinkerHandAdvancedL6(Node): self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.003).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -174,22 +174,15 @@ class LinkerHandAdvancedL6(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): @@ -222,9 +215,9 @@ class LinkerHandAdvancedL6(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_advanced_l6 --hand_type left --can can0 --is_touch true ''' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py index 7ed58bd..37a5da2 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_l7.py @@ -79,17 +79,17 @@ class LinkerHandAdvancedL7(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -185,7 +185,7 @@ class LinkerHandAdvancedL7(Node): self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.006).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -208,22 +208,15 @@ class LinkerHandAdvancedL7(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): @@ -255,9 +248,9 @@ class LinkerHandAdvancedL7(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 40Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 40Hz 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_advanced_l7 --hand_type left --can can0 --is_touch true ''' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py index 42638c8..882913a 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_advanced_o6.py @@ -77,17 +77,17 @@ class LinkerHandAdvancedO6(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -160,7 +160,7 @@ class LinkerHandAdvancedO6(Node): self.matrix_dic["little_matrix"] = self.api.get_little_matrix_touch(sleep_time=0.002).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 self.pub_matrix_point_cloud() @@ -179,22 +179,15 @@ class LinkerHandAdvancedO6(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = sum(sum(row) for row in dic["thumb_matrix"]) - self.matrix_mass_dic["index_mass"] = sum(sum(row) for row in dic["index_matrix"]) - self.matrix_mass_dic["middle_mass"] = sum(sum(row) for row in dic["middle_matrix"]) - self.matrix_mass_dic["ring_mass"] = sum(sum(row) for row in dic["ring_matrix"]) - self.matrix_mass_dic["little_mass"] = sum(sum(row) for row in dic["little_matrix"]) - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(sum(row) for row in dic["thumb_matrix"])), + float(sum(sum(row) for row in dic["index_matrix"])), + float(sum(sum(row) for row in dic["middle_matrix"])), + float(sum(sum(row) for row in dic["ring_matrix"])), + float(sum(sum(row) for row in dic["little_matrix"])), + ] self.matrix_touch_mass_pub.publish(msg) def pub_matrix_point_cloud(self): @@ -226,9 +219,9 @@ class LinkerHandAdvancedO6(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 50Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 50Hz + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 50Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 50Hz 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_advanced_o6 --hand_type right --can can0 --is_touch true ''' diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py index eda447c..8a19efc 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand_g20_palm_touch.py @@ -88,17 +88,17 @@ class LinkerHandAdvancedG20(Node): self.api = LinkerHandApi(hand_type=self.hand_type, hand_joint=self.hand_joint,modbus=self.modbus,can=self.can) time.sleep(0.1) self.touch_type = self.api.get_touch_type() - self.hand_cmd_sub = self.create_subscription(JointState, f'/cb_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) - self.hand_state_pub = self.create_publisher(JointState, f'/cb_{self.hand_type}_hand_state',10) + self.hand_cmd_sub = self.create_subscription(JointState, f'/l20_{self.hand_type}_hand_control_cmd', self.hand_control_cb,10) + self.hand_state_pub = self.create_publisher(JointState, f'/l20_{self.hand_type}_hand_state',10) if self.is_touch == True: if self.touch_type > 1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with matrix pressure sensing", color='green') - self.matrix_touch_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch', 10) - #self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/cb_{self.hand_type}_hand_matrix_touch_pc', 10) - self.matrix_touch_mass_pub = self.create_publisher(String, f'/cb_{self.hand_type}_hand_matrix_touch_mass', 10) + self.matrix_touch_pub = self.create_publisher(String, f'/l20_{self.hand_type}_hand_matrix_touch', 10) + #self.matrix_touch_pub_pc = self.create_publisher(PointCloud2, f'/l20_{self.hand_type}_hand_matrix_touch_pc', 10) + self.matrix_touch_mass_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_matrix_touch_mass', 10) elif self.touch_type != -1: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Equipped with pressure sensor", color="green") - self.touch_pub = self.create_publisher(Float32MultiArray, f'/cb_{self.hand_type}_hand_force', 10) + self.touch_pub = self.create_publisher(Float32MultiArray, f'/l20_{self.hand_type}_hand_force', 10) else: ColorMsg(msg=f"{self.hand_type} {self.hand_joint} Not equipped with any pressure sensors", color="red") self.is_touch = False @@ -172,7 +172,7 @@ class LinkerHandAdvancedG20(Node): self.matrix_dic["palm_matrix"] = self.api.get_palm_matrix_touch(sleep_time=self.touch_sleep_time).tolist() # 发布矩阵压感数据JSON格式 self.pub_matrix_dic() - # 发布矩阵压感和值JSON格式 + # 发布矩阵压感合值浮点数组 self.pub_matrix_mass(dic=self.matrix_dic) # 发布矩阵压感点云格式 #self.pub_matrix_point_cloud() @@ -194,23 +194,15 @@ class LinkerHandAdvancedG20(Node): self.matrix_touch_pub.publish(msg) def pub_matrix_mass(self, dic): - """发布矩阵数据合值 单位g 克 JSON格式""" - msg = String() - # 获取当前的 ROS 时间 - current_time = self.stamp_clock.now() - # 提取 secs 和 nsecs - t_secs = current_time.to_msg().sec - t_nsecs = current_time.to_msg().nanosec - self.matrix_mass_dic["stamp"]["secs"] = t_secs - self.matrix_mass_dic["stamp"]["nsecs"] = t_nsecs - self.matrix_mass_dic["unit"] = "g" - self.matrix_mass_dic["thumb_mass"] = self.api.hand.thumb_matrix_palm_mass - self.matrix_mass_dic["index_mass"] = self.api.hand.index_matrix_palm_mass - self.matrix_mass_dic["middle_mass"] = self.api.hand.middle_matrix_palm_mass - self.matrix_mass_dic["ring_mass"] = self.api.hand.ring_matrix_palm_mass - self.matrix_mass_dic["little_mass"] = self.api.hand.little_matrix_palm_mass - self.matrix_mass_dic["palm_mass"] = self.api.hand.palm_matrix_palm_mass - msg.data = json.dumps(self.matrix_mass_dic) + """发布矩阵数据合值,顺序为 thumb, index, middle, ring, little,单位 g。""" + msg = Float32MultiArray() + msg.data = [ + float(sum(self.api.hand.thumb_matrix_palm_mass)), + float(sum(self.api.hand.index_matrix_palm_mass)), + float(sum(self.api.hand.middle_matrix_palm_mass)), + float(sum(self.api.hand.ring_matrix_palm_mass)), + float(sum(self.api.hand.little_matrix_palm_mass)), + ] self.matrix_touch_mass_pub.publish(msg) # def pub_matrix_point_cloud(self): @@ -242,9 +234,9 @@ class LinkerHandAdvancedG20(Node): def main(args=None): ''' 本节点用于收集手指状态和压感数据。 - '/cb_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz - /cb_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz - '/cb_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz + '/l20_{self.hand_type}_hand_control_cmd' 话题类型为 sensor_msgs/msg/JointState 控制话题,限制 30Hz + /l20_{self.hand_type}_hand_state 话题类型为 sensor_msgs/msg/JointState 30Hz + '/l20_{self.hand_type}_hand_matrix_touch' 话题类型为 std_msgs/msg/String 30Hz 启动命令: ros2 run linker_hand_ros2_sdk linker_hand_g20_palm_touch --hand_type left --can can0 --is_touch true ''' @@ -265,4 +257,4 @@ def main(args=None): node.close_can() # 关闭 CAN 或其他硬件资源 # node.destroy_node() # 销毁 ROS 节点 # rclpy.shutdown() # 关闭 ROS - print("程序已退出。") \ No newline at end of file + print("程序已退出。") diff --git a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py index 2a06b5a..296bdc0 100644 --- a/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py +++ b/src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/o6_joint_curve.py @@ -145,7 +145,7 @@ class O6JointCurveRecorder(Node): self.saved_paths = None self.finished = False - topic = f"/cb_{hand_type}_hand_state" + topic = f"/l20_{hand_type}_hand_state" self.subscription = self.create_subscription( JointState, topic, self._state_callback, 100 ) -- 2.52.0 From 58aa9e296467a202b8bba15bbf5e4c3faa21d8d6 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Fri, 18 Sep 2026 18:39:55 +0800 Subject: [PATCH 15/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E7=81=B5=E5=B7=A7?= =?UTF-8?q?=E6=89=8B=E7=9A=84rosbag=E5=BD=95=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/linker_hand_control.sh | 131 +++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/scripts/linker_hand_control.sh b/scripts/linker_hand_control.sh index 0e55d2f..0361de3 100755 --- a/scripts/linker_hand_control.sh +++ b/scripts/linker_hand_control.sh @@ -6,9 +6,20 @@ WS_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" STATE_DIR="/tmp/linker_hand_control" PID_FILE="${STATE_DIR}/linker_hand.pid" PGID_FILE="${STATE_DIR}/linker_hand.pgid" +BAG_PID_FILE="${STATE_DIR}/rosbag.pid" +BAG_PGID_FILE="${STATE_DIR}/rosbag.pgid" LOG_FILE="${WS_DIR}/log/linker_hand_control.log" +BAG_LOG_FILE="${WS_DIR}/log/linker_hand_rosbag.log" CAN_INTERFACES=(can0 can1) CAN_BITRATE=1000000 +ROSBAG_TOPICS=( + /l20_left_hand_control_cmd + /l20_left_hand_state + /l20_left_hand_matrix_touch_mass + /l20_right_hand_control_cmd + /l20_right_hand_state + /l20_right_hand_matrix_touch_mass +) # Keep this driver isolated from ROS 2 nodes on other machines in the same LAN. # Without this, DDS discovery can pick up stale linker_hand_sdk nodes from the @@ -27,6 +38,14 @@ is_running() { kill -0 "${pid}" 2>/dev/null } +is_bag_running() { + [[ -f "${BAG_PID_FILE}" ]] || return 1 + local pid + pid="$(cat "${BAG_PID_FILE}")" + [[ -n "${pid}" ]] || return 1 + kill -0 "${pid}" 2>/dev/null +} + source_workspace() { set +u if [[ -n "${ROS_DISTRO:-}" && -f "/opt/ros/${ROS_DISTRO}/setup.bash" ]]; then @@ -78,11 +97,110 @@ stop_can() { done } +start_bag_recorder() { + mkdir -p "${STATE_DIR}" "$(dirname "${BAG_LOG_FILE}")" + + if is_bag_running; then + echo "rosbag recorder is already running, pid=$(cat "${BAG_PID_FILE}")" + return 0 + fi + + echo "Starting rosbag recorder..." + echo "Rosbag log: ${BAG_LOG_FILE}" + + : > "${BAG_LOG_FILE}" + setsid env \ + WS_DIR="${WS_DIR}" \ + ROS_AUTOMATIC_DISCOVERY_RANGE="${ROS_AUTOMATIC_DISCOVERY_RANGE}" \ + bash -lc ' + set -euo pipefail + set +u + source "${WS_DIR}/install/setup.bash" + set -u + while true; do + start_time=$(date +%Y%m%d_%H%M%S) + output=/tmp/L20_Hand_${start_time}.mcap + echo "Starting rosbag: ${output}" + set +e + timeout 3600 ros2 bag record \ + --storage mcap \ + --output "${output}" \ + --polling-interval 2 \ + --include-unpublished-topics \ + --disable-keyboard-controls \ + --log-level warn \ + --topics \ + /l20_left_hand_control_cmd \ + /l20_left_hand_state \ + /l20_left_hand_matrix_touch_mass \ + /l20_right_hand_control_cmd \ + /l20_right_hand_state \ + /l20_right_hand_matrix_touch_mass + rc=$? + set -e + if [[ ${rc} -ne 124 ]]; then + exit ${rc} + fi + done + ' >> "${BAG_LOG_FILE}" 2>&1 & + + local pid=$! + local pgid + pgid="$(ps -o pgid= -p "${pid}" | tr -d ' ')" + echo "${pid}" > "${BAG_PID_FILE}" + echo "${pgid:-${pid}}" > "${BAG_PGID_FILE}" + + sleep 1 + if is_bag_running; then + echo "rosbag recorder started, pid=${pid}, pgid=$(cat "${BAG_PGID_FILE}")" + else + echo "rosbag recorder failed to start. Check log: ${BAG_LOG_FILE}" + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + return 1 + fi +} + +stop_bag_recorder() { + if ! [[ -f "${BAG_PID_FILE}" ]]; then + return 0 + fi + + local pid pgid + pid="$(cat "${BAG_PID_FILE}")" + pgid="${pid}" + if [[ -f "${BAG_PGID_FILE}" ]]; then + pgid="$(cat "${BAG_PGID_FILE}")" + fi + + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + return 0 + fi + + echo "Stopping rosbag recorder, pid=${pid}, pgid=${pgid}..." + kill -TERM -- "-${pgid}" 2>/dev/null || kill -TERM "${pid}" 2>/dev/null || true + + for _ in {1..30}; do + if ! kill -0 "${pid}" 2>/dev/null; then + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + echo "rosbag recorder stopped" + return 0 + fi + sleep 0.2 + done + + echo "rosbag recorder did not exit after SIGTERM, forcing stop..." + kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true + rm -f "${BAG_PID_FILE}" "${BAG_PGID_FILE}" + echo "rosbag recorder stopped" +} + start_driver() { mkdir -p "${STATE_DIR}" "$(dirname "${LOG_FILE}")" if is_running; then echo "linker_hand service is already running, pid=$(cat "${PID_FILE}")" + start_bag_recorder return 0 fi @@ -106,6 +224,7 @@ start_driver() { sleep 1 if is_running; then echo "linker_hand service started, pid=${pid}, pgid=$(cat "${PGID_FILE}")" + start_bag_recorder else echo "linker_hand service failed to start. Check log: ${LOG_FILE}" rm -f "${PID_FILE}" "${PGID_FILE}" @@ -117,6 +236,7 @@ start_driver() { stop_driver() { if ! [[ -f "${PID_FILE}" ]]; then echo "linker_hand service is not running" + stop_bag_recorder stop_can return 0 fi @@ -131,6 +251,7 @@ stop_driver() { if ! kill -0 "${pid}" 2>/dev/null; then echo "linker_hand process is not alive, cleaning state" rm -f "${PID_FILE}" "${PGID_FILE}" + stop_bag_recorder stop_can return 0 fi @@ -142,6 +263,7 @@ stop_driver() { if ! kill -0 "${pid}" 2>/dev/null; then rm -f "${PID_FILE}" "${PGID_FILE}" echo "linker_hand service stopped" + stop_bag_recorder stop_can return 0 fi @@ -152,6 +274,7 @@ stop_driver() { kill -KILL -- "-${pgid}" 2>/dev/null || kill -KILL "${pid}" 2>/dev/null || true rm -f "${PID_FILE}" "${PGID_FILE}" echo "linker_hand service stopped" + stop_bag_recorder stop_can } @@ -164,6 +287,14 @@ status_driver() { echo "ROS_AUTOMATIC_DISCOVERY_RANGE=${ROS_AUTOMATIC_DISCOVERY_RANGE}" + if is_bag_running; then + echo "rosbag recorder is running, pid=$(cat "${BAG_PID_FILE}"), pgid=$(cat "${BAG_PGID_FILE}" 2>/dev/null || cat "${BAG_PID_FILE}")" + else + echo "rosbag recorder is not running" + fi + + echo "rosbag topics: ${ROSBAG_TOPICS[*]}" + for can_if in "${CAN_INTERFACES[@]}"; do if ip link show "${can_if}" >/dev/null 2>&1; then ip -details link show "${can_if}" | sed -n '1,2p' -- 2.52.0 From 3ac6498bcb0902e01a2b3fb493b081c19e38c92a Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Sun, 20 Sep 2026 09:57:55 +0800 Subject: [PATCH 16/18] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=E5=85=B3?= =?UTF-8?q?=E4=BA=8E=E7=81=B5=E5=B7=A7=E6=89=8B=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 245 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 241 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a39cb43..bc57e13 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,16 @@ -# wpz_LbotArm 接口说明 +# weipz-Lbot_Bridge 接口说明 本工程当前使用 `src/officer_sdk/lbot_driver` 作为机器人网口连接和 ROS2 bridge。启动脚本为: ```bash -cd ~/workspace/wpz_LbotArm/scripts +cd ~/workspace/weipz-Lbot_Bridge/scripts ./lbot_arm_control.sh start ``` 停止服务: ```bash -cd ~/workspace/wpz_LbotArm/scripts +cd ~/workspace/weipz-Lbot_Bridge/scripts ./lbot_arm_control.sh stop ``` @@ -111,7 +111,7 @@ ros2 topic echo /rosout 也可以查看启动脚本日志: ```bash -tail -f ~/workspace/wpz_LbotArm/log/lbot_arm_control.log +tail -f ~/workspace/weipz-Lbot_Bridge/log/lbot_arm_control.log ``` 如果后续需要稳定给外部软件使用的完整健康状态快照,建议在 `SystemError.msg` 之外再新增 `RobotHealth.msg`,并在连接/重连逻辑、使能/急停服务回调中维护并周期发布该状态。需要修改的位置: @@ -247,6 +247,235 @@ ros2 topic pub --once /robot1/right_hand/set_l20_joint std_msgs/msg/Int32MultiAr "{data: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}" ``` +## Linker Hand L20 独立 CAN 控制 + +当前工程也集成了 `src/linker_hand_ros2_sdk`,用于在灵巧手单独接入域控 CAN 口时控制左右 L20 灵巧手,并获取关节状态和矩阵触觉合力反馈。 + +启动服务: + +```bash +cd ~/workspace/weipz-Lbot_Bridge +./scripts/linker_hand_control.sh start +``` + +停止服务: + +```bash +cd ~/workspace/weipz-Lbot_Bridge +./scripts/linker_hand_control.sh stop +``` + +查看状态: + +```bash +./scripts/linker_hand_control.sh status +``` + +脚本当前会启动 `can0` 和 `can1`,波特率为 `1000000`。默认 launch 配置为: + +```text +左手:can1,hand_type = left,hand_joint = L20 +右手:can0,hand_type = right,hand_joint = L20 +``` + +脚本会强制设置: + +```text +ROS_AUTOMATIC_DISCOVERY_RANGE=LOCALHOST +``` + +这样当前机器只发现本机 ROS 2 节点,避免同一局域网内旧的 `/linker_hand_sdk` 节点污染话题。 + +### 灵巧手控制话题 + +左手整手位置控制: + +```text +/l20_left_hand_control_cmd +``` + +右手整手位置控制: + +```text +/l20_right_hand_control_cmd +``` + +消息类型: + +```text +sensor_msgs/msg/JointState +``` + +字段含义: + +```text +position[] L20 目标关节位置数组,当前按 20 个 uint8 位置值使用,范围 0~255 +velocity[] 可选速度数组;不需要设置速度时可以不填 +effort[] 当前控制链路不使用 +``` + +左手发布示例: + +```bash +ros2 topic pub --once /l20_left_hand_control_cmd sensor_msgs/msg/JointState \ +"{position: [205, 206, 230, 220, 226, 159, 127, 127, 32, 101, 80, 0, 0, 0, 0, 226, 254, 254, 248, 253]}" +``` + +右手发布示例: + +```bash +ros2 topic pub --once /l20_right_hand_control_cmd sensor_msgs/msg/JointState \ +"{position: [252, 147, 150, 136, 96, 255, 254, 202, 88, 0, 198, 0, 0, 0, 0, 254, 244, 241, 235, 211]}" +``` + +如果需要同时设置速度,可以填入 `velocity[]`: + +```bash +ros2 topic pub --once /l20_right_hand_control_cmd sensor_msgs/msg/JointState \ +"{position: [252, 147, 150, 136, 96, 255, 254, 202, 88, 0, 198, 0, 0, 0, 0, 254, 244, 241, 235, 211], velocity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}" +``` + +注意:这里的 `position[]` 不是弧度,是 Linker Hand CAN 协议中的 0~255 位置值。 + +### 灵巧手状态反馈 + +左手状态: + +```text +/l20_left_hand_state +``` + +右手状态: + +```text +/l20_right_hand_state +``` + +消息类型: + +```text +sensor_msgs/msg/JointState +``` + +字段含义: + +```text +name[] 关节名称 +position[] 当前 L20 关节位置反馈,0~255 +velocity[] 当前 L20 速度/速度阈值反馈,按 SDK 原始值发布 +effort[] 当前未填有效力矩,通常为 0 +``` + +订阅示例: + +```bash +ros2 topic echo /l20_left_hand_state +ros2 topic echo /l20_right_hand_state +``` + +### 灵巧手矩阵触觉合力反馈 + +左手触觉合力: + +```text +/l20_left_hand_matrix_touch_mass +``` + +右手触觉合力: + +```text +/l20_right_hand_matrix_touch_mass +``` + +消息类型: + +```text +std_msgs/msg/Float32MultiArray +``` + +字段含义: + +```text +data[0] thumb_mass,拇指矩阵触觉合力 +data[1] index_mass,食指矩阵触觉合力 +data[2] middle_mass,中指矩阵触觉合力 +data[3] ring_mass,无名指矩阵触觉合力 +data[4] little_mass,小指矩阵触觉合力 +``` + +订阅示例: + +```bash +ros2 topic echo /l20_left_hand_matrix_touch_mass +ros2 topic echo /l20_right_hand_matrix_touch_mass +``` + +完整矩阵触觉和点云话题也会发布: + +```text +/l20_left_hand_matrix_touch +/l20_left_hand_matrix_touch_pc +/l20_right_hand_matrix_touch +/l20_right_hand_matrix_touch_pc +``` + +其中 `matrix_touch` 当前仍是 JSON 字符串,`matrix_touch_pc` 为 `sensor_msgs/msg/PointCloud2`。 + +### 灵巧手配置指令 + +配置指令话题: + +```text +/l20_hand_setting_cmd +``` + +消息类型: + +```text +std_msgs/msg/String +``` + +该话题用于向 SDK 发送设置类 JSON 指令,例如清故障、读取故障、设置电流等。一般控制整手位置不需要使用该话题。 + +### 灵巧手 rosbag 录制 + +`linker_hand_control.sh start` 会同时启动 rosbag 录制,`stop` 会停止录制。每满 1 小时自动切一个新包。 + +录制文件命名: + +```text +/tmp/L20_Hand_YYYYmmdd_HHMMSS.mcap +``` + +录制话题: + +```text +/l20_left_hand_control_cmd +/l20_left_hand_state +/l20_left_hand_matrix_touch_mass +/l20_right_hand_control_cmd +/l20_right_hand_state +/l20_right_hand_matrix_touch_mass +``` + +录制日志: + +```bash +tail -f ~/workspace/weipz-Lbot_Bridge/log/linker_hand_rosbag.log +``` + +查看录包进程状态: + +```bash +./scripts/linker_hand_control.sh status +``` + +回放示例: + +```bash +ros2 bag play /tmp/L20_Hand_YYYYmmdd_HHMMSS.mcap +``` + ## 相关代码位置 反馈发布: @@ -286,3 +515,11 @@ ROS 接口定义: ```text src/officer_sdk/lbot_arm_interfaces/srv/MoveJ.srv ``` + +Linker Hand L20 独立 CAN 控制: + +```text +src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py +src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py +scripts/linker_hand_control.sh +``` -- 2.52.0 From 14fa685f7f1a71cfd70986dd89b3f82cbbc98c6a Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Sun, 20 Sep 2026 10:10:17 +0800 Subject: [PATCH 17/18] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=BA=86=E5=85=B3?= =?UTF-8?q?=E4=BA=8E=E7=81=B5=E5=B7=A7=E6=89=8Bcan=E5=8F=A3=E6=9B=B4?= =?UTF-8?q?=E6=8D=A2=E7=9A=84=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index bc57e13..a930af5 100644 --- a/README.md +++ b/README.md @@ -523,3 +523,37 @@ src/linker_hand_ros2_sdk/linker_hand_ros2_sdk/linker_hand.py src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py scripts/linker_hand_control.sh ``` + +## 左右灵巧手 CAN 口对调 + +如果后续需要再次颠倒左右灵巧手的 CAN 口,只需要修改: + +```text +src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py +``` + +当前默认配置为: + +```python +左手:'hand_type': 'left', 'can': 'can1' +右手:'hand_type': 'right', 'can': 'can0' +``` + +如果要左右 CAN 口对调,改成: + +```python +左手:'hand_type': 'left', 'can': 'can0' +右手:'hand_type': 'right', 'can': 'can1' +``` + +修改后重新编译并重启服务: + +```bash +cd ~/workspace/weipz-Lbot_Bridge +colcon build --packages-select linker_hand_ros2_sdk +source install/setup.bash +./scripts/linker_hand_control.sh stop +./scripts/linker_hand_control.sh start +``` + +注意:只改 `can` 字段,不要交换 `hand_type`。`hand_type` 决定 ROS 话题名和左右手设备 ID,交换它会导致 `/l20_left_*` 和 `/l20_right_*` 语义反掉。 -- 2.52.0 From 6427597624d1e9f423b611e5e6df30edd86b6129 Mon Sep 17 00:00:00 2001 From: weipengzhe <3389096668@qq.com> Date: Sun, 20 Sep 2026 13:39:08 +0800 Subject: [PATCH 18/18] =?UTF-8?q?=E5=8A=A0=E5=85=A5=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../launch/linker_hand_double.launch.py | 4 +- test_tools/README.md | 37 +++ test_tools/l20_long_run_test.py | 266 ++++++++++++++++++ 3 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 test_tools/README.md create mode 100755 test_tools/l20_long_run_test.py diff --git a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py index 4f50f53..f5c283a 100644 --- a/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py +++ b/src/linker_hand_ros2_sdk/launch/linker_hand_double.launch.py @@ -14,7 +14,7 @@ def generate_launch_description(): 'hand_type': 'left', # 配置Linker Hand灵巧手类型 left | right 字母为小写 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False - 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* }], ), @@ -28,7 +28,7 @@ def generate_launch_description(): 'hand_type': 'right', # 配置Linker Hand灵巧手类型 left | right 字母为小写 'hand_joint': "L20", # O6\L6P\L6\L7\L10\L20\G20(工业版)\L21 字母为大写 'is_touch': True, # 配置Linker Hand灵巧手是否有压力传感器 True | False - 'can': 'can0', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 + 'can': 'can1', # 这里需要修改为实际的CAN总线名称 如果是win系统则类似于 PCAN_USBBUS1 "modbus": "None" # "None" | "/dev/ttyUSB0" 这里需要修改为实际的Modbus总线名称 如果是win系统则 COM* Ubuntu则为/dev/ttyUSB* }], ), diff --git a/test_tools/README.md b/test_tools/README.md new file mode 100644 index 0000000..5a5301d --- /dev/null +++ b/test_tools/README.md @@ -0,0 +1,37 @@ +# L20 Dual Arm Long-Run Test + +该目录下的 `l20_long_run_test.py` 是独立长测工具,只负责发布/调用动作接口,不负责启动机器人、机械臂 driver、灵巧手 driver、CAN 或 rosbag。 + +先启动机器人相关服务后,再运行: + +```bash +cd ~/workspace/weipz-Lbot_Bridge +source install/setup.bash +python3 test_tools/l20_long_run_test.py +``` + +有限循环示例,执行 10 轮动作 1/动作 2: + +```bash +python3 test_tools/l20_long_run_test.py --cycles 10 +``` + +常用参数: + +```text +--cycles 动作 1/动作 2 的循环轮数,0 表示无限循环 +--arm-speed MoveJ 速度标量,默认 1.0 +--arm-acce MoveJ 加速度标量,默认 1.0 +--arm-tolerance 机械臂关节到位容差,单位 rad,默认 0.03 +--action-timeout 单个动作到位超时时间,默认 30 s +--hand-publish-count 灵巧手开环命令每个动作重复发布次数,默认 5 +--hand-publish-rate 灵巧手开环命令发布频率,默认 20 Hz +``` + +判定逻辑: + +```text +灵巧手:开环发布 /l20_left_hand_control_cmd 和 /l20_right_hand_control_cmd,不做反馈校验 +机械臂:调用 /robot1/left_arm/move_joint 和 /robot1/right_arm/move_joint,再订阅 joint_states 判断是否到位 +错误:收到 /robot1/system_error、MoveJ 返回失败、服务超时、joint_states 超时或机械臂一直不到位时打印 ERROR 并退出 +``` diff --git a/test_tools/l20_long_run_test.py b/test_tools/l20_long_run_test.py new file mode 100755 index 0000000..1c0d7d2 --- /dev/null +++ b/test_tools/l20_long_run_test.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python3 +"""Long-run robustness test for LBot dual arms and L20 hands. + +This tool only publishes/calls existing ROS interfaces. It does not start or +stop robot drivers, CAN devices, or rosbag recorders. +""" + +import argparse +import math +import signal +import sys +import time +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +import rclpy +from lbot_arm_interfaces.msg import SystemError +from lbot_arm_interfaces.srv import MoveJ +from rclpy.node import Node +from sensor_msgs.msg import JointState + + +LEFT_HAND_ACTION_1 = [252, 253, 239, 237, 234, 255, 245, 190, 112, 40, 228, 0, 0, 0, 0, 254, 247, 252, 253, 252] +LEFT_HAND_ACTION_2 = [252, 194, 203, 193, 194, 254, 195, 165, 118, 116, 246, 0, 0, 0, 0, 249, 254, 254, 254, 254] + +RIGHT_HAND_ACTION_1 = [252, 194, 203, 193, 194, 254, 195, 165, 118, 116, 246, 0, 0, 0, 0, 249, 254, 254, 254, 254] +RIGHT_HAND_ACTION_2 = [252, 1, 1, 0, 0, 65, 133, 133, 127, 129, 137, 0, 0, 0, 0, 249, 197, 195, 211, 215] + +LEFT_ARM_ACTION_1 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +LEFT_ARM_ACTION_2 = [0.5, -0.5, 0.5, 0.5, 0.5, 0.5, 0.5] + +RIGHT_ARM_ACTION_1 = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] +RIGHT_ARM_ACTION_2 = [-0.5, 0.5, -0.5, -0.5, -0.5, -0.5, -0.5] + + +@dataclass(frozen=True) +class TestAction: + name: str + left_hand: List[int] + right_hand: List[int] + left_arm: List[float] + right_arm: List[float] + + +ACTIONS = [ + TestAction("action_1", LEFT_HAND_ACTION_1, RIGHT_HAND_ACTION_1, LEFT_ARM_ACTION_1, RIGHT_ARM_ACTION_1), + TestAction("action_2", LEFT_HAND_ACTION_2, RIGHT_HAND_ACTION_2, LEFT_ARM_ACTION_2, RIGHT_ARM_ACTION_2), +] + + +class L20LongRunTester(Node): + def __init__(self, args: argparse.Namespace): + super().__init__("l20_long_run_test") + self.args = args + self.shutdown_requested = False + self.last_system_error: Optional[SystemError] = None + self.arm_states: Dict[str, Tuple[List[float], float]] = {} + + self.left_hand_pub = self.create_publisher(JointState, "/l20_left_hand_control_cmd", 10) + self.right_hand_pub = self.create_publisher(JointState, "/l20_right_hand_control_cmd", 10) + + self.left_move_client = self.create_client(MoveJ, "/robot1/left_arm/move_joint") + self.right_move_client = self.create_client(MoveJ, "/robot1/right_arm/move_joint") + + self.create_subscription(JointState, "/robot1/left_arm/joint_states", self._left_arm_state_cb, 10) + self.create_subscription(JointState, "/robot1/right_arm/joint_states", self._right_arm_state_cb, 10) + self.create_subscription(SystemError, "/robot1/system_error", self._system_error_cb, 10) + + def request_shutdown(self) -> None: + self.shutdown_requested = True + + def _left_arm_state_cb(self, msg: JointState) -> None: + self.arm_states["left"] = (list(msg.position), time.monotonic()) + + def _right_arm_state_cb(self, msg: JointState) -> None: + self.arm_states["right"] = (list(msg.position), time.monotonic()) + + def _system_error_cb(self, msg: SystemError) -> None: + if msg.error_code != 0 or not msg.connected: + self.last_system_error = msg + + def wait_until_ready(self) -> bool: + self._info("waiting for MoveJ services...") + if not self.left_move_client.wait_for_service(timeout_sec=self.args.service_timeout): + self._error("left MoveJ service is not available") + return False + if not self.right_move_client.wait_for_service(timeout_sec=self.args.service_timeout): + self._error("right MoveJ service is not available") + return False + + self._info("waiting for arm joint state feedback...") + deadline = time.monotonic() + self.args.state_timeout + while time.monotonic() < deadline and not self.shutdown_requested: + rclpy.spin_once(self, timeout_sec=0.05) + if "left" in self.arm_states and "right" in self.arm_states: + self._info("joint state feedback is ready") + return True + self._error("joint state feedback timeout") + return False + + def execute_action(self, action: TestAction, cycle_index: int) -> bool: + self.last_system_error = None + self._info(f"cycle={cycle_index} start {action.name}") + + self.publish_hands(action.left_hand, action.right_hand) + + if not self.call_movej("left", self.left_move_client, action.left_arm): + return False + if not self.call_movej("right", self.right_move_client, action.right_arm): + return False + + reached = self.wait_arms_reached(action.left_arm, action.right_arm) + if not reached: + return False + + self._info(f"cycle={cycle_index} reached {action.name}") + return True + + def publish_hands(self, left_position: List[int], right_position: List[int]) -> None: + left_msg = JointState() + left_msg.position = [float(value) for value in left_position] + right_msg = JointState() + right_msg.position = [float(value) for value in right_position] + + for _ in range(self.args.hand_publish_count): + now_msg = self.get_clock().now().to_msg() + left_msg.header.stamp = now_msg + right_msg.header.stamp = now_msg + self.left_hand_pub.publish(left_msg) + self.right_hand_pub.publish(right_msg) + rclpy.spin_once(self, timeout_sec=0.0) + time.sleep(1.0 / self.args.hand_publish_rate) + + self._info("hand commands published open-loop") + + def call_movej(self, side: str, client, joints: List[float]) -> bool: + req = MoveJ.Request() + req.joints = [float(value) for value in joints] + req.speed = float(self.args.arm_speed) + req.acce = float(self.args.arm_acce) + req.block = False + + self._info(f"send {side} arm MoveJ joints={self._fmt(joints)} speed={req.speed:.3f} acce={req.acce:.3f}") + future = client.call_async(req) + deadline = time.monotonic() + self.args.service_timeout + while time.monotonic() < deadline and not self.shutdown_requested: + rclpy.spin_once(self, timeout_sec=0.05) + if future.done(): + try: + response = future.result() + except Exception as exc: # noqa: BLE001 - log ROS service failure directly. + self._error(f"{side} arm MoveJ service exception: {exc}") + return False + if not response.success: + self._error(f"{side} arm MoveJ returned success=false") + return False + return True + self._error(f"{side} arm MoveJ service response timeout") + return False + + def wait_arms_reached(self, left_target: List[float], right_target: List[float]) -> bool: + deadline = time.monotonic() + self.args.action_timeout + next_log_time = 0.0 + + while time.monotonic() < deadline and not self.shutdown_requested: + rclpy.spin_once(self, timeout_sec=0.05) + + if self.last_system_error is not None: + err = self.last_system_error + self._error( + f"system error received: code={err.error_code}, connected={err.connected}, msg={err.error_msg}" + ) + return False + + left_ok, left_err = self._arm_reached("left", left_target) + right_ok, right_err = self._arm_reached("right", right_target) + if left_ok and right_ok: + return True + + now = time.monotonic() + if now >= next_log_time: + self._info(f"waiting reach: left_max_err={left_err:.4f}, right_max_err={right_err:.4f}") + next_log_time = now + self.args.progress_period + + left_ok, left_err = self._arm_reached("left", left_target) + right_ok, right_err = self._arm_reached("right", right_target) + self._error( + f"arm reach timeout: left_ok={left_ok}, left_max_err={left_err:.4f}, " + f"right_ok={right_ok}, right_max_err={right_err:.4f}" + ) + return False + + def _arm_reached(self, side: str, target: List[float]) -> Tuple[bool, float]: + state = self.arm_states.get(side) + if state is None: + return False, math.inf + positions, stamp = state + if time.monotonic() - stamp > self.args.state_stale_timeout: + return False, math.inf + if len(positions) < len(target): + return False, math.inf + max_error = max(abs(float(current) - float(goal)) for current, goal in zip(positions[: len(target)], target)) + return max_error <= self.args.arm_tolerance, max_error + + @staticmethod + def _fmt(values: List[float]) -> str: + return "[" + ", ".join(f"{value:.3f}" for value in values) + "]" + + def _info(self, msg: str) -> None: + print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] INFO {msg}", flush=True) + + def _error(self, msg: str) -> None: + print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] ERROR {msg}", flush=True) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Long-run alternating action test for dual arms and L20 hands.") + parser.add_argument("--cycles", type=int, default=0, help="number of action pairs to run; 0 means infinite") + parser.add_argument("--arm-speed", type=float, default=1.0, help="MoveJ speed scalar") + parser.add_argument("--arm-acce", type=float, default=1.0, help="MoveJ acceleration scalar") + parser.add_argument("--arm-tolerance", type=float, default=0.03, help="arm joint reach tolerance in rad") + parser.add_argument("--action-timeout", type=float, default=30.0, help="seconds to wait for both arms to reach one action") + parser.add_argument("--service-timeout", type=float, default=5.0, help="seconds to wait for services and service responses") + parser.add_argument("--state-timeout", type=float, default=5.0, help="seconds to wait for initial joint state feedback") + parser.add_argument("--state-stale-timeout", type=float, default=1.0, help="joint state older than this is treated as stale") + parser.add_argument("--progress-period", type=float, default=1.0, help="seconds between progress logs while waiting") + parser.add_argument("--hand-publish-count", type=int, default=5, help="open-loop hand command publish repeats per action") + parser.add_argument("--hand-publish-rate", type=float, default=20.0, help="open-loop hand command publish rate in Hz") + return parser.parse_args() + + +def main() -> int: + args = parse_args() + rclpy.init() + node = L20LongRunTester(args) + + def _signal_handler(_signum, _frame): + node.request_shutdown() + + signal.signal(signal.SIGINT, _signal_handler) + signal.signal(signal.SIGTERM, _signal_handler) + + try: + if not node.wait_until_ready(): + return 1 + + cycle = 1 + while rclpy.ok() and not node.shutdown_requested: + for action in ACTIONS: + if node.shutdown_requested: + break + if not node.execute_action(action, cycle): + return 2 + cycle += 1 + if args.cycles > 0 and cycle > args.cycles: + node._info("requested cycles completed") + return 0 + finally: + node.destroy_node() + rclpy.shutdown() + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- 2.52.0