commit fae881649b0620a066bea6a7615af1e8f6ac2341
Author: lxp <2770281812@qq.com>
Date: Tue Jun 30 10:42:11 2026 +0800
init
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..f14db3d
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,419 @@
+# 1. LinkerFFG Glove
+
+## 1.1 Product Introduction
+For specific product introduction, please refer to the calibration example description contained within
+Attachment 1: Linker FFG (FFG01) Product Instruction Manual
+
+## 1.2 Software Usage
+This product provides ROS1 and ROS2 SDK packages under Ubuntu, working together with Linkerhand's ROS1 SDK and ROS2 SDK to achieve teleoperation actions
+For specific usage, please jump to Chapter 4: Teleoperation Retargeting System
+
+## 1.3 Communication Protocol
+For specific product introduction, please refer to Attachment 2: Linker FFG (FFG01) Communication Protocol
+
+---
+
+# 2. Teleoperation SDK System
+
+## 2.1 System Introduction
+This system's retargeting program is developed based on the ROS1/ROS2 platform, consisting of one node, four publisher topics, and two subscription topics
+The Linker FFG glove pushes data to the retargeting program via serial port protocol
+
+### 2.1.1 Download Address
+https://github.com/linker-bot/linkerhand-ros-teleo
+Please contact customer service to obtain the latest SDK
+
+### 2.1.2 Installing the Teleoperation Retargeting Program
+The teleoperation retargeting program is provided in src form, therefore requiring users to compile it themselves for use on different platforms. The teleoperation retargeting program provides a ROS2 version; the ROS1 version is still under development
+
+Copy src to the workspace, then compile
+```bash
+colcon build --symlink-install
+```
+
+There may be permission issues during the process preventing execution; use the following command
+```bash
+chmod 777 -R *
+```
+
+### 2.1.3 Program Configuration
+Enter the ls command in this directory
+```bash
+cd src/linkerhand_retarget/linkerhand_retarget/config
+ls -1
+```
+After entering the above command, you should normally enter the config directory. After `ls -1`, you should see the following list
+```
+base_config.yml
+body_custom_pose.yml
+body_unity_pose.yml
+hand_config.yml
+human_hand_info.yml
+linker_hand_info.yml
+model_config.yml
+retarget_config.yml
+speed_config.yml
+```
+
+The only configuration file we need to modify is `base_config.yml`; other configuration files must not be modified.
+
+The following is an introduction to the configuration content in `base_config.yml`:
+
+**1. Configure Robot Hand Type**
+```yaml
+robotname_r: l10
+robotname_l: l10
+```
+- LinkerHand_L10 series hands should be configured as: `l10`
+- LinkerHand_L20 series hands should be configured as: `l20`
+- LinkerHand_O7 series hands should be configured as: `o7`
+- LinkerHand_T25 series hands should be configured as: `t25`
+- LinkerHand_L25 series hands should be configured as: `l25`
+- LinkerHand_L6 series hands should be configured as: `l6`
+- LinkerHand_O6 series hands should be configured as: `o6`
+
+**Note:** All the above must be in lowercase; uppercase will not be correctly recognized.
+
+**2. Configure Data Printing**
+```yaml
+debug:
+ joint_pub_debug: false
+ joint_motor_debug_r: true
+ joint_motor_debug_l: false
+```
+- `joint_pub_debug`: Set to true to print the content after Topic Pub, refresh rate is slow, can display left and right hands simultaneously
+- `joint_motor_debug_r`: Set to true to quickly refresh the output data of the right hand, range 255-0, representing the change from open to closed
+- `joint_motor_debug_l`: Set to true to quickly refresh the output data of the left hand, range 255-0, representing the change from open to closed
+
+**3. Configure Data Source**
+```yaml
+motion_type: linkerforce
+```
+- `motion_type`: When using the LinkerFFG glove, set to `linkerforce`, `motion_device` does not need to be set
+
+### 2.1.4 Program Launch (ROS2 Version)
+Next, execute the following command to confirm the USB device list on your device. Then insert the 2 force-feedback gloves, execute the command again, and confirm the refreshed USB device numbers. Take `/dev/ttyUSB0` and `/dev/ttyUSB1` as examples:
+```bash
+ls /dev/ttyUSB*
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+```
+
+Then execute
+```bash
+# There are two launch modes,
+# One with calibration. Must be executed for the first time without calibration
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration := True
+# One normal launch, calibration not executed by default
+ros2 run linkerhand_retarget handretarget
+```
+
+**Notes:**
+For customer convenience, a startup script is provided in the same directory as `src`, named `startup_linkerforce.sh`
+
+```bash
+#!/bin/bash
+# LinkerForce Glove Teleoperation Startup Script
+
+# Set serial port permissions
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+
+# source ROS workspace (please enter the correct directory)
+source ~/project/linkerhand_telop_ws/ros2/v2.8.7/install/setup.bash
+
+# Launch program (forced calibration)
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=True
+
+# Launch without calibration
+# ros2 run linkerhand_retarget handretarget
+```
+
+After editing, launch it like this
+```bash
+./startup_linkerforce.sh
+# If you encounter Tab not recognized, grant permissions
+sudo chmod a+x ./startup_linkerforce.sh
+```
+
+### 2.1.5 Program Launch (ROS1 Version)
+Next, execute the following command to confirm the USB device list on your device. Then insert the 2 force-feedback gloves, execute the command again, and confirm the refreshed USB device numbers. Take `/dev/ttyUSB0` and `/dev/ttyUSB1` as examples:
+```bash
+ls /dev/ttyUSB*
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+```
+
+Then execute
+```bash
+# There are two launch modes,
+# One with calibration. Must be executed for the first time without calibration
+rosrun ros_linkerhand_retarget handretarget.py _calibrate:=true
+# One normal launch, calibration not executed by default
+rosrun ros_linkerhand_retarget handretarget.py
+```
+
+**Notes:**
+For customer convenience, a startup script is provided in the same directory as `src`, named `startup_linkerforce.sh`
+
+```bash
+#!/bin/bash
+# LinkerForce Glove Teleoperation Startup Script
+
+# Set serial port permissions
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+
+# source ROS workspace (please enter the correct directory)
+source ~/linkerhand_telop/ros1/v2.8.6/install/setup.bash
+
+# Launch program (forced calibration)
+rosrun ros_linkerhand_retarget handretarget.py _calibrate:=true
+
+# Launch without calibration
+# rosrun ros_linkerhand_retarget handretarget.py
+```
+
+After editing, launch it like this
+```bash
+./startup_linkerforce.sh
+# If you encounter Tab not recognized, grant permissions
+sudo chmod a+x ./startup_linkerforce.sh
+```
+
+## 2.2 Topic Description
+The working principle of the SDK is to clean and organize the data transmitted from the upper computer, then distribute the data uniformly via Topics. Therefore, Topics are the data transfer stations and an important basis for judging whether the teleoperation is working properly.
+
+- ROS1 command: `rostopic list`
+- ROS2 command: `ros2 topic list`
+
+**1. ROS2 command:**
+```bash
+ros2 topic echo /cb_left_hand_control_cmd
+# or
+ros2 topic echo /cb_right_hand_control_cmd
+```
+
+### Data Description
+
+#### L25 Robotic Hand (21 DOF)
+| Joint | Finger | Action | Default Value |
+|-------|--------|--------|---------------|
+| joint1 | Thumb | Proximal Flexion | 255 |
+| joint2 | Index | Proximal Flexion | 255 |
+| joint3 | Middle | Proximal Flexion | 255 |
+| joint4 | Ring | Proximal Flexion | 255 |
+| joint5 | Pinky | Proximal Flexion | 255 |
+| joint6 | Thumb | Abduction | 255 |
+| joint7 | Index | Abduction | 128 |
+| joint8 | Middle | Abduction | 128 |
+| joint9 | Ring | Abduction | 128 |
+| joint10 | Pinky | Abduction | 128 |
+| joint11 | Thumb | Rotation | 255 |
+| joint12 | Reserved | - | - |
+| joint13 | Reserved | - | - |
+| joint14 | Reserved | - | - |
+| joint15 | Reserved | - | - |
+| joint16 | Thumb | Distal Flexion | 255 |
+| joint17 | Index | Distal Flexion | 255 |
+| joint18 | Middle | Distal Flexion | 255 |
+| joint19 | Ring | Distal Flexion | 255 |
+| joint20 | Pinky | Distal Flexion | 255 |
+| joint21 | Thumb | Tip Flexion | 255 |
+| joint22 | Index | Tip Flexion | 255 |
+| joint23 | Middle | Tip Flexion | 255 |
+| joint24 | Ring | Tip Flexion | 255 |
+| joint25 | Pinky | Tip Flexion | 255 |
+
+#### L20 Robotic Hand (16 DOF)
+| Joint | Finger | Action | Default Value |
+|-------|--------|--------|---------------|
+| joint1 | Thumb | Proximal Flexion | 255 |
+| joint2 | Index | Proximal Flexion | 255 |
+| joint3 | Middle | Proximal Flexion | 255 |
+| joint4 | Ring | Proximal Flexion | 255 |
+| joint5 | Pinky | Proximal Flexion | 255 |
+| joint6 | Thumb | Abduction | 255 |
+| joint7 | Index | Abduction | 128 |
+| joint8 | Middle | Abduction | 128 |
+| joint9 | Ring | Abduction | 128 |
+| joint10 | Pinky | Abduction | 128 |
+| joint11 | Thumb | Rotation | 255 |
+| joint12 | Reserved | - | - |
+| joint13 | Reserved | - | - |
+| joint14 | Reserved | - | - |
+| joint15 | Reserved | - | - |
+| joint16 | Thumb | Tip Flexion | 255 |
+| joint17 | Index | Tip Flexion | 255 |
+| joint18 | Middle | Tip Flexion | 255 |
+| joint19 | Ring | Tip Flexion | 255 |
+| joint20 | Pinky | Tip Flexion | 255 |
+
+#### L10 Robotic Hand (10 DOF)
+| Joint | Finger | Action | Default Value |
+|-------|--------|--------|---------------|
+| joint1 | Thumb | Proximal Flexion | 255 |
+| joint2 | Thumb | Abduction | 128 |
+| joint3 | Index | Proximal Flexion | 255 |
+| joint4 | Middle | Proximal Flexion | 255 |
+| joint5 | Ring | Proximal Flexion | 255 |
+| joint6 | Pinky | Proximal Flexion | 255 |
+| joint7 | Index | Abduction | 128 |
+| joint8 | Ring | Abduction | 128 |
+| joint9 | Pinky | Abduction | 128 |
+| joint10 | Thumb | Rotation | 255 |
+
+#### O7 Robotic Hand (7 DOF)
+| Joint | Finger | Action | Default Value |
+|-------|--------|--------|---------------|
+| joint1 | Thumb | Proximal Flexion | 255 |
+| joint2 | Thumb | Abduction | 128 |
+| joint3 | Index | Proximal Flexion | 255 |
+| joint4 | Middle | Proximal Flexion | 255 |
+| joint5 | Ring | Proximal Flexion | 255 |
+| joint6 | Pinky | Proximal Flexion | 255 |
+| joint7 | Thumb | Rotation | 255 |
+
+#### O6/L6 Robotic Hand (7 DOF)
+| Joint | Finger | Action | Default Value |
+|-------|--------|--------|---------------|
+| joint1 | Thumb | Proximal Flexion | 255 |
+| joint2 | Thumb | Abduction | 128 |
+| joint3 | Index | Proximal Flexion | 255 |
+| joint4 | Middle | Proximal Flexion | 255 |
+| joint5 | Ring | Proximal Flexion | 255 |
+| joint6 | Pinky | Proximal Flexion | 255 |
+
+## 2.3 Troubleshooting Self-Check
+
+### 2.3.1 Network Issues
+The teleoperation and control devices are connected via the network. Network abnormalities can cause devices to malfunction. First, check if the network cable connections are correct, ensure that both IPs can ping each other, and confirm that the corresponding ports are open on the firewall.
+
+### 2.3.2 Topic Anomalies
+Under normal network conditions, you can confirm whether topics exist on other network devices using commands.
+Refer to section 2.2 Topic Description for details.
+
+## 2.4 Mapping Parameter Settings
+
+### 2.4.1 Principle
+The mapping system has 3 modes
+
+| Mapping Mode | English Equivalent | Chinese Equivalent | Applicable Scenario |
+|--------------|--------------------|--------------------|---------------------|
+| Three-State Mapping | origin-opose-fist | Open Hand-O Pose-Fist | Aims to balance the maximum range of the robotic hand while satisfying pinch gestures. Suitable for most mapping requirements. |
+| Dual-End Mapping | origin-fist | Open Hand-Fist | Maximizes the full range of the robotic hand to the greatest extent. |
+| Pinch Mapping | origin-opose- | Open Hand-O Pose | Targets pinch gestures primarily, aiming to improve granularity for grasping actions. |
+
+### 2.4.2 Configuration Files
+This mapping system configures parameter binding for the robotic hand. The mapping configuration directory is located at:
+```
+~/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config
+```
+
+Currently provided mapping parameter packages are as follows:
+```
+g20_config.py
+l10_config.py
+l20_config.py
+l6_config.py
+o6_config.py
+o7_config.py
+```
+
+### 2.4.3 Configuration Introduction
+The following uses `l6_config.py` as an example.
+
+#### 2.4.3.1 Finger Configuration Constant `FINGER_CONFIGS`
+
+##### 2.4.3.1.1 Common Constant Table
+(Content omitted)
+
+##### 2.4.3.1.2 `reverse_motion` Attribute
+Reverse signal: Default is `False`
+
+##### 2.4.3.1.3 `dynamic_weight` Attribute
+Dynamic weight: Used for detailed expression within intervals. Default is `None`, not enabled in this version.
+
+##### 2.4.3.1.4 `extended_mapping` Attribute
+Extended mapping configuration
+```python
+'extended_mapping': { # New extended mapping configuration
+ 'enabled': True,
+ 'scale_factor': 1.2, # Scaling factor, default 1.0
+ 'extended_exp_factor': 80 # New: Extension index parameter
+}
+```
+- `enabled`: Enabled only for pinch mapping; `False` means disabled.
+- `scale_factor`: Scaling factor, used to quickly lock onto the O-Pose position.
+- `extended_exp_factor`: Extension parameter, used for continuation actions after the O-Pose. A smaller value makes it harder to reach the mechanical limits of the robotic hand, allowing for more detailed expression of grasping. A larger value makes it easier to quickly reach the drive joint limit positions.
+
+#### 2.4.3.2 Mapping Order `MAPPING_ORDER`
+Taking L6 as an example, indicates the priority order of processed joints
+```python
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_flexion',
+ 'index', 'middle', 'ring', 'pinky'
+]
+```
+
+#### 2.4.3.3 State Configuration `STATE_CONFIG` (Not Enabled)
+
+#### 2.4.3.4 Robotic Hand OPOSE Calibration Position: `ROBOT_OPOSE_LEFT`
+Taking the L6 configuration as an example, the following angles correspond to the OPOSE position of the left robotic hand. Note that the following order corresponds to the URDF sequence.
+```python
+ROBOT_OPOSE_LEFT = [
+ 1.4, 0.54, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0
+]
+```
+
+#### 2.4.3.5 Robotic Hand OPOSE Calibration Position: `ROBOT_OPOSE_RIGHT`
+Taking the L6 configuration as an example, the following angles correspond to the OPOSE position of the right robotic hand. Note that the following order corresponds to the URDF sequence.
+```python
+ROBOT_OPOSE_RIGHT = [
+ 1.4, 0.54, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0
+]
+```
+
+### 2.4.4 Robotic Hand Calibration
+Sections 2.4.3.4 and 2.4.3.5 introduced the OPOSE calibration positions for the left and right robotic hands respectively. These are used to handle discrepancies between the current robotic hand position and the URDF. When using for the first time, modifications need to be made in the following code.
+
+Path: `./src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand`
+
+Taking the L6 robotic hand as an example, after opening `linkerforce_l6.py`, around line 149:
+```python
+if self.calibrationoriginal is not None
+and self.calibrationfistpose is not None
+and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ # arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc) # Comment out
+ arc_value = ROBOT_OPOSE_RIGHT # Enable calibration
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+```
+
+Rewrite it as shown in the example above to enable calibration for the right robotic hand. After launching, the robotic hand will be fixed at the current angle according to the mapped angle.
+
+The corresponding section for the left hand is around line 360:
+```python
+if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ # arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc) # Comment out
+ # Enable calibration
+ arc_value = ROBOT_OPOSE_LEFT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+```
+
+Rewrite it as shown in the example above to enable calibration for the right robotic hand. After launching, the robotic hand will be fixed at the current angle according to the mapped angle.
+
+Once both left and right hands have reached the desired pinch position, you can restore the original state and use the teleoperation system normally.
\ No newline at end of file
diff --git a/Readme_ZH.md b/Readme_ZH.md
new file mode 100644
index 0000000..cfa1461
--- /dev/null
+++ b/Readme_ZH.md
@@ -0,0 +1,419 @@
+# 1. LinkerFFG手套
+
+## 1.1 产品介绍
+本产品的具体介绍参考,内含标定示例说明
+附件1、Linker FFG(FFG01)产品说明手册
+
+## 1.2 软件使用
+本产品提供了Ubuntu下的ROS1和ROS2 SDK包,配合Linkerhand的ROS1 SDK,ROS2 SDK实现遥操作
+具体使用请跳转到第四章节 遥操重定向系统
+
+## 1.3 通讯协议
+本产品的具体介绍参考附件2、Linker FFG(FFG01) 通讯协议
+
+---
+
+# 2. 遥操SDK系统
+
+## 2.1 系统介绍
+本系统重定向程序是基于ROS1/ROS2平台开发的,由一个节点和四个publisher的话题以及两个subscription的话题组成
+Linker FFG手套通过串口协议进行数据推送到重定向程序
+
+### 2.1.1 下载地址
+https://github.com/linker-bot/linkerhand-ros-teleo
+请联系客服获取最新SDK
+
+### 2.1.2 安装遥操重定向程序
+遥操重定向程序以src的形式提供,因此需要用户自行编译,才可以在多个不同的平台下使用,遥操重定向程序提供了ROS2版本,ROS1版尚在开发中
+
+将src复制到工作空间中,然后进行编译
+```bash
+colcon build --symlink-install
+```
+
+过程中可能会存在权限不足的问题导致无法运行,使用以下代码
+```bash
+chmod 777 -R *
+```
+
+### 2.1.3 程序配置
+该目录输入ls命令
+```bash
+cd src/linkerhand_retarget/linkerhand_retarget/config
+ls -1
+```
+输入以上命令后正常应该进入config的目录,ls -1后即可看到以下清单
+```
+base_config.yml
+body_custom_pose.yml
+body_unity_pose.yml
+hand_config.yml
+human_hand_info.yml
+linker_hand_info.yml
+model_config.yml
+retarget_config.yml
+speed_config.yml
+```
+
+其中我们要唯一修改的配置文件是base_config.yml,其他配置文件均不可修改
+
+以下是针对base_config.yml的配置内容展开进行介绍:
+
+**1. 配置机械手类型**
+```yaml
+robotname_r: l10
+robotname_l: l10
+```
+- LinkerHand_L10系列的手要配置成:l10
+- LinkerHand_L20系列的手要配置成:l20
+- LinkerHand_O7系列的手要配置成:o7
+- LinkerHand_T25系列的手要配置成:t25
+- LinkerHand_L25系列的手要配置成:l25
+- LinkerHand_L6系列的手要配置成:l6
+- LinkerHand_O6系列的手要配置成:o6
+
+注意事项:以上均为小写,大写不会被正确识别
+
+**2. 配置数据打印**
+```yaml
+debug:
+ joint_pub_debug: false
+ joint_motor_debug_r: true
+ joint_motor_debug_l: false
+```
+- joint_pub_debug:设置为true可打印Topic Pub后的内容,刷新率较慢,可左右手同时显示
+- joint_motor_debug_r:设置为true可快速刷新右手的输出数据,范围在255-0,代表张开到聚拢的变化幅度
+- joint_motor_debug_l:设置为true可快速刷新左手的输出数据,范围在255-0,代表张开到聚拢的变化幅度
+
+**3. 配置数据源**
+```yaml
+motion_type: linkerforce
+```
+- motion_type:使用LinkerFFG手套要设置为linkerforce,motion_device无需设置
+
+### 2.1.4 程序启动(ROS2版)
+之后执行以下代码确认本设备的USB清单,然后分别插入2个力反馈手套,再次执行命令,确认刷新出来的USB设备序号,以/dev/ttyUSB0和/dev/ttyUSB1为例
+```bash
+ls /dev/ttyUSB*
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+```
+
+然后执行
+```bash
+# 启动分为两种,
+# 一种带标定。第一次没标定过必须执行
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration := True
+# 一种正常启动,默认不执行标定
+ros2 run linkerhand_retarget handretarget
+```
+
+**注意事项:**
+为了客户启动方便,提供了一个启动脚本,在src同级目录下,文件名为startup_linkerforce.sh
+
+```bash
+#!/bin/bash
+# LinkerForce 手套遥操作启动脚本
+
+# 设置串口权限
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+
+# source ROS 工作空间(请输入正确的目录)
+source ~/project/linkerhand_telop_ws/ros2/v2.8.7/install/setup.bash
+
+# 启动程序(强制标定)
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=True
+
+# 无标定启动
+# ros2 run linkerhand_retarget handretarget
+```
+
+编辑完成后即可这样启动
+```bash
+./startup_linkerforce.sh
+# 如果遇到Tab无法识别的情况,请赋予权限
+sudo chmod a+x ./startup_linkerforce.sh
+```
+
+### 2.1.5 程序启动(ROS1版)
+之后执行以下代码确认本设备的USB清单,然后分别插入2个力反馈手套,再次执行命令,确认刷新出来的USB设备序号,以/dev/ttyUSB0和/dev/ttyUSB1为例
+```bash
+ls /dev/ttyUSB*
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+```
+
+然后执行
+```bash
+# 启动分为两种,
+# 一种带标定。第一次没标定过必须执行
+rosrun ros_linkerhand_retarget handretarget.py _calibrate:=true
+# 一种正常启动,默认不执行标定
+rosrun ros_linkerhand_retarget handretarget.py
+```
+
+**注意事项:**
+为了客户启动方便,提供了一个启动脚本,在src同级目录下,文件名为startup_linkerforce.sh
+
+```bash
+#!/bin/bash
+# LinkerForce 手套遥操作启动脚本
+
+# 设置串口权限
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+
+# source ROS 工作空间(请输入正确的目录)
+source ~/linkerhand_telop/ros1/v2.8.6/install/setup.bash
+
+# 启动程序(强制标定)
+rosrun ros_linkerhand_retarget handretarget.py _calibrate:=true
+
+# 无标定启动
+# rosrun ros_linkerhand_retarget handretarget.py
+```
+
+编辑完成后即可这样启动
+```bash
+./startup_linkerforce.sh
+# 如果遇到Tab无法识别的情况,请赋予权限
+sudo chmod a+x ./startup_linkerforce.sh
+```
+
+## 2.2 Topic话题说明
+SDK的工作原理是将上位机传输过来的数据经过清洗和整理后,统一通过Topic的方式进行数据分发,因此Topic是数据中转站,也是判断遥操工作是否正常的重要依据
+
+- ROS1命令:`rostopic list`
+- ROS2命令:`ros2 topic list`
+
+**1. ROS2命令:**
+```bash
+ros2 topic echo /cb_left_hand_control_cmd
+# 或
+ros2 topic echo /cb_right_hand_control_cmd
+```
+
+### 数据说明
+
+#### L25机械手(21个自由度)
+| 关节 | 手指 | 动作 | 默认值 |
+|------|------|------|--------|
+| joint1 | 拇指 | 根部弯曲 | 255 |
+| joint2 | 食指 | 根部弯曲 | 255 |
+| joint3 | 中指 | 根部弯曲 | 255 |
+| joint4 | 无名指 | 根部弯曲 | 255 |
+| joint5 | 小指 | 根部弯曲 | 255 |
+| joint6 | 拇指 | 侧摆 | 255 |
+| joint7 | 食指 | 侧摆 | 128 |
+| joint8 | 中指 | 侧摆 | 128 |
+| joint9 | 无名指 | 侧摆 | 128 |
+| joint10 | 小指 | 侧摆 | 128 |
+| joint11 | 拇指 | 旋转 | 255 |
+| joint12 | 预留 | - | - |
+| joint13 | 预留 | - | - |
+| joint14 | 预留 | - | - |
+| joint15 | 预留 | - | - |
+| joint16 | 拇指 | 第二关节弯曲 | 255 |
+| joint17 | 食指 | 第二关节弯曲 | 255 |
+| joint18 | 中指 | 第二关节弯曲 | 255 |
+| joint19 | 无名指 | 第二关节弯曲 | 255 |
+| joint20 | 小指 | 第二关节弯曲 | 255 |
+| joint21 | 拇指 | 末端弯曲 | 255 |
+| joint22 | 食指 | 末端弯曲 | 255 |
+| joint23 | 中指 | 末端弯曲 | 255 |
+| joint24 | 无名指 | 末端弯曲 | 255 |
+| joint25 | 小指 | 末端弯曲 | 255 |
+
+#### L20机械手(16个自由度)
+| 关节 | 手指 | 动作 | 默认值 |
+|------|------|------|--------|
+| joint1 | 拇指 | 根部弯曲 | 255 |
+| joint2 | 食指 | 根部弯曲 | 255 |
+| joint3 | 中指 | 根部弯曲 | 255 |
+| joint4 | 无名指 | 根部弯曲 | 255 |
+| joint5 | 小指 | 根部弯曲 | 255 |
+| joint6 | 拇指 | 侧摆 | 255 |
+| joint7 | 食指 | 侧摆 | 128 |
+| joint8 | 中指 | 侧摆 | 128 |
+| joint9 | 无名指 | 侧摆 | 128 |
+| joint10 | 小指 | 侧摆 | 128 |
+| joint11 | 拇指 | 旋转 | 255 |
+| joint12 | 预留 | - | - |
+| joint13 | 预留 | - | - |
+| joint14 | 预留 | - | - |
+| joint15 | 预留 | - | - |
+| joint16 | 拇指 | 末端弯曲 | 255 |
+| joint17 | 食指 | 末端弯曲 | 255 |
+| joint18 | 中指 | 末端弯曲 | 255 |
+| joint19 | 无名指 | 末端弯曲 | 255 |
+| joint20 | 小指 | 末端弯曲 | 255 |
+
+#### L10机械手(10个自由度)
+| 关节 | 手指 | 动作 | 默认值 |
+|------|------|------|--------|
+| joint1 | 拇指 | 根部弯曲 | 255 |
+| joint2 | 拇指 | 侧摆 | 128 |
+| joint3 | 食指 | 根部弯曲 | 255 |
+| joint4 | 中指 | 根部弯曲 | 255 |
+| joint5 | 无名指 | 根部弯曲 | 255 |
+| joint6 | 小指 | 根部弯曲 | 255 |
+| joint7 | 食指 | 侧摆 | 128 |
+| joint8 | 无名指 | 侧摆 | 128 |
+| joint9 | 小指 | 侧摆 | 128 |
+| joint10 | 拇指 | 旋转 | 255 |
+
+#### O7机械手(7个自由度)
+| 关节 | 手指 | 动作 | 默认值 |
+|------|------|------|--------|
+| joint1 | 拇指 | 根部弯曲 | 255 |
+| joint2 | 拇指 | 侧摆 | 128 |
+| joint3 | 食指 | 根部弯曲 | 255 |
+| joint4 | 中指 | 根部弯曲 | 255 |
+| joint5 | 无名指 | 根部弯曲 | 255 |
+| joint6 | 小指 | 根部弯曲 | 255 |
+| joint7 | 拇指 | 旋转 | 255 |
+
+#### O6/L6机械手(7个自由度)
+| 关节 | 手指 | 动作 | 默认值 |
+|------|------|------|--------|
+| joint1 | 拇指 | 根部弯曲 | 255 |
+| joint2 | 拇指 | 侧摆 | 128 |
+| joint3 | 食指 | 根部弯曲 | 255 |
+| joint4 | 中指 | 根部弯曲 | 255 |
+| joint5 | 无名指 | 根部弯曲 | 255 |
+| joint6 | 小指 | 根部弯曲 | 255 |
+
+## 2.3 故障自检排查
+
+### 2.3.1 网络故障
+遥操和控制设备都是通过网络连接的,网络异常会导致彼此设备工作不正常,首先要检查网线的连接是否正确,保证双方的ip相互ping通以及防火墙要确保对应的端口是开放的
+
+### 2.3.2 话题异常
+在网络正常的前提下,可以在其他网络的设备上通过命令确认话题是否存在
+具体细节参考 2.2 Topic话题说明
+
+## 2.4 映射参数的设置
+
+### 2.4.1 原理
+映射系统有3种模式
+
+| 映射模式 | 对应英文 | 对应中文 | 适用场景 |
+|----------|----------|----------|----------|
+| 三态映射 | origin-opose-fist | 张手-O手势-握拳 | 希望能兼顾机械手最大行程,同时能满足对指的,可以满足绝大部分映射要求 |
+| 双端映射 | origin-fist | 张手-握拳 | 最大限度覆盖机械手的全部行程 |
+| 对指映射 | origin-opose- | 张手-O手势 | 以对指为主要目标,以抓取为目标,提高抓取的颗粒度 |
+
+### 2.4.2 配置文件
+该映射系统配置对机械手进行参数绑定,映射配置目录在
+```
+~/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config
+```
+
+目前提供的映射参数包如下
+```
+g20_config.py
+l10_config.py
+l20_config.py
+l6_config.py
+o6_config.py
+o7_config.py
+```
+
+### 2.4.3 配置介绍
+以下以l6_config.py为例
+
+#### 2.4.3.1 手指配置常量FINGER_CONFIGS
+
+##### 2.4.3.1.1 公共常量表
+(内容省略)
+
+##### 2.4.3.1.2 reverse_motion属性
+反转信号:默认为False
+
+##### 2.4.3.1.3 dynamic_weight属性
+动态权重:用于区间细节表达,默认为None,本版本不启用
+
+##### 2.4.3.1.4 extended_mapping属性
+扩展映射配置
+```python
+'extended_mapping': { # 新增扩展映射配置
+ 'enabled': True,
+ 'scale_factor': 1.2, # 缩放因子,默认1.0
+ 'extended_exp_factor': 80 # 新增:延伸指数参数
+}
+```
+- enabled:仅在对指映射启用,False为不启用
+- scale_factor:缩放因子,用于快速锁定在opose的姿态位
+- extended_exp_factor:延伸参数,用于opose之后的延续动作,越小表示很难达到机械手的机械位置,利于更多的表达抓取细节,数值越大越容易快速达到驱动关节极限位置
+
+#### 2.4.3.2 映射顺序MAPPING_ORDER
+以L6为例,表示处理的关节顺序优先级
+```python
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_flexion',
+ 'index', 'middle', 'ring', 'pinky'
+]
+```
+
+#### 2.4.3.3 状态配置STATE_CONFIG(未启用)
+
+#### 2.4.3.4 机械手OPOSE校准位:ROBOT_OPOSE_LEFT
+以下以L6的配置为例,以下角度对应左手机械手的OPOSE位置,注意以下顺序对应URDF的序列
+```python
+ROBOT_OPOSE_LEFT = [
+ 1.4, 0.54, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0
+]
+```
+
+#### 2.4.3.5 机械手OPOSE校准位:ROBOT_OPOSE_RIGHT
+以下以L6的配置为例,以下角度对应右手机械手的OPOSE位置,注意以下顺序对应URDF的序列
+```python
+ROBOT_OPOSE_RIGHT = [
+ 1.4, 0.54, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0, 0.38, 0.0
+]
+```
+
+### 2.4.4 机械手校准
+在2.4.3.4和2.4.3.5章节已经分别介绍了左右机械手OPOSE校准位,用于处理当前机械手位置和URDF之间的差异,第一次使用的时候,需要在以下代码进行改写
+
+路径:`./src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand`
+
+以L6机械手为例,在打开linkerforce_l6.py之后,在约计149行
+```python
+if self.calibrationoriginal is not None
+and self.calibrationfistpose is not None
+and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ # arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc) # 注释掉
+ arc_value = ROBOT_OPOSE_RIGHT # 启用校准
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+```
+
+改写成如上图的示例,即可启用右机械手的校准,启动后就会让机械手按照映射的角度固定在当前角度
+
+左手对应的在360行
+```python
+if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ # arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc) # 注释掉
+ # 启用校准
+ arc_value = ROBOT_OPOSE_LEFT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+```
+
+改写成如上图的示例,即可启用右机械手的校准,启动后就会让机械手按照映射的角度固定在当前角度
+
+当左右两手都达到期望的对指位置后,就可以恢复原状,按正常顺序使用遥操系统
\ No newline at end of file
diff --git a/readme.txt b/readme.txt
new file mode 100644
index 0000000..72f6f40
--- /dev/null
+++ b/readme.txt
@@ -0,0 +1,9 @@
+colcon build --symlink-install
+source install/setup.bash
+
+启动分为两种,
+一种带标定
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration := True
+一种正常启动,默认不执行标定
+ros2 run linkerhand_retarget handretarget
+还是这些流程
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/ReadMe.md b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/ReadMe.md
new file mode 100644
index 0000000..80dff26
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/ReadMe.md
@@ -0,0 +1,19 @@
+## v1.0.0.1 update,更新日期2025-03-31
+1. 调整linker_hand_l10_left拇指旋转角不当的问题,子版本号变更为1.0.0.1
+2. 调整linker_hand_l10_left无名指偏航角的大小,子版本号变更为1.0.0.2
+3. 调整linker_hand_l10_left小指偏航角的大小,子版本号变更为1.0.0.3
+4. linker_hand_l10_right的拇指旋转角处于异常状态,需结构重新设定,版本封存
+5. 调整linker_hand_l20_right拇指旋转角不当的问题,子版本号变更为1.0.0.1
+6. 调整linker_hand_l20_right拇指偏航角不当的问题,子版本号变更为1.0.0.2
+7. 调整linker_hand_t25_left四指横滚角不当的问题,子版本号变更为1.0.0.1
+
+## v1.0.0.0 create
+1. 版本创建
+2. 添加linker_hand_l10_left,版本号v1.6.7995.38578
+3. 添加linker_hand_l10_right,版本号v1.0.0
+4. 添加linker_hand_l20_left,版本号v1.0.0
+5. 添加linker_hand_l20_right,版本号v1.0.0
+6. 添加linker_hand_t25_left,版本号v1.0.0
+7. 添加linker_hand_t25_right,版本号v1.0.0
+8. 添加linker_hand_o7_left,版本号v1.0.0
+9. 添加linker_hand_o7_right,版本号v1.0.0
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_g20_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_g20_left.urdf
new file mode 100644
index 0000000..9c0c1f1
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_g20_left.urdf
@@ -0,0 +1,1285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_l25_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_l25_left.urdf
new file mode 120000
index 0000000..b2a4b3b
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/linkerhand_l25_left.urdf
@@ -0,0 +1 @@
+linkerhand_g20_left.urdf
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..706606c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_distal.STL
new file mode 100644
index 0000000..281a53c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..608b860
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_middle.STL
new file mode 100644
index 0000000..9f00fc4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..4a4473d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..37d5121
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..96674b1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..3b17190
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..80b553d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..8e2e782
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..05a4d03
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..afab601
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..446789a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..6915e85
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..f502969
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..88b6ef5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..5a06a97
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..dc10991
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..156e9b3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..16e1059
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..cf5fced
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..b657d19
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_g20_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_g20_right.urdf
new file mode 100644
index 0000000..3d1b6cd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_g20_right.urdf
@@ -0,0 +1,1285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_l25_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_l25_right.urdf
new file mode 120000
index 0000000..6a0e843
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/linkerhand_l25_right.urdf
@@ -0,0 +1 @@
+linkerhand_g20_right.urdf
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..f277145
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_distal.STL
new file mode 100644
index 0000000..b812c38
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..a319384
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_middle.STL
new file mode 100644
index 0000000..5ea8f9c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..923012e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..cd1ee86
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..62a6b35
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..3b9861b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9febd39
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..8f25349
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..64b9b7b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..d1a62f6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..d26bc4b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..9b53ea5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..095a682
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..b25525e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..7b2a085
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..2dbc10f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..384fd60
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..71269cb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..e1c0763
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..d32cf2d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/g20_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/linkerhand_l10_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/linkerhand_l10_left.urdf
new file mode 100644
index 0000000..fbc114f
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/linkerhand_l10_left.urdf
@@ -0,0 +1,1247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..510452a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_distal.STL
new file mode 100644
index 0000000..45d509e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..b42a241
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_middle.STL
new file mode 100644
index 0000000..0f61c46
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..b234ac5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..744c102
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..45f42c9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9c9148a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..a093402
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..a1747e5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..2f1fc95
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..9959832
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..85da730
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..65147dd
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..15d8948
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..e999f11
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..0092eb3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..22a1345
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..541c6c9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..dbf06fb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..a90bd00
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/linkerhand_l10_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/linkerhand_l10_right.urdf
new file mode 100644
index 0000000..85b7010
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/linkerhand_l10_right.urdf
@@ -0,0 +1,1247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..b3e7b00
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_distal.STL
new file mode 100644
index 0000000..70d4f4d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..aee6dd9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_middle.STL
new file mode 100644
index 0000000..8a58ace
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..0f2c0e1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..764f7cc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..dfd7957
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9d5f9ff
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..94105f0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..572bde2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..77c0de2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..ea2d507
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..8d1c8cc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..6d5ecd9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..f4eae39
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..a771017
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..7f0d214
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..c2dbe24
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..34d2a0b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..21a2bcc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..b9d1b8e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/linkerhand_l10v6_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/linkerhand_l10v6_left.urdf
new file mode 100644
index 0000000..eff1aaf
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/linkerhand_l10v6_left.urdf
@@ -0,0 +1,615 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..2b7578d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_distal.STL
new file mode 100644
index 0000000..bba5f2f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..27e08c6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_middle.STL
new file mode 100644
index 0000000..7c6b2e7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..522c623
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..21bcc83
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..b006486
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..b13502a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..085a529
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..f4967be
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..1b4136e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..902a7f8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..bcfce44
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..ef61fa7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..83fa397
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..4e0434a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..315d789
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..a67f34a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..7eaf56e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..ee81940
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..bb30342
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/linkerhand_l10v6_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/linkerhand_l10v6_right.urdf
new file mode 100644
index 0000000..e411b5d
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/linkerhand_l10v6_right.urdf
@@ -0,0 +1,616 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..aa07554
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_distal.STL
new file mode 100644
index 0000000..4fd659c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..33faba3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_middle.STL
new file mode 100644
index 0000000..e258715
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..4609136
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..aedaa1e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..3dda919
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..3b55a6b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..9998406
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..f0476ae
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..2294bd8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..2daeb46
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..ae50a09
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..c7656a1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..c88f1c8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..c30fb04
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..432195c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..4035b15
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..1fe20c0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..6c448d8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..202608f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v6_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/linkerhand_l10v7_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/linkerhand_l10v7_left.urdf
new file mode 100644
index 0000000..fbc114f
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/linkerhand_l10v7_left.urdf
@@ -0,0 +1,1247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..510452a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_distal.STL
new file mode 100644
index 0000000..45d509e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..b42a241
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_middle.STL
new file mode 100644
index 0000000..0f61c46
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..b234ac5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..744c102
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..45f42c9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9c9148a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..a093402
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..a1747e5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..2f1fc95
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..9959832
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..85da730
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..65147dd
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..15d8948
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..e999f11
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..0092eb3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..22a1345
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..541c6c9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..dbf06fb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..a90bd00
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/linkerhand_l10v7_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/linkerhand_l10v7_right.urdf
new file mode 100644
index 0000000..85b7010
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/linkerhand_l10v7_right.urdf
@@ -0,0 +1,1247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..b3e7b00
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_distal.STL
new file mode 100644
index 0000000..70d4f4d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..aee6dd9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_middle.STL
new file mode 100644
index 0000000..8a58ace
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..0f2c0e1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..764f7cc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..dfd7957
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9d5f9ff
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..94105f0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..572bde2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..77c0de2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..ea2d507
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..8d1c8cc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..6d5ecd9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..f4eae39
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..a771017
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..7f0d214
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..c2dbe24
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..34d2a0b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..21a2bcc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..b9d1b8e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l10v7_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/linkerhand_l20_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/linkerhand_l20_left.urdf
new file mode 100644
index 0000000..3105751
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/linkerhand_l20_left.urdf
@@ -0,0 +1,1536 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/base_link.STL
new file mode 100644
index 0000000..334f5df
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link0.STL
new file mode 100644
index 0000000..35c6b91
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link1.STL
new file mode 100644
index 0000000..2ef499e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link2.STL
new file mode 100644
index 0000000..7f4b8d0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link3.STL
new file mode 100644
index 0000000..d7b27fb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link4.STL
new file mode 100644
index 0000000..e53e3f5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/index_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link0.STL
new file mode 100644
index 0000000..4f236e6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link1.STL
new file mode 100644
index 0000000..e20c785
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link2.STL
new file mode 100644
index 0000000..630aff8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link3.STL
new file mode 100644
index 0000000..f6262b5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link4.STL
new file mode 100644
index 0000000..6511b29
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/little_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link0.STL
new file mode 100644
index 0000000..48a30ea
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link1.STL
new file mode 100644
index 0000000..40796aa
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link2.STL
new file mode 100644
index 0000000..81c524a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link3.STL
new file mode 100644
index 0000000..5df3b80
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link4.STL
new file mode 100644
index 0000000..b7c61cb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/middle_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link0.STL
new file mode 100644
index 0000000..3714a04
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link1.STL
new file mode 100644
index 0000000..0776fd0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link2.STL
new file mode 100644
index 0000000..630aff8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link3.STL
new file mode 100644
index 0000000..d5e6a4b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link4.STL
new file mode 100644
index 0000000..f002fb5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/ring_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link0.STL
new file mode 100644
index 0000000..481bc60
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link1.STL
new file mode 100644
index 0000000..faccfca
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link2.STL
new file mode 100644
index 0000000..fdb350f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link3.STL
new file mode 100644
index 0000000..cb95087
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link4.STL
new file mode 100644
index 0000000..fc217c7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link5.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link5.STL
new file mode 100644
index 0000000..58b8cf1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_left/meshes/thumb_link5.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/linkerhand_l20_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/linkerhand_l20_right.urdf
new file mode 100644
index 0000000..69f3552
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/linkerhand_l20_right.urdf
@@ -0,0 +1,1285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/base_link.STL
new file mode 100644
index 0000000..7fbeaf1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_distal.STL
new file mode 100644
index 0000000..521f0c1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link0.STL
new file mode 100644
index 0000000..a572166
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link1.STL
new file mode 100644
index 0000000..869fdbc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link2.STL
new file mode 100644
index 0000000..5f5ffc3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link3.STL
new file mode 100644
index 0000000..6da1cc2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link4.STL
new file mode 100644
index 0000000..c1f6ec7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..ebcbac7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_middle.STL
new file mode 100644
index 0000000..0d7c0c7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..31deda4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link0.STL
new file mode 100644
index 0000000..25e4401
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link1.STL
new file mode 100644
index 0000000..40bbdef
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link2.STL
new file mode 100644
index 0000000..27609a0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link3.STL
new file mode 100644
index 0000000..6da1cc2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link4.STL
new file mode 100644
index 0000000..1d6e30e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/little_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..f910042
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link0.STL
new file mode 100644
index 0000000..dde5eca
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link1.STL
new file mode 100644
index 0000000..6aa00ce
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link2.STL
new file mode 100644
index 0000000..4fdc281
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link3.STL
new file mode 100644
index 0000000..6da1cc2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link4.STL
new file mode 100644
index 0000000..f09fc8e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..e55bd87
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..94b80dc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..db97b4b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..02ef45f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..f925ae3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..0fbfc83
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..de065df
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..9bc5aa9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link0.STL
new file mode 100644
index 0000000..f5f3e87
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link1.STL
new file mode 100644
index 0000000..fe8a90d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link2.STL
new file mode 100644
index 0000000..136c34d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link3.STL
new file mode 100644
index 0000000..6da1cc2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link4.STL
new file mode 100644
index 0000000..c97b64c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..819bebd
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..ab9d881
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..ddf3b47
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..bd8de50
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link0.STL
new file mode 100644
index 0000000..7b1b3c2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link1.STL
new file mode 100644
index 0000000..ed67d9f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link2.STL
new file mode 100644
index 0000000..d76314c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link3.STL
new file mode 100644
index 0000000..332665c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link4.STL
new file mode 100644
index 0000000..7bd7b82
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link5.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link5.STL
new file mode 100644
index 0000000..9cf2093
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_link5.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..950a2fc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..47489bf
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..ccac673
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..ec23b9d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l20_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/linkerhand_l21_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/linkerhand_l21_left.urdf
new file mode 100644
index 0000000..49b7b7e
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/linkerhand_l21_left.urdf
@@ -0,0 +1,1033 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..3c8a804
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..dee80a8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_middle.STL
new file mode 100644
index 0000000..d283916
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..bef360b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..0d4a05d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..73f8068
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..0adfd93
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..c7c6e46
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..1e27abf
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..3e1bd8a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..3fb6123
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..6986186
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..d0f1637
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..2eb54d8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..e4f3158
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..da39fc0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..3d24ae2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..6b7f4d1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/linkerhand_l21_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/linkerhand_l21_right.urdf
new file mode 100644
index 0000000..f42dc99
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/linkerhand_l21_right.urdf
@@ -0,0 +1,1033 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..9d1ea49
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..e0e4944
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_middle.STL
new file mode 100644
index 0000000..7d16604
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..c001f27
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..56cae1c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..9c22903
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..534cff7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..e78b9ca
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..05fb04f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..992adda
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..96c1afa
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..fce4efb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..69a8833
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..36c6f8e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..40d76f2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..3fdfee8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..5585fe8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..3f5b5af
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l21_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/linkerhand_l25_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/linkerhand_l25_left.urdf
new file mode 100644
index 0000000..9c0c1f1
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/linkerhand_l25_left.urdf
@@ -0,0 +1,1285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..706606c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_distal.STL
new file mode 100644
index 0000000..281a53c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..608b860
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_middle.STL
new file mode 100644
index 0000000..9f00fc4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..4a4473d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..37d5121
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..96674b1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..3b17190
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..80b553d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..8e2e782
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..05a4d03
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..afab601
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..446789a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..6915e85
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..f502969
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..88b6ef5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..5a06a97
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..dc10991
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..156e9b3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..16e1059
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..cf5fced
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..b657d19
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/linkerhand_l25_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/linkerhand_l25_right.urdf
new file mode 100644
index 0000000..3d1b6cd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/linkerhand_l25_right.urdf
@@ -0,0 +1,1285 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..f277145
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_distal.STL
new file mode 100644
index 0000000..b812c38
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_metacarpals.STL
new file mode 100644
index 0000000..a319384
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_middle.STL
new file mode 100644
index 0000000..5ea8f9c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..923012e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..cd1ee86
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_metacarpals.STL
new file mode 100644
index 0000000..62a6b35
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..3b9861b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..9febd39
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..8f25349
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_metacarpals.STL
new file mode 100644
index 0000000..64b9b7b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..d1a62f6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..d26bc4b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..9b53ea5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_metacarpals.STL
new file mode 100644
index 0000000..095a682
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..b25525e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..7b2a085
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..2dbc10f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..384fd60
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..71269cb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..e1c0763
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..d32cf2d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l25_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/linkerhand_l6_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/linkerhand_l6_left.urdf
new file mode 100644
index 0000000..c858b83
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/linkerhand_l6_left.urdf
@@ -0,0 +1,705 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..861415f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_distal.STL
new file mode 100644
index 0000000..15ba6e1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..07cd2ab
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..b5e1445
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..c3b110d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..abcf4da
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..0d48b8e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..03cdc8d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..1f4901a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..a2d41ba
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..93ef2a4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..40d2484
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/linkerhand_l6_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/linkerhand_l6_right.urdf
new file mode 100644
index 0000000..2b6bfc0
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/linkerhand_l6_right.urdf
@@ -0,0 +1,705 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..ca10953
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_distal.STL
new file mode 100644
index 0000000..70409df
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..452052d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..c4bad01
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..6b1f4ed
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..8fb8d74
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..4eff46c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..019bac7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..7ac57c8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..d3d6067
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..be42f33
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..6e16930
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l6_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/linkerhand_l7_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/linkerhand_l7_left.urdf
new file mode 100644
index 0000000..9b356c9
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/linkerhand_l7_left.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/base_link.STL
new file mode 100644
index 0000000..66bd83f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..4bce1d7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_distal.STL
new file mode 100644
index 0000000..f531771
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link0.STL
new file mode 100644
index 0000000..8eb082a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link1.STL
new file mode 100644
index 0000000..44d513f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link2.STL
new file mode 100644
index 0000000..b62a597
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link3.STL
new file mode 100644
index 0000000..8c29da2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link4.STL
new file mode 100644
index 0000000..dd215f0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_middle.STL
new file mode 100644
index 0000000..e8a0fbe
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..b34814e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link0.STL
new file mode 100644
index 0000000..754c500
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link1.STL
new file mode 100644
index 0000000..194847e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link2.STL
new file mode 100644
index 0000000..fd6c112
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link3.STL
new file mode 100644
index 0000000..1fccf5c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link4.STL
new file mode 100644
index 0000000..67a0339
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/little_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..0fb0ad0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link0.STL
new file mode 100644
index 0000000..09ab875
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link1.STL
new file mode 100644
index 0000000..4813804
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link2.STL
new file mode 100644
index 0000000..fda4dff
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link3.STL
new file mode 100644
index 0000000..a3746e9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..6913a0a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..92ec987
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..0e3f91c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..e5112a4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..6643d29
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..cd2cd55
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link0.STL
new file mode 100644
index 0000000..acfbe14
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link1.STL
new file mode 100644
index 0000000..365d4c8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link2.STL
new file mode 100644
index 0000000..7115054
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link3.STL
new file mode 100644
index 0000000..c848977
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link4.STL
new file mode 100644
index 0000000..77b4fc6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..aa63904
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..f49490e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..e3511b7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link0.STL
new file mode 100644
index 0000000..995681c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link1.STL
new file mode 100644
index 0000000..48a0bb7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link2.STL
new file mode 100644
index 0000000..6f32a9a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link3.STL
new file mode 100644
index 0000000..67276b2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link4.STL
new file mode 100644
index 0000000..06fff54
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link5.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link5.STL
new file mode 100644
index 0000000..0313140
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_link5.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..a233832
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..53326f8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..7d01688
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..463afed
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/linkerhand_l7_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/linkerhand_l7_right.urdf
new file mode 100644
index 0000000..0d39d6f
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/linkerhand_l7_right.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/base_link.STL
new file mode 100644
index 0000000..a13c5e9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..38df479
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_distal.STL
new file mode 100644
index 0000000..ff78165
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link0.STL
new file mode 100644
index 0000000..79be9d9
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link1.STL
new file mode 100644
index 0000000..ff23d89
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link2.STL
new file mode 100644
index 0000000..1562779
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link3.STL
new file mode 100644
index 0000000..c1b7332
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link4.STL
new file mode 100644
index 0000000..024d190
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_middle.STL
new file mode 100644
index 0000000..4131b7c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..af9a7c3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link0.STL
new file mode 100644
index 0000000..925cac8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link1.STL
new file mode 100644
index 0000000..a204f8a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link2.STL
new file mode 100644
index 0000000..b686aa4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link3.STL
new file mode 100644
index 0000000..9914fcf
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link4.STL
new file mode 100644
index 0000000..f9c539e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/little_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..7d891e8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link0.STL
new file mode 100644
index 0000000..e0246e7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link1.STL
new file mode 100644
index 0000000..905e8ae
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link2.STL
new file mode 100644
index 0000000..44d98b6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link3.STL
new file mode 100644
index 0000000..f3d5d24
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..4ff71a0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..60fb788
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..3a52181
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..b7e5eca
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..f8afc25
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..a6877e7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link0.STL
new file mode 100644
index 0000000..0644bed
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link1.STL
new file mode 100644
index 0000000..75b6914
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link2.STL
new file mode 100644
index 0000000..0d7e140
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link3.STL
new file mode 100644
index 0000000..d2cb151
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link4.STL
new file mode 100644
index 0000000..0ac9e8f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_mcp_pitch.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_mcp_pitch.STL
new file mode 100644
index 0000000..167eff4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_mcp_pitch.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..b0523c3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..143dd86
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link0.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link0.STL
new file mode 100644
index 0000000..eb73343
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link0.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link1.STL
new file mode 100644
index 0000000..a93e934
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link2.STL
new file mode 100644
index 0000000..c429e07
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link3.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link3.STL
new file mode 100644
index 0000000..0c6599a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link3.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link4.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link4.STL
new file mode 100644
index 0000000..8476b29
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link4.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link5.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link5.STL
new file mode 100644
index 0000000..b043327
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_link5.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..20c1b96
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..34a678f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..10c810e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..7837d9f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/l7_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/linkerhand_o6_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/linkerhand_o6_left.urdf
new file mode 100644
index 0000000..4a712bc
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/linkerhand_o6_left.urdf
@@ -0,0 +1,706 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..b7ab0c1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_distal.STL
new file mode 100644
index 0000000..db30679
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..f427e3c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..a4838a5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..e8d4268
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..abcb9c0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..5f013e6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..b8247b1
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..7c47ff6
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..19eee73
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..9f1ab59
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..5e66834
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/linkerhand_o6_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/linkerhand_o6_right.urdf
new file mode 100644
index 0000000..579a8d1
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/linkerhand_o6_right.urdf
@@ -0,0 +1,705 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..6ab2f6f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_distal.STL
new file mode 100644
index 0000000..e530ec5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..86a10cb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..1582dfe
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..2a46d3f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..581901e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..b0326b3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..1c2830a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..b98a3db
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..e102dbe
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..df9bccf
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..0546e91
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o6_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/linkerhand_o7_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/linkerhand_o7_left.urdf
new file mode 100644
index 0000000..bcbf0e7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/linkerhand_o7_left.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..bb03464
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_distal.STL
new file mode 100644
index 0000000..91e3884
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_middle.STL
new file mode 100644
index 0000000..7724c51
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..12c331e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..cbb926c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..e9eedee
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..663efcc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..91e3884
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..d881b40
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..3ed6faa
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..741446b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..7724c51
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..2e30249
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..2974104
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..d18f5b4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..5b02a89
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..bbaad54
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..6d93c0d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/linkerhand_o7_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/linkerhand_o7_right.urdf
new file mode 100644
index 0000000..73df739
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/linkerhand_o7_right.urdf
@@ -0,0 +1,1074 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..fe6aa6f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_distal.STL
new file mode 100644
index 0000000..3687fee
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_middle.STL
new file mode 100644
index 0000000..2c6258e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..f63a032
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..aca2960
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..5c6f798
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..234c2fb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..9ea6727
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..6cbc6a3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..734c4b2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..a9efa03
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..afb7100
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..48fe472
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..db0b1fc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..3f97010
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..d832906
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..0f5d370
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..79391d5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/likerhand_o7v1_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/likerhand_o7v1_left.urdf
new file mode 100644
index 0000000..8ad8938
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/likerhand_o7v1_left.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..4bce1d7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_distal.STL
new file mode 100644
index 0000000..f531771
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_middle.STL
new file mode 100644
index 0000000..e8a0fbe
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..b34814e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..0fb0ad0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..6913a0a
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..92ec987
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..0e3f91c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..e5112a4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..6643d29
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..cd2cd55
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..aa63904
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..f49490e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..e3511b7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..a233832
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..53326f8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..7d01688
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..463afed
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/linkerhand_o7v1_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/linkerhand_o7v1_right.urdf
new file mode 100644
index 0000000..bc92bec
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/linkerhand_o7v1_right.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..38df479
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_distal.STL
new file mode 100644
index 0000000..ff78165
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_middle.STL
new file mode 100644
index 0000000..4131b7c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..af9a7c3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..7d891e8
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..4ff71a0
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..60fb788
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..3a52181
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..b7e5eca
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..f8afc25
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..a6877e7
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_mcp_pitch.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_mcp_pitch.STL
new file mode 100644
index 0000000..167eff4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_mcp_pitch.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..b0523c3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..143dd86
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..20c1b96
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..34a678f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..10c810e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..7837d9f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v1_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/linkerhand_o7v3_left.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/linkerhand_o7v3_left.urdf
new file mode 100644
index 0000000..bcbf0e7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/linkerhand_o7v3_left.urdf
@@ -0,0 +1,1073 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/hand_base_link.STL
new file mode 100644
index 0000000..bb03464
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_distal.STL
new file mode 100644
index 0000000..91e3884
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_middle.STL
new file mode 100644
index 0000000..7724c51
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_proximal.STL
new file mode 100644
index 0000000..12c331e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_distal.STL
new file mode 100644
index 0000000..cbb926c
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_middle.STL
new file mode 100644
index 0000000..e9eedee
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_proximal.STL
new file mode 100644
index 0000000..663efcc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_distal.STL
new file mode 100644
index 0000000..91e3884
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_middle.STL
new file mode 100644
index 0000000..d881b40
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..3ed6faa
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_distal.STL
new file mode 100644
index 0000000..741446b
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_middle.STL
new file mode 100644
index 0000000..7724c51
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_proximal.STL
new file mode 100644
index 0000000..2e30249
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_distal.STL
new file mode 100644
index 0000000..2974104
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..d18f5b4
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..5b02a89
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..bbaad54
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..6d93c0d
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_left/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/linkerhand_o7v3_right.urdf b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/linkerhand_o7v3_right.urdf
new file mode 100644
index 0000000..73df739
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/linkerhand_o7v3_right.urdf
@@ -0,0 +1,1074 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/hand_base_link.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/hand_base_link.STL
new file mode 100644
index 0000000..fe6aa6f
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/hand_base_link.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_distal.STL
new file mode 100644
index 0000000..3687fee
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_middle.STL
new file mode 100644
index 0000000..2c6258e
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_proximal.STL
new file mode 100644
index 0000000..f63a032
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/index_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_distal.STL
new file mode 100644
index 0000000..aca2960
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_middle.STL
new file mode 100644
index 0000000..5c6f798
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_proximal.STL
new file mode 100644
index 0000000..234c2fb
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/middle_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_distal.STL
new file mode 100644
index 0000000..9ea6727
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_middle.STL
new file mode 100644
index 0000000..6cbc6a3
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_proximal.STL
new file mode 100644
index 0000000..734c4b2
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/pinky_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_distal.STL
new file mode 100644
index 0000000..a9efa03
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_middle.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_middle.STL
new file mode 100644
index 0000000..afb7100
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_middle.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_proximal.STL
new file mode 100644
index 0000000..48fe472
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/ring_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_distal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_distal.STL
new file mode 100644
index 0000000..db0b1fc
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_distal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals.STL
new file mode 100644
index 0000000..3f97010
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base1.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base1.STL
new file mode 100644
index 0000000..d832906
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base1.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base2.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base2.STL
new file mode 100644
index 0000000..0f5d370
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_metacarpals_base2.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_proximal.STL b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_proximal.STL
new file mode 100644
index 0000000..79391d5
Binary files /dev/null and b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/o7v3_right/meshes/thumb_proximal.STL differ
diff --git a/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/version.md b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/version.md
new file mode 100644
index 0000000..80dff26
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/assets/robots/hands/linker_hand/version.md
@@ -0,0 +1,19 @@
+## v1.0.0.1 update,更新日期2025-03-31
+1. 调整linker_hand_l10_left拇指旋转角不当的问题,子版本号变更为1.0.0.1
+2. 调整linker_hand_l10_left无名指偏航角的大小,子版本号变更为1.0.0.2
+3. 调整linker_hand_l10_left小指偏航角的大小,子版本号变更为1.0.0.3
+4. linker_hand_l10_right的拇指旋转角处于异常状态,需结构重新设定,版本封存
+5. 调整linker_hand_l20_right拇指旋转角不当的问题,子版本号变更为1.0.0.1
+6. 调整linker_hand_l20_right拇指偏航角不当的问题,子版本号变更为1.0.0.2
+7. 调整linker_hand_t25_left四指横滚角不当的问题,子版本号变更为1.0.0.1
+
+## v1.0.0.0 create
+1. 版本创建
+2. 添加linker_hand_l10_left,版本号v1.6.7995.38578
+3. 添加linker_hand_l10_right,版本号v1.0.0
+4. 添加linker_hand_l20_left,版本号v1.0.0
+5. 添加linker_hand_l20_right,版本号v1.0.0
+6. 添加linker_hand_t25_left,版本号v1.0.0
+7. 添加linker_hand_t25_right,版本号v1.0.0
+8. 添加linker_hand_o7_left,版本号v1.0.0
+9. 添加linker_hand_o7_right,版本号v1.0.0
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/base_config.yml b/src/linkerhand_retarget/linkerhand_retarget/config/base_config.yml
new file mode 100644
index 0000000..0f864e6
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/base_config.yml
@@ -0,0 +1,56 @@
+calibration:
+ fist_extend_ratio: 0.5
+ show_fist: true
+debug:
+ joint_motor_debug_l: false
+ joint_motor_debug_r: false
+ joint_pub_debug: false
+ mapper_debug: false
+humanset:
+ bodyfile: body_unity_pose
+ targethandfile: human_hand_info
+linkereg:
+ password: i
+ port: null
+netpub:
+ ip: 192.168.11.33
+ port: 20008
+serial:
+ auto_scan: false
+ baudrates:
+ - 2000000
+ - 460800
+ - 1000000
+ - 921600
+ exclude_ports: []
+ left:
+ baudrate: 460800
+ port: /dev/ttyUSB1
+ right:
+ baudrate: 460800
+ port: /dev/ttyUSB0
+ serial_debug: false
+system:
+ can:
+ bitrate: 1000000
+ dofs: 25
+ id: 40
+ datasource_type: motion
+ leftpub: true
+ motion_device: eric
+ motion_type: linkerforce
+ retargeting_type: projection
+ rightpub: true
+ robotname_l: l10
+ robotname_r: l10
+ sapientype: left
+ usecan: false
+ usegui: false
+ usenetpub: false
+ usesapien: true
+ useudp: true
+ useudpserver: true
+udp:
+ ip: 0.0.0.0
+ port: 8888
+ serverport: 5551
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/body_custom_pose.yml b/src/linkerhand_retarget/linkerhand_retarget/config/body_custom_pose.yml
new file mode 100644
index 0000000..ccded40
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/body_custom_pose.yml
@@ -0,0 +1,69 @@
+initial_positions:
+ body:
+ - [0, 1, 0]
+ - [0.1, 0.9, 0]
+ - [0.1, 0.5, 0]
+ - [0.1, 0.09, 0]
+ - [0.1, 0.02, 0.07]
+ - [-0.1, 0.9, 0]
+ - [-0.1, 0.5, 0]
+ - [-0.1, 0.09, 0]
+ - [-0.1, 0.02, 0.07]
+ - [0, 1.05, 0]
+ - [0, 1.15, 0]
+ - [0, 1.2, 0]
+ - [0, 1.3, 0]
+ - [0, 1.392, 0]
+ - [0, 1.6, 0]
+ - [0.04, 1.392, 0]
+ - [0.1835, 1.392, 0]
+ - [0.4343, 1.392, 0]
+ - [0.6612, 1.392, 0]
+ - [-0.04, 1.392, 0]
+ - [-0.1835, 1.392, 0]
+ - [-0.4343, 1.392, 0]
+ - [-0.6612, 1.392, 0]
+ right_hand:
+ - [0.5000, 0.0000, 0.1000]
+ # - [0.5740, 0.0534, 0.0974]
+ - [0.5370, 0.0370, 0.0974]
+ - [0.5764, 0.0764, 0.0965]
+ - [0.5901, 0.0901, 0.0959]
+ - [0.5000, 0.0000, 0.1000]
+ - [0.6060, 0.0324, 0.0968]
+ - [0.6378, 0.0325, 0.0959]
+ - [0.6697, 0.0326, 0.0944]
+ - [0.5000, 0.0000, 0.1000]
+ - [0.6061, 0.0111, 0.0970]
+ - [0.6432, 0.0113, 0.0960]
+ - [0.6804, 0.0114, 0.0943]
+ - [0.5000, 0.0000, 0.1000]
+ - [0.6062, -0.0101, 0.0972]
+ - [0.6380, -0.0100, 0.0963]
+ - [0.6699, -0.0098, 0.0949]
+ - [0.5000, 0.0000, 0.1000]
+ - [0.6063, -0.0314, 0.0974]
+ - [0.6328, -0.0312, 0.0967]
+ - [0.6593, -0.0311, 0.0955]
+ left_hand:
+ - [-0.5000, 0.0000, 0.1000]
+ # - [0.5740, 0.0534, 0.0974]
+ - [-0.5370, 0.0370, 0.0974]
+ - [-0.5764, 0.0764, 0.0965]
+ - [-0.5901, 0.0901, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.6060, 0.0324, 0.0968]
+ - [-0.6378, 0.0325, 0.0959]
+ - [-0.6697, 0.0326, 0.0944]
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.6061, 0.0111, 0.0970]
+ - [-0.6432, 0.0113, 0.0960]
+ - [-0.6804, 0.0114, 0.0943]
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.6062, -0.0101, 0.0972]
+ - [-0.6380, -0.0100, 0.0963]
+ - [-0.6699, -0.0098, 0.0949]
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.6063, -0.0314, 0.0974]
+ - [-0.6328, -0.0312, 0.0967]
+ - [-0.6593, -0.0311, 0.0955]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/body_unity_pose.yml b/src/linkerhand_retarget/linkerhand_retarget/config/body_unity_pose.yml
new file mode 100644
index 0000000..ed18e66
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/body_unity_pose.yml
@@ -0,0 +1,67 @@
+initial_positions:
+ body:
+ - [0, 1.01417, -0.01136745]
+ - [0.09608424, 0.9339905, -0.01918257]
+ - [0.1070137, 0.5183502, -0.03298733]
+ - [0.1029733, 0.09305052, -0.03527176]
+ - [0.1029734, 0.02005178, 0.07543653]
+ - [-0.09608424, 0.9339905, -0.01918257]
+ - [-0.1070137, 0.5183502, -0.03298733]
+ - [-0.1029733, 0.09305052, -0.03527176]
+ - [-0.1029734, 0.02005178, 0.07543653]
+ - [1.610145E-15, 1.101661, -0.01136744]
+ - [-1.967892E-16, 1.204164, -0.01136744]
+ - [1.722054E-16, 1.313723, -0.01136744]
+ - [3.313691E-17, 1.420459, 2.017152E-09]
+ - [1.412962E-16, 1.530355, -0.01136744]
+ - [-1.426205E-16, 1.62144, -0.01136744]
+ - [0.04465847, 1.45745, -0.01136743]
+ - [0.1871382, 1.45745, -0.01136743]
+ - [0.4227339, 1.45745, -0.01136743]
+ - [0.6813283, 1.45745, -0.01136743]
+ - [-0.04465845, 1.457453, -0.01136744]
+ - [-0.1871379, 1.457453, -0.01136744]
+ - [-0.4227338, 1.457453, -0.01136744]
+ - [-0.6813283, 1.457453, -0.01136744]
+ right_hand:
+ - [0.6813, 1.4575, -0.0114]
+ - [0.7121, 1.4598, 0.0272]
+ - [0.7444, 1.4597, 0.0592]
+ - [0.7669, 1.4598, 0.0815]
+ - [0.6813, 1.4575, -0.0114]
+ - [0.7857, 1.4626, 0.0254]
+ - [0.8304, 1.4604, 0.0255]
+ - [0.8557, 1.4589, 0.0255]
+ - [0.6813, 1.4575, -0.0114]
+ - [0.7871, 1.4628, 0.0019]
+ - [0.8358, 1.4595, 0.0017]
+ - [0.8663, 1.4571, 0.0018]
+ - [0.6813, 1.4575, -0.0114]
+ - [0.7802, 1.4639, -0.0189]
+ - [0.8226, 1.4599, -0.0188]
+ - [0.8520, 1.4573, -0.0189]
+ - [0.6813, 1.4575, -0.0114]
+ - [0.7716, 1.4629, -0.0397]
+ - [0.8056, 1.4611, -0.0398]
+ - [0.8270, 1.4594, -0.0398]
+ left_hand:
+ - [-0.6813, 1.4575, -0.0114]
+ - [-0.7121, 1.4598, 0.0272]
+ - [-0.7444, 1.4599, 0.0593]
+ - [-0.7666, 1.4598, 0.0817]
+ - [-0.6813, 1.4575, -0.0114]
+ - [-0.7857, 1.4626, 0.0254]
+ - [-0.8304, 1.4604, 0.0253]
+ - [-0.8557, 1.4588, 0.0254]
+ - [-0.6813, 1.4575, -0.0114]
+ - [-0.7871, 1.4628, 0.0019]
+ - [-0.8358, 1.4595, 0.0017]
+ - [-0.8663, 1.4571, 0.0019]
+ - [-0.6813, 1.4575, -0.0114]
+ - [-0.7802, 1.4639, -0.0189]
+ - [-0.8226, 1.4606, -0.0189]
+ - [-0.8521, 1.4585, -0.0189]
+ - [-0.6813, 1.4575, -0.0114]
+ - [-0.7716, 1.4629, -0.0397]
+ - [-0.8056, 1.4611, -0.0396]
+ - [-0.8270, 1.4594, -0.0397]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/calibration_sample.yml b/src/linkerhand_retarget/linkerhand_retarget/config/calibration_sample.yml
new file mode 100644
index 0000000..4de87ac
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/calibration_sample.yml
@@ -0,0 +1,133 @@
+jointanglefist_l:
+- 1.2437261653212208
+- 3.175084430630538
+- 1.9181389961835325
+- 3.945504443472827
+- 3.176513677347769
+- 4.15470097472291
+- 1.2220743742244509
+- 5.559884890958649
+- 4.359119825243289
+- 4.092220656591438
+- 1.191495397918279
+- 5.462992890591046
+- 4.491218635943924
+- 4.200856485664777
+- 1.3686122344439422
+- 4.9037060043950795
+- 3.9838837108819094
+- 4.308879495669112
+- 0.9698557684365023
+- 5.547674363135352
+- 4.032751679729586
+jointanglefist_r:
+- 1.1044308306023618
+- 1.5951796058121013
+- 3.6276991375224035
+- 5.47734400510711
+- 5.116904467934635
+- 2.794897842149621
+- 4.45722081173913
+- 2.6553320763879045
+- 5.816282943026772
+- 2.7213461356079076
+- 4.369521194059152
+- 2.451310885672715
+- 7.396954804596076
+- 2.7479907704823434
+- 4.328249766846534
+- 2.4081802807772505
+- 5.57164877596065
+- 2.7861285486955643
+- 4.76350470576917
+- 2.774348560437687
+- 5.953534512596457
+jointangleopose_l:
+- 1.2793574933684364
+- 3.483006318161248
+- 2.331541885879661
+- 3.7824928786437844
+- 2.970712731931335
+- 4.142190635278191
+- 1.7971720165469458
+- 5.247289642697985
+- 3.4463685780825344
+- 4.100680552379041
+- 1.7967017729109331
+- 5.190892714154727
+- 3.604353676476713
+- 4.174859433782973
+- 1.925651520397901
+- 4.825046277195627
+- 3.3016353177490636
+- 4.293594016705458
+- 1.5552237533819402
+- 5.241486378132036
+- 3.3450599532328815
+jointangleopose_r:
+- 1.2290999401142237
+- 1.3007013686522553
+- 3.402433098354701
+- 5.014432913946464
+- 4.331938085144293
+- 2.7775602287644148
+- 4.012928856914155
+- 2.487202411250892
+- 5.0280960910012995
+- 2.723595482905546
+- 3.923280471696764
+- 2.3498730343038363
+- 4.931762287623787
+- 2.734774309055077
+- 3.834016447185085
+- 2.2936636508756503
+- 4.818287076901581
+- 2.743915917282852
+- 4.166702619233486
+- 2.545942118281719
+- 5.167868347521494
+jointangleoriginal_l:
+- 1.3506201494628678
+- 4.098850093222668
+- 3.1583476652719176
+- 3.456469748985699
+- 2.559110841098467
+- 4.1171699563887545
+- 2.9473673011919357
+- 4.622099146176655
+- 1.620866083761026
+- 4.117600343954245
+- 3.0071145228962415
+- 4.646692361282089
+- 1.8306237575422901
+- 4.122865330019365
+- 3.0397300923058186
+- 4.6677268227967215
+- 1.9371385314833716
+- 4.263023058778151
+- 2.725959723272816
+- 4.6291104081254035
+- 1.9696765002394736
+jointangleoriginal_r:
+- 1.4784381591379478
+- 0.7117448943325635
+- 2.9519010200192963
+- 4.088610731625172
+- 2.7620053195636087
+- 2.742885001994002
+- 3.124344947264206
+- 2.1509430809768673
+- 3.451722386950354
+- 2.7280941775008234
+- 3.0307990269719896
+- 2.146997331566079
+- 0.0013772536792077154
+- 2.7083413862005443
+- 2.845549807862187
+- 2.0646303910724497
+- 3.3115636787834424
+- 2.6594906544574264
+- 2.9730984461621164
+- 2.0891292339697816
+- 3.5965360173715664
+timestamp: '2026-03-24T16:59:53.113578'
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/hand_config.yml b/src/linkerhand_retarget/linkerhand_retarget/config/hand_config.yml
new file mode 100644
index 0000000..8e32e01
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/hand_config.yml
@@ -0,0 +1,112 @@
+commandlower_right_l10: [255, 255, 255, 255, 255, 255, 255, 0, 0, 255, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_l10: [0, 0, 0, 0, 0, 0, 0, 255, 255, 0, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_l10: [255, 255, 255, 255, 255, 255, 0, 255, 255, 255, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_l10: [0, 0, 0, 0, 0, 0, 255, 0, 0, 0, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_l10v7: [255, 255, 255, 255, 255, 255, 255, 0, 0, 255, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_l10v7: [0, 0, 0, 0, 0, 0, 0, 255, 255, 0, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_l10v7: [255, 255, 255, 255, 255, 255, 0, 255, 255, 255, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_l10v7: [0, 0, 0, 0, 0, 0, 255, 0, 0, 0, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_t25: [255, 255, 255, 255, 255, 0, 255, None, 0, 0, 255, None, None, None, None, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
+commandupper_right_t25: [0, 0, 0, 0, 0, 255, 0, None, 255, 255, 0, None, None, None, None, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+commandlower_left_t25: [0, 255, 255, 255, 255, 255, 0, None, 255, 255, 0, None, None, None, None, 0, 255, 255, 255, 255, 0, 255, 255, 255, 255]
+commandupper_left_t25: [255, 0, 0, 0, 0, 0, 255, None, 0, 0, 255, None, None, None, None, 255, 0, 0, 0, 0, 255, 0, 0, 0, 0]
+commandlower_right_l20: [255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_right_l20: [0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_left_l20: [255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_left_l20: [0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_right_g20: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_right_g20: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_left_g20: [255, 255, 255, 255, 255, 0, 0, 0, 0, 0, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_left_g20: [0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_right_l7: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_l7: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_l7: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_l7: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_o7: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_o7: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_o7: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_o7: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_o7v1: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_o7v1: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_o7v1: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_o7v1: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_o7v3: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_o7v3: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_o7v3: [255, 255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_o7v3: [0, 0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_l25: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_right_l25: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_left_l25: [255, 255, 255, 255, 255, 255, 0, 0, 0, 0, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_left_l25: [0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_right_l21: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, None, None, None, None, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_right_l21: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_left_l21: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, None, None, None, None, 255, None, None, None, None, 255, 255, 255, 255, 255]
+commandupper_left_l21: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, None, None, None, None, 0, None, None, None, None, 0, 0, 0, 0, 0]
+commandlower_right_l6: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_l6: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_l6: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_l6: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_o6: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_o6: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_o6: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_o6: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_right_o6v1: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_right_o6v1: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandlower_left_o6v1: [255, 255, 255, 255, 255, 255, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandupper_left_o6v1: [0, 0, 0, 0, 0, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_l20: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_left_l20: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_right_g20: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_left_g20: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_right_l10: [20, 17, 1, 9, 13, 5, 0, 12, 4, 16, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_l10: [20, 17, 1, 9, 13, 5, 0, 12, 4, 16, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_l10v7: [20, 17, 1, 9, 13, 5, 0, 12, 4, 16, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_l10v7: [20, 17, 1, 9, 13, 5, 0, 12, 4, 16, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_t25: [18, 1, 9, 13, 5, 17, 0, None, 12, 4, 16, None, None, None, None, 19, 2, 10, 14, 6, 20, 3, 11, 15, 7]
+commandsourcedataindex_left_t25: [18, 1, 9, 13, 5, 17, 0, None, 12, 4, 16, None, None, None, None, 19, 2, 10, 14, 6, 20, 3, 11, 15, 7]
+commandsourcedataindex_right_l7: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_l7: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_o7: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_o7: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_o7v1: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_o7v1: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_o7v3: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_o7v3: [20, 17, 1, 9, 13, 5, 16, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_l25: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_left_l25: [18, 1, 9, 13, 5, 16, 0, 8, 12, 4, 17, None, None, None, None, 19, 2, 10, 14, 6]
+commandsourcedataindex_right_l21: [18, 1, 9, 13, 5, 17, 0, 8, 12, 4, 16, None, None, None, None, 19, None, None, None, None, 20, 3, 11, 15, 7]
+commandsourcedataindex_left_l21: [18, 1, 9, 13, 5, 17, 0, 8, 12, 4, 16, None, None, None, None, 19, None, None, None, None, 20, 3, 11, 15, 7]
+commandsourcedataindex_right_l6: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_l6: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_o6: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_o6: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_right_o6v1: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+commandsourcedataindex_left_o6v1: [20, 17, 1, 9, 13, 5, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_l20: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_left_l20: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_right_g20: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_left_g20: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_right_l25: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_left_l25: [2, 6, 10, 14, 18, 0, 5, 9, 13, 17, 1, None, None, None, None, 3, 7, 11, 15, 19]
+urdfdataindex_right_l7: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_l7: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_o7: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_o7: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_o7v1: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_o7v1: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_o7v3: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_o7v3: [4, 1, 5, 8, 11, 14, 0, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_l10: [4, 1, 6, 9, 13, 17, 5, 12, 16, 0, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_l10: [4, 1, 6, 9, 13, 17, 5, 12, 16, 0, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_l10v7: [4, 1, 6, 9, 13, 17, 5, 12, 16, 0, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_l10v7: [4, 1, 6, 9, 13, 17, 5, 12, 16, 0, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_t25: [2, 6, 9, 13, 17, 1, 5, None, 12, 16, 0, None, None, None, None, 3, 7, 10, 14, 18, 4, 8, 11, 15, 19]
+urdfdataindex_left_t25: [2, 6, 9, 13, 17, 1, 5, None, 12, 16, 0, None, None, None, None, 3, 7, 10, 14, 18, 4, 8, 11, 15, 19]
+urdfdataindex_right_l21: [14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, None, None, None, None, 15, None, None, None, None, 16, 2, 5, 8, 11]
+urdfdataindex_left_l21: [14, 1, 4, 7, 10, 13, 0, 3, 6, 9, 12, None, None, None, None, 15, None, None, None, None, 16, 2, 5, 8, 11]
+urdfdataindex_right_l6: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_l6: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_o6: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_o6: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_right_o6v1: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
+urdfdataindex_left_o6v1: [1, 0, 3, 5, 7, 9, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/human_hand_info.yml b/src/linkerhand_retarget/linkerhand_retarget/config/human_hand_info.yml
new file mode 100644
index 0000000..2918085
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/human_hand_info.yml
@@ -0,0 +1,53 @@
+initial_positions:
+ right_hand:
+ - [0.00000, 0.00000, 0.00000]
+ - [0.03380, 0.04230, 0.00000]
+ - [0.06920, 0.07760, 0.00000]
+ - [0.09370, 0.10220, 0.00000]
+ - [0.11820, 0.12680, 0.00000]
+ - [0.04380, 0.02680, 0.00000]
+ - [0.11460, 0.04040, 0.00000]
+ - [0.16370, 0.04040, 0.00000]
+ - [0.19150, 0.04040, 0.00000]
+ - [0.21750, 0.04040, 0.00000]
+ - [0.04590, 0.01030, 0.00000]
+ - [0.11610, 0.01450, 0.00000]
+ - [0.16960, 0.01450, 0.00000]
+ - [0.20310, 0.01450, 0.00000]
+ - [0.22910, 0.01450, 0.00000]
+ - [0.04570, -0.00180, 0.00000]
+ - [0.10860, -0.00830, 0.00000]
+ - [0.15520, -0.00830, 0.00000]
+ - [0.18750, -0.00830, 0.00000]
+ - [0.21350, -0.00830, 0.00000]
+ - [0.04290, -0.01630, 0.00000]
+ - [0.09910, -0.03110, 0.00000]
+ - [0.13650, -0.03110, 0.00000]
+ - [0.16000, -0.03110, 0.00000]
+ - [0.18600, -0.03110, 0.00000]
+ left_hand:
+ - [0.00000, 0.00000, 0.00000]
+ - [-0.03380, 0.04230, 0.00000]
+ - [-0.06920, 0.07760, 0.00000]
+ - [-0.09370, 0.10220, 0.00000]
+ - [-0.11820, 0.12680, 0.00000]
+ - [-0.04380, 0.02680, 0.00000]
+ - [-0.11460, 0.04040, 0.00000]
+ - [-0.16370, 0.04040, 0.00000]
+ - [-0.19150, 0.04040, 0.00000]
+ - [-0.21750, 0.04040, 0.00000]
+ - [-0.04590, 0.01030, 0.00000]
+ - [-0.11610, 0.01450, 0.00000]
+ - [-0.16960, 0.01450, 0.00000]
+ - [-0.20310, 0.01450, 0.00000]
+ - [-0.22910, 0.01450, 0.00000]
+ - [-0.04570, -0.00180, 0.00000]
+ - [-0.10860, -0.00830, 0.00000]
+ - [-0.15520, -0.00830, 0.00000]
+ - [-0.18750, -0.00830, 0.00000]
+ - [-0.21350, -0.00830, 0.00000]
+ - [-0.04290, -0.01630, 0.00000]
+ - [-0.09910, -0.03110, 0.00000]
+ - [-0.13650, -0.03110, 0.00000]
+ - [-0.16000, -0.03110, 0.00000]
+ - [-0.18600, -0.03110, 0.00000]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/linker_hand_info.yml b/src/linkerhand_retarget/linkerhand_retarget/config/linker_hand_info.yml
new file mode 100644
index 0000000..e12a786
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/linker_hand_info.yml
@@ -0,0 +1,289 @@
+initial_positions:
+# right_hand:
+# - [0.00000, 0.00000, 0.00000]
+# - [0.03380, 0.04230, 0.00000]
+# - [0.06920, 0.07760, 0.00000]
+# - [0.09370, 0.10220, 0.00000]
+# - [0.11820, 0.12680, 0.00000]
+# - [0.04380, 0.02680, 0.00000]
+# - [0.11460, 0.04040, 0.00000]
+# - [0.16370, 0.04040, 0.00000]
+# - [0.19150, 0.04040, 0.00000]
+# - [0.21750, 0.04040, 0.00000]
+# - [0.04590, 0.01030, 0.00000]
+# - [0.11610, 0.01450, 0.00000]
+# - [0.16960, 0.01450, 0.00000]
+# - [0.20310, 0.01450, 0.00000]
+# - [0.22910, 0.01450, 0.00000]
+# - [0.04570, -0.00180, 0.00000]
+# - [0.10860, -0.00830, 0.00000]
+# - [0.15520, -0.00830, 0.00000]
+# - [0.18750, -0.00830, 0.00000]
+# - [0.21350, -0.00830, 0.00000]
+# - [0.04290, -0.01630, 0.00000]
+# - [0.09910, -0.03110, 0.00000]
+# - [0.13650, -0.03110, 0.00000]
+# - [0.16000, -0.03110, 0.00000]
+# - [0.18600, -0.03110, 0.00000]
+ right_hand:
+ l16_3: # l20_8_右手
+ - [ 0.0, 0.0, 0.0 ]
+ - [ 0.013, 0.041, 0.008 ]
+ - [ 0.015, 0.058, -0.002 ]
+ - [ 0.048, 0.086, -0.002 ]
+ - [ 0.092, 0.127, 0.008 ]
+ - [ 0.073, 0.032, -0.012 ]
+ - [ 0.082, 0.041, -0.003 ]
+ - [ 0.126, 0.041, 0.0 ]
+ - [ 0.170, 0.041, 0.0 ]
+ - [ 0.209, 0.032, 0.0 ]
+ - [ 0.073, 0.0, -0.012 ]
+ - [ 0.082, 0.009, -0.003 ]
+ - [ 0.126, 0.009, 0.0 ]
+ - [ 0.170, 0.009, 0.0 ]
+ - [ 0.209, 0.0, 0.0 ]
+ - [ 0.073, -0.032, -0.012 ]
+ - [ 0.082, -0.023, -0.003 ]
+ - [ 0.126, -0.023, 0.0 ]
+ - [ 0.170, -0.023, 0.0 ]
+ - [ 0.209, -0.033, 0.0 ]
+ - [ 0.073, -0.064, -0.012 ]
+ - [ 0.082, -0.055, -0.003 ]
+ - [ 0.126, -0.055, 0.0 ]
+ - [ 0.170, -0.055, 0.0 ]
+ - [ 0.209, -0.064, 0.0 ]
+ l20_8: # l20_8_右手
+ - [0.0, 0.0, 0.0]
+ - [0.0490, 0.0080, -0.0220]
+ - [0.1210, 0.0560, -0.0320]
+ - [0.1490, 0.0760, -0.0190]
+ - [0.1690, 0.1000, -0.0230]
+ - [0.1540, 0.0270, 0.0290]
+ - [0.1540, 0.0310, 0.0150]
+ - [0.1990, 0.0290, 0.0180]
+ - [0.2300, 0.0350, 0.0100]
+ - [0.2550, 0.0270, 0.0230]
+ - [0.1590, 0.0050, 0.0330]
+ - [0.1590, 0.0100, 0.0190]
+ - [0.2040, 0.0070, 0.0230]
+ - [0.2340, 0.0130, 0.0150]
+ - [0.2590, 0.0060, 0.0280]
+ - [0.1540, -0.0170, 0.0310]
+ - [0.1540, -0.0120, 0.0170]
+ - [0.1990, -0.0140, 0.0210]
+ - [0.2300, -0.0090, 0.0120]
+ - [0.2550, -0.0160, 0.0260]
+ - [0.1450, -0.0380, 0.0270]
+ - [0.1450, -0.0330, 0.0140]
+ - [0.1900, -0.0360, 0.0170]
+ - [0.2210, -0.0300, 0.0090]
+ - [0.2460, -0.0380, 0.0220]
+ l20_6t: # l20_6t_右手
+ - [0.0, 0.0, 0.0]
+ - [0.0862, 0.0285, -0.0259]
+ - [0.1505, 0.0810, -0.0359]
+ - [0.1722, 0.1079, -0.0338]
+ - [0.1920, 0.1248, -0.0338]
+ - [0.1827, 0.0267, 0.0231]
+ - [0.1827, 0.0219, 0.0127]
+ - [0.2275, 0.0212, 0.0169]
+ - [0.2592, 0.0189, 0.0136]
+ - [0.2827, 0.0189, 0.0331]
+ - [0.1872, 0.0051, 0.0278]
+ - [0.1872, -0.0003, 0.0174]
+ - [0.2320, -0.0004, 0.0216]
+ - [0.2637, -0.0027, 0.0183]
+ - [0.2872, -0.0027, 0.0378]
+ - [0.1827, -0.0165, 0.0255]
+ - [0.1827, -0.0213, 0.0151]
+ - [0.2275, -0.0220, 0.0193]
+ - [0.2592, -0.0243, 0.0160]
+ - [0.2827, -0.0243, 0.0355]
+ - [0.1782, -0.0381, 0.0219]
+ - [0.1782, -0.0429, 0.0115]
+ - [0.2230, -0.0436, 0.0157]
+ - [0.2547, -0.0459, 0.0124]
+ - [0.2782, -0.0459, 0.0319]
+ l10_6: # l10_6右手
+ - [0.0, 0.0, 0.0]
+ - [0.085, 0.047, -0.013]
+ - [0.128, 0.075, -0.014]
+ - [0.154, 0.099, -0.009]
+ - [0.168, 0.123, -0.001]
+ - [0.158, 0.023, -0.012]
+ - [0.176, 0.031, 0.001]
+ - [0.209, 0.028, 0.006]
+ - [0.234, 0.028, 0.016]
+ - [0.256, 0.024, 0.031]
+ - [0.181, 0.012, 0.003]
+ - [0.181, 0.012, 0.003]
+ - [0.214, 0.009, 0.008]
+ - [0.239, 0.009, 0.018]
+ - [0.261, 0.005, 0.033]
+ - [0.158, -0.015, -0.012]
+ - [0.176, -0.007, 0.001]
+ - [0.209, -0.010, 0.006]
+ - [0.234, -0.010, 0.016]
+ - [0.256, -0.014, 0.031]
+ - [0.153, -0.034, -0.014]
+ - [0.171, -0.026, -0.001]
+ - [0.204, -0.029, 0.004]
+ - [0.229, -0.029, 0.014]
+ - [0.251, -0.033, 0.029]
+ t24_1: # t24_1_右手
+ - [0, 0, 0]
+ - [0.0680, 0.0210, -0.0100]
+ - [0.1020, 0.0430, -0.0120]
+ - [0.1360, 0.0700, -0.0220]
+ - [0.1620, 0.1030, -0.0400]
+ - [0.1340, 0.0340, -0.0140]
+ - [0.1510, 0.0360, -0.0080]
+ - [0.1930, 0.0400, -0.0080]
+ - [0.2370, 0.0400, -0.0060]
+ - [0.2820, 0.0340, 0.0050]
+ - [0.1420, 0.0110, -0.0140]
+ - [0.1590, 0.0140, -0.0080]
+ - [0.2010, 0.0170, -0.0080]
+ - [0.2450, 0.0170, -0.0060]
+ - [0.2890, 0.0110, 0.0070]
+ - [0.1320, -0.0110, -0.0140]
+ - [0.1490, -0.0090, -0.0080]
+ - [0.1910, -0.0050, -0.0090]
+ - [0.2360, -0.0060, -0.0070]
+ - [0.2800, -0.0120, 0.0040]
+ - [0.1190, -0.0340, -0.0140]
+ - [0.1360, -0.0320, -0.0080]
+ - [0.1780, -0.0280, -0.0070]
+ - [0.2220, -0.0280, -0.0040]
+ - [0.2670, -0.0340, 0.0070]
+# right_hand: # Human
+# - [0.00000, 0.00000, 0.00000]
+# - [0.03380, 0.04230, 0.00000]
+# - [0.06920, 0.07760, 0.00000]
+# - [0.09370, 0.10220, 0.00000]
+# - [0.11820, 0.12680, 0.00000]
+# - [0.04380, 0.02680, 0.00000]
+# - [0.11460, 0.04040, 0.00000]
+# - [0.16370, 0.04040, 0.00000]
+# - [0.19150, 0.04040, 0.00000]
+# - [0.21750, 0.04040, 0.00000]
+# - [0.04590, 0.01030, 0.00000]
+# - [0.11610, 0.01450, 0.00000]
+# - [0.16960, 0.01450, 0.00000]
+# - [0.20310, 0.01450, 0.00000]
+# - [0.22910, 0.01450, 0.00000]
+# - [0.04570, -0.00180, 0.00000]
+# - [0.10860, -0.00830, 0.00000]
+# - [0.15520, -0.00830, 0.00000]
+# - [0.18750, -0.00830, 0.00000]
+# - [0.21350, -0.00830, 0.00000]
+# - [0.04290, -0.01630, 0.00000]
+# - [0.09910, -0.03110, 0.00000]
+# - [0.13650, -0.03110, 0.00000]
+# - [0.16000, -0.03110, 0.00000]
+# - [0.18600, -0.03110, 0.00000]
+ left_hand: # l18_2_左手
+ l16_3:
+ - [0.0, 0.0, 0.0]
+ - [-0.0130, 0.0520, -0.0360]
+ - [-0.0450, 0.0630, -0.0360]
+ - [-0.0780, 0.0740, -0.0360]
+ - [-0.1200, 0.0810, -0.0270]
+ - [-0.0760, 0.0320, -0.0150]
+ - [-0.0820, 0.0220, -0.0030]
+ - [-0.1170, 0.0220, -0.0030]
+ - [-0.1510, 0.0220, -0.0030]
+ - [-0.1930, 0.0310, -0.0100]
+ - [-0.0760, 0.0000, -0.0150]
+ - [-0.0820, -0.0100, -0.0030]
+ - [-0.1170, -0.0100, -0.0030]
+ - [-0.1510, -0.0100, -0.0030]
+ - [-0.1930, -0.0010, -0.0100]
+ - [-0.0760, -0.0320, -0.0150]
+ - [-0.0820, -0.0420, -0.0030]
+ - [-0.1170, -0.0420, -0.0030]
+ - [-0.1510, -0.0420, -0.0030]
+ - [-0.1930, -0.0330, -0.0100]
+ - [-0.0760, -0.0640, -0.0150]
+ - [-0.0820, -0.0100, -0.0030]
+ - [-0.1170, -0.0100, -0.0030]
+ - [-0.1510, -0.0100, -0.0030]
+ - [-0.1930, -0.0010, -0.0100]
+ l20_8: # l20_8_右手
+ - [0.0, 0.0, 0.0]
+ - [-0.0490, 0.0080, -0.0220]
+ - [-0.1210, 0.0560, -0.0320]
+ - [-0.1490, 0.0760, -0.0190]
+ - [-0.1690, 0.1000, -0.0230]
+ - [-0.1540, 0.0270, 0.0290]
+ - [-0.1540, 0.0310, 0.0150]
+ - [-0.1990, 0.0290, 0.0180]
+ - [-0.2300, 0.0350, 0.0100]
+ - [-0.2550, 0.0270, 0.0230]
+ - [-0.1590, 0.0050, 0.0330]
+ - [-0.1590, 0.0100, 0.0190]
+ - [-0.2040, 0.0070, 0.0230]
+ - [-0.2340, 0.0130, 0.0150]
+ - [-0.2590, 0.0060, 0.0280]
+ - [-0.1540, -0.0170, 0.0310]
+ - [-0.1540, -0.0120, 0.0170]
+ - [-0.1990, -0.0140, 0.0210]
+ - [-0.2300, -0.0090, 0.0120]
+ - [-0.2550, -0.0160, 0.0260]
+ - [-0.1450, -0.0380, 0.0270]
+ - [-0.1450, -0.0330, 0.0140]
+ - [-0.1900, -0.0360, 0.0170]
+ - [-0.2210, -0.0300, 0.0090]
+ - [-0.2460, -0.0380, 0.0220]
+ l10_6: # l10_6右手
+ - [0.0, 0.0, 0.0]
+ - [-0.085, 0.047, -0.013]
+ - [-0.128, 0.075, -0.014]
+ - [-0.154, 0.099, -0.009]
+ - [-0.168, 0.123, -0.001]
+ - [-0.158, 0.023, -0.012]
+ - [-0.176, 0.031, 0.001]
+ - [-0.209, 0.028, 0.006]
+ - [-0.234, 0.028, 0.016]
+ - [-0.256, 0.024, 0.031]
+ - [-0.181, 0.012, 0.003]
+ - [-0.181, 0.012, 0.003]
+ - [-0.214, 0.009, 0.008]
+ - [-0.239, 0.009, 0.018]
+ - [-0.261, 0.005, 0.033]
+ - [-0.158, -0.015, -0.012]
+ - [-0.176, -0.007, 0.001]
+ - [-0.209, -0.010, 0.006]
+ - [-0.234, -0.010, 0.016]
+ - [-0.256, -0.014, 0.031]
+ - [-0.153, -0.034, -0.014]
+ - [-0.171, -0.026, -0.001]
+ - [-0.204, -0.029, 0.004]
+ - [-0.229, -0.029, 0.014]
+ - [-0.251, -0.033, 0.029]
+ t24_1: # t24_1_右手
+ - [0, 0, 0]
+ - [-0.0820, 0.0170, -0.0090]
+ - [-0.1090, 0.0480, -0.0130]
+ - [-0.1320, 0.0840, -0.0250]
+ - [-0.1480, 0.1230, -0.0420]
+ - [-0.1340, 0.0340, -0.0140]
+ - [-0.1510, 0.0360, -0.0080]
+ - [-0.1930, 0.0400, -0.0080]
+ - [-0.2370, 0.0400, -0.0060]
+ - [-0.2820, 0.0340, 0.0050]
+ - [-0.1420, 0.0110, -0.0140]
+ - [-0.1590, 0.0140, -0.0080]
+ - [-0.2010, 0.0170, -0.0080]
+ - [-0.2450, 0.0170, -0.0060]
+ - [-0.2890, 0.0110, 0.0070]
+ - [-0.1320, -0.0110, -0.0140]
+ - [-0.1490, -0.0090, -0.0080]
+ - [-0.1910, -0.0050, -0.0090]
+ - [-0.2360, -0.0060, -0.0070]
+ - [-0.2800, -0.0120, 0.0040]
+ - [-0.1190, -0.0340, -0.0140]
+ - [-0.1360, -0.0320, -0.0080]
+ - [-0.1780, -0.0280, -0.0070]
+ - [-0.2220, -0.0280, -0.0040]
+ - [-0.2670, -0.0340, 0.0070]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/model_config.yml b/src/linkerhand_retarget/linkerhand_retarget/config/model_config.yml
new file mode 100644
index 0000000..fb1ed63
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/model_config.yml
@@ -0,0 +1,410 @@
+target_position_end:
+ right_hand:
+ l20_8:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [ 0.0120, 0.0320, 0.0730]
+ - [ 0.0130, 0.0410, 0.1260]
+ - [ 0.0850, 0.0310, 0.1010]
+ - [-0.0080, 0.1270, 0.0920]
+ - [-0.0009, 0.0610, 0.1180]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ l20_8:
+ - [0.005, 0.034, 0.15] #Trumb + littleroot
+ - [0.006, 0.018, 0.149] #Trumb + littleroot
+ - [0.016, -0.002, 0.155] #Trumb + littleroot
+ - [0.023, -0.017, 0.149] #Trumb + littleroot
+ - [0.023, -0.144, 0.112] #Trumb default
+ - [0.023, -0.061, 0.184] #Trumb nearly index
+ - [0.077, -0.018, 0.179] #Trumb + index + print
+ l10_6:
+ - [0.078, 0.000, 0.123] #Trumb + littleroot
+ - [0.078, -0.006, 0.126] #Trumb + littleroot
+ - [0.078, -0.012, 0.127] #Trumb + littleroot
+ - [0.078, -0.018, 0.169] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.064, -0.017, 0.192] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+refer_position_end:
+ right_hand:
+ l20_8:
+ - [0.09, -0.05, 0.11]
+ - [0.1, -0.05, 0.14]
+ - [0.085, -0.03, 0.17]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ l10_6:
+ - [0.023, 0.034, 0.153] #Trumb + littleroot
+ - [0.023, 0.015, 0.158] #Trumb + littleroot
+ - [0.023, -0.012, 0.181] #Trumb + littleroot
+ - [0.023, -0.023, 0.158] #Trumb + littleroot
+ - [0.096, -0.122, 0.127] #Trumb default
+ - [0.025, -0.043, 0.175] #Trumb nearly index
+ - [0.058, -0.024, 0.184] #Trumb + index + print
+ l16_3:
+ - [ 0.0120, 0.0320, 0.0730]
+ - [ 0.0130, 0.0410, 0.1260]
+ - [ 0.0850, 0.0310, 0.1010]
+ - [-0.0080, 0.1270, 0.0920]
+ - [-0.0009, 0.0610, 0.1180]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1100]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ l20_8:
+ - [0.09, -0.05, 0.11]
+ - [0.1, -0.05, 0.14]
+ - [0.085, -0.03, 0.17]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ - [0.05, -0.02, 0.18]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+refer_rangerange_end:
+ right_hand:
+ l20_8:
+ - [0.02, 0.01, 0.01]
+ - [0.05, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01 ]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [ 0.0120, 0.0320, 0.0730]
+ - [ 0.0130, 0.0410, 0.1260]
+ - [ 0.0850, 0.0310, 0.1010]
+ - [-0.0080, 0.1270, 0.0920]
+ - [-0.0009, 0.0610, 0.1180]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ l20_8:
+ - [0.02, 0.01, 0.01]
+ - [0.05, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01 ]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+target_position_secondary:
+ right_hand:
+ l20_8:
+ - [0.051, -0.008, 0.114]
+ - [0.059, -0.012, 0.114]
+ - [0.064, 0.015, 0.114]
+ - [0.032, 0.061, 0.104]
+ - [0.032, 0.081, 0.090]
+ - [0.032, 0.045, 0.128]
+ - [0.064, 0.006, 0.115]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [ 0.0120, 0.0320, 0.0730]
+ - [ 0.0130, 0.0410, 0.1260]
+ - [ 0.0850, 0.0310, 0.1010]
+ - [-0.0080, 0.1270, 0.0920]
+ - [-0.0009, 0.0610, 0.1180]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ l20_8:
+ - [0.051, 0.008, 0.114]
+ - [0.059, 0.012, 0.114]
+ - [0.064, -0.015, 0.114]
+ - [0.032, -0.061, 0.104]
+ - [0.032, -0.081, 0.090]
+ - [0.032, -0.045, 0.128]
+ - [0.064, -0.006, 0.115]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+refer_position_secondary:
+ right_hand:
+ l20_8:
+ - [0.09, 0, 0.1]
+ - [0.07, 0, 0.12]
+ - [0.06, 0, 0.125]
+ - [0.04, 0.04, 0.13]
+ - [0.032, 0.081, 0.090]
+ - [0.032, 0.045, 0.128]
+ - [0.032, 0.045, 0.128]
+ l10_6:
+ - [ 0.011, 0.045, 0.126 ] #Trumb + littleroot
+ - [ 0.012, 0.022, 0.137 ] #Trumb + littleroot
+ - [ 0.011, 0.03, 0.141 ] #Trumb + littleroot
+ - [ 0.013, -0.013, 0.141 ] #Trumb + littleroot
+ - [ 0.032, -0.107, 0.145 ] #Trumb default
+ - [ 0.030, -0.051, 0.179 ] #Trumb nearly index
+ - [ 0.098, -0.020, 0.167 ] #Trumb + index + print
+ l16_3:
+ - [ 0.0120, 0.0320, 0.0730]
+ - [ 0.0130, 0.0410, 0.1260]
+ - [ 0.0850, 0.0310, 0.1010]
+ - [-0.0080, 0.1270, 0.0920]
+ - [-0.0009, 0.0610, 0.1180]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1100]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ l20_8:
+ - [0.09, 0, 0.1]
+ - [0.07, 0, 0.12]
+ - [0.06, 0, 0.125]
+ - [0.04, 0.04, 0.13]
+ - [0.032, 0.081, 0.090]
+ - [0.032, 0.045, 0.128]
+ - [0.032, 0.045, 0.128]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+refer_rangerange_secondary:
+ right_hand:
+ l20_8:
+ - [0.02, 0.01, 0.01]
+ - [0.05, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
+ left_hand:
+ l18_2:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ l20_8:
+ - [0.02, 0.01, 0.01]
+ - [0.05, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ - [0.01, 0.01, 0.01]
+ l10_6:
+ - [0.011, 0.045, 0.126] #Trumb + littleroot
+ - [0.012, 0.022, 0.137] #Trumb + littleroot
+ - [0.011, 0.03, 0.141] #Trumb + littleroot
+ - [0.013, -0.013, 0.141] #Trumb + littleroot
+ - [0.032, -0.107, 0.145] #Trumb default
+ - [0.030, -0.051, 0.179] #Trumb nearly index
+ - [0.098, -0.020, 0.167] #Trumb + index + print
+ l16_3:
+ - [-0.5000, 0.0000, 0.1000]
+ - [-0.5270, 0.0270, 0.0974]
+ - [-0.5927, 0.0723, 0.0965]
+ - [-0.6121, 0.0904, 0.0959]
+ - [-0.5000, 0.0000, 0.1000]
+ t24_1:
+ - [0.005, -0.034, 0.15] #Trumb + littleroot
+ - [0.006, -0.018, 0.149] #Trumb + littleroot
+ - [0.016, 0.002, 0.155] #Trumb + littleroot
+ - [0.023, 0.017, 0.149] #Trumb + littleroot
+ - [0.023, 0.144, 0.112] #Trumb default
+ - [0.023, 0.061, 0.184] #Trumb nearly index
+ - [0.077, 0.018, 0.179] #Trumb + index + print
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/retarget_config.yml b/src/linkerhand_retarget/linkerhand_retarget/config/retarget_config.yml
new file mode 100644
index 0000000..a521cd7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/retarget_config.yml
@@ -0,0 +1,98 @@
+right_hand:
+ l20:
+ - [ 0, 0, 0.8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.42, 0.99, 0.36, 0.91, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0]
+ - [ 0, 0, 0, 0, 0, 0.26, 0, 0, 0, 0.13, 0, 0, 0,-0.13, 0, 0, 0,-0.26, 0, 0, 0]
+ - [ 0, 0.97, 0.31, 0.81, 0, 0, 0.63, 1.06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.16, 0.97, 0.40, 0.81, 0, 0, 0, 0, 0, 0, 0.56, 1.08, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.31, 1.18, 0.38, 0.81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.61, 1.08, 0, 0, 0, 0, 0]
+ - [ 0.54, 1.18, 0.28, 0.91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.61, 1.08, 0]
+ l10:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l7:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l24:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l25:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l30:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+left_hand:
+ l20:
+ - [ 0, 0, 0.8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.42, 0.99, 0.36, 0.91, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0, 0, 1.40, 1.08, 0]
+ - [ 0, 0, 0, 0, 0, 0.26, 0, 0, 0, 0.13, 0, 0, 0,-0.13, 0, 0, 0,-0.26, 0, 0, 0]
+ - [ 0, 0.97, 0.31, 0.81, 0, 0, 0.63, 1.06, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.16, 0.97, 0.40, 0.81, 0, 0, 0, 0, 0, 0, 0.56, 1.08, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0.31, 1.18, 0.38, 0.81, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.61, 1.08, 0, 0, 0, 0, 0]
+ - [ 0.54, 1.18, 0.28, 0.91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.61, 1.08, 0]
+ l10:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l7:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l24:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l25:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ l30:
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
+ - [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/speed_config.yml b/src/linkerhand_retarget/linkerhand_retarget/config/speed_config.yml
new file mode 100644
index 0000000..631cab2
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/speed_config.yml
@@ -0,0 +1 @@
+l20_8:
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_left.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_left.yml
new file mode 100644
index 0000000..4b033fb
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_left.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/l10v7_left/linkerhand_l10v7_left.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_mcp_pitch', 'index_mcp_roll',
+ 'pinky_mcp_pitch', 'pinky_mcp_roll',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch','ring_mcp_roll',
+ 'thumb_cmc_roll', 'thumb_cmc_yaw', 'thumb_cmc_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ # target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 9, 14, 19, 24 ] ]
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_right.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_right.yml
new file mode 100644
index 0000000..2fa2561
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l10v7_right.yml
@@ -0,0 +1,22 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/linker_hand_l10_6_right.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_mcp_pitch', 'index_mcp_roll',
+ 'pinky_mcp_pitch', 'pinky_mcp_roll',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch','ring_mcp_roll',
+ 'thumb_cmc_roll', 'thumb_cmc_yaw', 'thumb_cmc_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ # target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 9, 14, 19, 24 ] ]
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_left.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_left.yml
new file mode 100644
index 0000000..7187a9c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_left.yml
@@ -0,0 +1,20 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/linker_hand_l20_8_left.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_joint0', 'index_joint1', 'index_joint2',
+ 'little_joint0', 'little_joint1', 'little_joint2',
+ 'middle_joint0', 'middle_joint1', 'middle_joint2',
+ 'ring_joint0', 'ring_joint1', 'ring_joint2',
+ 'thumb_joint0', 'thumb_joint1', 'thumb_joint2', 'thumb_joint3']
+ target_origin_link_names: [ "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link" ]
+ target_task_link_names: [ "thumb_link5", "thumb_link3", "index_link4", "middle_link4", "ring_link4", "little_link4" ,"index_link2", "middle_link2", "ring_link2", "little_link2"]
+ scaling_factor: 1.0
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [4, 2, 9, 14, 19, 24, 7, 12, 17, 22 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_right.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_right.yml
new file mode 100644
index 0000000..a8a7d12
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l20_right.yml
@@ -0,0 +1,19 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/linker_hand_l20_8_right.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_joint0', 'index_joint1', 'index_joint2',
+ 'little_joint0', 'little_joint1', 'little_joint2',
+ 'middle_joint0', 'middle_joint1', 'middle_joint2',
+ 'ring_joint0', 'ring_joint1', 'ring_joint2',
+ 'thumb_joint0', 'thumb_joint1', 'thumb_joint2', 'thumb_joint3']
+ target_origin_link_names: [ "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link", "base_link" ]
+ target_task_link_names: [ "thumb_link5", "thumb_link3", "index_link4", "middle_link4", "ring_link4", "little_link4" ,"index_link2", "middle_link2", "ring_link2", "little_link2"]
+ scaling_factor: 1.0
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], [4, 2, 9, 14, 19, 24, 7, 12, 17, 22 ] ]
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_left.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_left.yml
new file mode 100644
index 0000000..0161306
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_left.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/l6_left/linkerhand_l6_left.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['thumb_cmc_roll', 'thumb_cmc_pitch',
+ 'index_mcp_pitch',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch',
+ 'pinky_mcp_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 4, 4, 4, 4 ] ]
+# target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_right.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_right.yml
new file mode 100644
index 0000000..de5d884
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_l6_right.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/l6_right/linkerhand_l6_right.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['thumb_cmc_roll', 'thumb_cmc_pitch',
+ 'index_mcp_pitch',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch',
+ 'pinky_mcp_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 4, 4, 4, 4 ] ]
+# target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_left.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_left.yml
new file mode 100644
index 0000000..5885f58
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_left.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/o6_left/linkerhand_o6_left.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['thumb_cmc_yaw', 'thumb_cmc_pitch',
+ 'index_mcp_pitch',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch',
+ 'pinky_mcp_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 4, 4, 4, 4 ] ]
+# target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_right.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_right.yml
new file mode 100644
index 0000000..ecb5170
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o6_right.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/o6_right/linkerhand_o6_right.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['thumb_cmc_yaw', 'thumb_cmc_pitch',
+ 'index_mcp_pitch',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch',
+ 'pinky_mcp_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 4, 4, 4, 4 ] ]
+# target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_left.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_left.yml
new file mode 100644
index 0000000..a8dbbe2
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_left.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/o7v3_left/linkerhand_o7v3_left.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_mcp_pitch',
+ 'pinky_mcp_pitch',
+ 'middle_mcp_pitch',
+ 'ring_mcp_pitch',
+ 'thumb_cmc_roll', 'thumb_cmc_yaw', 'thumb_cmc_pitch']
+ target_origin_link_names: [ "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link", "hand_base_link"]
+ target_task_link_names: [ "thumb_distal", "index_distal", "middle_distal", "ring_distal", "pinky_distal"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ # target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 9, 14, 19, 24 ] ]
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_right.yml b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_right.yml
new file mode 100644
index 0000000..892cec2
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/config/teleop/linker_hand_o7v3_right.yml
@@ -0,0 +1,21 @@
+retargeting:
+ type: vector
+ urdf_path: linker_hand/linker_hand_l10_6_right.urdf
+
+ # Target refers to the retargeting target, which is the robot hand
+ target_joint_names: ['index_joint0', 'index_joint2',
+ 'little_joint0', 'little_joint2',
+ 'middle_joint1',
+ 'ring_joint0', 'ring_joint2',
+ 'thumb_joint0', 'thumb_joint1', 'thumb_joint3']
+ target_origin_link_names: [ "base_link", "base_link", "base_link", "base_link", "base_link"]
+ target_task_link_names: [ "thumb_link5", "index_link4", "middle_link3", "ring_link4", "little_link4"]
+ scaling_factor: 1
+
+ # Source refers to the retargeting input, which usually corresponds to the human hand
+ # The joint indices of human hand joint which corresponds to each link in the target_link_names
+ target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 9, 14, 19, 24 ] ]
+# target_link_human_indices: [ [ 0, 0, 0, 0, 0], [ 4, 8, 12, 16, 20 ] ]
+
+ # A smaller alpha means stronger filtering, i.e. more smooth but also larger latency
+ low_pass_alpha: 0.2
diff --git a/src/linkerhand_retarget/linkerhand_retarget/handretarget.py b/src/linkerhand_retarget/linkerhand_retarget/handretarget.py
new file mode 100644
index 0000000..9c9f0c0
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/handretarget.py
@@ -0,0 +1,354 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import sys
+import os
+from pathlib import Path
+
+# 强制使用src目录的路径
+def setup_src_paths():
+ """确保使用src目录而不是build目录"""
+ # 获取工作空间的绝对路径
+ current_file = Path(__file__).absolute()
+ workspace_dir = current_file.parent.parent.parent.parent
+
+ # 添加src目录到Python路径
+ src_package_dir = workspace_dir / "src" / "linkerhand_retarget" / "linkerhand_retarget"
+ if src_package_dir.exists():
+ paths_to_add = [
+ src_package_dir,
+ src_package_dir / "linkerhand",
+ ]
+
+ for path in paths_to_add:
+ if path.exists() and str(path) not in sys.path:
+ sys.path.insert(0, str(path))
+
+ return src_package_dir
+
+workspace_dir = setup_src_paths()
+
+
+import time
+from threading import Thread, Event
+from pathlib import Path
+from queue import Empty
+from typing import Optional
+import numpy as np
+import enum
+import signal, sys
+
+
+_script_dir = str(Path(__file__).parent)
+# 使用本地 linkerhand
+if _script_dir not in sys.path:
+ sys.path.insert(0, _script_dir)
+
+from linkerhand.utils import *
+from linkerhand.vtrdyncore import *
+from linkerhand.handcore import HandCore
+from linkerhand.config import HandConfig
+from linkerhand.constants import RetargetingType, DataSource, MotionSource, RobotName
+
+from ament_index_python.packages import get_package_share_directory
+from pathlib import Path
+
+import rclpy
+from rclpy.node import Node
+from rclpy.executors import MultiThreadedExecutor
+from sensor_msgs.msg import JointState
+from geometry_msgs.msg import PoseArray
+from std_msgs.msg import String
+from rcl_interfaces.msg import ParameterDescriptor
+
+import json
+
+
+vr_pose_cache_r = []
+vr_pose_cache_l = []
+video_pose_cache_r = []
+video_pose_cache_l = []
+reangle_r = []
+reangle_l = []
+right_hand_pose_end = []
+left_hand_pose_end = []
+
+
+def signal_handler(sig, frame):
+ rclpy.shutdown()
+
+
+class HandRetargetNode(Node):
+ def __init__(self):
+ super().__init__('handretarget_node')
+ print("Ready Create HandRetargetNode!")
+
+ package_share_dir = workspace_dir
+
+ self.robot_dir = package_share_dir / "assets" / "robots" / "hands"
+ self.base_config = package_share_dir
+
+ self.handconfig = HandConfig(str(self.robot_dir), str(self.base_config))
+ self.handcore = HandCore(self.handconfig)
+
+ self.baseconfig = self.handconfig.baseconfig
+ self.retagetconfig = self.handconfig.retagetconfig
+
+ # 声明参数并提供默认值
+ # 兼容 Foxy (无 dynamic_typing) 和 Jazzy (有 dynamic_typing)
+ try:
+ auto_scan_desc = ParameterDescriptor(dynamic_typing=True)
+ except (TypeError, AttributeError, AssertionError):
+ auto_scan_desc = ParameterDescriptor()
+
+ self.declare_parameters(
+ namespace='',
+ parameters=[
+ ('calibration', False),
+ ('ports', ['']),
+ ('baudrate', 0),
+ ('auto_scan', None, auto_scan_desc),
+ ]
+ )
+ #
+ self.scene, self.retargeting_r, self.retargeting_l, self.config_r, self.config_l = None, None, None, None, None
+ self.robot_name_r, self.robot_name_l = None, None
+ self.retargeting_type = None
+ self.datasource_type = None
+ self.motion_type = None
+ self.udp_ip, self.udp_port, self.use_can, self.motion_device = None, None, None, None
+
+ self.calibration = self.get_parameter('calibration').value
+ print(f"是否启用标定: {self.calibration} ")
+
+ # 读取命令行串口参数(候选列表)
+ cmd_ports = self.get_parameter('ports').value
+ self.cmd_ports = [p for p in cmd_ports if p] if cmd_ports else None
+ self.cmd_baudrate = self.get_parameter('baudrate').value or None
+ self.cmd_auto_scan = self.get_parameter('auto_scan').value
+
+ if self.cmd_ports:
+ print(f"命令行指定候选串口: {self.cmd_ports} @ {self.cmd_baudrate}")
+ if self.cmd_auto_scan:
+ print(f"命令行启用自动扫描")
+
+ self.calibrationopen_r, self.calibrationopen_l, self.calibrationclose_r, self.calibrationclose_l = None, None, None, None
+ self.retarget = None
+ self.datasource_type = DataSource[self.baseconfig["system"]["datasource_type"]]
+ self.retargeting_type = RetargetingType[self.baseconfig["system"]["retargeting_type"]]
+ self.motion_type = MotionSource[self.baseconfig["system"]["motion_type"]]
+ self.robot_name_r = RobotName[self.baseconfig["system"]["robotname_r"]]
+ self.robot_name_l = RobotName[self.baseconfig["system"]["robotname_l"]]
+
+ self.udp_ip = self.baseconfig["udp"]["ip"]
+ self.udp_port = int(self.baseconfig["udp"]["port"])
+ self.use_can = bool(self.baseconfig["system"]["usecan"])
+ self.motion_device = self.baseconfig["system"]["motion_device"]
+
+ # LinkerEG 配置
+ self.linkereg_port = self.baseconfig.get("linkereg", {}).get("port", None)
+ self.linkereg_password = self.baseconfig.get("linkereg", {}).get("password", "i")
+
+ self.righthandprint = bool(self.baseconfig["debug"]["joint_motor_debug_r"])
+ self.lefthandprint = bool(self.baseconfig["debug"]["joint_motor_debug_l"])
+
+ # if self.datasource_type == DataSource.vr:
+ # self.vr_right_sub = self.create_subscription(
+ # JointState,
+ # '/vr_right_hand_pose',
+ # self.vr_right_pose_callback,
+ # 10)
+ # self.vr_left_sub = self.create_subscription(
+ # JointState,
+ # '/vr_left_hand_pose',
+ # self.vr_left_pose_callback,
+ # 10)
+ # elif self.datasource_type == DataSource.video:
+ # self.video_right_sub = self.create_subscription(
+ # JointState,
+ # '/video_right_hand_pose',
+ # self.video_right_pose_callback,
+ # 10)
+ # self.video_left_sub = self.create_subscription(
+ # JointState,
+ # '/video_left_hand_pose',
+ # self.video_left_pose_callback,
+ # 10)
+
+ self.pubprintcount = 0
+
+ # 订阅遥操作参数话题
+ self.teleop_param_sub = self.create_subscription(
+ String,
+ '/hand_teleop_param',
+ self.teleop_param_callback,
+ 10
+ )
+
+ # 发布遥操作状态话题
+ self.teleop_state_pub = self.create_publisher(
+ String,
+ '/hand_teleop_state',
+ 10
+ )
+
+ # 当前模式
+ self.current_mode = 'glove'
+
+ def teleop_param_callback(self, msg):
+ """处理遥操作参数话题回调"""
+ try:
+ param = json.loads(msg.data)
+ mode = param.get('mode') # 可能为 None
+
+ if self.retarget is not None and hasattr(self.retarget, 'set_mode'):
+ self.retarget.set_mode(mode, param)
+ if mode:
+ self.current_mode = mode
+ # 发布状态反馈
+ state_msg = String()
+ state_msg.data = json.dumps({
+ 'mode': mode or self.current_mode,
+ 'status': 'success'
+ })
+ self.teleop_state_pub.publish(state_msg)
+ else:
+ self.get_logger().warn("retarget 未初始化或不支持 set_mode")
+ state_msg = String()
+ state_msg.data = json.dumps({
+ 'mode': mode or 'unknown',
+ 'status': 'failed',
+ 'error': 'retarget not initialized'
+ })
+ self.teleop_state_pub.publish(state_msg)
+ except json.JSONDecodeError as e:
+ self.get_logger().error(f"JSON 解析错误: {e}")
+ state_msg = String()
+ state_msg.data = json.dumps({
+ 'mode': 'unknown',
+ 'status': 'failed',
+ 'error': str(e)
+ })
+ self.teleop_state_pub.publish(state_msg)
+ except Exception as e:
+ self.get_logger().error(f"参数处理错误: {e}")
+
+ def retargetrun(self):
+ if self.motion_type == MotionSource.udexreal:
+ from linkerhand_retarget.motion.udexreal.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ ip=self.udp_ip,
+ port=self.udp_port,
+ deviceid=self.motion_device,
+ righthand=self.robot_name_r,
+ lefthand=self.robot_name_l,
+ handcore=self.handcore,
+ lefthandpubprint=self.lefthandprint,
+ righthandpubprint=self.righthandprint
+ )
+ elif self.motion_type == MotionSource.udexrealv2t:
+ from linkerhand_retarget.motion.udexrealv2t.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ ip=self.udp_ip,
+ port=self.udp_port,
+ deviceid=self.motion_device,
+ righthand=self.robot_name_r,
+ lefthand=self.robot_name_l,
+ handcore=self.handcore,
+ lefthandpubprint=self.lefthandprint,
+ righthandpubprint=self.righthandprint,
+ calibration = self.calibration
+ )
+ elif self.motion_type == MotionSource.linkerforce:
+ from linkerhand_retarget.motion.linkerforce.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ righthand=self.robot_name_r,
+ lefthand=self.robot_name_l,
+ handcore=self.handcore,
+ lefthandpubprint=self.lefthandprint,
+ righthandpubprint=self.righthandprint,
+ calibration = self.calibration,
+ baseconfig = self.baseconfig,
+ cmd_ports=self.cmd_ports,
+ cmd_baudrate=self.cmd_baudrate,
+ cmd_auto_scan=self.cmd_auto_scan
+ )
+ elif self.motion_type == MotionSource.vtrdyn:
+ from linkerhand_retarget.motion.vtrdyn.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ ip=self.udp_ip,
+ port=self.udp_port,
+ righthand=self.robot_name_r,
+ lefthand=self.robot_name_l,
+ handcore=self.handcore,
+ lefthandpubprint=self.lefthandprint,
+ righthandpubprint=self.righthandprint,
+ calibration = self.calibration
+ )
+ elif self.motion_type == MotionSource.linkermcg:
+ from linkerhand_retarget.motion.linkermcg.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ ip=self.udp_ip,
+ port=self.udp_port,
+ righthand=self.robot_name_r,
+ lefthand=self.robot_name_l,
+ handcore=self.handcore,
+ lefthandpubprint=self.lefthandprint,
+ righthandpubprint=self.righthandprint
+ )
+ elif self.motion_type == MotionSource.linkereg2:
+ from linkerhand_retarget.motion.linkereg.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ port=self.linkereg_port,
+ baudrate=921600,
+ password=self.linkereg_password,
+ isdebug=bool(self.baseconfig["debug"]["joint_pub_debug"]),
+ mode='sdk' # SDK控制模式
+ )
+ elif self.motion_type == MotionSource.linkereg1:
+ from linkerhand_retarget.motion.linkereg.retarget import Retarget
+ self.retarget = Retarget(
+ self,
+ port=self.linkereg_port,
+ baudrate=921600,
+ password=self.linkereg_password,
+ isdebug=bool(self.baseconfig["debug"]["joint_pub_debug"]),
+ mode='receiver' # 接收器控制模式 (需要连接灵巧手)
+ )
+ if self.retarget is None:
+ self.get_logger().error("未正确创建应用实例")
+ else:
+ print("启动应用实例")
+ self.retarget.process()
+
+def main(args=None):
+ rclpy.init(args=args)
+ node = None
+
+ try:
+ signal.signal(signal.SIGINT, signal_handler)
+ node = HandRetargetNode()
+ executor = MultiThreadedExecutor()
+ node.retargetrun()
+
+ # Keep the node alive
+ rclpy.spin(node, executor)
+ except KeyboardInterrupt:
+ if node is not None:
+ node.get_logger().info("收到终止信号")
+ finally:
+ if node is not None:
+ # 停止串口线程
+ if hasattr(node, 'retarget') and node.retarget and hasattr(node.retarget, 'stop_serial_threads'):
+ node.retarget.stop_serial_threads()
+ node.destroy_node()
+ rclpy.shutdown()
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/launch/linkerhand_retarget.launch.py b/src/linkerhand_retarget/linkerhand_retarget/launch/linkerhand_retarget.launch.py
new file mode 100644
index 0000000..5dbac64
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/launch/linkerhand_retarget.launch.py
@@ -0,0 +1,24 @@
+from launch import LaunchDescription
+from launch_ros.actions import Node
+from launch.actions import DeclareLaunchArgument
+from launch.substitutions import LaunchConfiguration
+import os
+
+def generate_launch_description():
+ return LaunchDescription([
+ DeclareLaunchArgument(
+ 'calibration',
+ default_value='True',
+ description='Enable Calibration'
+ ),
+
+ Node(
+ package='linkerhand_retarget',
+ executable='handretarget',
+ name='handretarget',
+ output='screen',
+ parameters=[{
+ 'calibration': LaunchConfiguration('calibration'),
+ }]
+ ),
+ ])
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/__init__.py
new file mode 100644
index 0000000..5147606
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/__init__.py
@@ -0,0 +1 @@
+__version__ = "2.7.0"
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/config.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/config.py
new file mode 100644
index 0000000..26979a5
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/config.py
@@ -0,0 +1,19 @@
+from .utils import *
+
+
+class HandConfig():
+ def __init__(self, robot_dir: str, config_path: str):
+ package_share_dir = config_path
+ self.handconfig = read_yaml(os.path.join(package_share_dir, 'config', 'hand_config.yml'))
+ self.baseconfig = read_yaml(os.path.join(package_share_dir, 'config', 'base_config.yml'))
+ self.retagetconfig = read_yaml(os.path.join(package_share_dir, 'config', 'retarget_config.yml'))
+ self.modelconfig = read_yaml(os.path.join(package_share_dir, 'config', 'model_config.yml'))
+
+ self.robot_dir = robot_dir
+
+ self.bodypose = read_yaml(
+ os.path.join(package_share_dir, 'config', f'{self.baseconfig["humanset"]["bodyfile"]}.yml'))
+ self.targetpose = read_yaml(
+ os.path.join(package_share_dir, 'config', f'{self.baseconfig["humanset"]["targethandfile"]}.yml'))
+ self.retagetconfig = read_yaml(os.path.join(package_share_dir, 'config', 'retarget_config.yml'))
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/constants.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/constants.py
new file mode 100644
index 0000000..7be3311
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/constants.py
@@ -0,0 +1,130 @@
+import enum
+from pathlib import Path
+from typing import Optional
+
+import numpy as np
+
+OPERATOR2MANO_RIGHT = np.array(
+ [
+ [0, 0, -1],
+ [-1, 0, 0],
+ [0, 1, 0],
+ ]
+)
+
+OPERATOR2MANO_LEFT = np.array(
+ [
+ [0, 0, -1],
+ [1, 0, 0],
+ [0, -1, 0],
+ ]
+)
+
+
+class RobotName(enum.Enum):
+ o7 = enum.auto()
+ o7v1 = enum.auto()
+ o7v3 = enum.auto()
+ o6 = enum.auto()
+ l6 = enum.auto()
+ l7 = enum.auto()
+ l10 = enum.auto()
+ l10v7 = enum.auto()
+ l20 = enum.auto()
+ l20lite = enum.auto()
+ l25 = enum.auto()
+ g20 = enum.auto()
+
+
+class RetargetingType(enum.Enum):
+ vector = enum.auto() # For teleoperation, no finger closing prior
+ position = enum.auto() # For offline data processing, especially hand-object interaction data
+ dexpilot = enum.auto() # For teleoperation, with finger closing prior
+ projection = enum.auto()
+
+
+class HandType(enum.Enum):
+ right = enum.auto()
+ left = enum.auto()
+
+
+class DataSource(enum.Enum):
+ motion = enum.auto()
+ video = enum.auto()
+ vr = enum.auto()
+
+
+class MotionSource(enum.Enum):
+ vtrdyn = enum.auto()
+ udexreal = enum.auto()
+ udexrealv2t = enum.auto()
+ linkerforce = enum.auto()
+ sensenova = enum.auto()
+ linkermcg = enum.auto()
+ linkereg1 = enum.auto()
+ linkereg2 = enum.auto()
+
+
+ROBOT_NAME_MAP = {
+ RobotName.o7: "linker_hand_o7",
+ RobotName.l7: "linker_hand_l7",
+ RobotName.o6: "linker_hand_o6",
+ RobotName.l6: "linker_hand_l6",
+ RobotName.o7v1: "linker_hand_o7v1",
+ RobotName.o7v3: "linker_hand_o7v3",
+ RobotName.l10: "linker_hand_l10",
+ RobotName.l10v7: "linker_hand_l10v7",
+ RobotName.l20: "linker_hand_l20",
+ RobotName.l20lite: "linker_hand_l20lite",
+ RobotName.l25: "linker_hand_l25",
+ RobotName.g20: "linker_hand_g20",
+}
+
+ROBOT_NAMES = list(ROBOT_NAME_MAP.keys())
+
+
+ROBOT_LEN_MAP ={
+ RobotName.o7: 7,
+ RobotName.l7: 7,
+ RobotName.o6: 6,
+ RobotName.l6: 6,
+ RobotName.o7v1: 7,
+ RobotName.o7v3: 7,
+ RobotName.l10: 10,
+ RobotName.l10v7: 10,
+ RobotName.l20: 20,
+ RobotName.l20lite: 10,
+ RobotName.l25: 20,
+ RobotName.g20: 20,
+}
+
+ROBOT_LEN = list(ROBOT_LEN_MAP.keys())
+
+
+def get_default_config_path(
+ robot_name: RobotName, retargeting_type: RetargetingType, hand_type: HandType
+) -> Optional[Path]:
+ config_path = Path(__file__).parent.parent / "config"
+ if retargeting_type is RetargetingType.position:
+ config_path = config_path / "offline"
+ else:
+ config_path = config_path / "teleop"
+
+ robot_name_str = ROBOT_NAME_MAP[robot_name]
+ hand_type_str = hand_type.name
+ if "gripper" in robot_name_str: # For gripper robots, only use gripper config file.
+ if retargeting_type == RetargetingType.dexpilot:
+ config_name = f"{robot_name_str}_dexpilot.yml"
+ else:
+ config_name = f"{robot_name_str}.yml"
+ else:
+ if retargeting_type == RetargetingType.dexpilot:
+ config_name = f"{robot_name_str}_{hand_type_str}_dexpilot.yml"
+ else:
+ config_name = f"{robot_name_str}_{hand_type_str}.yml"
+ return config_path / config_name
+
+OPERATOR2MANO = {
+ HandType.right: OPERATOR2MANO_RIGHT,
+ HandType.left: OPERATOR2MANO_LEFT,
+}
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/filter.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/filter.py
new file mode 100644
index 0000000..7401edd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/filter.py
@@ -0,0 +1,784 @@
+import numpy as np
+from typing import List, Optional
+from collections import deque
+
+class LCFilter:
+ """
+ LC低通滤波器(一阶低通滤波器)
+ 离散时间实现,常用于信号平滑
+ """
+
+ def __init__(self, alpha: float = 0.1, initial_value: float = 0.0):
+ """
+ 初始化LC滤波器
+
+ 参数:
+ alpha: 滤波系数 (0 < alpha <= 1)
+ alpha越小,滤波效果越强(更平滑)
+ alpha越大,响应越快(更灵敏)
+ initial_value: 初始值
+ """
+ if alpha <= 0 or alpha > 1:
+ raise ValueError("alpha必须在(0, 1]范围内")
+
+ self.alpha = alpha
+ self.filtered_value = initial_value
+ self.previous_raw = initial_value
+ self.previous_filtered = initial_value
+
+ # 历史记录(可选,用于调试)
+ self.history_raw = []
+ self.history_filtered = []
+
+ def update(self, new_value: float) -> float:
+ """
+ 更新滤波器并返回滤波后的值
+
+ 公式:y[n] = α * x[n] + (1-α) * y[n-1]
+ 其中:x[n]是当前输入,y[n-1]是上一次输出
+
+ 参数:
+ new_value: 新的输入值
+ 返回:
+ 滤波后的值
+ """
+ # 保存历史值
+ self.previous_raw = new_value
+ self.previous_filtered = self.filtered_value
+
+ # LC滤波公式
+ self.filtered_value = self.alpha * new_value + (1 - self.alpha) * self.filtered_value
+
+ # 记录历史(可选)
+ self.history_raw.append(new_value)
+ self.history_filtered.append(self.filtered_value)
+
+ return self.filtered_value
+
+ def update_array(self, new_values: List[float]) -> List[float]:
+ """
+ 批量更新数组
+
+ 参数:
+ new_values: 新的输入值列表
+ 返回:
+ 滤波后的值列表
+ """
+ filtered_values = []
+ for value in new_values:
+ filtered = self.update(value)
+ filtered_values.append(filtered)
+ return filtered_values
+
+ def reset(self, initial_value: float = 0.0):
+ """重置滤波器状态"""
+ self.filtered_value = initial_value
+ self.previous_raw = initial_value
+ self.previous_filtered = initial_value
+ self.history_raw = []
+ self.history_filtered = []
+
+ def get_state(self):
+ """获取当前状态"""
+ return {
+ 'filtered_value': self.filtered_value,
+ 'alpha': self.alpha,
+ 'history_length': len(self.history_raw)
+ }
+
+
+class MultiChannelLCFilter:
+ """
+ 多通道LC滤波器
+ 同时对多个信号进行滤波
+ """
+
+ def __init__(self, num_channels: int, alpha: float = 0.1,
+ initial_values: Optional[List[float]] = None):
+ """
+ 初始化多通道滤波器
+
+ 参数:
+ num_channels: 通道数量
+ alpha: 滤波系数
+ initial_values: 初始值列表,长度需等于num_channels
+ """
+ self.num_channels = num_channels
+ self.alpha = alpha
+
+ if initial_values is None:
+ initial_values = [0.0] * num_channels
+ elif len(initial_values) != num_channels:
+ raise ValueError(f"初始值长度必须等于通道数 {num_channels}")
+
+ # 为每个通道创建一个滤波器
+ self.filters = [LCFilter(alpha, initial_values[i]) for i in range(num_channels)]
+
+ def update(self, new_values: List[float]) -> List[float]:
+ """
+ 更新所有通道
+
+ 参数:
+ new_values: 新的输入值列表,长度需等于num_channels
+ 返回:
+ 滤波后的值列表
+ """
+ if len(new_values) != self.num_channels:
+ raise ValueError(f"输入值长度必须等于通道数 {self.num_channels}")
+
+ filtered_values = []
+ for i in range(self.num_channels):
+ filtered = self.filters[i].update(new_values[i])
+ filtered_values.append(filtered)
+
+ return filtered_values
+
+ def update_channel(self, channel_idx: int, new_value: float) -> float:
+ """
+ 更新单个通道
+
+ 参数:
+ channel_idx: 通道索引 (0-based)
+ new_value: 新的输入值
+ 返回:
+ 滤波后的值
+ """
+ if channel_idx < 0 or channel_idx >= self.num_channels:
+ raise ValueError(f"通道索引必须在[0, {self.num_channels-1}]范围内")
+
+ return self.filters[channel_idx].update(new_value)
+
+ def reset(self, initial_values: Optional[List[float]] = None):
+ """重置所有通道"""
+ if initial_values is None:
+ initial_values = [0.0] * self.num_channels
+
+ for i in range(self.num_channels):
+ self.filters[i].reset(initial_values[i])
+
+ def get_state(self):
+ """获取所有通道的状态"""
+ states = []
+ for i, filter_obj in enumerate(self.filters):
+ state = filter_obj.get_state()
+ state['channel'] = i
+ states.append(state)
+ return states
+
+
+class AdaptiveLCFilter(LCFilter):
+ """
+ 自适应LC滤波器
+ 根据信号变化自动调整alpha值
+ """
+
+ def __init__(self, alpha_min: float = 0.05, alpha_max: float = 0.3,
+ change_threshold: float = 0.1, initial_value: float = 0.0):
+ """
+ 初始化自适应滤波器
+
+ 参数:
+ alpha_min: 最小alpha值(信号稳定时使用)
+ alpha_max: 最大alpha值(信号快速变化时使用)
+ change_threshold: 变化阈值,超过此阈值认为信号在快速变化
+ initial_value: 初始值
+ """
+ super().__init__(alpha_max, initial_value) # 初始使用最大alpha
+ self.alpha_min = alpha_min
+ self.alpha_max = alpha_max
+ self.change_threshold = change_threshold
+
+ def update(self, new_value: float) -> float:
+ """
+ 自适应更新滤波器
+
+ 策略:如果信号变化大,使用较大的alpha快速响应
+ 如果信号稳定,使用较小的alpha平滑滤波
+ """
+ # 计算信号变化量
+ change_amount = abs(new_value - self.previous_raw)
+
+ # 自适应调整alpha
+ if change_amount > self.change_threshold:
+ # 信号快速变化,使用大alpha快速响应
+ self.alpha = self.alpha_max
+ else:
+ # 信号稳定,使用小alpha平滑滤波
+ self.alpha = self.alpha_min
+
+ # 调用父类更新方法
+ return super().update(new_value)
+
+
+def apply_lc_filter(data: List[float], alpha: float = 0.1) -> List[float]:
+ """
+ 对数据应用LC滤波(函数式版本)
+
+ 参数:
+ data: 输入数据列表
+ alpha: 滤波系数
+ 返回:
+ 滤波后的数据列表
+ """
+ if alpha <= 0 or alpha > 1:
+ raise ValueError("alpha必须在(0, 1]范围内")
+
+ if not data:
+ return []
+
+ filtered = [data[0]] # 第一个值直接使用
+
+ for i in range(1, len(data)):
+ # LC滤波公式
+ y = alpha * data[i] + (1 - alpha) * filtered[i-1]
+ filtered.append(y)
+
+ return filtered
+
+class KalmanFilter:
+ """
+ 卡尔曼滤波器(简化版)
+ 用于一维信号的滤波
+ """
+
+ def __init__(self,
+ process_variance: float = 1e-5,
+ measurement_variance: float = 0.1,
+ initial_value: float = 0.0,
+ initial_estimate_error: float = 1.0):
+ """
+ 初始化卡尔曼滤波器
+
+ 参数:
+ process_variance: 过程噪声方差(Q,系统不确定性)
+ measurement_variance: 测量噪声方差(R,传感器噪声)
+ initial_value: 初始状态估计值
+ initial_estimate_error: 初始估计误差协方差
+ """
+ # 系统模型(简单的一维模型)
+ self.process_variance = process_variance # Q
+ self.measurement_variance = measurement_variance # R
+
+ # 状态估计
+ self.x_hat = initial_value # 状态估计值
+ self.p = initial_estimate_error # 估计误差协方差
+
+ # 历史记录(可选)
+ self.history_measurement = []
+ self.history_estimate = []
+ self.history_kalman_gain = []
+
+ def update(self, measurement: float) -> float:
+ """
+ 卡尔曼滤波更新步骤
+
+ 参数:
+ measurement: 测量值
+ 返回:
+ 滤波后的估计值
+ """
+ # 1. 预测步骤
+ # 对于简单的一维模型,假设状态不变
+ x_hat_minus = self.x_hat # 先验状态估计
+ p_minus = self.p + self.process_variance # 先验估计误差
+
+ # 2. 更新步骤
+ # 计算卡尔曼增益
+ k = p_minus / (p_minus + self.measurement_variance) # 卡尔曼增益
+
+ # 更新状态估计
+ self.x_hat = x_hat_minus + k * (measurement - x_hat_minus)
+
+ # 更新估计误差协方差
+ self.p = (1 - k) * p_minus
+
+ # 记录历史
+ self.history_measurement.append(measurement)
+ self.history_estimate.append(self.x_hat)
+ self.history_kalman_gain.append(k)
+
+ return self.x_hat
+
+ def update_batch(self, measurements: List[float]) -> List[float]:
+ """
+ 批量更新
+
+ 参数:
+ measurements: 测量值列表
+ 返回:
+ 滤波后的估计值列表
+ """
+ estimates = []
+ for measurement in measurements:
+ estimate = self.update(measurement)
+ estimates.append(estimate)
+ return estimates
+
+ def reset(self,
+ initial_value: float = 0.0,
+ initial_estimate_error: float = 1.0):
+ """
+ 重置滤波器状态
+ """
+ self.x_hat = initial_value
+ self.p = initial_estimate_error
+ self.history_measurement = []
+ self.history_estimate = []
+ self.history_kalman_gain = []
+
+ def get_state(self) -> dict:
+ """
+ 获取当前状态
+ """
+ return {
+ 'estimate': self.x_hat,
+ 'error_covariance': self.p,
+ 'process_variance': self.process_variance,
+ 'measurement_variance': self.measurement_variance
+ }
+
+
+class MultiChannelKalmanFilter:
+ """
+ 多通道卡尔曼滤波器
+ 同时对多个独立信号进行滤波
+ """
+
+ def __init__(self,
+ num_channels: int,
+ process_variance: float = 1e-5,
+ measurement_variance: float = 0.1,
+ initial_values: Optional[List[float]] = None):
+ """
+ 初始化多通道卡尔曼滤波器
+
+ 参数:
+ num_channels: 通道数量
+ process_variance: 过程噪声方差
+ measurement_variance: 测量噪声方差
+ initial_values: 初始值列表
+ """
+ self.num_channels = num_channels
+
+ if initial_values is None:
+ initial_values = [0.0] * num_channels
+ elif len(initial_values) != num_channels:
+ raise ValueError(f"初始值长度必须等于通道数 {num_channels}")
+
+ # 为每个通道创建独立的卡尔曼滤波器
+ self.filters = [
+ KalmanFilter(
+ process_variance=process_variance,
+ measurement_variance=measurement_variance,
+ initial_value=initial_values[i],
+ initial_estimate_error=1.0
+ ) for i in range(num_channels)
+ ]
+
+ def update(self, measurements: List[float]) -> List[float]:
+ """
+ 更新所有通道
+
+ 参数:
+ measurements: 测量值列表,长度需等于num_channels
+ 返回:
+ 滤波后的估计值列表
+ """
+ if len(measurements) != self.num_channels:
+ raise ValueError(f"测量值长度必须等于通道数 {self.num_channels}")
+
+ estimates = []
+ for i in range(self.num_channels):
+ estimate = self.filters[i].update(measurements[i])
+ estimates.append(estimate)
+
+ return estimates
+
+ def update_channel(self, channel_idx: int, measurement: float) -> float:
+ """
+ 更新单个通道
+
+ 参数:
+ channel_idx: 通道索引
+ measurement: 测量值
+ 返回:
+ 滤波后的估计值
+ """
+ if channel_idx < 0 or channel_idx >= self.num_channels:
+ raise ValueError(f"通道索引必须在[0, {self.num_channels-1}]范围内")
+
+ return self.filters[channel_idx].update(measurement)
+
+ def reset(self, initial_values: Optional[List[float]] = None):
+ """
+ 重置所有通道
+ """
+ if initial_values is None:
+ initial_values = [0.0] * self.num_channels
+
+ for i in range(self.num_channels):
+ self.filters[i].reset(
+ initial_value=initial_values[i],
+ initial_estimate_error=1.0
+ )
+
+ def get_state(self, channel_idx: Optional[int] = None) -> dict:
+ """
+ 获取状态信息
+ """
+ if channel_idx is not None:
+ if channel_idx < 0 or channel_idx >= self.num_channels:
+ raise ValueError(f"通道索引必须在[0, {self.num_channels-1}]范围内")
+ return self.filters[channel_idx].get_state()
+ else:
+ states = []
+ for i, filter_obj in enumerate(self.filters):
+ state = filter_obj.get_state()
+ state['channel'] = i
+ states.append(state)
+ return {'channels': states}
+
+
+class AdaptiveKalmanFilter(KalmanFilter):
+ """
+ 自适应卡尔曼滤波器
+ 根据测量噪声自动调整参数
+ """
+
+ def __init__(self,
+ min_process_variance: float = 1e-6,
+ max_process_variance: float = 1e-3,
+ initial_measurement_variance: float = 0.1,
+ adaptation_rate: float = 0.01,
+ initial_value: float = 0.0):
+ """
+ 初始化自适应卡尔曼滤波器
+
+ 参数:
+ min_process_variance: 最小过程噪声方差
+ max_process_variance: 最大过程噪声方差
+ initial_measurement_variance: 初始测量噪声方差
+ adaptation_rate: 自适应调整速率
+ """
+ super().__init__(
+ process_variance=(min_process_variance + max_process_variance) / 2,
+ measurement_variance=initial_measurement_variance,
+ initial_value=initial_value
+ )
+
+ self.min_process_variance = min_process_variance
+ self.max_process_variance = max_process_variance
+ self.adaptation_rate = adaptation_rate
+ self.measurement_history = []
+
+ def update(self, measurement: float) -> float:
+ """
+ 自适应更新
+ """
+ # 保存测量历史
+ self.measurement_history.append(measurement)
+ if len(self.measurement_history) > 10:
+ self.measurement_history.pop(0)
+
+ # 计算最近的测量噪声
+ if len(self.measurement_history) >= 5:
+ recent_std = np.std(self.measurement_history[-5:])
+ # 根据噪声水平调整过程噪声方差
+ if recent_std > 0.1:
+ # 噪声大,增加过程噪声方差
+ self.process_variance = min(
+ self.process_variance * (1 + self.adaptation_rate),
+ self.max_process_variance
+ )
+ else:
+ # 噪声小,减小过程噪声方差
+ self.process_variance = max(
+ self.process_variance * (1 - self.adaptation_rate),
+ self.min_process_variance
+ )
+
+ # 调用父类更新方法
+ return super().update(measurement)
+
+
+class SavitzkyGolayFilter:
+ """
+ Savitzky-Golay滤波器(实时版本)
+ 适合保留波形特征的平滑
+ """
+
+ def __init__(self, window_length: int = 7, polyorder: int = 2,
+ deriv: int = 0, delta: float = 1.0):
+ """
+ 初始化Savitzky-Golay滤波器
+
+ 参数:
+ window_length: 窗口长度(必须为奇数,且大于polyorder)
+ polyorder: 多项式阶数
+ deriv: 微分阶数(0表示平滑,1表示一阶导等)
+ delta: 采样间隔
+ """
+ if window_length % 2 == 0:
+ raise ValueError("window_length必须是奇数")
+ if window_length <= polyorder:
+ raise ValueError("window_length必须大于polyorder")
+
+ self.window_length = window_length
+ self.polyorder = polyorder
+ self.deriv = deriv
+ self.delta = delta
+
+ # 数据缓冲区
+ self.buffer = deque(maxlen=window_length)
+
+ # 计算滤波器系数
+ self.coefficients = self._compute_coefficients()
+
+ # 历史记录
+ self.history_input = []
+ self.history_output = []
+
+ def _compute_coefficients(self) -> np.ndarray:
+ """
+ 计算Savitzky-Golay滤波器系数
+
+ 返回:
+ 滤波器系数数组
+ """
+ # 简单实现:使用滑动窗口多项式拟合
+ # 对于实时应用,我们只需要中心点的系数
+ half_window = self.window_length // 2
+
+ # 构建范德蒙矩阵
+ x = np.arange(-half_window, half_window + 1, dtype=float)
+ A = np.vander(x, self.polyorder + 1, increasing=True)
+
+ # 使用最小二乘法求解系数
+ # 对于Savitzky-Golay,我们只需要中心点的拟合值
+ # 这相当于取A的伪逆的第一行
+ coeff = np.linalg.pinv(A)[self.deriv]
+
+ # 考虑微分和采样间隔
+ if self.deriv > 0:
+ for i in range(self.deriv):
+ coeff = np.polyder(coeff)
+ coeff = coeff / (self.delta ** self.deriv)
+
+ return coeff
+
+ def update(self, new_value: float) -> float:
+ """
+ 更新滤波器并返回滤波后的值
+
+ 参数:
+ new_value: 新的输入值
+ 返回:
+ 滤波后的值
+ """
+ # 添加到缓冲区
+ self.buffer.append(new_value)
+
+ # 如果缓冲区未满,直接返回原值
+ if len(self.buffer) < self.window_length:
+ self.history_input.append(new_value)
+ self.history_output.append(new_value)
+ return new_value
+
+ # 应用Savitzky-Golay滤波
+ # 将缓冲区转换为数组
+ window_data = np.array(self.buffer)
+
+ # 使用预计算的系数进行卷积
+ filtered_value = np.dot(window_data, self.coefficients)
+
+ # 记录历史
+ self.history_input.append(new_value)
+ self.history_output.append(filtered_value)
+
+ # 限制历史长度
+ max_history = 1000
+ if len(self.history_input) > max_history:
+ self.history_input = self.history_input[-max_history:]
+ self.history_output = self.history_output[-max_history:]
+
+ return filtered_value
+
+ def update_batch(self, new_values: List[float]) -> List[float]:
+ """
+ 批量更新
+
+ 参数:
+ new_values: 新的输入值列表
+ 返回:
+ 滤波后的值列表
+ """
+ filtered_values = []
+ for value in new_values:
+ filtered = self.update(value)
+ filtered_values.append(filtered)
+ return filtered_values
+
+ def reset(self):
+ """重置滤波器状态"""
+ self.buffer.clear()
+ self.history_input = []
+ self.history_output = []
+
+ def get_state(self) -> dict:
+ """获取当前状态"""
+ return {
+ 'window_length': self.window_length,
+ 'polyorder': self.polyorder,
+ 'deriv': self.deriv,
+ 'buffer_size': len(self.buffer),
+ 'coefficients': self.coefficients.tolist()
+ }
+
+
+class MultiChannelSavitzkyGolayFilter:
+ """
+ 多通道Savitzky-Golay滤波器
+ """
+
+ def __init__(self, num_channels: int,
+ window_length: int = 7, polyorder: int = 2,
+ initial_values: Optional[List[float]] = None):
+ """
+ 初始化多通道滤波器
+
+ 参数:
+ num_channels: 通道数量
+ window_length: 窗口长度
+ polyorder: 多项式阶数
+ initial_values: 初始值列表
+ """
+ self.num_channels = num_channels
+
+ if initial_values is None:
+ initial_values = [0.0] * num_channels
+ elif len(initial_values) != num_channels:
+ raise ValueError(f"初始值长度必须等于通道数 {num_channels}")
+
+ # 为每个通道创建滤波器
+ self.filters = []
+ for i in range(num_channels):
+ filter_obj = SavitzkyGolayFilter(
+ window_length=window_length,
+ polyorder=polyorder
+ )
+ # 用初始值填充缓冲区
+ for _ in range(window_length // 2):
+ filter_obj.update(initial_values[i])
+ self.filters.append(filter_obj)
+
+ def update(self, new_values: List[float]) -> List[float]:
+ """
+ 更新所有通道
+
+ 参数:
+ new_values: 新的输入值列表
+ 返回:
+ 滤波后的值列表
+ """
+ if len(new_values) != self.num_channels:
+ raise ValueError(f"输入值长度必须等于通道数 {self.num_channels}")
+
+ filtered_values = []
+ for i in range(self.num_channels):
+ filtered = self.filters[i].update(new_values[i])
+ filtered_values.append(filtered)
+
+ return filtered_values
+
+ def reset(self, initial_values: Optional[List[float]] = None):
+ """重置所有通道"""
+ if initial_values is None:
+ initial_values = [0.0] * self.num_channels
+
+ for i in range(self.num_channels):
+ self.filters[i].reset()
+ # 用初始值预热
+ for _ in range(self.filters[i].window_length // 2):
+ self.filters[i].update(initial_values[i])
+
+
+class AdaptiveSavitzkyGolayFilter:
+ """
+ 自适应Savitzky-Golay滤波器
+ 根据信号特性自动调整参数
+ """
+
+ def __init__(self,
+ min_window: int = 5,
+ max_window: int = 15,
+ base_polyorder: int = 2,
+ noise_threshold: float = 0.05,
+ initial_value: float = 0.0):
+ """
+ 初始化自适应滤波器
+
+ 参数:
+ min_window: 最小窗口长度
+ max_window: 最大窗口长度
+ base_polyorder: 基础多项式阶数
+ noise_threshold: 噪声阈值
+ initial_value: 初始值
+ """
+ self.min_window = min_window
+ self.max_window = max_window
+ self.base_polyorder = base_polyorder
+ self.noise_threshold = noise_threshold
+
+ # 当前滤波器
+ self.current_filter = SavitzkyGolayFilter(
+ window_length=(min_window + max_window) // 2,
+ polyorder=base_polyorder
+ )
+
+ # 信号特性跟踪
+ self.signal_buffer = deque(maxlen=20)
+ self.current_noise_level = 0.0
+
+ def update(self, new_value: float) -> float:
+ """
+ 自适应更新
+ """
+ # 更新信号缓冲区
+ self.signal_buffer.append(new_value)
+
+ # 计算信号特性(噪声水平)
+ if len(self.signal_buffer) >= 10:
+ recent_data = np.array(self.signal_buffer)
+ self.current_noise_level = np.std(recent_data)
+
+ # 根据噪声水平调整窗口大小
+ if len(self.signal_buffer) >= 5:
+ if self.current_noise_level > self.noise_threshold * 2:
+ # 高噪声,使用大窗口强滤波
+ new_window = self.max_window
+ elif self.current_noise_level > self.noise_threshold:
+ # 中等噪声,使用中等窗口
+ new_window = (self.min_window + self.max_window) // 2
+ else:
+ # 低噪声,使用小窗口保留细节
+ new_window = self.min_window
+
+ # 如果窗口大小需要改变,创建新滤波器
+ if new_window != self.current_filter.window_length:
+ # 获取当前滤波器的输出作为新滤波器的初始状态
+ current_output = self.current_filter.update(new_value)
+
+ # 创建新滤波器
+ self.current_filter = SavitzkyGolayFilter(
+ window_length=new_window,
+ polyorder=min(self.base_polyorder, new_window - 1)
+ )
+
+ # 用当前输出预热新滤波器
+ for _ in range(new_window // 2):
+ self.current_filter.update(current_output)
+
+ return current_output
+
+ # 使用当前滤波器
+ return self.current_filter.update(new_value)
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcore.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcore.py
new file mode 100644
index 0000000..a0424ab
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcore.py
@@ -0,0 +1,338 @@
+from .config import *
+from .yourdfpy import URDF
+from .constants import RobotName,ROBOT_LEN,ROBOT_LEN_MAP
+import threading
+from pathlib import Path
+
+class HandCore():
+ def __init__(self, hand_config: HandConfig):
+ handconfig = hand_config.handconfig
+ self.baseconfig = hand_config.baseconfig
+ self.retagetconfig = hand_config.retagetconfig
+ self.modelconfig = hand_config.modelconfig
+ robot_dir = hand_config.robot_dir
+ targetpose = hand_config.targetpose
+ self.robot_name_str_r = self.baseconfig["system"]["robotname_r"]
+ self.robot_name_str_l = self.baseconfig["system"]["robotname_l"]
+
+ self.righturdfpath = os.path.join(robot_dir, 'linker_hand', f'{self.robot_name_str_r}_right', f'linkerhand_{self.robot_name_str_r}_right.urdf')
+ urdf_path = Path(self.righturdfpath)
+ if not urdf_path.exists():
+ raise ValueError(f"URDF path {urdf_path} does not exist")
+ self.dataminvalue_r = change_list(handconfig[f'commandlower_right_{self.robot_name_str_r}'])
+ self.datamaxvalue_r = change_list(handconfig[f'commandupper_right_{self.robot_name_str_r}'])
+ self.sourcedataindex_r = change_list(handconfig[f'commandsourcedataindex_right_{self.robot_name_str_r}'])
+ self.urdfdataindex_r = change_list(handconfig[f'urdfdataindex_right_{self.robot_name_str_r}'])
+ self.RightHandId = URDF.load(self.righturdfpath)
+
+
+ self.lefturdfpath = os.path.join(robot_dir, 'linker_hand', f'{self.robot_name_str_l}_left', f'linkerhand_{self.robot_name_str_l}_left.urdf')
+ urdf_path = Path(self.lefturdfpath)
+ if not urdf_path.exists():
+ raise ValueError(f"URDF path {urdf_path} does not exist")
+ self.dataminvalue_l = change_list(handconfig[f'commandlower_left_{self.robot_name_str_l}'])
+ self.datamaxvalue_l = change_list(handconfig[f'commandupper_left_{self.robot_name_str_l}'])
+ self.sourcedataindex_l = change_list(handconfig[f'commandsourcedataindex_left_{self.robot_name_str_l}'])
+ self.urdfdataindex_l = change_list(handconfig[f'urdfdataindex_left_{self.robot_name_str_l}'])
+ self.LeftHandId = URDF.load(self.lefturdfpath)
+
+ self.hand_lower_limits_r, self.hand_upper_limits_r, self.hand_joint_ranges_r = self.get_joint_limits(
+ self.RightHandId)
+ self.hand_lower_limits_l, self.hand_upper_limits_l, self.hand_joint_ranges_l = self.get_joint_limits(
+ self.LeftHandId)
+
+ self.hand_numjoints_r = ROBOT_LEN_MAP[RobotName[self.robot_name_str_r]]
+ self.hand_numjoints_l = ROBOT_LEN_MAP[RobotName[self.robot_name_str_l]]
+
+ if "human" in self.baseconfig["humanset"]["targethandfile"]:
+ self.right_hand_targetpose = np.array(targetpose['initial_positions']['right_hand'])
+ self.left_hand_targetpose = np.array(targetpose['initial_positions']['left_hand'])
+ else:
+ self.right_hand_targetpose = np.array(targetpose['initial_positions']['right_hand'][f'{self.robot_name_str_r}'])
+ self.left_hand_targetpose = np.array(targetpose['initial_positions']['left_hand'][f'{self.robot_name_str_l}'])
+ self.debugcount = 0
+ self.multi_target_kf_r = MultiTargetKalman(self.hand_numjoints_r)
+ self.multi_target_kf_l = MultiTargetKalman(self.hand_numjoints_l)
+
+ # 四元数测试用
+ self.angle = 0
+ self.counter = 0
+
+ # 共享数据
+ self.lock = threading.Lock() # 线程锁
+ self.right_joint_angles = [] # 存储关节角度等数据
+ self.left_joint_angles = [] # 存储关节角度等数据
+
+ @staticmethod
+ def get_joint_limits(robot):
+ joint_lower_limits = []
+ joint_upper_limits = []
+ joint_ranges = []
+ # 遍历所有关节
+ for joint_name, joint in robot.joint_map.items():
+ # 跳过固定关节
+ if joint.type == "fixed":
+ continue
+ # 获取关节限位值
+ if joint.limit is not None:
+ lower = joint.limit.lower
+ upper = joint.limit.upper
+ else:
+ # 对于没有明确限位的关节,使用默认值
+ # 连续旋转关节使用 ±π
+ if joint.type == "revolute":
+ lower = -3.1415926535 # -180°
+ upper = 3.1415926535 # +180°
+ # 平移关节使用 ±1m
+ elif joint.type == "prismatic":
+ lower = -1.0
+ upper = 1.0
+ # 其他类型关节使用 ±∞
+ else:
+ lower = float('-inf')
+ upper = float('inf')
+ # 添加到结果列表
+ joint_lower_limits.append(lower)
+ joint_upper_limits.append(upper)
+ joint_ranges.append(upper - lower)
+
+ return joint_lower_limits, joint_upper_limits, joint_ranges
+
+ @staticmethod
+ def projection_process(hand_position):
+ qpos = [0.0] * 30
+ cos_theta = 0
+ # 处理拇指部分,占用5个数据位
+ trumb_a = hand_position[0, :] # 对应MATLAB的 position_rightHand(1,:)
+ trumb_b = hand_position[1, :]
+ trumb_c = hand_position[2, :]
+ trumb_d = hand_position[3, :] # 夹角顶点
+ trumb_e = hand_position[4, :]
+ # 拇指侧摆部分处理成YZ平面
+ A_proj = np.array([trumb_a[1], trumb_a[2]])
+ B_proj = np.array([trumb_b[1], trumb_b[2]])
+ C_proj = np.array([trumb_c[1], trumb_c[2]])
+ vec_BA_proj = A_proj - B_proj
+ vec_BC_proj = C_proj - B_proj
+ dot_product = np.dot(vec_BA_proj, vec_BC_proj)
+ norm_AB = np.linalg.norm(vec_BA_proj)
+ norm_BC = np.linalg.norm(vec_BC_proj)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.arccos(dot_product / (norm_AB * norm_BC))
+ angle_deg = np.pi - cos_theta
+ # 拇指旋转
+ qpos[0] = angle_deg
+
+ # 拇指侧摆部分处理成XY平面
+ A_proj = trumb_a[:2] # 提取[X, Y]
+ B_proj = trumb_b[:2]
+ C_proj = trumb_c[:2]
+ vec_BA_proj = A_proj - B_proj
+ vec_BC_proj = C_proj - B_proj
+ dot_product = np.dot(vec_BA_proj, vec_BC_proj)
+ norm_AB = np.linalg.norm(vec_BA_proj)
+ norm_BC = np.linalg.norm(vec_BC_proj)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.arccos(dot_product / (norm_AB * norm_BC))
+ angle_deg = np.pi - cos_theta
+ # 拇指侧摆
+ qpos[1] = angle_deg
+
+ vecDC = trumb_c - trumb_d # 对应MATLAB的 vecBA = C - D
+ vecDE = trumb_e - trumb_d # 对应MATLAB的 vecBC = E - D
+ dot_product = np.dot(vecDC, vecDE)
+ norm_AB = np.linalg.norm(vecDC)
+ norm_BC = np.linalg.norm(vecDE)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 拇指末端夹角
+ qpos[4] = cos_theta
+
+ vecBC = trumb_b - trumb_c # 对应MATLAB的 vecBA = C - D
+ vecDC = trumb_d - trumb_c # 对应MATLAB的 vecBC = E - D
+ dot_product = np.dot(vecDC, vecBC)
+ norm_AB = np.linalg.norm(vecBC)
+ norm_BC = np.linalg.norm(vecDC)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 拇指中部夹角
+ qpos[3] = cos_theta
+
+ vecAB = trumb_a - trumb_b # 对应MATLAB的 vecBA = C - D
+ vecBC = trumb_c - trumb_b # 对应MATLAB的 vecBC = E - D
+ dot_product = np.dot(vecAB, vecBC)
+ norm_AB = np.linalg.norm(vecAB)
+ norm_BC = np.linalg.norm(vecBC)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 拇指根部夹角
+ qpos[2] = cos_theta
+
+ # 处理四指部分,占用5个数据位
+ # 食指侧摆部分处理成YZ平面
+ for i in range(4):
+ other_a = hand_position[5 + 5 * i, :]
+ other_b = hand_position[6 + 5 * i, :]
+ other_c = hand_position[7 + 5 * i, :]
+ other_d = hand_position[8 + 5 * i, :] # 夹角顶点
+ other_e = hand_position[9 + 5 * i, :]
+
+ A_proj = np.array([other_b[1], other_b[2] + 0.1])
+ B_proj = np.array([other_b[1], other_b[2]])
+ C_proj = np.array([other_c[1], other_b[2] + 0.1])
+ vecAB = A_proj - B_proj
+ vecBC = C_proj - B_proj
+ dot_product = np.dot(vecAB, vecBC)
+ norm_AB = np.linalg.norm(vecAB)
+ norm_BC = np.linalg.norm(vecBC)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.arccos(dot_product / (norm_AB * norm_BC))
+ # 侧摆
+ qpos[5 + 5 * i] = cos_theta
+
+ # 其余四指部分处理成XZ平面
+ # 处理末端CDE3点
+ A_proj = np.array([other_c[0], other_c[2]])
+ B_proj = np.array([other_d[0], other_d[2]])
+ C_proj = np.array([other_e[0], other_e[2]])
+ vecAB = A_proj - B_proj
+ vecBC = C_proj - B_proj
+ dot_product = np.dot(vecAB, vecBC)
+ norm_AB = np.linalg.norm(vecAB)
+ norm_BC = np.linalg.norm(vecBC)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 末端夹角
+ qpos[9 + 5 * i] = cos_theta
+
+ # 处理中部BCD3点
+ A_proj = np.array([other_b[0], other_b[2]])
+ B_proj = np.array([other_c[0], other_c[2]])
+ C_proj = np.array([other_d[0], other_d[2]])
+ vecAB = A_proj - B_proj
+ vecBC = C_proj - B_proj
+ dot_product = np.dot(vecAB, vecBC)
+ norm_AB = np.linalg.norm(vecAB)
+ norm_BC = np.linalg.norm(vecBC)
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 中部夹角
+ qpos[8 + 5 * i] = cos_theta
+
+ # 处理根部ABC3点
+ A_proj = np.array([other_a[0], other_a[2]])
+ B_proj = np.array([other_b[0], other_b[2]])
+ C_proj = np.array([other_c[0], other_c[2]])
+ vecAB = A_proj - B_proj
+ vecBC = C_proj - B_proj
+ dot_product = np.dot(vecAB, vecBC)
+ norm_AB = np.linalg.norm(vecAB)
+ norm_BC = np.linalg.norm(vecBC)
+ if norm_AB != 0 and norm_BC != 0:
+ cos_theta = np.pi - np.arccos(dot_product / (norm_AB * norm_BC))
+ # 根部夹角
+ qpos[7 + 5 * i] = cos_theta
+ return qpos
+
+ def trans_to_motor_left(self, temp_l):
+ jointpositions_l = [255.0] * self.hand_numjoints_l
+ for i in range(self.hand_numjoints_l):
+ if self.sourcedataindex_l[i] is not None:
+ val_l = temp_l[self.sourcedataindex_l[i]]
+ val_l = is_within_range(val_l,
+ self.hand_lower_limits_l[self.urdfdataindex_l[i]],
+ self.hand_upper_limits_l[self.urdfdataindex_l[i]])
+ jointpositions_l[i] = int(scale_value(val_l,
+ self.hand_lower_limits_l[self.urdfdataindex_l[i]],
+ self.hand_upper_limits_l[self.urdfdataindex_l[i]],
+ self.dataminvalue_l[i],
+ self.datamaxvalue_l[i]))
+ return jointpositions_l
+
+ def trans_to_motor_right(self, temp_r):
+ jointpositions_r = [255.0] * self.hand_numjoints_r
+ for i in range(self.hand_numjoints_r):
+ if self.sourcedataindex_r[i] is not None:
+ val_r = temp_r[self.sourcedataindex_r[i]]
+ val_r = is_within_range(val_r,
+ self.hand_lower_limits_r[self.urdfdataindex_r[i]],
+ self.hand_upper_limits_r[self.urdfdataindex_r[i]])
+
+ jointpositions_r[i] = int(scale_value(val_r,
+ self.hand_lower_limits_r[self.urdfdataindex_r[i]],
+ self.hand_upper_limits_r[self.urdfdataindex_r[i]],
+ self.dataminvalue_r[i],
+ self.datamaxvalue_r[i]))
+ return jointpositions_r
+
+
+ def generate_position(self, quaternion_r, quaternion_l):
+ rootorin_correct_r = get_quaternion_relative(trans_wxyzori_to_xyzwori(quaternion_r[0]),
+ [0, 0, 0, 1])
+ rootorin_correct_l = get_quaternion_relative(trans_wxyzori_to_xyzwori(quaternion_l[0]),
+ [0, 0, 0, 1])
+ handorin_correct_r = []
+ handorin_correct_l = []
+ for i in range(20):
+ handorin_correct_r.append(
+ trans_xyzwori_to_wxyzori(get_child_quaternion(trans_wxyzori_to_xyzwori(quaternion_r[i]),
+ rootorin_correct_r)))
+ for i in range(20):
+ handorin_correct_l.append(
+ trans_xyzwori_to_wxyzori(get_child_quaternion(trans_wxyzori_to_xyzwori(quaternion_l[i]),
+ rootorin_correct_l)))
+
+ right_hand_pose = quat2handposition(handorin_correct_r, self.right_hand_targetpose)
+ left_hand_pose = quat2handposition(handorin_correct_l, self.left_hand_targetpose)
+
+ # 绕 Y 轴的旋转-90度
+ Ry = rotate_matrix_y(np.radians(-90))
+ right_hand_pose = np.dot(Ry, right_hand_pose.T).T
+ # 先绕 Z 轴的旋转180度再绕Y轴旋转-90度
+ Rz = rotate_matrix_z(np.radians(-180))
+ left_hand_pose = np.dot(Rz, left_hand_pose.T).T
+ Ry = rotate_matrix_y(np.radians(-90))
+ left_hand_pose = np.dot(Ry, left_hand_pose.T).T
+ return right_hand_pose, left_hand_pose
+
+ def update_angles(self, rightangles, leftangle):
+ with self.lock:
+ self.right_joint_angles = rightangles
+ self.left_joint_angles = leftangle
+
+ def get_angles(self):
+ with self.lock:
+ return self.right_joint_angles.copy(), self.left_joint_angles.copy()
+
+class KalmanFilter:
+ def __init__(self, process_variance, measurement_variance, estimated_error, initial_value):
+ self.process_variance = process_variance # 过程噪声
+ self.measurement_variance = measurement_variance # 测量噪声
+ self.estimated_error = estimated_error # 初始估计误差
+ self.current_estimate = initial_value # 初始值
+
+ def update(self, measurement):
+ # 预测更新
+ self.estimated_error += self.process_variance
+
+ # 计算卡尔曼增益
+ kalman_gain = self.estimated_error / (self.estimated_error + self.measurement_variance)
+
+ # 更新估计值
+ self.current_estimate += kalman_gain * (measurement - self.current_estimate)
+ # 更新误差
+ self.estimated_error *= (1 - kalman_gain)
+
+ return self.current_estimate
+
+
+class MultiTargetKalman:
+ def __init__(self, num_targets, process_variance=0.01, measurement_variance=0.1, estimated_error=1,
+ initial_value=255):
+ self.kalman_filters = [KalmanFilter(process_variance, measurement_variance, estimated_error, initial_value) for
+ _ in range(num_targets)]
+ self.num_targets = num_targets
+ self.smoothed_data = [[] for _ in range(num_targets)]
+
+ def update(self, measurements, index):
+ smooth_value = self.kalman_filters[index].update(measurements)
+ return smooth_value
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcoreex.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcoreex.py
new file mode 100644
index 0000000..64a6a4b
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/handcoreex.py
@@ -0,0 +1,940 @@
+"""
+多态线性映射器
+支持任意数量的状态
+"""
+import numpy as np
+from colorama import Fore, init
+from typing import List, Dict, Tuple
+from .filter import MultiChannelLCFilter, MultiChannelSavitzkyGolayFilter, MultiChannelKalmanFilter
+
+class MultiStateLinearMapper:
+ """
+ 多态线性映射器
+ 支持任意数量的手势状态
+ """
+
+ def __init__(self,FINGER_CONFIGS,MAPPING_ORDER,is_debug = False):
+ self.finger_configs = FINGER_CONFIGS.copy()
+ self.mapping_order = MAPPING_ORDER.copy()
+
+ # 状态存储
+ self.glove_states = {} # {状态名: 手套角度数组}
+ self.robot_states = {} # {状态名: 机械手角度数组}
+ self.state_order = [] # 状态顺序列表
+ self.debug_value = [0.0] * 20 # 长度20的debug缓冲数据
+ self.isdebug = is_debug
+ self.debug_fingers = None # None=全部, []=全部, ["finger_name"]=指定手指
+
+ # self.filters = MultiChannelLCFilter(num_channels=11, alpha=0.1)
+ num_joints = 21
+
+ # 创建多通道Savitzky-Golay滤波器
+ self.filters = MultiChannelKalmanFilter(
+ num_channels=num_joints,
+ process_variance=1e-5,
+ measurement_variance=0.0005,
+ initial_values=[0.0] * num_joints
+ )
+
+ # self.filters = MultiChannelSavitzkyGolayFilter(
+ # num_channels=num_joints,
+ # window_length=,
+ # polyorder=3
+ # )
+
+ # 滤波参数
+ # self.filter_params = {
+ # 'window_length': 7,
+ # 'polyorder': 2,
+ # 'filter_type': 'Savitzky-Golay'
+ # }
+
+ # 历史记录(用于调试和可视化)
+ self.raw_history = []
+ self.filtered_history = []
+
+ def add_state(self, state_name: str,
+ glove_angles: List[float],
+ robot_angles: List[float]):
+ """
+ 添加一个手势状态
+
+ 参数:
+ state_name: 状态名称,如 'original', 'opose', 'fist'等
+ glove_angles: 手套角度 (21维)
+ robot_angles: 机械手角度 (11维)
+ """
+ self.glove_states[state_name] = np.array(glove_angles)
+ self.robot_states[state_name] = np.array(robot_angles)
+
+ if state_name not in self.state_order:
+ self.state_order.append(state_name)
+
+ def remove_state(self, state_name: str):
+ """移除一个状态"""
+ if state_name in self.glove_states:
+ del self.glove_states[state_name]
+ del self.robot_states[state_name]
+ if state_name in self.state_order:
+ self.state_order.remove(state_name)
+
+ def set_state_order(self, state_order: List[str]):
+ """
+ 设置状态顺序(从原始到最弯曲)
+
+ 示例:
+ ['original', 'opose', 'fist']
+ """
+ # 验证所有状态都存在
+ for state in state_order:
+ if state not in self.glove_states:
+ raise ValueError(f"状态 '{state}' 未定义")
+
+ self.state_order = state_order
+
+ def map_glove_to_robot(self, glove_current):
+ """
+ 动态权重映射
+ 在映射过程中根据其他手指状态调整权重
+ """
+
+ if isinstance(glove_current, np.ndarray):
+ glove_current = glove_current.tolist()
+ elif isinstance(glove_current, list):
+ glove_current = glove_current
+ else:
+ glove_current = list(glove_current)
+
+ if len(self.state_order) < 2:
+ raise ValueError("请至少设置两个状态")
+
+ if 'original' not in self.glove_states:
+ raise ValueError("必须包含 'original' 状态作为基准")
+
+ glove_current_arr = np.array(glove_current)
+ robot_angles = self.robot_states['original'].copy()
+
+ for config_name in self.mapping_order:
+ config = self.finger_configs[config_name]
+ angle = self._map_finger_multi_state(glove_current_arr, config)
+ robot_angles[config['robot_idx']] = angle
+
+ # self.debug_value[config['robot_idx']] = angle
+
+
+
+ return robot_angles
+
+ def _map_finger_multi_state(self, glove_current: np.ndarray,
+ config: dict) -> float:
+ """
+ 多状态手指映射
+ """
+ joints = config['joints']
+ weights = self._normalize_weights(config['weights'])
+ robot_idx = config['robot_idx']
+
+ # 计算当前融合值
+ current_fused = self._calculate_fused_value(
+ glove_current, joints, weights
+ )
+ # self.debug_value[robot_idx] = current_fused
+
+ # 计算所有状态的融合值
+ state_fused_values = {}
+ for state_name in self.state_order:
+ fused = self._calculate_reference_fused(
+ joints, weights, self.glove_states[state_name]
+ )
+ state_fused_values[state_name] = fused
+
+ # 获取所有状态的角度
+ state_angles = {}
+ for state_name in self.state_order:
+ state_angles[state_name] = self.robot_states[state_name][robot_idx]
+
+ # 分段线性插值
+ result_angle = self._multi_state_interpolation(
+ current_fused, state_fused_values, state_angles
+ )
+
+ # 处理反向运动
+ if config.get('reverse_motion', True):
+ # 找到最小和最大角度
+ min_angle = min(state_angles.values())
+ max_angle = max(state_angles.values())
+
+ result_angle = max_angle - (result_angle - min_angle)
+ # print("触发反向运动")
+
+ return result_angle
+
+ def _calculate_fused_value(self, data: np.ndarray,
+ joints: List[int],
+ weights) -> float:
+ """
+ 完整的融合值计算,处理上下限越界
+ """
+ # 确保有原始状态
+ if 'original' not in self.glove_states:
+ return 0.0
+
+ original = self.glove_states['original']
+ weights = np.array(weights)
+
+ # 归一化权重
+ if np.sum(weights) > 0:
+ weights = weights / np.sum(weights)
+
+ fused = 0.0
+
+ for i, idx in enumerate(joints):
+ # 获取当前值和原始值
+ current = data[idx]
+ orig = original[idx]
+
+ # 步骤1: 找到该关节在所有状态中的最小值和最大值
+ all_vals = [orig]
+ for state_data in self.glove_states.values():
+ all_vals.append(state_data[idx])
+
+ min_val = min(all_vals)
+ max_val = max(all_vals)
+
+ # 步骤2: 截断当前值到[min_val, max_val]范围
+ clamped = np.clip(current, min_val, max_val)
+
+ # 步骤3: 计算归一化位置
+ if abs(max_val - min_val) < 1e-6:
+ normalized_diff = 0.0
+ else:
+ orig_norm = (orig - min_val) / (max_val - min_val)
+ clamped_norm = (clamped - min_val) / (max_val - min_val)
+ normalized_diff = abs(clamped_norm - orig_norm)
+
+ fused += weights[i] * normalized_diff
+ return fused
+
+ def _calculate_reference_fused(self, joints: List[int],
+ weights: np.ndarray,
+ reference_data: np.ndarray) -> float:
+ """
+ 计算参考融合值
+ """
+ return self._calculate_fused_value(reference_data, joints, weights)
+
+ def _multi_state_interpolation(self, current_fused: float,
+ state_fused_values: Dict[str, float],
+ state_angles: Dict[str, float]) -> float:
+ """
+ 多状态分段线性插值
+ """
+ # 确保状态顺序正确
+ if not self.state_order:
+ return 0.0
+
+ # 处理边界情况
+ if current_fused <= state_fused_values[self.state_order[0]]:
+ return state_angles[self.state_order[0]]
+
+ if current_fused >= state_fused_values[self.state_order[-1]]:
+ return state_angles[self.state_order[-1]]
+
+ # 找到当前融合值所在区间
+ for i in range(len(self.state_order) - 1):
+ state1 = self.state_order[i]
+ state2 = self.state_order[i + 1]
+
+ fused1 = state_fused_values[state1]
+ fused2 = state_fused_values[state2]
+
+ # 确保区间有效
+ if fused1 <= current_fused <= fused2:
+ if fused2 - fused1 > 1e-6:
+ t = (current_fused - fused1) / (fused2 - fused1)
+ else:
+ t = 0.0
+
+ angle1 = state_angles[state1]
+ angle2 = state_angles[state2]
+ return angle1 + t * (angle2 - angle1)
+
+ # 如果没有找到区间(理论上不会发生),返回最近状态的角度
+ min_diff = float('inf')
+ nearest_angle = 0.0
+ for state_name in self.state_order:
+ diff = abs(current_fused - state_fused_values[state_name])
+ if diff < min_diff:
+ min_diff = diff
+ nearest_angle = state_angles[state_name]
+
+ return nearest_angle
+
+ def _normalize_weights(self, weights: List[float]) -> List[float]:
+ """
+ 归一化权重
+ """
+ if hasattr(weights, 'tolist'):
+ # 如果是 NumPy 数组
+ weight_list = weights.tolist()
+ elif isinstance(weights, list):
+ # 如果已经是列表
+ weight_list = weights
+ else:
+ # 其他情况,尝试转换
+ weight_list = list(weights)
+ total = np.sum(weight_list)
+ if total > 0:
+ result_array = weight_list / total
+ else:
+ result_array = weight_list
+
+ # 关键:转换回列表
+ return result_array.tolist()
+
+ def get_state_info(self) -> Dict:
+ """
+ 获取状态信息
+ """
+ # 基础信息
+ info = {
+ 'states': list(self.glove_states.keys()),
+ 'state_order': self.state_order,
+ 'has_original': 'original' in self.glove_states
+ }
+
+ return info
+
+
+ def clear_states(self):
+ """清除所有状态"""
+ self.glove_states.clear()
+ self.robot_states.clear()
+ self.state_order.clear()
+
+ def set_debug(self, enabled):
+ """
+ 设置 debug 模式
+
+ Args:
+ enabled: bool 或 list
+ - True: 开启调试,显示全部手指
+ - False: 关闭调试
+ - []: 开启调试,显示全部手指
+ - ["finger_name", ...]: 开启调试,只显示指定手指
+ """
+ if isinstance(enabled, bool):
+ self.isdebug = enabled
+ self.debug_fingers = None
+ elif isinstance(enabled, list):
+ self.isdebug = True
+ self.debug_fingers = enabled if enabled else None
+ else:
+ self.isdebug = bool(enabled)
+ self.debug_fingers = None
+
+ def _should_debug(self, finger_name: str) -> bool:
+ """检查是否应该输出该手指的调试信息"""
+ if not self.isdebug:
+ return False
+ if self.debug_fingers is None:
+ return True
+ return finger_name in self.debug_fingers
+
+
+class DynamicWeightMultiStateLinearMapper(MultiStateLinearMapper):
+ """
+ 动态权重多态线性映射器
+ 继承自MultiStateLinearMapper,增加动态权重调整功能
+ 增加扩展线性映射功能:基于open/opose线性映射,可以继续延伸
+ """
+
+ def __init__(self, FINGER_CONFIGS, MAPPING_ORDER,is_debug=False):
+ super().__init__(FINGER_CONFIGS, MAPPING_ORDER,is_debug)
+
+ # 动态权重配置
+ self.dynamic_weight_configs = {}
+
+ # 扩展映射配置
+ self.extended_mapping_enabled = {}
+ self.scale_factors = {}
+ self.exp_factors = {}
+ # self.isdebug = is_debug
+ # 缓存计算过的关节映射值
+ self.cached_mapped_values = {}
+
+ # 从配置表初始化扩展映射
+ self._init_extended_mapping_from_config()
+
+ def _init_extended_mapping_from_config(self):
+ """从配置表初始化扩展映射设置"""
+ for finger_name, config in self.finger_configs.items():
+ if config.get('dynamic_weight'):
+ self.set_dynamic_weight_config(finger_name, config['dynamic_weight'])
+ ext_config = config.get('extended_mapping')
+ if ext_config and ext_config.get('enabled', False):
+ self.extended_mapping_enabled[finger_name] = True
+
+ # 设置缩放因子
+ scale_factor = ext_config.get('scale_factor', 1.0)
+ if scale_factor != 1.0:
+ self.scale_factors[finger_name] = scale_factor
+ exp_factor = ext_config.get('extended_exp_factor', 1.0)
+ if exp_factor != 1.0:
+ self.exp_factors[finger_name] = exp_factor
+
+ def set_dynamic_weight_config(self, finger_name: str, config: Dict):
+ """
+ 设置动态权重配置
+ """
+ self.dynamic_weight_configs[finger_name] = config
+
+ def set_extended_mapping(self, finger_name: str, enabled: bool = True,
+ scale_factor: float = 1.0):
+ """
+ 手动设置扩展映射
+
+ 参数:
+ finger_name: 手指名称
+ enabled: 是否启用扩展映射
+ scale_factor: 缩放因子,>1加快映射,<1减慢映射
+ """
+ self.extended_mapping_enabled[finger_name] = enabled
+ if scale_factor != 1.0:
+ self.scale_factors[finger_name] = scale_factor
+
+ def fit_exp_factor(self, finger_name: str, current_fused: float,
+ fused_open: float, fused_opose: float,
+ angle_open: float, angle_opose: float, angle_fist: float) -> float:
+ """
+ 根据当前握拳值自动拟合延伸因子
+
+ 目标:使 current_fused 映射到 angle_fist
+
+ 公式:extension = slope * t * (1 + (exp-1) * t)
+ 其中 slope = angle_opose - angle_open, t = normalized - 1
+
+ 参数:
+ finger_name: 手指名称
+ current_fused: 当前握拳时的融合值
+ fused_open: 张开时的融合值
+ fused_opose: O型时的融合值
+ angle_open: 张开时的机械手角度
+ angle_opose: O型时的机械手角度
+ angle_fist: 握拳极限时的机械手角度
+
+ 返回:
+ 计算出的延伸因子
+ """
+ if abs(fused_opose - fused_open) < 1e-6:
+ return 1.0
+
+ normalized = (current_fused - fused_open) / (fused_opose - fused_open)
+
+ if normalized <= 1.0:
+ return 1.0
+
+ t = normalized - 1.0
+
+ slope = angle_opose - angle_open
+ target_extension = angle_fist - angle_opose
+
+ if abs(slope * t) < 1e-6 or abs(target_extension) < 1e-6:
+ return 1.0
+
+ base_extension = slope * t
+ ratio = target_extension / base_extension
+
+ exp_factor = (ratio - 1.0) / t + 1.0
+
+ return max(1.0, min(100.0, exp_factor))
+
+ def _apply_scale_factor(self, fused_value: float,
+ fused_open: float, fused_opose: float,
+ finger_name: str) -> float:
+ """
+ 应用缩放因子,基于归一化的[0,1]范围
+
+ 参数:
+ fused_value: 原始融合值
+ fused_open: open状态的融合值(映射到0)
+ fused_opose: opose状态的融合值(映射到1)
+ finger_name: 手指名称
+ """
+ scale_factor = self.scale_factors.get(finger_name, 1.0)
+
+ if scale_factor == 1.0:
+ return fused_value
+
+ # 将原始融合值归一化到[0,1]范围
+ # 融合值范围 [fused_open, fused_opose] -> [0, 1]
+ if abs(fused_opose - fused_open) < 1e-6:
+ normalized = 0.0
+ else:
+ normalized = (fused_value - fused_open) / (fused_opose - fused_open)
+
+ # 如果已经到达 opose 位置,不应用缩放
+ if abs(normalized - 1.0) < 1e-6:
+ return fused_value
+
+ # 应用缩放因子到归一化的值
+ scaled_normalized = normalized * scale_factor
+
+ # 将缩放后的归一化值转换回原始融合值范围
+ scaled_fused = fused_open + scaled_normalized * (fused_opose - fused_open)
+
+ return scaled_fused
+
+ def _get_max_angle(self, robot_idx: int) -> float:
+ """
+ 获取关节的最大角度
+ 如果有fist状态,使用fist状态的角度作为最大角度
+ 否则使用默认的最大角度
+ """
+ # 如果有fist状态,使用fist状态的角度
+ if 'fist' in self.robot_states:
+ return self.robot_states['fist'][robot_idx]
+
+ # 默认最大角度(可以根据需要调整)
+ return 1.57 # 默认90度
+
+ def map_glove_to_robot(self, source_current):
+ """
+ 动态权重映射
+ 在映射过程中根据其他手指状态调整权重
+ """
+ self.debug_value[3] = source_current[1]
+
+ glove_current = self.filters.update(source_current)
+ # 应用Savitzky-Golay滤波
+ # filtered_angles = self.filters.update(robot_angles)
+
+ # 记录历史(用于调试和分析)
+ self.raw_history.append(source_current.copy())
+ self.filtered_history.append(glove_current.copy())
+
+ # filtered_angles = self.filters.update(robot_angles)
+ # 限制历史记录长度
+ max_history = 100
+ if len(self.raw_history) > max_history:
+ self.raw_history = self.raw_history[-max_history:]
+ self.filtered_history = self.filtered_history[-max_history:]
+
+ self.debug_value[4] = glove_current[1]
+
+ if isinstance(glove_current, np.ndarray):
+ glove_current = glove_current.tolist()
+ elif isinstance(glove_current, list):
+ glove_current = glove_current
+ else:
+ glove_current = list(glove_current)
+
+ if len(self.state_order) < 2:
+ raise ValueError("请至少设置两个状态")
+
+ if 'original' not in self.glove_states:
+ raise ValueError("必须包含 'original' 状态作为基准")
+
+ # 重置缓存
+ self.cached_mapped_values = {}
+
+ glove_current_arr = np.array(glove_current)
+ robot_angles = self.robot_states['original'].copy()
+
+ # 第一遍:计算所有需要用于触发判断的手指映射值
+ for config_name in self.mapping_order:
+ if config_name in self.dynamic_weight_configs:
+ trigger_finger = self.dynamic_weight_configs[config_name]['trigger_finger']
+ # 先计算触发手指的映射值
+ if trigger_finger not in self.cached_mapped_values:
+ trigger_value = self._calculate_trigger_value(
+ glove_current_arr, trigger_finger
+ )
+ self.cached_mapped_values[trigger_finger] = trigger_value
+
+
+ i = 0
+ # 第二遍:使用动态权重进行映射
+ for config_name in self.mapping_order:
+ # 获取动态配置(如果有)
+ dynamic_config = self.dynamic_weight_configs.get(config_name)
+
+ if dynamic_config:
+ # 使用动态权重进行映射
+ config = self.finger_configs[config_name]
+ angle = self._map_finger_dynamic_weight(
+ glove_current_arr, config_name, dynamic_config, config
+ )
+ else:
+ # 使用多状态方法映射(支持扩展映射)
+ config = self.finger_configs[config_name]
+ angle = self._map_finger_multi_state(glove_current_arr, config)
+
+ robot_idx = self.finger_configs[config_name]['robot_idx']
+ robot_angles[robot_idx] = angle
+
+
+ return robot_angles
+
+ def _calculate_trigger_value(self, glove_current: np.ndarray,
+ trigger_finger: str) -> float:
+ """
+ 计算触发手指的归一化映射值(0-1范围)
+
+ 返回:
+ 归一化的映射值,0表示原始状态,1表示最弯曲状态
+ """
+ if trigger_finger not in self.finger_configs:
+ raise ValueError(f"触发手指配置 '{trigger_finger}' 不存在")
+
+ config = self.finger_configs[trigger_finger]
+
+ # 计算当前融合值
+ joints = config['joints']
+ weights = self._normalize_weights(config['weights'])
+
+ current_fused = self._calculate_fused_value(
+ glove_current, joints, weights
+ )
+
+ # 计算所有状态的融合值
+ state_fused_values = {}
+ for state_name in self.state_order:
+ fused = self._calculate_reference_fused(
+ joints, weights, self.glove_states[state_name]
+ )
+ state_fused_values[state_name] = fused
+
+ # 归一化到0-1范围
+ min_fused = min(state_fused_values.values())
+ max_fused = max(state_fused_values.values())
+
+ if abs(max_fused - min_fused) < 1e-6:
+ return 0.0
+
+ normalized = (current_fused - min_fused) / (max_fused - min_fused)
+ return np.clip(normalized, 0.0, 1.0)
+
+ def _map_finger_dynamic_weight(self, glove_current: np.ndarray,
+ finger_name: str,
+ dynamic_config: Dict,
+ base_config: Dict) -> float:
+ """
+ 使用动态权重进行手指映射
+ """
+ # 获取触发值
+ trigger_finger = dynamic_config['trigger_finger']
+ if trigger_finger not in self.cached_mapped_values:
+ trigger_value = self._calculate_trigger_value(
+ glove_current, trigger_finger
+ )
+ self.cached_mapped_values[trigger_finger] = trigger_value
+ else:
+ trigger_value = self.cached_mapped_values[trigger_finger]
+
+ # 根据阈值选择配置
+ threshold = dynamic_config['threshold']
+ temp_config = self.finger_configs[finger_name].copy() # 默认使用基础配置
+
+ if trigger_value < threshold:
+ weight_config = dynamic_config['low_weight_config']
+ # 创建临时配置
+
+ temp_config['joints'] = weight_config['joints']
+ temp_config['weights'] = weight_config['weights']
+ if 'reverse_motion' in weight_config:
+ temp_config['reverse_motion'] = weight_config['reverse_motion']
+ else:
+ temp_config['reverse_motion'] = base_config.get('reverse_motion', False)
+ else:
+ # 使用高权重配置
+ weight_config = dynamic_config.get('high_weight_config', {})
+ # 创建临时配置,合并基础配置和高权重配置
+ if weight_config: # 如果有高权重配置
+ temp_config['joints'] = weight_config.get('joints', temp_config['joints'])
+ temp_config['weights'] = weight_config.get('weights', temp_config['weights'])
+ # 优先使用高权重配置的reverse_motion
+ if 'reverse_motion' in weight_config:
+ temp_config['reverse_motion'] = weight_config['reverse_motion']
+
+ # 使用临时配置进行映射(支持扩展映射)
+ return self._map_finger_multi_state(glove_current, temp_config)
+
+ def _map_finger_multi_state(self, glove_current: np.ndarray,
+ config: dict) -> float:
+ """
+ 手指映射主方法
+ 支持扩展映射和原始多状态映射
+ """
+ # 查找手指名称
+ finger_name = None
+ for name, cfg in self.finger_configs.items():
+ if cfg['robot_idx'] == config['robot_idx']:
+ finger_name = name
+ break
+ # print(self.extended_mapping_enabled)
+ # 检查是否启用扩展映射
+ if (finger_name and finger_name in self.extended_mapping_enabled and
+ self.extended_mapping_enabled[finger_name]):
+ # print("触发线性映射")
+ return self._map_finger_extended(glove_current, config, finger_name)
+ else:
+ # 使用原始的多状态映射
+ return self._map_finger_original(glove_current, config, finger_name)
+
+ def _map_finger_original(self, glove_current: np.ndarray,
+ config: dict, finger_name: str = None) -> float:
+ """
+ 原始的多状态手指映射
+ """
+ joints = config['joints']
+ weights = self._normalize_weights(config['weights'])
+ robot_idx = config['robot_idx']
+
+ # 计算当前融合值
+ current_fused = self._calculate_fused_value(
+ glove_current, joints, weights
+ )
+
+ # 计算所有状态的融合值
+ state_fused_values = {}
+ for state_name in self.state_order:
+ fused = self._calculate_reference_fused(
+ joints, weights, self.glove_states[state_name]
+ )
+ state_fused_values[state_name] = fused
+
+ # 获取所有状态的角度
+ state_angles = {}
+ for state_name in self.state_order:
+ state_angles[state_name] = self.robot_states[state_name][robot_idx]
+
+ if self._should_debug(finger_name):
+ print(f"\n=== {finger_name} 调试信息 (original) ===")
+ print(f"启用状态: {self.state_order}")
+ print(f"权重: {config['weights']}")
+ joints = config['joints']
+ glove_joints_vals = {f"glove[{j}]": glove_current[j] for j in joints}
+ print(f"手套数据: {glove_joints_vals}")
+ print(f"融合值: {current_fused:.6f}")
+ print(f"状态融合值: {state_fused_values}")
+ print(f"状态角度: {state_angles}")
+
+ # 分段线性插值
+ result_angle = self._multi_state_interpolation(
+ current_fused, state_fused_values, state_angles
+ )
+
+ if self._should_debug(finger_name):
+ print(f"插值结果: {result_angle:.6f}")
+
+ # 处理反向运动
+ if config.get('reverse_motion', True):
+ # 找到最小和最大角度
+ min_angle = min(state_angles.values())
+ max_angle = max(state_angles.values())
+
+ result_angle = max_angle - (result_angle - min_angle)
+ if self._should_debug(finger_name):
+ print(f"reverse_motion=True, 反转后: {result_angle:.6f}")
+
+ return result_angle
+
+ def _map_finger_extended(self, glove_current: np.ndarray,
+ config: dict, finger_name: str) -> float:
+ """
+ 多段映射实现
+
+ 根据 state_order 决定映射段数:
+ - ['origin', 'opose', 'fist'] → 三段映射,截断到 fist
+ - ['origin', 'opose'] + extended_mapping.enabled=True → 两段映射,延伸到 fist 截断
+ - ['origin', 'opose'] + extended_mapping.enabled=False → 两段映射,截断到 opose
+ """
+ joints = config['joints']
+ weights = self._normalize_weights(config['weights'])
+ robot_idx = config['robot_idx']
+
+ # 获取启用的状态列表
+ states = self.state_order
+ num_states = len(states)
+
+ if num_states < 2:
+ print(f"警告:状态数量不足,回退到原始映射")
+ return self._map_finger_original(glove_current, config)
+
+ # 计算当前融合值
+ current_fused_raw = self._calculate_fused_value(glove_current, joints, weights)
+
+ # 计算第一个和最后一个状态的融合值
+ fused_first = self._calculate_reference_fused(joints, weights, self.glove_states[states[0]])
+ fused_last = self._calculate_reference_fused(joints, weights, self.glove_states[states[-1]])
+
+ # 应用缩放因子
+ current_fused = self._apply_scale_factor(current_fused_raw, fused_first, fused_last, finger_name)
+
+ # 获取第一个和最后一个状态的角度
+ angle_first = self.robot_states[states[0]][robot_idx]
+ angle_last = self.robot_states[states[-1]][robot_idx]
+
+ # 确保顺序正确
+ if angle_first > angle_last:
+ angle_first, angle_last = angle_last, angle_first
+
+ if self._should_debug(finger_name):
+ print(f"\n=== {finger_name} 调试信息 ===")
+ print(f"启用状态: {states}")
+ print(f"权重: {config['weights']}")
+ print(f"原始融合值: {current_fused_raw:.6f}")
+ print(f"缩放后融合值: {current_fused:.6f}")
+ print(f"融合值范围: [{fused_first:.6f}, {fused_last:.6f}]")
+ print(f"机械手角度范围: [{angle_first:.6f}, {angle_last:.6f}]")
+
+ # 归一化融合值
+ if abs(fused_last - fused_first) < 1e-6:
+ normalized_fused = 0.5
+ else:
+ normalized_fused = (current_fused - fused_first) / (fused_last - fused_first)
+
+ if self._should_debug(finger_name):
+ print(f"归一化融合值: {normalized_fused:.6f}")
+
+ # 判断是否需要延伸(只有 original + opose 两段模式才启用)
+ extrapolation_enabled = self.extended_mapping_enabled.get(finger_name, False)
+ use_extrapolation = extrapolation_enabled and states == ['original', 'opose']
+
+ if num_states >= 3:
+ result_angle = self._multi_state_map(joints, weights, robot_idx, current_fused_raw, finger_name)
+ elif use_extrapolation:
+ result_angle = self._extrapolate_to_fist(
+ current_fused, fused_first, fused_last,
+ angle_first, angle_last, robot_idx, finger_name, joints, weights
+ )
+ else:
+ result_angle = self._two_state_map(
+ current_fused, fused_first, fused_last,
+ angle_first, angle_last, finger_name
+ )
+
+ if config.get('reverse_motion', False):
+ min_angle = min(angle_first, angle_last)
+ max_angle = max(angle_first, angle_last)
+ clamped = np.clip(result_angle, min_angle, max_angle)
+ result_angle = max_angle - (clamped - min_angle)
+
+ return result_angle
+
+ def _multi_state_map(self, joints, weights, robot_idx, current_fused, finger_name):
+ """多段映射:使用所有启用的状态进行分段插值"""
+ state_fused_values = {}
+ state_angles = {}
+
+ for state_name in self.state_order:
+ fused = self._calculate_reference_fused(joints, weights, self.glove_states[state_name])
+ state_fused_values[state_name] = fused
+ state_angles[state_name] = self.robot_states[state_name][robot_idx]
+
+ result_angle = self._multi_state_interpolation(current_fused, state_fused_values, state_angles)
+
+ if self._should_debug(finger_name):
+ print(f"多段映射结果: {result_angle:.6f}")
+
+ return result_angle
+
+ def _extrapolate_to_fist(self, current_fused, fused_first, fused_last,
+ angle_first, angle_last, robot_idx, finger_name, joints, weights):
+ """两段映射 + 延伸映射,截断到 fist 角度"""
+ if abs(fused_last - fused_first) < 1e-6:
+ normalized = 0.5
+ else:
+ normalized = (current_fused - fused_first) / (fused_last - fused_first)
+
+ if self._should_debug(finger_name):
+ print(f"归一化融合值: {normalized:.6f}")
+
+ exp_factor = self.exp_factors.get(finger_name, 1.0)
+ slope = angle_last - angle_first
+
+ if normalized <= 0:
+ result_angle = angle_first
+ if self._should_debug(finger_name):
+ print(f"归一化值<=0: result_angle={result_angle:.6f}")
+ elif normalized <= 1:
+ result_angle = angle_first + normalized * slope
+ if self._should_debug(finger_name):
+ print(f"归一化值在[0,1]: result_angle={result_angle:.6f}")
+ else:
+ t = normalized - 1.0
+ extension = slope * t * (1.0 + (exp_factor - 1.0) * t)
+ result_angle = angle_last + extension
+
+ if self._should_debug(finger_name):
+ print(f"延伸: normalized={normalized:.6f}, t={t:.4f}, exp_factor={exp_factor:.2f}, result={result_angle:.6f}")
+
+ if 'fist' in self.robot_states:
+ angle_fist = self.robot_states['fist'][robot_idx]
+ if slope > 0:
+ result_angle = min(result_angle, angle_fist)
+ else:
+ result_angle = max(result_angle, angle_fist)
+ if self._should_debug(finger_name):
+ print(f"截断到fist: angle_fist={angle_fist:.6f}, result={result_angle:.6f}")
+
+ return result_angle
+
+ def _two_state_map(self, current_fused, fused_first, fused_last,
+ angle_first, angle_last, finger_name):
+ """两段映射:线性插值并截断到最后一个状态"""
+ if abs(fused_last - fused_first) < 1e-6:
+ normalized = 0.5
+ else:
+ normalized = (current_fused - fused_first) / (fused_last - fused_first)
+
+ # 截断到 [0, 1]
+ normalized = max(0.0, min(1.0, normalized))
+
+ result_angle = angle_first + normalized * (angle_last - angle_first)
+
+ if self._should_debug(finger_name):
+ print(f"两段映射截断: normalized={normalized:.6f}, result={result_angle:.6f}")
+
+ return result_angle
+
+ return result_angle
+
+ def get_mapping_info(self, finger_name: str = None) -> Dict:
+ """
+ 获取映射信息
+ """
+ if finger_name:
+ return self._get_finger_info(finger_name)
+ else:
+ return {name: self._get_finger_info(name) for name in self.finger_configs}
+
+ def _get_finger_info(self, finger_name: str) -> Dict:
+ """获取单个手指的信息"""
+ if finger_name not in self.finger_configs:
+ return {}
+
+ robot_idx = self.finger_configs[finger_name]['robot_idx']
+ max_angle = self._get_max_angle(robot_idx)
+
+ info = {
+ 'name': self.finger_configs[finger_name]['name'],
+ 'robot_idx': robot_idx,
+ 'has_dynamic_weight': finger_name in self.dynamic_weight_configs,
+ 'has_extended_mapping': self.extended_mapping_enabled.get(finger_name, False),
+ 'scale_factor': self.scale_factors.get(finger_name, 1.0),
+ 'max_angle': max_angle
+ }
+
+ # 如果有open和opose状态,显示相关信息
+ if 'open' in self.robot_states and 'opose' in self.robot_states:
+ open_angle = self.robot_states['open'][robot_idx]
+ opose_angle = self.robot_states['opose'][robot_idx]
+ info.update({
+ 'open_angle': open_angle,
+ 'opose_angle': opose_angle,
+ 'available_extension': max_angle - opose_angle
+ })
+
+ return info
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkerforce.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkerforce.py
new file mode 100644
index 0000000..2f217d7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkerforce.py
@@ -0,0 +1,732 @@
+import array
+import threading
+import numpy as np
+import time
+import re
+import struct
+import serial
+import serial.tools.list_ports
+from threading import Thread, Event
+from enum import Enum
+from typing import List, Dict, Optional, Callable, Any, Set
+from .constants import HandType
+
+
+# ============== 常量定义 ==============
+
+class CommandCode(Enum):
+ VERSION_QUERY = 0x01
+ SET_FLAG = 0x02
+ POSITION_QUERY = 0x03
+ FORCE_FEEDBACK = 0x04
+ A3_POSITION = 0xA3
+ A6_POSITION = 0xA6
+ A7_FORCE = 0xA7
+
+
+# 协议常量
+BUFFER_SIZE = 1024
+MAX_FRAME_DATA_SIZE = 255
+FRAME_HEADER = 0x5D
+
+# 时序常量
+WARMUP_DELAY = 0.15
+RESPONSE_WAIT = 0.3
+FINAL_WAIT = 1.0
+READ_INTERVAL = 0.003
+ERROR_DELAY = 0.01
+RETRY_DELAY = 0.5
+QUERY_INTERVAL = 10
+
+# 超时常量
+CONNECTION_TIMEOUT = 5.0
+CHECK_INTERVAL = 5.0
+
+# USB 设备匹配模式
+USB_PATTERNS = [
+ r'/dev/ttyUSB\d+',
+ r'/dev/ttyACM\d+',
+ r'/dev/ttyXRUSB\d+',
+ r'/dev/ttyOBC\d+',
+]
+
+
+# ============== 环形缓冲区 ==============
+
+class CircularBuffer:
+ def __init__(self):
+ self.data = array.array('B', [0] * BUFFER_SIZE)
+ self.read_pos = 0
+ self.write_pos = 0
+ self.data_len = 0
+
+ def write(self, data):
+ for byte in data:
+ self.data[self.write_pos] = byte
+ self.write_pos = (self.write_pos + 1) % BUFFER_SIZE
+ if self.data_len < BUFFER_SIZE:
+ self.data_len += 1
+ else:
+ self.read_pos = (self.read_pos + 1) % BUFFER_SIZE
+
+ def read_byte(self):
+ if self.data_len == 0:
+ return None
+ byte = self.data[self.read_pos]
+ self.read_pos = (self.read_pos + 1) % BUFFER_SIZE
+ self.data_len -= 1
+ return byte
+
+
+# ============== 帧解析器 ==============
+
+class FrameParseState(Enum):
+ HEADER = 0
+ CMD = 1
+ LENGTH = 2
+ DATA = 3
+ CHECKSUM = 4
+
+
+class FrameParser:
+ def __init__(self):
+ self.state = FrameParseState.HEADER
+ self.frame_buf = array.array('B', [0] * (3 + MAX_FRAME_DATA_SIZE + 1))
+ self.expected_len = 0
+ self.current_pos = 0
+ self.checksum = 0
+
+ def reset(self):
+ self.state = FrameParseState.HEADER
+ self.current_pos = 0
+ self.checksum = 0
+ for i in range(len(self.frame_buf)):
+ self.frame_buf[i] = 0
+
+ def process_byte(self, byte):
+ byte = byte & 0xFF
+ if self.state == FrameParseState.HEADER:
+ if byte == FRAME_HEADER:
+ self.frame_buf[0] = byte
+ self.current_pos = 1
+ self.checksum = 0
+ self.state = FrameParseState.CMD
+ elif self.state == FrameParseState.CMD:
+ self.frame_buf[1] = byte
+ self.current_pos = 2
+ self.state = FrameParseState.LENGTH
+ elif self.state == FrameParseState.LENGTH:
+ self.frame_buf[2] = byte
+ self.expected_len = 3 + byte + 1
+ self.current_pos = 3
+ if 0 < byte <= MAX_FRAME_DATA_SIZE:
+ self.state = FrameParseState.DATA
+ else:
+ self.state = FrameParseState.CHECKSUM
+ elif self.state == FrameParseState.DATA:
+ self.frame_buf[self.current_pos] = byte
+ self.current_pos += 1
+ if self.current_pos >= self.expected_len - 1:
+ self.state = FrameParseState.CHECKSUM
+ elif self.state == FrameParseState.CHECKSUM:
+ if self.checksum == byte:
+ self.frame_buf[self.current_pos] = byte
+ return True
+ else:
+ self.reset()
+
+ if self.state != FrameParseState.HEADER:
+ self.checksum = (self.checksum + byte) & 0xFF
+
+ return False
+
+
+# ============== 日志工具 ==============
+
+class Logger:
+ def __init__(self, logger_func: Optional[Callable[[str, str], None]] = None, isdebug: bool = False):
+ self.logger = logger_func
+ self.isdebug = isdebug
+
+ def log(self, level: str, msg: str) -> None:
+ if self.logger:
+ if self.isdebug and level == 'debug':
+ self.logger('info', msg)
+ else:
+ self.logger(level, msg)
+ else:
+ print(msg)
+
+
+# ============== 串口扫描器 ==============
+
+class SerialScanner:
+ def __init__(self, baudrates: Optional[List[int]] = None,
+ exclude_ports: Optional[List[str]] = None,
+ logger: Optional[Logger] = None):
+ self.baudrates = baudrates or [2000000, 1000000, 921600, 460800]
+ self.exclude_ports = set(exclude_ports) if exclude_ports else set()
+ self.checked_ports: Set[str] = set()
+ self.logger = logger
+
+ def is_usb_device(self, port_name):
+ for pattern in USB_PATTERNS:
+ if re.match(pattern, port_name):
+ return True
+ try:
+ ports = serial.tools.list_ports.comports()
+ for port_info in ports:
+ if port_info.device == port_name:
+ description = (port_info.description or "").lower()
+ if any(kw in description for kw in ['usb', 'serial', 'com']):
+ return True
+ if port_info.hwid and 'USB' in port_info.hwid.upper():
+ return True
+ except:
+ pass
+ return False
+
+ def scan_available_ports(self):
+ ports = serial.tools.list_ports.comports()
+ available = []
+ for port in ports:
+ device = port.device
+ if not self.is_usb_device(device):
+ continue
+ if device in self.exclude_ports:
+ if self.logger:
+ self.logger.log('debug', f"跳过排除的串口: {device}")
+ continue
+ if device not in self.checked_ports:
+ available.append(device)
+ return available
+
+
+# ============== 帧处理器 ==============
+
+class FrameHandler:
+ def __init__(self, handtype: HandType, logger: Optional['Logger'] = None):
+ self._handtype = handtype # 期望的手类型
+ self.logger = logger
+ self._data_lock = threading.Lock()
+ self._poslist: List[float] = [0.0] * 21
+ self._forcelist: List[float] = [0.0] * 5
+ self._realforcelist: List[int] = [0] * 5
+
+ @property
+ def poslist(self) -> List[float]:
+ with self._data_lock:
+ return self._poslist.copy()
+
+ @poslist.setter
+ def poslist(self, value: List[float]):
+ with self._data_lock:
+ self._poslist = value
+
+ @property
+ def forcelist(self) -> List[float]:
+ with self._data_lock:
+ return self._forcelist.copy()
+
+ @forcelist.setter
+ def forcelist(self, value: List[float]):
+ with self._data_lock:
+ self._forcelist = value
+
+ @property
+ def realforcelist(self) -> List[int]:
+ with self._data_lock:
+ return self._realforcelist.copy()
+
+ @realforcelist.setter
+ def realforcelist(self, value: List[int]):
+ with self._data_lock:
+ self._realforcelist = value
+
+ def handle_frame(self, frame: array.array) -> Optional[Dict[str, Any]]:
+ cmd = frame[1]
+ data_len = frame[2]
+ frame_data = frame[3:3 + data_len]
+
+ if cmd == CommandCode.VERSION_QUERY.value:
+ return self._handle_version(frame_data)
+ elif cmd == CommandCode.POSITION_QUERY.value:
+ return self._handle_position(frame_data, is_a3=False)
+ elif cmd == CommandCode.FORCE_FEEDBACK.value:
+ return self._handle_force(frame_data)
+ elif cmd == CommandCode.A3_POSITION.value:
+ return self._handle_position(frame_data, is_a3=True)
+ elif cmd == CommandCode.A6_POSITION.value:
+ return self._handle_a6_position(frame_data)
+ else:
+ if self.logger:
+ self.logger.log('warn', f"Unknown command: 0x{cmd:02X}")
+ return None
+
+ def _handle_version(self, frame_data: array.array) -> Dict[str, Any]:
+ value = struct.unpack(' Optional[Dict[str, Any]]:
+ if len(frame_data) % 4 != 0:
+ if self.logger:
+ self.logger.log('warn', f"Invalid position data length: {len(frame_data)}")
+ return None
+ floats: List[float] = []
+ for i in range(len(frame_data) // 4):
+ try:
+ val = struct.unpack(' Optional[Dict[str, Any]]:
+ if len(frame_data) % 2 != 0:
+ if self.logger:
+ self.logger.log('warn', f"Invalid force data length: {len(frame_data)}")
+ return None
+ values: List[int] = []
+ for i in range(len(frame_data) // 2):
+ try:
+ val = struct.unpack('>h', frame_data[i*2:(i+1)*2])[0]
+ values.append(val)
+ except struct.error as e:
+ if self.logger:
+ self.logger.log('warn', f"Unpack error: {e}")
+ self.realforcelist = values
+ return {'realforcelist': values}
+
+ def _handle_a6_position(self, frame_data: array.array) -> Optional[Dict[str, Any]]:
+ if len(frame_data) % 2 != 0:
+ if self.logger:
+ self.logger.log('warn', f"Invalid a6 data length: {len(frame_data)}")
+ return None
+ floats: List[float] = []
+ for i in range(len(frame_data) // 2):
+ try:
+ val = struct.unpack(' Optional[str]:
+ """返回手类型字符串,保持向后兼容"""
+ if status_code == 0 and self._handtype == HandType.left:
+ return "Left"
+ elif status_code == 1 and self._handtype == HandType.right:
+ return "Right"
+ return None
+
+ def detect_hand_type(self, status_code: int) -> Optional[str]:
+ """仅根据 status_code 检测手类型(不验证匹配)"""
+ if status_code == 0:
+ return "Left"
+ elif status_code == 1:
+ return "Right"
+ return None
+
+ @staticmethod
+ def calculate_checksum(data: bytes) -> int:
+ return sum(data) & 0xFF
+
+ @staticmethod
+ def pack_data(cmd: int, payload: bytes = b'') -> bytes:
+ header = struct.pack('BBB', FRAME_HEADER, cmd, len(payload))
+ checksum = FrameHandler.calculate_checksum(header + payload)
+ return header + payload + struct.pack('B', checksum)
+
+ def pack_version_query(self) -> bytes:
+ return self.pack_data(CommandCode.VERSION_QUERY.value)
+
+ def pack_position_query(self) -> bytes:
+ return self.pack_data(CommandCode.POSITION_QUERY.value)
+
+ def pack_force_feedback(self) -> bytes:
+ payload = struct.pack(f'{len(self._forcelist)}f', *self._forcelist)
+ return self.pack_data(CommandCode.FORCE_FEEDBACK.value, payload)
+
+
+# ============== 串口连接管理器 ==============
+
+class SerialConnection:
+ def __init__(self, logger: Optional['Logger'] = None, isdebug: bool = False):
+ self.serial_port: Optional[serial.Serial] = None
+ self.running = Event()
+ self.thread: Optional[Thread] = None
+ self.logger = logger
+ self.isdebug = isdebug
+ self._last_receive_time = time.time()
+ self._last_check_time = time.time()
+ self._disconnect_warned = False
+ self._on_disconnect: Optional[Callable[[], None]] = None
+ self._on_reconnect: Optional[Callable[[], None]] = None
+
+ def open(self, port: str, baudrate: int) -> bool:
+ try:
+ self.serial_port = serial.Serial(
+ port=port,
+ baudrate=baudrate,
+ timeout=0.001,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE,
+ bytesize=serial.EIGHTBITS
+ )
+ self.running = Event()
+ return True
+ except serial.SerialException as e:
+ if self.logger:
+ self.logger.log('error', f"串口打开失败: {e}")
+ return False
+
+ def close(self) -> None:
+ # 先关闭串口,解除阻塞,让线程快速退出
+ if self.serial_port and self.serial_port.is_open:
+ self.serial_port.close()
+ self.serial_port = None
+
+ self.running.clear()
+ if self.thread and self.thread.is_alive():
+ self.thread.join(timeout=1.0)
+
+ def start(self, data_callback: Callable[[array.array], None],
+ query_callback: Callable[[], Optional[bytes]]) -> None:
+ if self.thread and self.thread.is_alive():
+ return
+ self.running.set()
+ self.thread = Thread(target=self._run, args=(data_callback, query_callback), daemon=True)
+ self.thread.start()
+
+ def stop(self) -> None:
+ self.close()
+
+ def _run(self, data_callback: Callable[[array.array], None],
+ query_callback: Callable[[], Optional[bytes]]) -> None:
+ parser = FrameParser()
+ sendcount = 0
+
+ while self.running.is_set():
+ try:
+ current_time = time.time()
+
+ # 断联检测
+ if current_time - self._last_check_time >= CHECK_INTERVAL:
+ self._last_check_time = current_time
+ elapsed = current_time - self._last_receive_time
+
+ if elapsed > CONNECTION_TIMEOUT:
+ if not self._disconnect_warned:
+ port_name = self.serial_port.port if self.serial_port else 'unknown'
+ if self.logger:
+ self.logger.log('error', f"串口 {port_name} 超过 {CONNECTION_TIMEOUT}秒 无响应,可能已断联")
+ self._disconnect_warned = True
+ if self._on_disconnect:
+ self._on_disconnect()
+
+ # 读取数据
+ if self.serial_port and self.serial_port.in_waiting > 0:
+ data = self.serial_port.read(self.serial_port.in_waiting)
+ if data:
+ if self._disconnect_warned:
+ port_name = self.serial_port.port if self.serial_port else 'unknown'
+ if self.logger:
+ self.logger.log('info', f"串口 {port_name} 已恢复连接")
+ self._disconnect_warned = False
+ if self._on_reconnect:
+ self._on_reconnect()
+
+ self._last_receive_time = current_time
+ for byte in data:
+ if parser.process_byte(byte):
+ if data_callback:
+ data_callback(parser.frame_buf)
+ parser.reset()
+
+ # 发送查询
+ if query_callback:
+ sendcount += 1
+ if sendcount > QUERY_INTERVAL:
+ query_data = query_callback()
+ if query_data and self.serial_port:
+ self.serial_port.write(query_data)
+ sendcount = 0
+
+ time.sleep(READ_INTERVAL)
+
+ except Exception as e:
+ if self.logger and self.isdebug:
+ self.logger.log('error', f"串口读取错误: {e}")
+ time.sleep(ERROR_DELAY)
+
+ def set_disconnect_callback(self, callback: Callable[[], None]) -> None:
+ self._on_disconnect = callback
+
+ def set_reconnect_callback(self, callback: Callable[[], None]) -> None:
+ self._on_reconnect = callback
+
+
+# ============== 主类:整合以上模块 ==============
+
+class ForceSerialReader:
+ def __init__(self, gettype: HandType, excludelist: Optional[List[str]] = None,
+ baudrates: Optional[List[int]] = None, isdebug: bool = False,
+ logger: Optional[Callable[[str, str], None]] = None):
+ self.gettype = gettype
+ self.isdebug = isdebug
+ self.connflag = False
+ self.version: Optional[str] = None
+ self.handtype: Optional[HandType] = None
+
+ # 初始化模块
+ self._logger = Logger(logger, isdebug)
+ self._scanner = SerialScanner(baudrates, excludelist, self._logger)
+ self._handler = FrameHandler(gettype, self._logger)
+ self._connection = SerialConnection(self._logger, isdebug)
+
+ # 串口参数代理
+ self.serial_port: Optional[serial.Serial] = None
+ self.baudrates = self._scanner.baudrates
+ self.checked_ports = self._scanner.checked_ports
+ self.exclude_ports = self._scanner.exclude_ports
+
+ # 数据属性代理
+ @property
+ def poslist(self) -> List[float]:
+ return self._handler.poslist
+
+ @poslist.setter
+ def poslist(self, value: List[float]):
+ self._handler.poslist = value
+
+ @property
+ def forcelist(self) -> List[float]:
+ return self._handler.forcelist
+
+ @forcelist.setter
+ def forcelist(self, value: List[float]):
+ self._handler.forcelist = value
+
+ @property
+ def realforcelist(self) -> List[int]:
+ return self._handler.realforcelist
+
+ @realforcelist.setter
+ def realforcelist(self, value: List[int]):
+ self._handler.realforcelist = value
+
+ def _log(self, level: str, msg: str) -> None:
+ self._logger.log(level, msg)
+
+ # 扫描方法
+ def is_usb_device(self, port_name: str) -> bool:
+ return self._scanner.is_usb_device(port_name)
+
+ def scan_serial_ports(self) -> List[str]:
+ return self._scanner.scan_available_ports()
+
+ def find_valid_ports(self, timeout: float = 2, scan_interval: float = 2) -> tuple:
+ if self.isdebug:
+ self._log('debug', "开始扫描串口...")
+ self._log('debug', f"排除列表: {list(self.exclude_ports)}")
+ self._log('debug', f"波特率组合: {self.baudrates}")
+
+ available_ports = self.scan_serial_ports()
+ if self.isdebug:
+ self._log('debug', f"发现 {len(available_ports)} 个未检查的串口: {available_ports}")
+
+ for port in available_ports:
+ success, baudrate, errorcode = self.query_serial_port(port, timeout)
+
+ if not success and errorcode != -2:
+ if self.isdebug:
+ self._log('debug', "首次连接失败,尝试重试...")
+ time.sleep(RETRY_DELAY)
+ success, baudrate, errorcode = self.query_serial_port(port, timeout)
+
+ if errorcode == -2:
+ self._log('warn', f"警告: 串口 {port} 权限不足,请手动执行: sudo chmod 666 {port}")
+
+ self.checked_ports.add(port)
+
+ if success:
+ if self.isdebug:
+ self._log('info', f"找到有效串口: {port} (波特率: {baudrate})")
+ return port, baudrate, errorcode
+
+ return None, None, None
+
+ def query_serial_port(self, port_name: str, timeout: float = 1) -> tuple:
+ best_baudrate: Optional[int] = None
+ errorcode: Optional[int] = None
+
+ for baudrate in self.baudrates:
+ ser: Optional[serial.Serial] = None
+ try:
+ ser = serial.Serial(port_name, baudrate, timeout=timeout,
+ bytesize=serial.EIGHTBITS,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE)
+ self.serial_port = ser
+
+ if self.isdebug:
+ self._log('debug', f"串口 {port_name} 波特率 {baudrate} 预热中...")
+
+ self.handtype = None
+ self.connflag = False
+
+ ser.reset_input_buffer()
+ ser.reset_output_buffer()
+
+ # 预热发送
+ for _ in range(3):
+ ser.write(self.pack_01_data())
+ time.sleep(WARMUP_DELAY)
+
+ # 启动临时读取线程
+ self._connection.running = Event() # 重置 Event
+ self._connection.serial_port = ser
+ self._connection.running.set()
+ self._connection.thread = Thread(target=self._connection._run,
+ args=(self._on_data_received, self._get_query_data),
+ daemon=True)
+ self._connection.thread.start()
+ time.sleep(RESPONSE_WAIT)
+
+ if self.isdebug:
+ self._log('debug', f"侦测串口 {port_name} 波特率 {baudrate} 是否联通...")
+
+ ser.write(self.pack_01_data())
+ time.sleep(FINAL_WAIT)
+
+ # 停止临时线程
+ self._connection.stop()
+ self.serial_port = None
+
+ if self.connflag and self.handtype is not None:
+ best_baudrate = baudrate
+ if self.isdebug:
+ self._log('info', f"串口 {port_name} 在 {baudrate} 波特率下有响应")
+ return True, best_baudrate, errorcode
+
+ except serial.SerialException as e:
+ # 确保清理
+ if self._connection.thread:
+ self._connection.stop()
+ if ser and ser.is_open:
+ ser.close()
+ self.serial_port = None
+
+ error_msg = str(e)
+ if "No such file" in error_msg or "[Errno 2]" in error_msg:
+ errorcode = -1
+ if self.isdebug:
+ self._log('debug', f"串口设备不存在: {port_name}")
+ break
+ elif "Permission denied" in error_msg or "[Errno 13]" in error_msg:
+ errorcode = -2
+ self._log('warn', f"权限被拒绝: {port_name}")
+ break
+ elif "Device or resource busy" in error_msg:
+ errorcode = -3
+ if self.isdebug:
+ self._log('debug', f"设备忙: {port_name}")
+ break
+ else:
+ errorcode = -99
+ if self.isdebug:
+ self._log('debug', f"串口打开失败: {e}")
+ continue
+
+ return False, None, errorcode
+
+ # 连接方法
+ def openserial(self, port: str, baudrate: int = 2000000) -> bool:
+ result = self._connection.open(port, baudrate)
+ if result:
+ self.serial_port = self._connection.serial_port
+ return result
+
+ def start(self) -> None:
+ self._connection.start(self._on_data_received, self._get_query_data)
+
+ def stop(self) -> None:
+ self._connection.stop()
+
+ def _on_data_received(self, frame: array.array) -> None:
+ self.connflag = True
+ result = self._handler.handle_frame(frame)
+
+ if result:
+ if 'version' in result:
+ self.version = result['version']
+ if 'raw_handtype' in result:
+ self.handtype = result['raw_handtype']
+ elif 'handtype' in result:
+ self.handtype = result['handtype']
+ if 'force_response' in result and self.serial_port:
+ self.serial_port.write(self.pack_A7_data(self.forcelist))
+
+ def _get_query_data(self) -> Optional[bytes]:
+ if self.handtype is not None:
+ return self.pack_03_data()
+ return None
+
+ def set_reconnect_callback(self, callback: Callable[[], None]) -> None:
+ self._connection.set_reconnect_callback(callback)
+
+ # 数据打包方法
+ @staticmethod
+ def calculate_checksum(data: bytes) -> int:
+ return FrameHandler.calculate_checksum(data)
+
+ def pack_01_data(self) -> bytes:
+ return self._handler.pack_version_query()
+
+ def pack_02_data(self, mastersendflag: int) -> bytes:
+ payload = struct.pack('BBBBB', mastersendflag, 0, 0, 0, 0)
+ return FrameHandler.pack_data(CommandCode.SET_FLAG.value, payload)
+
+ def pack_03_data(self) -> bytes:
+ return self._handler.pack_position_query()
+
+ def pack_A3_data(self) -> bytes:
+ return FrameHandler.pack_data(CommandCode.A3_POSITION.value)
+
+ def pack_04_data(self):
+ return self._handler.pack_force_feedback()
+
+ def pack_A4_data(self, float_data):
+ payload = struct.pack(f'{len(float_data)}f', *float_data)
+ return FrameHandler.pack_data(CommandCode.A6_POSITION.value, payload)
+
+ def pack_A7_data(self, float_data):
+ payload = struct.pack(f'{len(float_data)}f', *float_data)
+ return FrameHandler.pack_data(CommandCode.A7_FORCE.value, payload)
+
+ # 兼容性方法
+ def hex_dump(self, data):
+ return ' '.join(f'{b:02X}' for b in data)
+
+ def get_current_status(self):
+ return {
+ 'valid_ports': [],
+ 'checked_ports': list(self.checked_ports),
+ 'exclude_ports': list(self.exclude_ports),
+ 'baudrates': self.baudrates
+ }
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkermcgcore.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkermcgcore.py
new file mode 100644
index 0000000..dbff493
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/linkermcgcore.py
@@ -0,0 +1,264 @@
+from datetime import datetime
+import socket
+import time
+from threading import Thread
+import json
+from dataclasses import dataclass
+from typing import List, Dict, Union, Any
+import threading
+import numpy as np
+
+NODES_HAND = 25
+
+LOG_FILE_PATH = "/tmp/a.log"
+
+@dataclass
+class HandData:
+ pitch: List[int] # 5个手指的pitch值 [0-255]
+ side: List[int] # 5个手指的side值 [0-255]
+ roll: List[int] # 5个手指的roll值 [0-255]
+ two_pitch: List[int] # 5个手指的two_pitch值 [0-255]
+ end_pitch: List[int] # 5个手指的end_pitch值 [0-255]
+
+
+class HaoCunData:
+ def __init__(self):
+ self.is_update = False
+ self.frame_index = 0
+ self.frequency = 0
+ self.ns_result = 0
+ # 直接存储25个字节值,不需要转换
+ self.jointangle_rHand = [0.0] * NODES_HAND
+ self.jointangle_lHand = [0.0] * NODES_HAND
+
+
+class HaoCunScoketUdp:
+ def __init__(self, host='127.0.0.1', port=7000, buffer_size=2048):
+ """
+ 初始化UDP客户端
+
+ Args:
+ target_host: 目标服务器地址
+ target_port: 目标服务器端口
+ buffer_size: 缓冲区大小
+ device_id: 设备ID
+ """
+ self.socket_udp = None
+ self.is_use_face_blend_shapes_arkit = False
+ self.udp_thread = None
+ self.udp_running = False
+ self.isconnect = False
+ self.target_host = host
+ self.target_port = port
+ self.target_address = (host, port)
+ self.buffer_size = buffer_size
+ self.realmocapdata = HaoCunData()
+ self.data_lock = threading.Lock()
+ self.frame_counter = 0
+
+ def udp_initial(self) -> bool:
+ """初始化UDP客户端并连接到目标服务器"""
+ try:
+ # 创建UDP socket
+ self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+
+ # 设置超时时间
+ self.socket_udp.settimeout(10)
+
+ # UDP是面向无连接的,这里只是保存目标地址,不会真正建立连接
+ # 但我们可以发送一个测试包来验证连通性
+ try:
+ test_packet = b"CONNECT"
+ self.socket_udp.sendto(test_packet, self.target_address)
+ self.socket_udp.settimeout(2) # 设置较短的超时用于连接测试
+ # 尝试接收响应(如果服务器会响应的话)
+ # 注意:某些UDP服务可能不会响应,这并不代表连接失败
+ try:
+ data, addr = self.socket_udp.recvfrom(self.buffer_size)
+ print(f"成功连接到服务器 {addr}")
+ except socket.timeout:
+ print(f"已发送连接请求到 {self.target_address} (UDP协议,无连接确认)")
+ except Exception as e:
+ print(f"连接测试时出错: {e}")
+
+ # 恢复超时设置
+ self.socket_udp.settimeout(10)
+
+ self.isconnect = True
+ self.udp_running = True
+ self.udp_thread = Thread(target=self.__udp_process)
+ self.udp_thread.start()
+ return True
+ except socket.error as e:
+ self.isconnect = False
+ print(f"UDP客户端初始化错误: {e}")
+ if self.socket_udp:
+ self.socket_udp.close()
+ return False
+
+ def __recv(self) -> tuple:
+ """接收UDP数据"""
+ try:
+ data, addr = self.socket_udp.recvfrom(self.buffer_size)
+ return data, addr
+ except socket.timeout:
+ return None, None
+ except socket.error as e:
+ print(f"接收数据时出错: {e}")
+ return None, None
+
+ def udp_close(self) -> bool:
+ """关闭UDP连接"""
+ self.udp_running = False
+ if self.udp_thread and self.udp_thread.is_alive():
+ self.udp_thread.join(timeout=2)
+ time.sleep(0.1)
+ if self.socket_udp:
+ self.socket_udp.close()
+ self.isconnect = False
+ return True
+
+ def udp_is_connect(self) -> bool:
+ """检查连接状态"""
+ return self.isconnect
+
+ def __udp_process(self):
+ """UDP数据处理线程"""
+ errorprintcount = 0
+ while self.udp_running:
+ try:
+ bytes_data, addr = self.__recv()
+ if bytes_data is not None:
+ try:
+ # 将接收到的数据传递给处理函数
+ self.__process_received_data(bytes_data)
+ except Exception as e:
+ if errorprintcount > 100:
+ print(f"数据处理错误: {e}")
+ errorprintcount = 0
+ # 添加短暂休眠避免CPU占用过高
+ time.sleep(0.001)
+ except Exception as e:
+ if errorprintcount > 100:
+ print(f"UDP处理线程错误: {e}")
+ errorprintcount = 0
+ finally:
+ errorprintcount += 1
+
+ def __process_received_data(self, bytes_data: bytes):
+ """处理接收到的数据"""
+ try:
+ # 解码JSON数据
+ json_str = bytes_data.decode('utf-8', errors='replace')
+ data = json.loads(json_str)
+
+ # 更新帧计数器
+ self.frame_counter += 1
+
+ # 处理数据
+ with self.data_lock:
+ # 处理左手数据
+ if 'leftHand' in data:
+ left_hand = data['leftHand']
+ self.realmocapdata.jointangle_lHand = self.extract_25_bytes(left_hand)
+
+ # 处理右手数据
+ if 'rightHand' in data:
+ right_hand = data['rightHand']
+ self.realmocapdata.jointangle_rHand = self.extract_25_bytes(right_hand)
+
+ # 更新其他状态
+ self.realmocapdata.is_update = True
+ self.realmocapdata.frame_index = self.frame_counter
+
+ except json.JSONDecodeError as e:
+ print(f"JSON解析错误: {e}")
+ print(f"原始数据: {bytes_data.decode('utf-8', errors='replace')}")
+ except Exception as e:
+ print(f"处理数据时出错: {e}")
+
+ def extract_25_bytes(self, hand_data: Dict) -> List[float]:
+ """
+ 从手部数据字典中提取25个字节值,按以下顺序排列:
+ 1. pitch (5个值)
+ 2. side (5个值)
+ 3. roll (5个值)
+ 4. two_pitch (5个值)
+ 5. end_pitch (5个值)
+
+ 总计25个值
+
+ Args:
+ hand_data: 包含pitch, side, roll, two_pitch, end_pitch的字典
+
+ Returns:
+ 长度为25的字节值列表
+ """
+ byte_values = [0.0] * NODES_HAND
+ idx = 0
+
+ try:
+ # 按顺序提取5个数组,每个5个值,共25个值
+ arrays_to_extract = ['pitch', 'side', 'roll', 'two_pitch', 'end_pitch']
+
+ for array_name in arrays_to_extract:
+ if array_name in hand_data:
+ values = hand_data[array_name]
+ # 确保有5个值
+ if len(values) >= 5:
+ for i in range(5):
+ if idx < NODES_HAND:
+ byte_values[idx] = float(values[i])
+ idx += 1
+ else:
+ # 如果数据不足5个,填充0
+ for i in range(5):
+ if idx < NODES_HAND:
+ byte_values[idx] = 0.0
+ idx += 1
+ else:
+ # 如果缺少某个数组,填充5个0
+ for i in range(5):
+ if idx < NODES_HAND:
+ byte_values[idx] = 0.0
+ idx += 1
+
+ except Exception as e:
+ print(f"提取字节值时出错: {e}")
+
+ return byte_values
+
+ def udp_recv_mocap_data(self, mocap_data: HaoCunData) -> bool:
+ """获取最新的动捕数据"""
+ with self.data_lock:
+ mocap_data.frame_index = self.realmocapdata.frame_index
+ mocap_data.is_update = self.realmocapdata.is_update
+ mocap_data.frequency = self.realmocapdata.frequency
+ mocap_data.jointangle_rHand = self.realmocapdata.jointangle_rHand.copy()
+ mocap_data.jointangle_lHand = self.realmocapdata.jointangle_lHand.copy()
+ return True
+
+ def send_data(self, data: bytes) -> bool:
+ """发送数据到目标服务器"""
+ try:
+ if not self.isconnect or not self.socket_udp:
+ print("UDP客户端未连接")
+ return False
+
+ self.socket_udp.sendto(data, self.target_address)
+ return True
+ except Exception as e:
+ print(f"发送数据时出错: {e}")
+ return False
+
+ def get_hand_data_summary(self) -> Dict:
+ """获取手部数据摘要"""
+ with self.data_lock:
+ return {
+ 'frame_index': self.realmocapdata.frame_index,
+ 'is_update': self.realmocapdata.is_update,
+ 'left_hand_first_5': self.realmocapdata.jointangle_lHand[:5],
+ 'right_hand_first_5': self.realmocapdata.jointangle_rHand[:5],
+ 'left_hand_total': len(self.realmocapdata.jointangle_lHand),
+ 'right_hand_total': len(self.realmocapdata.jointangle_rHand)
+ }
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/sensenovacore.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/sensenovacore.py
new file mode 100644
index 0000000..02a87cc
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/sensenovacore.py
@@ -0,0 +1,270 @@
+from datetime import datetime
+import socket
+import numpy as np
+import time
+from threading import Thread
+import json
+from dataclasses import dataclass
+from typing import List, Dict, Union, Any
+import threading
+
+NODES_HAND = 30
+
+json_send_basic = {
+ "timsstamp": "2025-4-30 22:47:90.123",
+ "datatype": "datarecv",
+ "right": {
+ "thumb": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "index": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "middle": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "ring": {
+ "normalforce": 31.0,
+ "approachforce": 32.0,
+ "tangentialforce": 0.0
+ },
+ "pinky": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ }
+ },
+ "left": {
+ "thumb": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "index": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "middle": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "ring": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ },
+ "pinky": {
+ "normalforce": 0.0,
+ "approachforce": 0.0,
+ "tangentialforce": 0.0
+ }
+ }
+}
+
+
+class SenseNovaData:
+ def __init__(self):
+ self.is_update = False
+ self.frame_index = 0
+ self.frequency = 0
+ self.ns_result = 0
+ self.jointangle_rHand = [0.0] * NODES_HAND
+ self.jointangle_lHand = [0.0] * NODES_HAND
+ self.normalforce_rHand = [0.0] * 5
+ self.normalforce_lHand = [0.0] * 5
+ self.approachforce_rHand = [0.0] * 5
+ self.approachforce_lHand = [0.0] * 5
+
+
+class SenseNovaScoketUdp:
+ def __init__(self, host='0.0.0.0', port=7000, buffer_size=4098):
+ self.socket_udp = None
+ self.is_use_face_blend_shapes_arkit = False
+ self.udp_thread = None
+ self.udp_running = False
+ self.isconnect = False
+ self.host = host
+ self.port = port
+ self.buffer_size = buffer_size
+ self.udp_addr = self.udp_getsockaddr(host, port)
+ self.realmocapdata = SenseNovaData()
+ self.data_lock = threading.Lock()
+
+ def udp_initial(self) -> bool:
+ """初始化 UDP socket"""
+ try:
+ self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.socket_udp.bind(('', 8888))
+ self.socket_udp.settimeout(10)
+ self.isconnect = True
+ self.udp_running = True
+ self.udp_thread = Thread(target=self.__udp_process)
+ self.udp_thread.start()
+
+ return True
+ except socket.error as e:
+ self.isconnect = False
+ print(f"发生错误: {e},UDP套接字已关闭!")
+ if self.socket_udp:
+ self.socket_udp.close()
+ return False
+
+ @staticmethod
+ def udp_getsockaddr(ip: str, port: int) -> tuple:
+ """将 IP 地址及端口号转化为能识别的地址格式"""
+ return (ip, port)
+
+ def __send(self):
+ try:
+ json_send_basic["right"]["thumb"]["normalforce"] = self.realmocapdata.normalforce_rHand[0]
+ json_send_basic["right"]["index"]["normalforce"] = self.realmocapdata.normalforce_rHand[1]
+ json_send_basic["right"]["middle"]["normalforce"] = self.realmocapdata.normalforce_rHand[2]
+ json_send_basic["right"]["ring"]["normalforce"] = self.realmocapdata.normalforce_rHand[3]
+ json_send_basic["right"]["pinky"]["normalforce"] = self.realmocapdata.normalforce_rHand[4]
+ json_send_basic["left"]["thumb"]["normalforce"] = self.realmocapdata.normalforce_lHand[0]
+ json_send_basic["left"]["index"]["normalforce"] = self.realmocapdata.normalforce_lHand[1]
+ json_send_basic["left"]["middle"]["normalforce"] = self.realmocapdata.normalforce_lHand[2]
+ json_send_basic["left"]["ring"]["normalforce"] = self.realmocapdata.normalforce_lHand[3]
+ json_send_basic["left"]["pinky"]["normalforce"] = self.realmocapdata.normalforce_lHand[4]
+ json_send_basic["right"]["thumb"]["approachforce"] = self.realmocapdata.approachforce_rHand[0]
+ json_send_basic["right"]["index"]["approachforce"] = self.realmocapdata.approachforce_rHand[1]
+ json_send_basic["right"]["middle"]["approachforce"] = self.realmocapdata.approachforce_rHand[2]
+ json_send_basic["right"]["ring"]["approachforce"] = self.realmocapdata.approachforce_rHand[3]
+ json_send_basic["right"]["pinky"]["approachforce"] = self.realmocapdata.approachforce_rHand[4]
+ json_send_basic["left"]["thumb"]["approachforce"] = self.realmocapdata.approachforce_lHand[0]
+ json_send_basic["left"]["index"]["approachforce"] = self.realmocapdata.approachforce_lHand[1]
+ json_send_basic["left"]["middle"]["approachforce"] = self.realmocapdata.approachforce_lHand[2]
+ json_send_basic["left"]["ring"]["approachforce"] = self.realmocapdata.approachforce_lHand[3]
+ json_send_basic["left"]["pinky"]["approachforce"] = self.realmocapdata.approachforce_lHand[4]
+ json_data = json.dumps(json_send_basic)
+ self.socket_udp.sendto(json_data.encode('utf-8'), self.udp_addr)
+ except Exception as e:
+ print(f"Send未知错误: {e}")
+ finally:
+ pass
+
+ def __recv(self) -> tuple:
+ try:
+ data, addr = self.socket_udp.recvfrom(self.buffer_size)
+ return data, addr
+ except socket.timeout:
+ return None, None
+
+ def udp_close(self) -> bool:
+ self.udp_running = False
+ self.udp_thread.join()
+ time.sleep(0.1)
+ if self.socket_udp:
+ self.socket_udp.close()
+ return True
+
+ def udp_is_onnect(self) -> bool:
+ return self.isconnect
+
+ def __udp_process(self):
+ errorprintcount = 0
+ while self.udp_running:
+ self.__send()
+ try:
+ bytes_data, addr = self.__recv()
+ if bytes_data is not None:
+ try:
+ print(bytes_data)
+ json_data = json.loads(bytes_data.decode('utf-8'))
+ self.realmocapdata.jointangle_rHand[0:6] = [
+ json_data["euler"]["right"]["thumb"]["cmc_roll"],
+ json_data["euler"]["right"]["thumb"]["cmc_yaw"],
+ json_data["euler"]["right"]["thumb"]["cmc_pitch"],
+ json_data["euler"]["right"]["thumb"]["mcp"],
+ json_data["euler"]["right"]["thumb"]["pip"],
+ json_data["euler"]["right"]["thumb"]["dip"]]
+ self.realmocapdata.jointangle_rHand[6:12] = [
+ json_data["euler"]["right"]["index"]["cmc_roll"],
+ json_data["euler"]["right"]["index"]["cmc_yaw"],
+ json_data["euler"]["right"]["index"]["cmc_pitch"],
+ json_data["euler"]["right"]["index"]["mcp"],
+ json_data["euler"]["right"]["index"]["pip"],
+ json_data["euler"]["right"]["index"]["dip"]]
+ self.realmocapdata.jointangle_rHand[12:18] = [
+ json_data["euler"]["right"]["middle"]["cmc_roll"],
+ json_data["euler"]["right"]["middle"]["cmc_yaw"],
+ json_data["euler"]["right"]["middle"]["cmc_pitch"],
+ json_data["euler"]["right"]["middle"]["mcp"],
+ json_data["euler"]["right"]["middle"]["pip"],
+ json_data["euler"]["right"]["middle"]["dip"]]
+ self.realmocapdata.jointangle_rHand[18:24] = [
+ json_data["euler"]["right"]["ring"]["cmc_roll"],
+ json_data["euler"]["right"]["ring"]["cmc_yaw"],
+ json_data["euler"]["right"]["ring"]["cmc_pitch"],
+ json_data["euler"]["right"]["ring"]["mcp"],
+ json_data["euler"]["right"]["ring"]["pip"],
+ json_data["euler"]["right"]["ring"]["dip"]]
+ self.realmocapdata.jointangle_rHand[24:30] = [
+ json_data["euler"]["right"]["pinky"]["cmc_roll"],
+ json_data["euler"]["right"]["pinky"]["cmc_yaw"],
+ json_data["euler"]["right"]["pinky"]["cmc_pitch"],
+ json_data["euler"]["right"]["pinky"]["mcp"],
+ json_data["euler"]["right"]["pinky"]["pip"],
+ json_data["euler"]["right"]["pinky"]["dip"]]
+ self.realmocapdata.jointangle_lHand[0:6] = [
+ json_data["euler"]["left"]["thumb"]["cmc_roll"],
+ json_data["euler"]["left"]["thumb"]["cmc_yaw"],
+ json_data["euler"]["left"]["thumb"]["cmc_pitch"],
+ json_data["euler"]["left"]["thumb"]["mcp"],
+ json_data["euler"]["left"]["thumb"]["pip"],
+ json_data["euler"]["left"]["thumb"]["dip"]]
+ self.realmocapdata.jointangle_lHand[6:12] = [
+ json_data["euler"]["left"]["index"]["cmc_roll"],
+ json_data["euler"]["left"]["index"]["cmc_yaw"],
+ json_data["euler"]["left"]["index"]["cmc_pitch"],
+ json_data["euler"]["left"]["index"]["mcp"],
+ json_data["euler"]["left"]["index"]["pip"],
+ json_data["euler"]["left"]["index"]["dip"]]
+ self.realmocapdata.jointangle_lHand[12:18] = [
+ json_data["euler"]["left"]["middle"]["cmc_roll"],
+ json_data["euler"]["left"]["middle"]["cmc_yaw"],
+ json_data["euler"]["left"]["middle"]["cmc_pitch"],
+ json_data["euler"]["left"]["middle"]["mcp"],
+ json_data["euler"]["left"]["middle"]["pip"],
+ json_data["euler"]["left"]["middle"]["dip"]]
+ self.realmocapdata.jointangle_lHand[18:24] = [
+ json_data["euler"]["left"]["ring"]["cmc_roll"],
+ json_data["euler"]["left"]["ring"]["cmc_yaw"],
+ json_data["euler"]["left"]["ring"]["cmc_pitch"],
+ json_data["euler"]["left"]["ring"]["mcp"],
+ json_data["euler"]["left"]["ring"]["pip"],
+ json_data["euler"]["left"]["ring"]["dip"]]
+ self.realmocapdata.jointangle_lHand[24:30] = [
+ json_data["euler"]["left"]["pinky"]["cmc_roll"],
+ json_data["euler"]["left"]["pinky"]["cmc_yaw"],
+ json_data["euler"]["left"]["pinky"]["cmc_pitch"],
+ json_data["euler"]["left"]["pinky"]["mcp"],
+ json_data["euler"]["left"]["pinky"]["pip"],
+ json_data["euler"]["left"]["pinky"]["dip"]]
+ except json.JSONDecodeError as e:
+ if errorprintcount > 100:
+ print(f"JSON解析错误: {e}")
+ print(f"原始数据: {bytes_data.decode('utf-8', errors='replace')}")
+ errorprintcount = 0
+ except ValueError as e: # 新增:捕获 ValueError
+ if errorprintcount > 100:
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 设备ID错误: {e}")
+ errorprintcount = 0
+ except Exception as e:
+ if errorprintcount > 100:
+ if e.args[0] == 10054:
+ print("远程设备已经断开!")
+ else:
+ print(f"Recv未知错误: {e}")
+ errorprintcount = 0
+ finally:
+ errorprintcount += 1
+ time.sleep(0.001)
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/udexrealcore.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/udexrealcore.py
new file mode 100644
index 0000000..88f4f74
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/udexrealcore.py
@@ -0,0 +1,282 @@
+from datetime import datetime
+import socket
+import time
+from threading import Thread
+import json
+from dataclasses import dataclass
+from typing import List, Dict, Union, Any, Optional, Callable
+import threading
+import numpy as np
+
+NODES_HAND = 24
+NO_DATA_TIMEOUT = 1.0 # 无数据超时时间(秒)
+
+@dataclass
+class Bone:
+ Name: str
+ Parent: int
+ Location: List[float]
+ Rotation: List[float]
+ Scale: List[float]
+
+@dataclass
+class Parameter:
+ Name: str
+ Value: Union[float, int, bool]
+
+@dataclass
+class DeviceData:
+ Bones: List[Bone]
+ Parameter: List[Parameter]
+
+class MotionData:
+ def __init__(self, raw_data: Dict[str, Any]):
+ self.devices = {}
+ for device_id, device_content in raw_data.items():
+ bones = [Bone(**bone) for bone in device_content["Bones"]]
+ parameters = [Parameter(**param) for param in device_content["Parameter"]]
+ self.devices[device_id] = DeviceData(Bones=bones, Parameter=parameters)
+
+ def get_device(self, device_id: str) -> DeviceData:
+ return self.devices.get(device_id)
+
+ def list_sequence_params(self, device_id: str, prefix: str) -> Dict[str, Union[float, int, bool]]:
+ device = self.get_device(device_id)
+ if not device:
+ raise ValueError(f"Device {device_id} not found")
+
+ return {
+ param.Name: param.Value
+ for param in device.Parameter
+ if param.Name.startswith(prefix) and param.Name[len(prefix):].isdigit()
+ }
+
+
+class UdexRealData:
+ def __init__(self):
+ self.is_update = False
+ self.frame_index = 0
+ self.frequency = 0
+ self.ns_result = 0
+ self.jointangle_rHand = [0.0] * NODES_HAND
+ self.jointangle_lHand = [0.0] * NODES_HAND
+ # self.jointderict_rHand = [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
+ # self.jointderict_lHand = [-1,-1,-1,1,-1,-1,-1,1,-1,-1,-1,1,-1,-1,-1,1,-1,-1,-1,1,1,1,1,1]
+ self.jointderict_rHand = [1] * NODES_HAND
+ self.jointderict_lHand = [1] * NODES_HAND
+ self.last_data_time = 0.0
+ self.is_data_timeout = False
+
+
+@dataclass
+class TimeoutStatus:
+ """超时状态数据结构"""
+ is_timeout: bool # 当前是否超时
+ last_receive_time: float # 上次收到数据的时间戳
+ time_since_last_data: float # 距离上次收到数据的秒数
+ frame_index: int # 当前帧数
+ timeout_threshold: float # 超时阈值
+ consecutive_timeout_checks: int # 连续超时检查次数
+
+
+class UdexRealScoketUdp:
+ def __init__(self, host='0.0.0.0', port=7000, buffer_size=2048, device_id='eric'):
+ self.socket_udp = None
+ self.udp_thread = None
+ self.udp_running = False
+ self.isconnect = False
+ self.host = host
+ self.port = port
+ self.device_id = device_id
+ self.buffer_size = buffer_size
+ self.realmocapdata = UdexRealData()
+ self.data_lock = threading.Lock()
+
+ # 超时检测相关
+ self.no_data_timeout = NO_DATA_TIMEOUT
+ self.last_receive_time = 0.0
+ self.consecutive_timeout_checks = 0 # 连续超时检查次数
+
+ # 回调函数(可选)
+ self.on_timeout_callback: Optional[Callable[[TimeoutStatus], None]] = None
+ self.on_data_recovered_callback: Optional[Callable[[], None]] = None
+
+ def set_timeout_callback(self, callback: Callable[[TimeoutStatus], None]):
+ """设置超时回调函数"""
+ self.on_timeout_callback = callback
+
+ def set_data_recovered_callback(self, callback: Callable[[], None]):
+ """设置数据恢复回调函数"""
+ self.on_data_recovered_callback = callback
+
+ def udp_initial(self) -> bool:
+ try:
+ self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.socket_udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket_udp.settimeout(1.0)
+ self.socket_udp.bind((self.host, self.port))
+ self.isconnect = True
+ self.udp_running = True
+ self.last_receive_time = time.time()
+ self.consecutive_timeout_checks = 0
+
+ self.udp_thread = Thread(target=self.__udp_process, daemon=True)
+ self.udp_thread.start()
+ return True
+ except socket.error:
+ self.isconnect = False
+ if self.socket_udp:
+ self.socket_udp.close()
+ return False
+
+ def __recv(self) -> tuple:
+ try:
+ data, addr = self.socket_udp.recvfrom(self.buffer_size)
+ return data, addr
+ except socket.timeout:
+ return None, None
+ except socket.error:
+ return None, None
+
+ def udp_close(self) -> bool:
+ self.udp_running = False
+ if self.udp_thread and self.udp_thread.is_alive():
+ self.udp_thread.join(timeout=1.0)
+ time.sleep(0.1)
+ if self.socket_udp:
+ self.socket_udp.close()
+ self.socket_udp = None
+ self.isconnect = False
+ return True
+
+ def udp_is_connect(self) -> bool:
+ return self.isconnect
+
+ def check_timeout(self) -> TimeoutStatus:
+ """
+ 检查超时状态,返回超时状态信息
+ 外部调用此方法来获取超时状态,并决定如何打印
+ """
+ current_time = time.time()
+ time_since_last_data = current_time - self.last_receive_time
+ is_timeout = time_since_last_data > self.no_data_timeout
+
+ # 更新连续超时检查次数
+ if is_timeout:
+ self.consecutive_timeout_checks += 1
+ else:
+ self.consecutive_timeout_checks = 0
+
+ # 更新数据对象的超时状态
+ with self.data_lock:
+ self.realmocapdata.is_data_timeout = is_timeout
+
+ return TimeoutStatus(
+ is_timeout=is_timeout,
+ last_receive_time=self.last_receive_time,
+ time_since_last_data=time_since_last_data,
+ frame_index=self.realmocapdata.frame_index,
+ timeout_threshold=self.no_data_timeout,
+ consecutive_timeout_checks=self.consecutive_timeout_checks
+ )
+
+ def is_data_timeout(self) -> bool:
+ """快速检查是否超时"""
+ current_time = time.time()
+ time_since_last_data = current_time - self.last_receive_time
+ return time_since_last_data > self.no_data_timeout
+
+ def get_connection_status(self) -> Dict[str, Any]:
+ """获取完整的连接状态信息"""
+ current_time = time.time()
+ time_since_last_data = current_time - self.last_receive_time
+
+ with self.data_lock:
+ frame_index = self.realmocapdata.frame_index
+ is_data_timeout = self.realmocapdata.is_data_timeout
+
+ return {
+ 'is_connected': self.isconnect,
+ 'is_running': self.udp_running,
+ 'last_receive_time': self.last_receive_time,
+ 'last_receive_time_str': datetime.fromtimestamp(self.last_receive_time).strftime('%Y-%m-%d %H:%M:%S') if self.last_receive_time > 0 else '从未',
+ 'time_since_last_data': time_since_last_data,
+ 'is_data_timeout': is_data_timeout,
+ 'timeout_threshold': self.no_data_timeout,
+ 'frame_index': frame_index,
+ 'consecutive_timeout_checks': self.consecutive_timeout_checks,
+ 'device_id': self.device_id,
+ 'port': self.port
+ }
+
+ def __udp_process(self):
+ was_timeout = False # 记录上次检查是否超时
+
+ while self.udp_running and self.isconnect:
+ try:
+ bytes_data, addr = self.__recv()
+
+ if bytes_data is not None:
+ # 更新接收时间
+ current_time = time.time()
+ self.last_receive_time = current_time
+
+ # 检查是否从超时状态恢复
+ if was_timeout:
+ was_timeout = False
+ # 调用数据恢复回调
+ if self.on_data_recovered_callback:
+ self.on_data_recovered_callback()
+
+ try:
+ json_data = json.loads(bytes_data.decode('utf-8'))
+ with self.data_lock:
+ motion_data = MotionData(json_data)
+ self.realmocapdata.is_update = True
+ self.realmocapdata.last_data_time = current_time
+ self.realmocapdata.frame_index += 1
+
+ try:
+ l_params = motion_data.list_sequence_params(self.device_id, "l")
+ for name, value in sorted(l_params.items(), key=lambda x: int(x[0][1:])):
+ if int(name[1:]) >= NODES_HAND: break
+ self.realmocapdata.jointangle_lHand[int(name[1:])] = np.deg2rad(value) * self.realmocapdata.jointderict_lHand[int(name[1:])]
+ except Exception:
+ pass
+
+ try:
+ r_params = motion_data.list_sequence_params(self.device_id, "r")
+ for name, value in sorted(r_params.items(), key=lambda x: int(x[0][1:])):
+ if int(name[1:]) >= NODES_HAND: break
+ self.realmocapdata.jointangle_rHand[int(name[1:])] = np.deg2rad(value)* self.realmocapdata.jointderict_rHand[int(name[1:])]
+ except Exception:
+ pass
+
+ except (json.JSONDecodeError, ValueError, Exception):
+ pass
+
+ else:
+ # 没有收到数据,检查是否进入超时状态
+ current_timeout_status = self.check_timeout()
+ if current_timeout_status.is_timeout:
+ was_timeout = True
+ # 调用超时回调
+ if self.on_timeout_callback:
+ self.on_timeout_callback(current_timeout_status)
+
+ except Exception:
+ pass
+
+ def udp_recv_mocap_data(self, mocap_data: UdexRealData) -> bool:
+ with self.data_lock:
+ mocap_data.frame_index = self.realmocapdata.frame_index
+ mocap_data.is_update = self.realmocapdata.is_update
+ mocap_data.frequency = self.realmocapdata.frequency
+ mocap_data.jointangle_rHand = self.realmocapdata.jointangle_rHand.copy()
+ mocap_data.jointangle_lHand = self.realmocapdata.jointangle_lHand.copy()
+ mocap_data.last_data_time = self.realmocapdata.last_data_time
+ mocap_data.is_data_timeout = self.realmocapdata.is_data_timeout
+
+ self.realmocapdata.is_update = False
+
+ return True
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/utils.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/utils.py
new file mode 100644
index 0000000..74bbc12
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/utils.py
@@ -0,0 +1,346 @@
+import enum
+import copy
+import yaml
+import math
+import os
+import numpy as np
+from transforms3d.quaternions import axangle2quat, qmult
+from transforms3d.quaternions import mat2quat
+from transforms3d.euler import mat2euler, quat2mat, euler2mat
+from scipy.spatial.transform import Rotation as R
+
+
+class DataSource(enum.Enum):
+ motion = enum.auto()
+ video = enum.auto()
+ vr = enum.auto()
+
+
+def read_yaml(file_path):
+ with open(file_path, 'r') as file:
+ config = yaml.safe_load(file)
+ return config
+
+
+def extract_dataset_folder_last_two_digits(dir_name):
+ # Extract the last two characters, ensure they are digits, and convert to integer
+ last_two = dir_name[-2:] # Get the last two characters
+ if last_two.isdigit():
+ return int(last_two)
+ else:
+ return -1 # Return -1 (or some other value) if there are no digits
+
+
+def _back_project_batch(points, intrinsics):
+ """ Back-project a batch of points from 3D to 2D image space using vectorized operations """
+ points = np.array(points)
+
+ fx, fy, cx, cy = intrinsics[0, 0], intrinsics[1, 1], intrinsics[0, 2], intrinsics[1, 2]
+ x, y, z = points[:, 0], points[:, 1], points[:, 2]
+
+ u = (x * fx / z) + cx
+ v = (y * fy / z) + cy
+
+ projected_points = np.vstack((u, v)).T.astype(int)
+
+ # return projected_points
+
+
+def translate_wrist_to_origin(joint_positions):
+ wrist_position = joint_positions[0]
+ updated_positions = joint_positions - wrist_position
+ return updated_positions
+
+
+def apply_pose_matrix(joint_positions, pose_matrix):
+ homogeneous_joint_positions = np.hstack([joint_positions, np.ones((joint_positions.shape[0], 1))])
+ transformed_positions = np.dot(homogeneous_joint_positions, pose_matrix.T)
+ transformed_positions_3d = transformed_positions[:, :3]
+ return transformed_positions_3d
+
+
+def inverse_transformation(matrix):
+ # Assuming matrix is a 4x4 numpy array
+ R = matrix[:3, :3]
+ T = matrix[:3, 3]
+
+ R_inv = np.linalg.inv(R)
+ T_inv = -np.dot(R_inv, T)
+
+ inverse_matrix = np.eye(4) # Create a 4x4 identity matrix
+ inverse_matrix[:3, :3] = R_inv
+ inverse_matrix[:3, 3] = T_inv
+
+ return inverse_matrix
+
+
+def update_R_delta_init(frame_0_eef_pos, frame_0_eef_quat):
+ global R_delta_init
+
+ frame_0_pose = np.eye(4)
+ frame_0_pose[:3, :3] = quat2mat(frame_0_eef_quat)
+ frame_0_pose[:3, 3] = frame_0_eef_pos
+
+ pose_ori_matirx = frame_0_pose[:3, :3]
+ pose_ori_correction_matrix = np.dot(np.array([[0, -1, 0],
+ [0, 0, 1],
+ [1, 0, 0]]), euler2mat(0, 0, 0))
+ pose_ori_matirx = np.dot(pose_ori_matirx, pose_ori_correction_matrix)
+
+ canonical_t265_ori = np.array([[1, 0, 0],
+ [0, -1, 0],
+ [0, 0, -1]])
+ x_angle, y_angle, z_angle = mat2euler(frame_0_pose[:3, :3])
+ canonical_t265_ori = np.dot(canonical_t265_ori, euler2mat(-z_angle, x_angle + 0.3, y_angle))
+
+ R_delta_init = np.dot(canonical_t265_ori, pose_ori_matirx.T)
+
+
+def switch_axis(quaternion_xyzw, i, j):
+ q1 = np.array(
+ [quaternion_xyzw[3], quaternion_xyzw[0], quaternion_xyzw[1], quaternion_xyzw[2]]
+ )
+ rot_mat = quat2mat(q1)
+ rot_mat_copy = copy.deepcopy(rot_mat)
+ rot_mat[i] = rot_mat_copy[j]
+ rot_mat[j] = rot_mat_copy[i]
+ import pdb
+
+ pdb.set_trace()
+ q2 = mat2quat(rot_mat)
+ q3 = np.array([q2[1], q2[2], q2[3], q2[0]])
+ return q3
+
+
+def swap_quaternion_axes(quaternion, axis1, axis2):
+ """
+ Swap two axes in a quaternion without converting to Euler angles.
+
+ Args:
+ quaternion (list or np.ndarray): The input quaternion [x, y, z, w].
+ axis1 (int): The index of the first axis to swap (0 for X, 1 for Y, 2 for Z).
+ axis2 (int): The index of the second axis to swap (0 for X, 1 for Y, 2 for Z).
+
+ Returns:
+ np.ndarray: The new quaternion with swapped axes [x', y', z', w'].
+ """
+ if axis1 < 0 or axis1 > 2 or axis2 < 0 or axis2 > 2:
+ raise ValueError("Axis indices must be 0, 1, or 2.")
+
+ # Create a copy of the input quaternion
+ new_quaternion = quaternion.copy()
+
+ # Swap the elements corresponding to the specified axes
+ new_quaternion[axis1], new_quaternion[axis2] = quaternion[axis2], quaternion[axis1]
+
+ return new_quaternion
+
+
+def trans_xyzwori_to_wxyzori(ori):
+ return ((ori[3], ori[0], ori[1], ori[2]))
+
+
+def trans_wxyzori_to_xyzwori(ori):
+ return ((ori[1], ori[2], ori[3], ori[0]))
+
+
+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))
+
+
+def extend_line(point1, point2, distance):
+ vector = np.array(point2) - np.array(point1)
+ vector_magnitude = np.linalg.norm(vector)
+ unit_vector = vector / vector_magnitude
+ extended_point = point2 + unit_vector * distance
+ return extended_point
+
+
+def poseture_to_matrix(position, ori):
+ matrix = np.eye(4)
+ rotation_matrix = quat2mat(ori)
+ matrix[:3, :3] = rotation_matrix
+ matrix[:3, 3] = position
+ return matrix
+
+
+def make_reference_matrix(position, ori, lenpose, lenori):
+ rotated_quaternion_wxyz = np.array([ori[3], ori[0], ori[1], ori[2]])
+ rotated_quaternion_wxyz_len = np.array([lenori[3], lenori[0], lenori[1], lenori[2]])
+ base_matrix = poseture_to_matrix(position, rotated_quaternion_wxyz)
+ adder_matrix = poseture_to_matrix(lenpose, rotated_quaternion_wxyz_len)
+ return base_matrix @ adder_matrix
+
+
+def change_orientation(ori):
+ return np.array([ori[3], ori[0], ori[1], ori[2]])
+
+
+def cal_distance(point_s, point_t):
+ vector = [point_s[i] - point_t[i] for i in range(3)]
+ vector_magnitude = math.sqrt(sum(x ** 2 for x in vector))
+ return vector_magnitude
+
+
+def exponential_growth(minvalue, maxvalue, growth_factor, num_points):
+ valuelist = np.linspace(minvalue, maxvalue, num_points)
+ x = valuelist.astype(int)
+ y = num_points - np.exp(growth_factor * x)
+ min_y = np.min(y)
+ y -= min_y
+ y = y / np.max(y) * maxvalue
+ lookup_table = dict(zip(x, y))
+ return lookup_table
+
+
+def change_list(q):
+ converted_list = [None if value == 'None' else value for value in q]
+ return converted_list
+
+
+def quaternion_conjugate(q):
+ x, y, z, w = q
+ return np.array([-x, -y, -z, w])
+
+
+def quaternion_norm_squared(q):
+ return np.dot(q, q)
+
+
+def quaternion_inverse(q):
+ q_conjugate = quaternion_conjugate(q)
+ norm_sq = quaternion_norm_squared(q)
+ return q_conjugate / norm_sq
+
+
+def quaternion_multiply(q1, q2):
+ x1, y1, z1, w1 = q1
+ x2, y2, z2, w2 = q2
+ return np.array([
+ w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, # x
+ w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2, # y
+ w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2, # z
+ w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 # w
+ ])
+
+
+def exponential_growth_fun(x_values, c, a_min, a_max):
+ # 计算 k,使得 a(1) = a_max
+ k = a_max / (np.exp(c) - 1)
+
+ # 计算指数增长值
+ a_values = k * (np.exp(c * x_values) - 1)
+
+ # 将 a_values 映射到 [a_min, a_max] 范围
+ a_min_original = 0 # 原有公式的最小值
+ a_max_original = k * (np.exp(c) - 1) # 原有公式的最大值
+ a_values_mapped = a_min + (a_values - a_min_original) * (a_max - a_min) / (a_max_original - a_min_original)
+
+ return a_values_mapped
+
+
+def quaternion_matrixinv(q):
+ qw, qx, qy, qz = q
+ rotation_matrix = np.array([[2 * qw ** 2 + 2 * qx ** 2 - 1, 2 * qx * qy - 2 * qw * qz, 2 * qw * qy + 2 * qx * qz],
+ [2 * qw * qz + 2 * qx * qy, 2 * qw ** 2 + 2 * qy ** 2 - 1, 2 * qy * qz - 2 * qw * qx],
+ [2 * qx * qz - 2 * qw * qy, 2 * qw * qx + 2 * qy * qz, 2 * qw ** 2 + 2 * qz ** 2 - 1]])
+
+ return np.linalg.inv(rotation_matrix)
+
+
+def unitydata_to_worldspacedata(initial_positions):
+ new_positions = []
+ for position in initial_positions:
+ new_positions.append([position[0], position[2], position[1]])
+ return new_positions
+
+
+def get_quaternion_relative(ori, targetori):
+ q_target = R.from_quat(targetori)
+ q_parent = R.from_quat(ori)
+ q_parent_inv = q_parent.inv().as_quat() * -1
+ q_parent_inv = R.from_quat(q_parent_inv)
+ q_relative = q_parent_inv * q_target
+ return q_relative.as_quat()
+
+
+def get_child_quaternion(ori, ori_relative):
+ q_child = R.from_quat(ori)
+ q_relative = R.from_quat(ori_relative)
+ q_result = q_relative * q_child
+ return q_result.as_quat()
+
+
+def quat2handposition(quat, bone):
+ root = bone[0]
+ fn = np.array([0, 0, 1, 2, 3, 0, 5, 6, 7, 8, 0, 10, 11, 12, 13, 0, 15, 16, 17, 18, 0, 20, 21, 22, 23])
+ orin = np.array([0, 0, 1, 2, 3, 0, 4, 5, 6, 7, 0, 8, 9, 10, 11, 0, 12, 13, 14, 15, 0, 16, 17, 18, 19])
+ boneVer = bone[:25] - bone[fn]
+ pos = np.ones((bone.shape[0], 1)) * root
+ for i in range(1, 25):
+ qt = quat[orin[i]]
+ pos[i] = boneVer[i] @ quaternion_matrixinv(qt) + pos[fn[i]]
+ return pos
+
+
+def rotate_matrix_x(radians):
+ return np.array([
+ [1, 0, 0],
+ [0, np.cos(radians), -np.sin(radians)],
+ [0, np.sin(radians), np.cos(radians)]
+ ])
+
+
+def rotate_matrix_y(radians):
+ return np.array([
+ [np.cos(radians), 0, np.sin(radians)],
+ [0, 1, 0],
+ [-np.sin(radians), 0, np.cos(radians)]
+ ])
+
+
+def rotate_matrix_z(radians):
+ return np.array([
+ [np.cos(radians), -np.sin(radians), 0],
+ [np.sin(radians), np.cos(radians), 0],
+ [0, 0, 1]
+ ])
+
+
+def rotate_quaternion(original_quat, roll, pitch, yaw):
+ """应用绕X, Y, Z轴的旋转到原始四元数。
+
+ 参数:
+ original_quat (array_like): 原始四元数 [x, y, z, w] 格式。
+ roll (float): 绕X轴旋转的角度(度)。
+ pitch (float): 绕Y轴旋转的角度(度)。
+ yaw (float): 绕Z轴旋转的角度(度)。
+
+ 返回:
+ np.ndarray: 旋转后的四元数 [x, y, z, w]
+ """
+ # 原始四元数转换为旋转对象
+ original_rotation = R.from_quat(original_quat)
+
+ # 将角度转换为弧度
+ roll_rad = np.radians(roll)
+ pitch_rad = np.radians(pitch)
+ yaw_rad = np.radians(yaw)
+
+ # 创建旋转对象,从给定的欧拉角创建一个新的旋转对象
+ rotation = R.from_euler('xyz', [roll_rad, pitch_rad, yaw_rad])
+
+ # 组合旋转,先应用原始旋转,再应用新旋转
+ new_rotation = original_rotation * rotation
+
+ # 返回结果四元数,转换为 [x, y, z, w] 形式
+ return new_rotation.as_quat()
+
+
+def cubic_model(x, a, b, c, d):
+ """Cubic model for curve fitting"""
+ return a * x ** 3 + b * x ** 2 + c * x + d
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/vtrdyncore.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/vtrdyncore.py
new file mode 100644
index 0000000..93bf72b
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/vtrdyncore.py
@@ -0,0 +1,208 @@
+import socket
+import struct
+import time
+from threading import Thread
+import threading
+
+NODES_BODY = 23
+NODES_HAND = 20
+NODES_FACEBS_ARKIT = 52
+NODES_FACEBS_AUDIO = 26
+
+DC_QUAT = 1e-4 # short -> double
+DC_POSITION = 1e-3 # short -> double
+DC_POWER = 1e-2 # short -> double
+
+uc_ConnectsendBytes = bytes(
+ [0xfa, 0x00, 0x00, 0x0b, 0x04, 0x03, 0xa2, 0x53, 0x23, 0x52, 0xce, 0x32, 0x99, 0xf4, 0x32, 0xfb, 0x30])
+uc_DisConnectsendBytes = bytes([0xfa, 0x00, 0x00, 0x03, 0x04, 0x0b, 0xa1, 0xfb, 0xa8])
+
+
+class MocapData:
+ def __init__(self):
+ self.is_update = False
+ self.frame_index = 0
+ self.frequency = 0
+ self.ns_result = 0
+
+ self.sensor_state_body = [0] * NODES_BODY
+ self.position_body = [[0.0, 0.0, 0.0] for _ in range(NODES_BODY)]
+ self.quaternion_body = [[0.0, 0.0, 0.0, 0.0] for _ in range(NODES_BODY)]
+ self.gyr_body = [[0.0, 0.0, 0.0] for _ in range(NODES_BODY)]
+ self.acc_body = [[0.0, 0.0, 0.0] for _ in range(NODES_BODY)]
+ self.velocity_body = [[0.0, 0.0, 0.0] for _ in range(NODES_BODY)]
+
+ self.sensor_state_r_hand = [0] * NODES_HAND
+ self.position_rHand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.quaternion_rHand = [[0.0, 0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.gyr_r_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.acc_r_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.velocity_r_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+
+ self.sensor_state_l_hand = [0] * NODES_HAND
+ self.position_lHand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.quaternion_lHand = [[0.0, 0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.gyr_l_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.acc_l_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+ self.velocity_l_hand = [[0.0, 0.0, 0.0] for _ in range(NODES_HAND)]
+
+ self.is_use_face_blend_shapes_arkit = False
+ self.is_use_face_blend_shapes_audio = False
+ self.face_blend_shapes_arkit = [0.0] * NODES_FACEBS_ARKIT
+ self.face_blend_shapes_audio = [0.0] * NODES_FACEBS_AUDIO
+ self.local_quat_right_eyeball = [0.0] * 4
+ self.local_quat_left_eyeball = [0.0] * 4
+
+
+class VtrdynSocketUdp:
+ def __init__(self, debug = False):
+ self.socket_udp = None
+ self.is_use_face_blend_shapes_arkit = False
+ self.send_thread = None
+ self.send_running = False
+ self.mocap_data_realtime = MocapData()
+ self.data_lock = threading.Lock()
+ self.isconnect = False
+ self.debug = debug
+
+ def udp_initial(self, local_port: int) -> bool:
+ """初始化 UDP socket"""
+ try:
+ self.socket_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ self.socket_udp.bind(('', local_port))
+ self.socket_udp.settimeout(10)
+ self.isconnect = True
+ return True
+ except socket.error as e:
+ self.isconnect = False
+ if self.debug:
+ print(f"Socket initialization failed: {e}")
+ if self.socket_udp:
+ self.socket_udp.close()
+ return False
+
+ @staticmethod
+ def udp_getsockaddr(ip: str, port: int) -> tuple:
+ """将 IP 地址及端口号转化为能识别的地址格式"""
+ return (ip, port)
+
+ def __recv(self, buffer_size: int = 3415) -> tuple:
+ try:
+ data, addr = self.socket_udp.recvfrom(buffer_size)
+ return data, addr
+ except socket.error as e:
+ return None, None
+
+ def udp_close(self, dst_addr: tuple) -> bool:
+ self.socket_udp.sendto(uc_DisConnectsendBytes, dst_addr)
+ self.send_running = False
+ self.send_thread.join()
+ time.sleep(0.1)
+ if self.socket_udp:
+ self.socket_udp.close()
+ return self.send_running
+
+ def udp_send_request_connect(self, dst_addr: tuple) -> bool:
+ connerrflag = False
+ try:
+ self.socket_udp.sendto(uc_ConnectsendBytes, dst_addr)
+ if self.debug:
+ print(f"Initialization packet sent to {dst_addr}")
+ # 等待确认响应
+ bytes_data, addr = self.socket_udp.recvfrom(1024)
+ if addr == dst_addr:
+ connerrflag = True
+ if self.debug:
+ print(f"Connection established with {addr}")
+ except socket.timeout:
+ if self.debug:
+ print("Initialization timeout: No response from target")
+ except socket.error as e:
+ if self.debug:
+ print(f"Initialization error: {str(e)}")
+ self.send_running = True
+ self.send_thread = Thread(target=self.__udp_process)
+ self.send_thread.start()
+ return connerrflag
+
+ def udp_is_onnect(self) -> bool:
+ return self.isconnect
+
+ def __udp_process(self):
+ while self.send_running:
+ try:
+ bytes_data, addr = self.__recv()
+ if bytes_data is None:
+ self.isconnect = False
+ time.sleep(0.01)
+ continue
+ self.isconnect = True
+ if len(bytes_data) < 683 or (bytes_data[2] << 8 | bytes_data[3]) - 3 < (NODES_BODY * 8) or bytes_data[
+ 0] != 250 or bytes_data[681] != 251:
+ return False
+ mocap_temp = MocapData()
+ offset = 1
+ mocap_temp.frame_index = bytes_data[offset]
+ offset = 7 # Move to the next field
+ mocap_temp.is_update = bool(bytes_data[offset])
+ offset = 10 # Move to frequency
+ mocap_temp.frequency = bytes_data[offset]
+ offset = 11 # Move to hips_position
+ for i in range(3):
+ # 每个位置分量
+ mocap_temp.position_body[i] = \
+ struct.unpack('>h', bytes_data[offset + i * 2:offset + i * 2 + 2])[
+ 0] * DC_POSITION
+ offset = offset + 6 + NODES_BODY # 移动到 quaternion_body 开始的位置
+ for i in range(NODES_BODY):
+ for j in range(4):
+ current_offset = offset + i * 8 + j * 2
+ mocap_temp.quaternion_body[i][j] = \
+ struct.unpack('>h', bytes_data[current_offset:current_offset + 2])[
+ 0] * DC_QUAT
+ offset = offset + NODES_BODY * 8 + NODES_HAND # 移动到 quaternion_rightHand 开始的位置
+ for i in range(NODES_HAND):
+ for j in range(4):
+ current_offset = offset + i * 8 + j * 2
+ mocap_temp.quaternion_rHand[i][j] = \
+ struct.unpack('>h', bytes_data[current_offset:current_offset + 2])[
+ 0] * DC_QUAT
+ offset = offset + NODES_HAND * 8 + NODES_HAND # 移动到 quaternion_leftHand 开始的位置
+ for i in range(NODES_HAND):
+ for j in range(4):
+ current_offset = offset + i * 8 + j * 2
+ mocap_temp.quaternion_lHand[i][j] = \
+ struct.unpack('>h', bytes_data[current_offset:current_offset + 2])[
+ 0] * DC_QUAT
+ offset = offset + NODES_HAND * 8 + 1 # 移动到 isUseBlendShapeArkit
+ mocap_temp.is_use_face_blend_shapes_arkit = bool(bytes_data[offset])
+ with self.data_lock:
+ self.mocap_data_realtime.frame_index = mocap_temp.frame_index
+
+ self.mocap_data_realtime.is_update = mocap_temp.is_update
+ self.mocap_data_realtime.frequency = mocap_temp.frequency
+ self.mocap_data_realtime.quaternion_rHand = mocap_temp.quaternion_rHand
+ self.mocap_data_realtime.quaternion_lHand = mocap_temp.quaternion_lHand
+ time.sleep(0.01)
+ except socket.timeout:
+ # 检查连接超时
+ if self.connected:
+ if self.debug:
+ print("Connection timeout detected!")
+ self.connected = False
+ break
+ continue
+
+ except Exception as e:
+ if self.debug:
+ print(f"Receive error: {str(e)}")
+ break
+
+ def udp_recv_mocap_data(self, mocap_data: MocapData) -> bool:
+ with self.data_lock:
+ mocap_data.frame_index = self.mocap_data_realtime.frame_index
+ mocap_data.is_update = self.mocap_data_realtime.is_update
+ mocap_data.frequency = self.mocap_data_realtime.frequency
+ mocap_data.quaternion_rHand = self.mocap_data_realtime.quaternion_rHand
+ mocap_data.quaternion_lHand = self.mocap_data_realtime.quaternion_lHand
+ return True
diff --git a/src/linkerhand_retarget/linkerhand_retarget/linkerhand/yourdfpy.py b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/yourdfpy.py
new file mode 100644
index 0000000..6937c37
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/linkerhand/yourdfpy.py
@@ -0,0 +1,2237 @@
+# Code from yourdfpy with small modification for deprecated warning
+# Source: https://github.com/clemense/yourdfpy/blob/main/src/yourdfpy/urdf.py
+
+import copy
+import logging
+import os
+from dataclasses import dataclass, field, is_dataclass
+from functools import partial
+from typing import Dict, List, Optional, Union
+
+import anytree
+import numpy as np
+import six
+import trimesh
+import trimesh.transformations as tra
+from anytree import Node, LevelOrderIter
+from lxml import etree
+
+_logger = logging.getLogger(__name__)
+
+
+def _array_eq(arr1, arr2):
+ if arr1 is None and arr2 is None:
+ return True
+ return (
+ isinstance(arr1, np.ndarray)
+ and isinstance(arr2, np.ndarray)
+ and arr1.shape == arr2.shape
+ and (arr1 == arr2).all()
+ )
+
+
+@dataclass(eq=False)
+class TransmissionJoint:
+ name: str
+ hardware_interfaces: List[str] = field(default_factory=list)
+
+ def __eq__(self, other):
+ if not isinstance(other, TransmissionJoint):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and all(self_hi in other.hardware_interfaces for self_hi in self.hardware_interfaces)
+ and all(other_hi in self.hardware_interfaces for other_hi in other.hardware_interfaces)
+ )
+
+
+@dataclass(eq=False)
+class Actuator:
+ name: str
+ mechanical_reduction: Optional[float] = None
+ # The follwing is only valid for ROS Indigo and prior versions
+ hardware_interfaces: List[str] = field(default_factory=list)
+
+ def __eq__(self, other):
+ if not isinstance(other, Actuator):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and self.mechanical_reduction == other.mechanical_reduction
+ and all(self_hi in other.hardware_interfaces for self_hi in self.hardware_interfaces)
+ and all(other_hi in self.hardware_interfaces for other_hi in other.hardware_interfaces)
+ )
+
+
+@dataclass(eq=False)
+class Transmission:
+ name: str
+ type: Optional[str] = None
+ joints: List[TransmissionJoint] = field(default_factory=list)
+ actuators: List[Actuator] = field(default_factory=list)
+
+ def __eq__(self, other):
+ if not isinstance(other, Transmission):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and self.type == other.type
+ and all(self_joint in other.joints for self_joint in self.joints)
+ and all(other_joint in self.joints for other_joint in other.joints)
+ and all(self_actuator in other.actuators for self_actuator in self.actuators)
+ and all(other_actuator in self.actuators for other_actuator in other.actuators)
+ )
+
+
+@dataclass
+class Calibration:
+ rising: Optional[float] = None
+ falling: Optional[float] = None
+
+
+@dataclass
+class Mimic:
+ joint: str
+ multiplier: Optional[float] = None
+ offset: Optional[float] = None
+
+
+@dataclass
+class SafetyController:
+ soft_lower_limit: Optional[float] = None
+ soft_upper_limit: Optional[float] = None
+ k_position: Optional[float] = None
+ k_velocity: Optional[float] = None
+
+
+@dataclass
+class Sphere:
+ radius: float
+
+
+@dataclass
+class Cylinder:
+ radius: float
+ length: float
+
+
+@dataclass(eq=False)
+class Box:
+ size: np.ndarray
+
+ def __eq__(self, other):
+ if not isinstance(other, Box):
+ return NotImplemented
+ return _array_eq(self.size, other.size)
+
+
+@dataclass(eq=False)
+class Mesh:
+ filename: str
+ scale: Optional[Union[float, np.ndarray]] = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Mesh):
+ return NotImplemented
+
+ if self.filename != other.filename:
+ return False
+
+ if isinstance(self.scale, float) and isinstance(other.scale, float):
+ return self.scale == other.scale
+
+ return _array_eq(self.scale, other.scale)
+
+
+@dataclass
+class Geometry:
+ box: Optional[Box] = None
+ cylinder: Optional[Cylinder] = None
+ sphere: Optional[Sphere] = None
+ mesh: Optional[Mesh] = None
+
+
+@dataclass(eq=False)
+class Color:
+ rgba: np.ndarray
+
+ def __eq__(self, other):
+ if not isinstance(other, Color):
+ return NotImplemented
+ return _array_eq(self.rgba, other.rgba)
+
+
+@dataclass
+class Texture:
+ filename: str
+
+
+@dataclass
+class Material:
+ name: Optional[str] = None
+ color: Optional[Color] = None
+ texture: Optional[Texture] = None
+
+
+@dataclass(eq=False)
+class Visual:
+ name: Optional[str] = None
+ origin: Optional[np.ndarray] = None
+ geometry: Optional[Geometry] = None # That's not really optional according to ROS
+ material: Optional[Material] = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Visual):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and _array_eq(self.origin, other.origin)
+ and self.geometry == other.geometry
+ and self.material == other.material
+ )
+
+
+@dataclass(eq=False)
+class Collision:
+ name: str
+ origin: Optional[np.ndarray] = None
+ geometry: Geometry = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Collision):
+ return NotImplemented
+ return self.name == other.name and _array_eq(self.origin, other.origin) and self.geometry == other.geometry
+
+
+@dataclass(eq=False)
+class Inertial:
+ origin: Optional[np.ndarray] = None
+ mass: Optional[float] = None
+ inertia: Optional[np.ndarray] = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Inertial):
+ return NotImplemented
+ return (
+ _array_eq(self.origin, other.origin) and self.mass == other.mass and _array_eq(self.inertia, other.inertia)
+ )
+
+
+@dataclass(eq=False)
+class Link:
+ name: str
+ inertial: Optional[Inertial] = None
+ visuals: List[Visual] = field(default_factory=list)
+ collisions: List[Collision] = field(default_factory=list)
+
+ def __eq__(self, other):
+ if not isinstance(other, Link):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and self.inertial == other.inertial
+ and all(self_visual in other.visuals for self_visual in self.visuals)
+ and all(other_visual in self.visuals for other_visual in other.visuals)
+ and all(self_collision in other.collisions for self_collision in self.collisions)
+ and all(other_collision in self.collisions for other_collision in other.collisions)
+ )
+
+
+@dataclass
+class Dynamics:
+ damping: Optional[float] = None
+ friction: Optional[float] = None
+
+
+@dataclass
+class Limit:
+ effort: Optional[float] = None
+ velocity: Optional[float] = None
+ lower: Optional[float] = None
+ upper: Optional[float] = None
+
+
+@dataclass(eq=False)
+class Joint:
+ name: str
+ type: str = None
+ parent: str = None
+ child: str = None
+ origin: np.ndarray = None
+ axis: np.ndarray = None
+ dynamics: Optional[Dynamics] = None
+ limit: Optional[Limit] = None
+ mimic: Optional[Mimic] = None
+ calibration: Optional[Calibration] = None
+ safety_controller: Optional[SafetyController] = None
+
+ def __eq__(self, other):
+ if not isinstance(other, Joint):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and self.type == other.type
+ and self.parent == other.parent
+ and self.child == other.child
+ and _array_eq(self.origin, other.origin)
+ and _array_eq(self.axis, other.axis)
+ and self.dynamics == other.dynamics
+ and self.limit == other.limit
+ and self.mimic == other.mimic
+ and self.calibration == other.calibration
+ and self.safety_controller == other.safety_controller
+ )
+
+
+@dataclass(eq=False)
+class Robot:
+ name: str
+ links: List[Link] = field(default_factory=list)
+ joints: List[Joint] = field(default_factory=list)
+ materials: List[Material] = field(default_factory=list)
+ transmission: List[str] = field(default_factory=list)
+ gazebo: List[str] = field(default_factory=list)
+
+ def __eq__(self, other):
+ if not isinstance(other, Robot):
+ return NotImplemented
+ return (
+ self.name == other.name
+ and all(self_link in other.links for self_link in self.links)
+ and all(other_link in self.links for other_link in other.links)
+ and all(self_joint in other.joints for self_joint in self.joints)
+ and all(other_joint in self.joints for other_joint in other.joints)
+ and all(self_material in other.materials for self_material in self.materials)
+ and all(other_material in self.materials for other_material in other.materials)
+ and all(self_transmission in other.transmission for self_transmission in self.transmission)
+ and all(other_transmission in self.transmission for other_transmission in other.transmission)
+ and all(self_gazebo in other.gazebo for self_gazebo in self.gazebo)
+ and all(other_gazebo in self.gazebo for other_gazebo in other.gazebo)
+ )
+
+
+class URDFError(Exception):
+ """General URDF exception."""
+
+ def __init__(self, msg):
+ super(URDFError, self).__init__()
+ self.msg = msg
+
+ def __str__(self):
+ return type(self).__name__ + ": " + self.msg
+
+ def __repr__(self):
+ return type(self).__name__ + '("' + self.msg + '")'
+
+
+class URDFIncompleteError(URDFError):
+ """Raised when needed data for an object isn't there."""
+
+ pass
+
+
+class URDFAttributeValueError(URDFError):
+ """Raised when attribute value is not contained in the set of allowed values."""
+
+ pass
+
+
+class URDFBrokenRefError(URDFError):
+ """Raised when a referenced object is not found in the scope."""
+
+ pass
+
+
+class URDFMalformedError(URDFError):
+ """Raised when data is found to be corrupted in some way."""
+
+ pass
+
+
+class URDFUnsupportedError(URDFError):
+ """Raised when some unexpectedly unsupported feature is found."""
+
+ pass
+
+
+class URDFSaveValidationError(URDFError):
+ """Raised when XML validation fails when saving."""
+
+ pass
+
+
+def _str2float(s):
+ """Cast string to float if it is not None. Otherwise return None.
+
+ Args:
+ s (str): String to convert or None.
+
+ Returns:
+ str or NoneType: The converted string or None.
+ """
+ return float(s) if s is not None else None
+
+
+def apply_visual_color(
+ geom: trimesh.Trimesh,
+ visual: Visual,
+ material_map: Dict[str, Material],
+) -> None:
+ """Apply the color of the visual material to the mesh.
+
+ Args:
+ geom: Trimesh to color.
+ visual: Visual description from XML.
+ material_map: Dictionary mapping material names to their definitions.
+ """
+ if visual.material is None:
+ return
+
+ if visual.material.color is not None:
+ color = visual.material.color
+ elif visual.material.name is not None and visual.material.name in material_map:
+ color = material_map[visual.material.name].color
+ else:
+ return
+
+ if color is None:
+ return
+ if isinstance(geom.visual, trimesh.visual.ColorVisuals):
+ geom.visual.face_colors[:] = [int(255 * channel) for channel in color.rgba]
+
+
+def filename_handler_null(fname):
+ """A lazy filename handler that simply returns its input.
+
+ Args:
+ fname (str): A file name.
+
+ Returns:
+ str: Same file name.
+ """
+ return fname
+
+
+def filename_handler_ignore_directive(fname):
+ """A filename handler that removes anything before (and including) '://'.
+
+ Args:
+ fname (str): A file name.
+
+ Returns:
+ str: The file name without the prefix.
+ """
+ if "://" in fname or ":\\\\" in fname:
+ return ":".join(fname.split(":")[1:])[2:]
+ return fname
+
+
+def filename_handler_ignore_directive_package(fname):
+ """A filename handler that removes the 'package://' directive and the package it refers to.
+ It subsequently calls filename_handler_ignore_directive, i.e., it removes any other directive.
+
+ Args:
+ fname (str): A file name.
+
+ Returns:
+ str: The file name without 'package://' and the package name.
+ """
+ if fname.startswith("package://"):
+ string_length = len("package://")
+ return os.path.join(*os.path.normpath(fname[string_length:]).split(os.path.sep)[1:])
+ return filename_handler_ignore_directive(fname)
+
+
+def filename_handler_add_prefix(fname, prefix):
+ """A filename handler that adds a prefix.
+
+ Args:
+ fname (str): A file name.
+ prefix (str): A prefix.
+
+ Returns:
+ str: Prefix plus file name.
+ """
+ return prefix + fname
+
+
+def filename_handler_absolute2relative(fname, dir):
+ """A filename handler that turns an absolute file name into a relative one.
+
+ Args:
+ fname (str): A file name.
+ dir (str): A directory.
+
+ Returns:
+ str: The file name relative to the directory.
+ """
+ # TODO: that's not right
+ if fname.startswith(dir):
+ return fname[len(dir) :]
+ return fname
+
+
+def filename_handler_relative(fname, dir):
+ """A filename handler that joins a file name with a directory.
+
+ Args:
+ fname (str): A file name.
+ dir (str): A directory.
+
+ Returns:
+ str: The directory joined with the file name.
+ """
+ return os.path.join(dir, filename_handler_ignore_directive_package(fname))
+
+
+def filename_handler_relative_to_urdf_file(fname, urdf_fname):
+ return filename_handler_relative(fname, os.path.dirname(urdf_fname))
+
+
+def filename_handler_relative_to_urdf_file_recursive(fname, urdf_fname, level=0):
+ if level == 0:
+ return filename_handler_relative_to_urdf_file(fname, urdf_fname)
+ return filename_handler_relative_to_urdf_file_recursive(fname, os.path.split(urdf_fname)[0], level=level - 1)
+
+
+def _create_filename_handlers_to_urdf_file_recursive(urdf_fname):
+ return [
+ partial(
+ filename_handler_relative_to_urdf_file_recursive,
+ urdf_fname=urdf_fname,
+ level=i,
+ )
+ for i in range(len(os.path.normpath(urdf_fname).split(os.path.sep)))
+ ]
+
+
+def filename_handler_meta(fname, filename_handlers):
+ """A filename handler that calls other filename handlers until the resulting file name points to an existing file.
+
+ Args:
+ fname (str): A file name.
+ filename_handlers (list(fn)): A list of function pointers to filename handlers.
+
+ Returns:
+ str: The resolved file name that points to an existing file or the input if none of the files exists.
+ """
+ for fn in filename_handlers:
+ candidate_fname = fn(fname=fname)
+ _logger.debug(f"Checking filename: {candidate_fname}")
+ if os.path.isfile(candidate_fname):
+ return candidate_fname
+ _logger.warning(f"Unable to resolve filename: {fname}")
+ return fname
+
+
+def filename_handler_magic(fname, dir):
+ """A magic filename handler.
+
+ Args:
+ fname (str): A file name.
+ dir (str): A directory.
+
+ Returns:
+ str: The file name that exists or the input if nothing is found.
+ """
+ return filename_handler_meta(
+ fname=fname,
+ filename_handlers=[
+ partial(filename_handler_relative, dir=dir),
+ filename_handler_ignore_directive,
+ ]
+ + _create_filename_handlers_to_urdf_file_recursive(urdf_fname=dir),
+ )
+
+
+def validation_handler_strict(errors):
+ """A validation handler that does not allow any errors.
+
+ Args:
+ errors (list[yourdfpy.URDFError]): List of errors.
+
+ Returns:
+ bool: Whether any errors were found.
+ """
+ return len(errors) == 0
+
+
+class URDF:
+ def __init__(
+ self,
+ robot: Robot = None,
+ build_scene_graph: bool = True,
+ build_collision_scene_graph: bool = False,
+ load_meshes: bool = True,
+ load_collision_meshes: bool = False,
+ filename_handler=None,
+ mesh_dir: str = "",
+ force_mesh: bool = False,
+ force_collision_mesh: bool = True,
+ build_tree: bool = False,
+ ):
+ """A URDF model.
+
+ Args:
+ robot (Robot): The robot model. Defaults to None.
+ build_scene_graph (bool, optional): Wheter to build a scene graph to enable transformation queries and forward kinematics. Defaults to True.
+ build_collision_scene_graph (bool, optional): Wheter to build a scene graph for elements. Defaults to False.
+ load_meshes (bool, optional): Whether to load the meshes referenced in the elements. Defaults to True.
+ load_collision_meshes (bool, optional): Whether to load the collision meshes referenced in the elements. Defaults to False.
+ filename_handler ([type], optional): Any function f(in: str) -> str, that maps filenames in the URDF to actual resources. Can be used to customize treatment of `package://` directives or relative/absolute filenames. Defaults to None.
+ mesh_dir (str, optional): A root directory used for loading meshes. Defaults to "".
+ force_mesh (bool, optional): Each loaded geometry will be concatenated into a single one (instead of being turned into a graph; in case the underlying file contains multiple geometries). This might loose texture information but the resulting scene graph will be smaller. Defaults to False.
+ force_collision_mesh (bool, optional): Same as force_mesh, but for collision scene. Defaults to True.
+ build_tree (bool, optional): Build the tree structure for global kinematics computation
+ """
+ if filename_handler is None:
+ self._filename_handler = partial(filename_handler_magic, dir=mesh_dir)
+ else:
+ self._filename_handler = filename_handler
+
+ self.robot = robot
+ self._create_maps()
+ self._update_actuated_joints()
+
+ self._cfg = self.zero_cfg
+
+ if build_scene_graph or build_collision_scene_graph:
+ self._base_link = self._determine_base_link()
+ else:
+ self._base_link = None
+
+ self._errors = []
+
+ if build_scene_graph:
+ self._scene = self._create_scene(
+ use_collision_geometry=False,
+ load_geometry=load_meshes,
+ force_mesh=force_mesh,
+ force_single_geometry_per_link=force_mesh,
+ )
+ else:
+ self._scene = None
+
+ if build_collision_scene_graph:
+ self._scene_collision = self._create_scene(
+ use_collision_geometry=True,
+ load_geometry=load_collision_meshes,
+ force_mesh=force_collision_mesh,
+ force_single_geometry_per_link=force_collision_mesh,
+ )
+ else:
+ self._scene_collision = None
+
+ if build_tree:
+ self.tree_root = self.build_tree()
+ else:
+ self.tree_root = None
+
+ @property
+ def scene(self) -> trimesh.Scene:
+ """A scene object representing the URDF model.
+
+ Returns:
+ trimesh.Scene: A trimesh scene object.
+ """
+ return self._scene
+
+ @property
+ def collision_scene(self) -> trimesh.Scene:
+ """A scene object representing the elements of the URDF model
+
+ Returns:
+ trimesh.Scene: A trimesh scene object.
+ """
+ return self._scene_collision
+
+ @property
+ def link_map(self) -> dict:
+ """A dictionary mapping link names to link objects.
+
+ Returns:
+ dict: Mapping from link name (str) to Link.
+ """
+ return self._link_map
+
+ @property
+ def joint_map(self) -> dict:
+ """A dictionary mapping joint names to joint objects.
+
+ Returns:
+ dict: Mapping from joint name (str) to Joint.
+ """
+ return self._joint_map
+
+ @property
+ def joint_names(self):
+ """List of joint names.
+
+ Returns:
+ list[str]: List of joint names of the URDF model.
+ """
+ return [j.name for j in self.robot.joints]
+
+ @property
+ def actuated_joints(self):
+ """List of actuated joints. This excludes mimic and fixed joints.
+
+ Returns:
+ list[Joint]: List of actuated joints of the URDF model.
+ """
+ return self._actuated_joints
+
+ @property
+ def actuated_dof_indices(self):
+ """List of DOF indices per actuated joint. Can be used to reference configuration.
+
+ Returns:
+ list[list[int]]: List of DOF indices per actuated joint.
+ """
+ return self._actuated_dof_indices
+
+ @property
+ def actuated_joint_indices(self):
+ """List of indices of all joints that are actuated, i.e., not of type mimic or fixed.
+
+ Returns:
+ list[int]: List of indices of actuated joints.
+ """
+ return self._actuated_joint_indices
+
+ @property
+ def actuated_joint_names(self):
+ """List of names of actuated joints. This excludes mimic and fixed joints.
+
+ Returns:
+ list[str]: List of names of actuated joints of the URDF model.
+ """
+ return [j.name for j in self._actuated_joints]
+
+ @property
+ def num_actuated_joints(self):
+ """Number of actuated joints.
+
+ Returns:
+ int: Number of actuated joints.
+ """
+ return len(self.actuated_joints)
+
+ @property
+ def num_dofs(self):
+ """Number of degrees of freedom of actuated joints. Depending on the type of the joint, the number of DOFs might vary.
+
+ Returns:
+ int: Degrees of freedom.
+ """
+ total_num_dofs = 0
+ for j in self._actuated_joints:
+ if j.type in ["revolute", "prismatic", "continuous"]:
+ total_num_dofs += 1
+ elif j.type == "floating":
+ total_num_dofs += 6
+ elif j.type == "planar":
+ total_num_dofs += 2
+ return total_num_dofs
+
+ @property
+ def zero_cfg(self):
+ """Return the zero configuration.
+
+ Returns:
+ np.ndarray: The zero configuration.
+ """
+ return np.zeros(self.num_dofs)
+
+ @property
+ def center_cfg(self):
+ """Return center configuration of URDF model by using the average of each joint's limits if present, otherwise zero.
+
+ Returns:
+ (n), float: Default configuration of URDF model.
+ """
+ config = []
+ config_names = []
+ for j in self._actuated_joints:
+ if j.type == "revolute" or j.type == "prismatic":
+ if j.limit is not None:
+ cfg = [j.limit.lower + 0.5 * (j.limit.upper - j.limit.lower)]
+ else:
+ cfg = [0.0]
+ elif j.type == "continuous":
+ cfg = [0.0]
+ elif j.type == "floating":
+ cfg = [0.0] * 6
+ elif j.type == "planar":
+ cfg = [0.0] * 2
+
+ config.append(cfg)
+ config_names.append(j.name)
+
+ for i, j in enumerate(self.robot.joints):
+ if j.mimic is not None:
+ index = config_names.index(j.mimic.joint)
+ config[i][0] = config[index][0] * j.mimic.multiplier + j.mimic.offset
+
+ if len(config) == 0:
+ return np.array([], dtype=np.float64)
+ return np.concatenate(config)
+
+ @property
+ def cfg(self):
+ """Current configuration.
+
+ Returns:
+ np.ndarray: Current configuration of URDF model.
+ """
+ return self._cfg
+
+ @property
+ def base_link(self):
+ """Name of URDF base/root link.
+
+ Returns:
+ str: Name of base link of URDF model.
+ """
+ return self._base_link
+
+ @property
+ def errors(self) -> list:
+ """A list with validation errors.
+
+ Returns:
+ list: A list of validation errors.
+ """
+ return self._errors
+
+ def clear_errors(self):
+ """Clear the validation error log."""
+ self._errors = []
+
+ def show(self, collision_geometry=False, callback=None):
+ """Open a simpler viewer displaying the URDF model.
+
+ Args:
+ collision_geometry (bool, optional): Whether to display the or elements. Defaults to False.
+ """
+ if collision_geometry:
+ if self._scene_collision is None:
+ raise ValueError(
+ "No collision scene available. Use build_collision_scene_graph=True and load_collision_meshes=True during loading."
+ )
+ else:
+ self._scene_collision.show(callback=callback)
+ else:
+ if self._scene is None:
+ raise ValueError("No scene available. Use build_scene_graph=True and load_meshes=True during loading.")
+ elif len(self._scene.bounds_corners) < 1:
+ raise ValueError(
+ "Scene is empty, maybe meshes failed to load? Use build_scene_graph=True and load_meshes=True during loading."
+ )
+ else:
+ self._scene.show(callback=callback)
+
+ def validate(self, validation_fn=None) -> bool:
+ """Validate URDF model.
+
+ Args:
+ validation_fn (function, optional): A function f(list[yourdfpy.URDFError]) -> bool. None uses the strict handler (any error leads to False). Defaults to None.
+
+ Returns:
+ bool: Whether the model is valid.
+ """
+ self._errors = []
+ self._validate_robot(self.robot)
+
+ if validation_fn is None:
+ validation_fn = validation_handler_strict
+
+ return validation_fn(self._errors)
+
+ def _create_maps(self):
+ self._material_map = {}
+ for m in self.robot.materials:
+ self._material_map[m.name] = m
+
+ self._joint_map = {}
+ for j in self.robot.joints:
+ self._joint_map[j.name] = j
+
+ self._link_map = {}
+ for l in self.robot.links:
+ self._link_map[l.name] = l
+
+ def _update_actuated_joints(self):
+ self._actuated_joints = []
+ self._actuated_joint_indices = []
+ self._actuated_dof_indices = []
+
+ dof_indices_cnt = 0
+ for i, j in enumerate(self.robot.joints):
+ if j.mimic is None and j.type != "fixed":
+ self._actuated_joints.append(j)
+ self._actuated_joint_indices.append(i)
+
+ if j.type in ["prismatic", "revolute", "continuous"]:
+ self._actuated_dof_indices.append([dof_indices_cnt])
+ dof_indices_cnt += 1
+ elif j.type == "floating":
+ self._actuated_dof_indices.append([dof_indices_cnt, dof_indices_cnt + 1, dof_indices_cnt + 2])
+ dof_indices_cnt += 3
+ elif j.type == "planar":
+ self._actuated_dof_indices.append([dof_indices_cnt, dof_indices_cnt + 1])
+ dof_indices_cnt += 2
+
+ def _validate_required_attribute(self, attribute, error_msg, allowed_values=None):
+ if attribute is None:
+ self._errors.append(URDFIncompleteError(error_msg))
+ elif isinstance(attribute, str) and len(attribute) == 0:
+ self._errors.append(URDFIncompleteError(error_msg))
+
+ if allowed_values is not None and attribute is not None:
+ if attribute not in allowed_values:
+ self._errors.append(URDFAttributeValueError(error_msg))
+
+ @staticmethod
+ def load(fname_or_file, add_dummy_free_joints=False, **kwargs):
+ """Load URDF file from filename or file object.
+
+ Args:
+ fname_or_file (str or file object): A filename or file object, file-like object, stream representing the URDF file.
+ **build_scene_graph (bool, optional): Wheter to build a scene graph to enable transformation queries and forward kinematics. Defaults to True.
+ **build_collision_scene_graph (bool, optional): Wheter to build a scene graph for elements. Defaults to False.
+ **load_meshes (bool, optional): Whether to load the meshes referenced in the elements. Defaults to True.
+ **load_collision_meshes (bool, optional): Whether to load the collision meshes referenced in the elements. Defaults to False.
+ **filename_handler ([type], optional): Any function f(in: str) -> str, that maps filenames in the URDF to actual resources. Can be used to customize treatment of `package://` directives or relative/absolute filenames. Defaults to None.
+ **mesh_dir (str, optional): A root directory used for loading meshes. Defaults to "".
+ **force_mesh (bool, optional): Each loaded geometry will be concatenated into a single one (instead of being turned into a graph; in case the underlying file contains multiple geometries). This might loose texture information but the resulting scene graph will be smaller. Defaults to False.
+ **force_collision_mesh (bool, optional): Same as force_mesh, but for collision scene. Defaults to True.
+
+ Raises:
+ ValueError: If filename does not exist.
+
+ Returns:
+ yourdfpy.URDF: URDF model.
+ """
+ if isinstance(fname_or_file, six.string_types):
+ if not os.path.isfile(fname_or_file):
+ raise ValueError("{} is not a file".format(fname_or_file))
+
+ if not "mesh_dir" in kwargs:
+ kwargs["mesh_dir"] = os.path.dirname(fname_or_file)
+
+ try:
+ parser = etree.XMLParser(remove_blank_text=True)
+ tree = etree.parse(fname_or_file, parser=parser)
+ xml_root = tree.getroot()
+ except Exception as e:
+ _logger.error(e)
+ _logger.error("Using different parsing approach.")
+
+ events = ("start", "end", "start-ns", "end-ns")
+ xml = etree.iterparse(fname_or_file, recover=True, events=events)
+
+ # Iterate through all XML elements
+ for action, elem in xml:
+ # Skip comments and processing instructions,
+ # because they do not have names
+ if not (isinstance(elem, etree._Comment) or isinstance(elem, etree._ProcessingInstruction)):
+ # Remove a namespace URI in the element's name
+ # elem.tag = etree.QName(elem).localname
+ if action == "end" and ":" in elem.tag:
+ elem.getparent().remove(elem)
+
+ xml_root = xml.root
+
+ # Remove comments
+ etree.strip_tags(xml_root, etree.Comment)
+ etree.cleanup_namespaces(xml_root)
+
+ return URDF(
+ robot=URDF._parse_robot(xml_element=xml_root, add_dummy_free_joints=add_dummy_free_joints), **kwargs
+ )
+
+ def contains(self, key, value, element=None) -> bool:
+ """Checks recursively whether the URDF tree contains the provided key-value pair.
+
+ Args:
+ key (str): A key.
+ value (str): A value.
+ element (etree.Element, optional): The XML element from which to start the recursive search. None means URDF root. Defaults to None.
+
+ Returns:
+ bool: Whether the key-value pair was found.
+ """
+ if element is None:
+ element = self.robot
+
+ result = False
+ for field in element.__dataclass_fields__:
+ field_value = getattr(element, field)
+ if is_dataclass(field_value):
+ result = result or self.contains(key=key, value=value, element=field_value)
+ elif isinstance(field_value, list) and len(field_value) > 0 and is_dataclass(field_value[0]):
+ for field_value_element in field_value:
+ result = result or self.contains(key=key, value=value, element=field_value_element)
+ else:
+ if key == field and value == field_value:
+ result = True
+ return result
+
+ def _determine_base_link(self):
+ """Get the base link of the URDF tree by extracting all links without parents.
+ In case multiple links could be root chose the first.
+
+ Returns:
+ str: Name of the base link.
+ """
+ link_names = [l.name for l in self.robot.links]
+
+ for j in self.robot.joints:
+ link_names.remove(j.child)
+
+ if len(link_names) == 0:
+ # raise Error?
+ return None
+
+ return link_names[0]
+
+ def _forward_kinematics_joint(self, joint, q=None):
+ origin = np.eye(4) if joint.origin is None else joint.origin
+
+ if joint.mimic is not None:
+ if joint.mimic.joint in self.actuated_joint_names:
+ mimic_joint_index = self.actuated_joint_names.index(joint.mimic.joint)
+ q = self._cfg[mimic_joint_index] * joint.mimic.multiplier + joint.mimic.offset
+ else:
+ # _logger.warning(
+ # f"Joint '{joint.name}' is supposed to mimic '{joint.mimic.joint}'. But this joint is not actuated - will assume (0.0 + offset)."
+ # )
+ q = 0.0 + joint.mimic.offset
+
+ if joint.type in ["revolute", "prismatic", "continuous"]:
+ if q is None:
+ # Use internal cfg vector for forward kinematics
+ q = float(self.cfg[self.actuated_dof_indices[self.actuated_joint_names.index(joint.name)]])
+
+ if joint.type == "prismatic":
+ matrix = origin @ tra.translation_matrix(q * joint.axis)
+ else:
+ matrix = origin @ tra.rotation_matrix(q, joint.axis)
+ else:
+ # this includes: floating, planar, fixed
+ matrix = origin
+
+ return matrix, q
+
+ def update_cfg(self, configuration):
+ """Update joint configuration of URDF; does forward kinematics.
+
+ Args:
+ configuration (dict, list[float], tuple[float] or np.ndarray): A mapping from joints or joint names to configuration values, or a list containing a value for each actuated joint.
+
+ Raises:
+ ValueError: Raised if dimensionality of configuration does not match number of actuated joints of URDF model.
+ TypeError: Raised if configuration is neither a dict, list, tuple or np.ndarray.
+ """
+ joint_cfg = []
+
+ if isinstance(configuration, dict):
+ for joint in configuration:
+ if isinstance(joint, six.string_types):
+ joint_cfg.append((self._joint_map[joint], configuration[joint]))
+ elif isinstance(joint, Joint):
+ # TODO: Joint is not hashable; so this branch will not succeed
+ joint_cfg.append((joint, configuration[joint]))
+ elif isinstance(configuration, (list, tuple, np.ndarray)):
+ if len(configuration) == len(self.robot.joints):
+ for joint, value in zip(self.robot.joints, configuration):
+ joint_cfg.append((joint, value))
+ elif len(configuration) == self.num_actuated_joints:
+ for joint, value in zip(self._actuated_joints, configuration):
+ joint_cfg.append((joint, value))
+ else:
+ raise ValueError(
+ f"Dimensionality of configuration ({len(configuration)}) doesn't match number of all ({len(self.robot.joints)}) or actuated joints ({self.num_actuated_joints})."
+ )
+ else:
+ raise TypeError("Invalid type for configuration")
+
+ # append all mimic joints in the update
+ for j, q in joint_cfg + [(j, 0.0) for j in self.robot.joints if j.mimic is not None]:
+ matrix, joint_q = self._forward_kinematics_joint(j, q=q)
+
+ # update internal configuration vector - only consider actuated joints
+ if j.name in self.actuated_joint_names:
+ self._cfg[self.actuated_dof_indices[self.actuated_joint_names.index(j.name)]] = joint_q
+
+ if self._scene is not None:
+ self._scene.graph.update(frame_from=j.parent, frame_to=j.child, matrix=matrix)
+ if self._scene_collision is not None:
+ self._scene_collision.graph.update(frame_from=j.parent, frame_to=j.child, matrix=matrix)
+
+ def get_transform(self, frame_to, frame_from=None, collision_geometry=False):
+ """Get the transform from one frame to another.
+
+ Args:
+ frame_to (str): Node name.
+ frame_from (str, optional): Node name. If None it will be set to self.base_frame. Defaults to None.
+ collision_geometry (bool, optional): Whether to use the collision geometry scene graph (instead of the visual geometry). Defaults to False.
+
+ Raises:
+ ValueError: Raised if scene graph wasn't constructed during intialization.
+
+ Returns:
+ (4, 4) float: Homogeneous transformation matrix
+ """
+ if collision_geometry:
+ if self._scene_collision is None:
+ raise ValueError("No collision scene available. Use build_collision_scene_graph=True during loading.")
+ else:
+ return self._scene_collision.graph.get(frame_to=frame_to, frame_from=frame_from)[0]
+ else:
+ if self._scene is None:
+ raise ValueError("No scene available. Use build_scene_graph=True during loading.")
+ else:
+ return self._scene.graph.get(frame_to=frame_to, frame_from=frame_from)[0]
+
+ def _link_mesh(self, link, collision_geometry=True):
+ geometries = link.collisions if collision_geometry else link.visuals
+
+ if len(geometries) == 0:
+ return None
+
+ meshes = []
+ for g in geometries:
+ for m in g.geometry.meshes:
+ m = m.copy()
+ pose = g.origin
+ if g.geometry.mesh is not None:
+ if g.geometry.mesh.scale is not None:
+ S = np.eye(4)
+ S[:3, :3] = np.diag(g.geometry.mesh.scale)
+ pose = pose.dot(S)
+ m.apply_transform(pose)
+ meshes.append(m)
+ if len(meshes) == 0:
+ return None
+ self._collision_mesh = meshes[0] + meshes[1:]
+ return self._collision_mesh
+
+ def _geometry2trimeshscene(self, geometry, load_file, force_mesh, skip_materials):
+ new_s = None
+ if geometry.box is not None:
+ new_s = trimesh.primitives.Box(extents=geometry.box.size).scene()
+ elif geometry.sphere is not None:
+ new_s = trimesh.primitives.Sphere(radius=geometry.sphere.radius).scene()
+ elif geometry.cylinder is not None:
+ new_s = trimesh.primitives.Cylinder(
+ radius=geometry.cylinder.radius, height=geometry.cylinder.length
+ ).scene()
+ elif geometry.mesh is not None and load_file:
+ new_filename = self._filename_handler(fname=geometry.mesh.filename)
+
+ if os.path.isfile(new_filename):
+ _logger.debug(f"Loading {geometry.mesh.filename} as {new_filename}")
+
+ if force_mesh:
+ new_g = trimesh.load(
+ new_filename,
+ ignore_broken=True,
+ force="mesh",
+ skip_materials=skip_materials,
+ )
+
+ # add original filename
+ if "file_path" not in new_g.metadata:
+ new_g.metadata["file_path"] = os.path.abspath(new_filename)
+ new_g.metadata["file_name"] = os.path.basename(new_filename)
+
+ new_s = trimesh.Scene()
+ new_s.add_geometry(new_g)
+ else:
+ new_s = trimesh.load(
+ new_filename,
+ ignore_broken=True,
+ force="scene",
+ skip_materials=skip_materials,
+ )
+
+ if "file_path" in new_s.metadata:
+ for i, (_, geom) in enumerate(new_s.geometry.items()):
+ if "file_path" not in geom.metadata:
+ geom.metadata["file_path"] = new_s.metadata["file_path"]
+ geom.metadata["file_name"] = new_s.metadata["file_name"]
+ geom.metadata["file_element"] = i
+
+ # scale mesh appropriately
+ if geometry.mesh.scale is not None:
+ if isinstance(geometry.mesh.scale, float):
+ new_s = new_s.scaled(geometry.mesh.scale)
+ elif isinstance(geometry.mesh.scale, np.ndarray):
+ new_s = new_s.scaled(geometry.mesh.scale)
+ else:
+ _logger.warning(f"Warning: Can't interpret scale '{geometry.mesh.scale}'")
+ else:
+ _logger.warning(f"Can't find {new_filename}")
+ return new_s
+
+ def _add_geometries_to_scene(
+ self,
+ s,
+ geometries,
+ link_name,
+ load_geometry,
+ force_mesh,
+ force_single_geometry,
+ skip_materials,
+ ):
+ if force_single_geometry:
+ tmp_scene = trimesh.Scene(base_frame=link_name)
+
+ first_geom_name = None
+
+ for v in geometries:
+ if v.geometry is not None:
+ if first_geom_name is None:
+ first_geom_name = v.name
+
+ new_s = self._geometry2trimeshscene(
+ geometry=v.geometry,
+ load_file=load_geometry,
+ force_mesh=force_mesh,
+ skip_materials=skip_materials,
+ )
+ if new_s is not None:
+ origin = v.origin if v.origin is not None else np.eye(4)
+
+ if force_single_geometry:
+ for name, geom in new_s.geometry.items():
+ if isinstance(v, Visual):
+ apply_visual_color(geom, v, self._material_map)
+ tmp_scene.add_geometry(
+ geometry=geom,
+ geom_name=v.name,
+ parent_node_name=link_name,
+ transform=origin @ new_s.graph.get(name)[0],
+ )
+ else:
+ # The following map is used to deal with glb format
+ # when the graph node and geometry have different names
+ geom_name_map = {new_s.graph[node_name][1]: node_name for node_name in new_s.graph.nodes}
+ for name, geom in new_s.geometry.items():
+ if isinstance(v, Visual):
+ apply_visual_color(geom, v, self._material_map)
+ s.add_geometry(
+ geometry=geom,
+ geom_name=v.name,
+ parent_node_name=link_name,
+ transform=origin @ new_s.graph.get(geom_name_map[name])[0],
+ )
+
+ if force_single_geometry and len(tmp_scene.geometry) > 0:
+ s.add_geometry(
+ geometry=tmp_scene.dump(concatenate=True),
+ geom_name=first_geom_name,
+ parent_node_name=link_name,
+ transform=np.eye(4),
+ )
+
+ def _create_scene(
+ self,
+ use_collision_geometry=False,
+ load_geometry=True,
+ force_mesh=False,
+ force_single_geometry_per_link=False,
+ ):
+ s = trimesh.scene.Scene(base_frame=self._base_link)
+
+ for j in self.robot.joints:
+ matrix, _ = self._forward_kinematics_joint(j)
+
+ s.graph.update(frame_from=j.parent, frame_to=j.child, matrix=matrix)
+
+ for l in self.robot.links:
+ if l.name not in s.graph.nodes and l.name != s.graph.base_frame:
+ _logger.warning(f"{l.name} not connected via joints. Will add link to base frame.")
+ s.graph.update(frame_from=s.graph.base_frame, frame_to=l.name)
+
+ meshes = l.collisions if use_collision_geometry else l.visuals
+ self._add_geometries_to_scene(
+ s,
+ geometries=meshes,
+ link_name=l.name,
+ load_geometry=load_geometry,
+ force_mesh=force_mesh,
+ force_single_geometry=force_single_geometry_per_link,
+ skip_materials=use_collision_geometry,
+ )
+
+ return s
+
+ def _successors(self, node):
+ """
+ Get all nodes of the scene that succeeds a specified node.
+
+ Parameters
+ ------------
+ node : any
+ Hashable key in `scene.graph`
+
+ Returns
+ -----------
+ subnodes : set[str]
+ Set of nodes.
+ """
+ # get every node that is a successor to specified node
+ # this includes `node`
+ return self._scene.graph.transforms.successors(node)
+
+ def _create_subrobot(self, robot_name, root_link_name):
+ subrobot = Robot(name=robot_name)
+ subnodes = self._successors(node=root_link_name)
+
+ if len(subnodes) > 0:
+ for node in subnodes:
+ if node in self.link_map:
+ subrobot.links.append(copy.deepcopy(self.link_map[node]))
+ for joint_name, joint in self.joint_map.items():
+ if joint.parent in subnodes and joint.child in subnodes:
+ subrobot.joints.append(copy.deepcopy(self.joint_map[joint_name]))
+
+ return subrobot
+
+ def split_along_joints(self, joint_type="floating", **kwargs):
+ """Split URDF model along a particular joint type.
+ The result is a set of URDF models which together compose the original URDF.
+
+ Args:
+ joint_type (str, or list[str], optional): Type of joint to use for splitting. Defaults to "floating".
+ **kwargs: Arguments delegated to URDF constructor of new URDF models.
+
+ Returns:
+ list[(np.ndarray, yourdfpy.URDF)]: A list of tuples (np.ndarray, yourdfpy.URDF) whereas each homogeneous 4x4 matrix describes the root transformation of the respective URDF model w.r.t. the original URDF.
+ """
+ root_urdf = URDF(robot=copy.deepcopy(self.robot), build_scene_graph=False, load_meshes=False)
+ result = []
+
+ joint_types = joint_type if isinstance(joint_type, list) else [joint_type]
+
+ # find all relevant joints
+ joint_names = [j.name for j in self.robot.joints if j.type in joint_types]
+ for joint_name in joint_names:
+ root_link = self.link_map[self.joint_map[joint_name].child]
+ new_robot = self._create_subrobot(
+ robot_name=root_link.name,
+ root_link_name=root_link.name,
+ )
+
+ result.append(
+ (
+ self._scene.graph.get(root_link.name)[0],
+ URDF(robot=new_robot, **kwargs),
+ )
+ )
+
+ # remove links and joints from root robot
+ for j in new_robot.joints:
+ root_urdf.robot.joints.remove(root_urdf.joint_map[j.name])
+ for l in new_robot.links:
+ root_urdf.robot.links.remove(root_urdf.link_map[l.name])
+
+ # remove joint that connects root urdf to root_link
+ if root_link.name in [j.child for j in root_urdf.robot.joints]:
+ root_urdf.robot.joints.remove(
+ root_urdf.robot.joints[[j.child for j in root_urdf.robot.joints].index(root_link.name)]
+ )
+
+ result.insert(0, (np.eye(4), URDF(robot=root_urdf.robot, **kwargs)))
+
+ return result
+
+ def validate_filenames(self):
+ for l in self.robot.links:
+ meshes = [m.geometry.mesh for m in l.collisions + l.visuals if m.geometry.mesh is not None]
+ for m in meshes:
+ _logger.debug(m.filename, "-->", self._filename_handler(m.filename))
+ if not os.path.isfile(self._filename_handler(m.filename)):
+ return False
+ return True
+
+ def write_xml(self):
+ """Write URDF model to an XML element hierarchy.
+
+ Returns:
+ etree.ElementTree: XML data.
+ """
+ xml_element = self._write_robot(self.robot)
+ return etree.ElementTree(xml_element)
+
+ def write_xml_string(self, **kwargs):
+ """Write URDF model to a string.
+
+ Returns:
+ str: String of the xml representation of the URDF model.
+ """
+ xml_element = self.write_xml()
+ return etree.tostring(xml_element, xml_declaration=True, *kwargs)
+
+ def write_xml_file(self, fname):
+ """Write URDF model to an xml file.
+
+ Args:
+ fname (str): Filename of the file to be written. Usually ends in `.urdf`.
+ """
+ xml_element = self.write_xml()
+ xml_element.write(fname, xml_declaration=True, pretty_print=True)
+
+ def _parse_mimic(xml_element):
+ if xml_element is None:
+ return None
+
+ return Mimic(
+ joint=xml_element.get("joint"),
+ multiplier=_str2float(xml_element.get("multiplier", 1.0)),
+ offset=_str2float(xml_element.get("offset", 0.0)),
+ )
+
+ def _write_mimic(self, xml_parent, mimic):
+ etree.SubElement(
+ xml_parent,
+ "mimic",
+ attrib={
+ "joint": mimic.joint,
+ "multiplier": str(mimic.multiplier),
+ "offset": str(mimic.offset),
+ },
+ )
+
+ def _parse_safety_controller(xml_element):
+ if xml_element is None:
+ return None
+
+ return SafetyController(
+ soft_lower_limit=_str2float(xml_element.get("soft_lower_limit")),
+ soft_upper_limit=_str2float(xml_element.get("soft_upper_limit")),
+ k_position=_str2float(xml_element.get("k_position")),
+ k_velocity=_str2float(xml_element.get("k_velocity")),
+ )
+
+ def _write_safety_controller(self, xml_parent, safety_controller):
+ etree.SubElement(
+ xml_parent,
+ "safety_controller",
+ attrib={
+ "soft_lower_limit": str(safety_controller.soft_lower_limit),
+ "soft_upper_limit": str(safety_controller.soft_upper_limit),
+ "k_position": str(safety_controller.k_position),
+ "k_velocity": str(safety_controller.k_velocity),
+ },
+ )
+
+ def _parse_transmission_joint(xml_element):
+ if xml_element is None:
+ return None
+
+ transmission_joint = TransmissionJoint(name=xml_element.get("name"))
+
+ for h in xml_element.findall("hardware_interface"):
+ transmission_joint.hardware_interfaces.append(h.text)
+
+ return transmission_joint
+
+ def _write_transmission_joint(self, xml_parent, transmission_joint):
+ xml_element = etree.SubElement(
+ xml_parent,
+ "joint",
+ attrib={
+ "name": str(transmission_joint.name),
+ },
+ )
+ for h in transmission_joint.hardware_interfaces:
+ tmp = etree.SubElement(
+ xml_element,
+ "hardwareInterface",
+ )
+ tmp.text = h
+
+ def _parse_actuator(xml_element):
+ if xml_element is None:
+ return None
+
+ actuator = Actuator(name=xml_element.get("name"))
+ if xml_element.find("mechanicalReduction"):
+ actuator.mechanical_reduction = float(xml_element.find("mechanicalReduction").text)
+
+ for h in xml_element.findall("hardwareInterface"):
+ actuator.hardware_interfaces.append(h.text)
+
+ return actuator
+
+ def _write_actuator(self, xml_parent, actuator):
+ xml_element = etree.SubElement(
+ xml_parent,
+ "actuator",
+ attrib={
+ "name": str(actuator.name),
+ },
+ )
+ if actuator.mechanical_reduction is not None:
+ tmp = etree.SubElement("mechanicalReduction")
+ tmp.text = str(actuator.mechanical_reduction)
+
+ for h in actuator.hardware_interfaces:
+ tmp = etree.SubElement(
+ xml_element,
+ "hardwareInterface",
+ )
+ tmp.text = h
+
+ def _parse_transmission(xml_element):
+ if xml_element is None:
+ return None
+
+ transmission = Transmission(name=xml_element.get("name"))
+
+ for j in xml_element.findall("joint"):
+ transmission.joints.append(URDF._parse_transmission_joint(j))
+ for a in xml_element.findall("actuator"):
+ transmission.actuators.append(URDF._parse_actuator(a))
+
+ return transmission
+
+ def _write_transmission(self, xml_parent, transmission):
+ xml_element = etree.SubElement(
+ xml_parent,
+ "transmission",
+ attrib={
+ "name": str(transmission.name),
+ },
+ )
+
+ for j in transmission.joints:
+ self._write_transmission_joint(xml_element, j)
+
+ for a in transmission.actuators:
+ self._write_actuator(xml_element, a)
+
+ def _parse_calibration(xml_element):
+ if xml_element is None:
+ return None
+
+ return Calibration(
+ rising=_str2float(xml_element.get("rising")),
+ falling=_str2float(xml_element.get("falling")),
+ )
+
+ def _write_calibration(self, xml_parent, calibration):
+ etree.SubElement(
+ xml_parent,
+ "calibration",
+ attrib={
+ "rising": str(calibration.rising),
+ "falling": str(calibration.falling),
+ },
+ )
+
+ def _parse_box(xml_element):
+ return Box(size=np.array(xml_element.attrib["size"].split(), dtype=float))
+
+ def _write_box(self, xml_parent, box):
+ etree.SubElement(xml_parent, "box", attrib={"size": " ".join(map(str, box.size))})
+
+ def _parse_cylinder(xml_element):
+ return Cylinder(
+ radius=float(xml_element.attrib["radius"]),
+ length=float(xml_element.attrib["length"]),
+ )
+
+ def _write_cylinder(self, xml_parent, cylinder):
+ etree.SubElement(
+ xml_parent,
+ "cylinder",
+ attrib={"radius": str(cylinder.radius), "length": str(cylinder.length)},
+ )
+
+ def _parse_sphere(xml_element):
+ return Sphere(radius=float(xml_element.attrib["radius"]))
+
+ def _write_sphere(self, xml_parent, sphere):
+ etree.SubElement(xml_parent, "sphere", attrib={"radius": str(sphere.radius)})
+
+ def _parse_scale(xml_element):
+ if "scale" in xml_element.attrib:
+ s = xml_element.get("scale").split()
+ if len(s) == 0:
+ return None
+ elif len(s) == 1:
+ return float(s[0])
+ else:
+ return np.array(list(map(float, s)))
+ return None
+
+ def _write_scale(self, xml_parent, scale):
+ if scale is not None:
+ if isinstance(scale, float) or isinstance(scale, int):
+ xml_parent.set("scale", " ".join([str(scale)] * 3))
+ else:
+ xml_parent.set("scale", " ".join(map(str, scale)))
+
+ def _parse_mesh(xml_element):
+ return Mesh(filename=xml_element.get("filename"), scale=URDF._parse_scale(xml_element))
+
+ def _write_mesh(self, xml_parent, mesh):
+ # TODO: turn into different filename handler
+ xml_element = etree.SubElement(
+ xml_parent,
+ "mesh",
+ attrib={"filename": self._filename_handler(mesh.filename)},
+ )
+
+ self._write_scale(xml_element, mesh.scale)
+
+ def _parse_geometry(xml_element):
+ geometry = Geometry()
+ if xml_element[0].tag == "box":
+ geometry.box = URDF._parse_box(xml_element[0])
+ elif xml_element[0].tag == "cylinder":
+ geometry.cylinder = URDF._parse_cylinder(xml_element[0])
+ elif xml_element[0].tag == "sphere":
+ geometry.sphere = URDF._parse_sphere(xml_element[0])
+ elif xml_element[0].tag == "mesh":
+ geometry.mesh = URDF._parse_mesh(xml_element[0])
+ else:
+ raise ValueError(f"Unknown tag: {xml_element[0].tag}")
+
+ return geometry
+
+ def _validate_geometry(self, geometry):
+ if geometry is None:
+ self._errors.append(URDFIncompleteError(" is missing."))
+
+ num_nones = sum(
+ [
+ x is not None
+ for x in [
+ geometry.box,
+ geometry.cylinder,
+ geometry.sphere,
+ geometry.mesh,
+ ]
+ ]
+ )
+ if num_nones < 1:
+ self._errors.append(
+ URDFIncompleteError(
+ "One of , , , needs to be defined as a child of ."
+ )
+ )
+ elif num_nones > 1:
+ self._errors.append(
+ URDFError(
+ "Too many of , , , defined as a child of . Only one allowed."
+ )
+ )
+
+ def _write_geometry(self, xml_parent, geometry):
+ if geometry is None:
+ return
+
+ xml_element = etree.SubElement(xml_parent, "geometry")
+ if geometry.box is not None:
+ self._write_box(xml_element, geometry.box)
+ elif geometry.cylinder is not None:
+ self._write_cylinder(xml_element, geometry.cylinder)
+ elif geometry.sphere is not None:
+ self._write_sphere(xml_element, geometry.sphere)
+ elif geometry.mesh is not None:
+ self._write_mesh(xml_element, geometry.mesh)
+
+ def _parse_origin(xml_element):
+ if xml_element is None:
+ return None
+
+ xyz = xml_element.get("xyz", default="0 0 0")
+ rpy = xml_element.get("rpy", default="0 0 0")
+
+ return tra.compose_matrix(
+ translate=np.array(list(map(float, xyz.split()))),
+ angles=np.array(list(map(float, rpy.split()))),
+ )
+
+ def _write_origin(self, xml_parent, origin):
+ if origin is None:
+ return
+
+ etree.SubElement(
+ xml_parent,
+ "origin",
+ attrib={
+ "xyz": " ".join(map(str, tra.translation_from_matrix(origin))),
+ "rpy": " ".join(map(str, tra.euler_from_matrix(origin))),
+ },
+ )
+
+ def _parse_color(xml_element):
+ if xml_element is None:
+ return None
+
+ rgba = xml_element.get("rgba", default="1 1 1 1")
+
+ return Color(rgba=np.array(list(map(float, rgba.split()))))
+
+ def _write_color(self, xml_parent, color):
+ if color is None:
+ return
+
+ etree.SubElement(xml_parent, "color", attrib={"rgba": " ".join(map(str, color.rgba))})
+
+ def _parse_texture(xml_element):
+ if xml_element is None:
+ return None
+
+ # TODO: use texture filename handler
+ return Texture(filename=xml_element.get("filename", default=None))
+
+ def _write_texture(self, xml_parent, texture):
+ if texture is None:
+ return
+
+ # TODO: use texture filename handler
+ etree.SubElement(xml_parent, "texture", attrib={"filename": texture.filename})
+
+ def _parse_material(xml_element):
+ if xml_element is None:
+ return None
+
+ material = Material(name=xml_element.get("name"))
+ material.color = URDF._parse_color(xml_element.find("color"))
+ material.texture = URDF._parse_texture(xml_element.find("texture"))
+
+ return material
+
+ def _write_material(self, xml_parent, material):
+ if material is None:
+ return
+
+ attrib = {"name": material.name} if material.name is not None else {}
+ xml_element = etree.SubElement(
+ xml_parent,
+ "material",
+ attrib=attrib,
+ )
+
+ self._write_color(xml_element, material.color)
+ self._write_texture(xml_element, material.texture)
+
+ def _parse_visual(xml_element):
+ visual = Visual(name=xml_element.get("name"))
+
+ visual.geometry = URDF._parse_geometry(xml_element.find("geometry"))
+ visual.origin = URDF._parse_origin(xml_element.find("origin"))
+ visual.material = URDF._parse_material(xml_element.find("material"))
+
+ return visual
+
+ def _validate_visual(self, visual):
+ self._validate_geometry(visual.geometry)
+
+ def _write_visual(self, xml_parent, visual):
+ attrib = {"name": visual.name} if visual.name is not None else {}
+ xml_element = etree.SubElement(
+ xml_parent,
+ "visual",
+ attrib=attrib,
+ )
+
+ self._write_geometry(xml_element, visual.geometry)
+ self._write_origin(xml_element, visual.origin)
+ self._write_material(xml_element, visual.material)
+
+ def _parse_collision(xml_element):
+ collision = Collision(name=xml_element.get("name"))
+
+ collision.geometry = URDF._parse_geometry(xml_element.find("geometry"))
+ collision.origin = URDF._parse_origin(xml_element.find("origin"))
+
+ return collision
+
+ def _validate_collision(self, collision):
+ self._validate_geometry(collision.geometry)
+
+ def _write_collision(self, xml_parent, collision):
+ attrib = {"name": collision.name} if collision.name is not None else {}
+ xml_element = etree.SubElement(
+ xml_parent,
+ "collision",
+ attrib=attrib,
+ )
+
+ self._write_geometry(xml_element, collision.geometry)
+ self._write_origin(xml_element, collision.origin)
+
+ def _parse_inertia(xml_element):
+ if xml_element is None:
+ return None
+
+ x = xml_element
+
+ return np.array(
+ [
+ [
+ x.get("ixx", default=1.0),
+ x.get("ixy", default=0.0),
+ x.get("ixz", default=0.0),
+ ],
+ [
+ x.get("ixy", default=0.0),
+ x.get("iyy", default=1.0),
+ x.get("iyz", default=0.0),
+ ],
+ [
+ x.get("ixz", default=0.0),
+ x.get("iyz", default=0.0),
+ x.get("izz", default=1.0),
+ ],
+ ],
+ dtype=np.float64,
+ )
+
+ def _write_inertia(self, xml_parent, inertia):
+ if inertia is None:
+ return None
+
+ etree.SubElement(
+ xml_parent,
+ "inertia",
+ attrib={
+ "ixx": str(inertia[0, 0]),
+ "ixy": str(inertia[0, 1]),
+ "ixz": str(inertia[0, 2]),
+ "iyy": str(inertia[1, 1]),
+ "iyz": str(inertia[1, 2]),
+ "izz": str(inertia[2, 2]),
+ },
+ )
+
+ def _parse_mass(xml_element):
+ if xml_element is None:
+ return None
+
+ return _str2float(xml_element.get("value", default=0.0))
+
+ def _write_mass(self, xml_parent, mass):
+ if mass is None:
+ return
+
+ etree.SubElement(
+ xml_parent,
+ "mass",
+ attrib={
+ "value": str(mass),
+ },
+ )
+
+ def _parse_inertial(xml_element):
+ if xml_element is None:
+ return None
+
+ inertial = Inertial()
+ inertial.origin = URDF._parse_origin(xml_element.find("origin"))
+ inertial.inertia = URDF._parse_inertia(xml_element.find("inertia"))
+ inertial.mass = URDF._parse_mass(xml_element.find("mass"))
+
+ return inertial
+
+ def _write_inertial(self, xml_parent, inertial):
+ if inertial is None:
+ return
+
+ xml_element = etree.SubElement(xml_parent, "inertial")
+
+ self._write_origin(xml_element, inertial.origin)
+ self._write_mass(xml_element, inertial.mass)
+ self._write_inertia(xml_element, inertial.inertia)
+
+ def _parse_link(xml_element):
+ link = Link(name=xml_element.attrib["name"])
+
+ link.inertial = URDF._parse_inertial(xml_element.find("inertial"))
+
+ for v in xml_element.findall("visual"):
+ link.visuals.append(URDF._parse_visual(v))
+
+ for c in xml_element.findall("collision"):
+ link.collisions.append(URDF._parse_collision(c))
+
+ return link
+
+ def _validate_link(self, link):
+ self._validate_required_attribute(attribute=link.name, error_msg="The tag misses a 'name' attribute.")
+
+ for v in link.visuals:
+ self._validate_visual(v)
+
+ for c in link.collisions:
+ self._validate_collision(c)
+
+ def _write_link(self, xml_parent, link):
+ xml_element = etree.SubElement(
+ xml_parent,
+ "link",
+ attrib={
+ "name": link.name,
+ },
+ )
+
+ self._write_inertial(xml_element, link.inertial)
+ for visual in link.visuals:
+ self._write_visual(xml_element, visual)
+ for collision in link.collisions:
+ self._write_collision(xml_element, collision)
+
+ def _parse_axis(xml_element):
+ if xml_element is None:
+ return np.array([1.0, 0, 0])
+
+ xyz = xml_element.get("xyz", "1 0 0")
+ results = []
+ for x in xyz.split():
+ try:
+ x = float(x)
+ except ValueError:
+ x = 0
+ results.append(x)
+ return np.array(results)
+ # return np.array(list(map(float, xyz.split())))
+
+ def _write_axis(self, xml_parent, axis):
+ if axis is None:
+ return
+
+ etree.SubElement(xml_parent, "axis", attrib={"xyz": " ".join(map(str, axis))})
+
+ def _parse_limit(xml_element):
+ if xml_element is None:
+ return None
+
+ return Limit(
+ effort=_str2float(xml_element.get("effort", default=None)),
+ velocity=_str2float(xml_element.get("velocity", default=None)),
+ lower=_str2float(xml_element.get("lower", default=None)),
+ upper=_str2float(xml_element.get("upper", default=None)),
+ )
+
+ def _validate_limit(self, limit, type):
+ if type in ["revolute", "prismatic"]:
+ self._validate_required_attribute(
+ limit,
+ error_msg="The of a (prismatic, revolute) joint is missing.",
+ )
+
+ if limit is not None:
+ self._validate_required_attribute(
+ limit.upper,
+ error_msg="Tag of joint is missing attribute 'upper'.",
+ )
+ self._validate_required_attribute(
+ limit.lower,
+ error_msg="Tag of joint is missing attribute 'lower'.",
+ )
+
+ if limit is not None:
+ self._validate_required_attribute(
+ limit.effort,
+ error_msg="Tag of joint is missing attribute 'effort'.",
+ )
+
+ self._validate_required_attribute(
+ limit.velocity,
+ error_msg="Tag of joint is missing attribute 'velocity'.",
+ )
+
+ def _write_limit(self, xml_parent, limit):
+ if limit is None:
+ return
+
+ attrib = {}
+ if limit.effort is not None:
+ attrib["effort"] = str(limit.effort)
+ if limit.velocity is not None:
+ attrib["velocity"] = str(limit.velocity)
+ if limit.lower is not None:
+ attrib["lower"] = str(limit.lower)
+ if limit.upper is not None:
+ attrib["upper"] = str(limit.upper)
+
+ etree.SubElement(
+ xml_parent,
+ "limit",
+ attrib=attrib,
+ )
+
+ def _parse_dynamics(xml_element):
+ if xml_element is None:
+ return None
+
+ dynamics = Dynamics()
+ dynamics.damping = xml_element.get("damping", default=None)
+ dynamics.friction = xml_element.get("friction", default=None)
+
+ return dynamics
+
+ def _write_dynamics(self, xml_parent, dynamics):
+ if dynamics is None:
+ return
+
+ attrib = {}
+ if dynamics.damping is not None:
+ attrib["damping"] = str(dynamics.damping)
+ if dynamics.friction is not None:
+ attrib["friction"] = str(dynamics.friction)
+
+ etree.SubElement(
+ xml_parent,
+ "dynamics",
+ attrib=attrib,
+ )
+
+ def _parse_joint(xml_element):
+ joint = Joint(name=xml_element.attrib["name"])
+
+ joint.type = xml_element.get("type", default=None)
+ joint.parent = xml_element.find("parent").get("link")
+ joint.child = xml_element.find("child").get("link")
+ joint.origin = URDF._parse_origin(xml_element.find("origin"))
+ joint.axis = URDF._parse_axis(xml_element.find("axis"))
+ joint.limit = URDF._parse_limit(xml_element.find("limit"))
+ joint.dynamics = URDF._parse_dynamics(xml_element.find("dynamics"))
+ joint.mimic = URDF._parse_mimic(xml_element.find("mimic"))
+ joint.calibration = URDF._parse_calibration(xml_element.find("calibration"))
+ joint.safety_controller = URDF._parse_safety_controller(xml_element.find("safety_controller"))
+
+ return joint
+
+ def _validate_joint(self, joint):
+ self._validate_required_attribute(
+ attribute=joint.name,
+ error_msg="The tag misses a 'name' attribute.",
+ )
+
+ allowed_types = [
+ "revolute",
+ "continuous",
+ "prismatic",
+ "fixed",
+ "floating",
+ "planar",
+ ]
+ self._validate_required_attribute(
+ attribute=joint.type,
+ error_msg=f"The tag misses a 'type' attribute or value is not part of allowed values [{', '.join(allowed_types)}].",
+ allowed_values=allowed_types,
+ )
+
+ self._validate_required_attribute(
+ joint.parent,
+ error_msg=f"The of a is missing.",
+ )
+
+ self._validate_required_attribute(
+ joint.child,
+ error_msg=f"The of a is missing.",
+ )
+
+ self._validate_limit(joint.limit, type=joint.type)
+
+ def _write_joint(self, xml_parent, joint):
+ xml_element = etree.SubElement(
+ xml_parent,
+ "joint",
+ attrib={
+ "name": joint.name,
+ "type": joint.type,
+ },
+ )
+
+ etree.SubElement(xml_element, "parent", attrib={"link": joint.parent})
+ etree.SubElement(xml_element, "child", attrib={"link": joint.child})
+ self._write_origin(xml_element, joint.origin)
+ self._write_axis(xml_element, joint.axis)
+ self._write_limit(xml_element, joint.limit)
+ self._write_dynamics(xml_element, joint.dynamics)
+
+ @staticmethod
+ def _parse_robot(xml_element, add_dummy_free_joints=False):
+ robot = Robot(name=xml_element.attrib["name"])
+
+ for l in xml_element.findall("link"):
+ robot.links.append(URDF._parse_link(l))
+ for j in xml_element.findall("joint"):
+ robot.joints.append(URDF._parse_joint(j))
+ for m in xml_element.findall("material"):
+ robot.materials.append(URDF._parse_material(m))
+
+ if add_dummy_free_joints:
+ # Determine root link
+ link_names = [l.name for l in robot.links]
+ for j in robot.joints:
+ link_names.remove(j.child)
+
+ if len(link_names) == 0:
+ raise RuntimeError(f"No root link found for robot.")
+
+ root_link_name = link_names[0]
+ _add_dummy_joints(robot, root_link_name)
+
+ return robot
+
+ def _validate_robot(self, robot):
+ if robot is not None:
+ self._validate_required_attribute(
+ attribute=robot.name,
+ error_msg="The tag misses a 'name' attribute.",
+ )
+
+ for l in robot.links:
+ self._validate_link(l)
+
+ for j in robot.joints:
+ self._validate_joint(j)
+
+ def _write_robot(self, robot):
+ xml_element = etree.Element("robot", attrib={"name": robot.name})
+ for link in robot.links:
+ self._write_link(xml_element, link)
+ for joint in robot.joints:
+ self._write_joint(xml_element, joint)
+ for material in robot.materials:
+ self._write_material(xml_element, material)
+
+ return xml_element
+
+ def __eq__(self, other):
+ if not isinstance(other, URDF):
+ raise NotImplemented
+ return self.robot == other.robot
+
+ @property
+ def filename_handler(self):
+ return self._filename_handler
+
+ def build_tree(self):
+ parent_child_map: Dict[str, List[str]] = {}
+ for joint in self.robot.joints:
+ if joint.parent in parent_child_map:
+ parent_child_map[joint.parent].append(joint.child)
+ else:
+ parent_child_map[joint.parent] = [joint.child]
+
+ # Sort link with bfs order
+ bfs_link_list = [self.base_link]
+ to_be_handle_list = [self.base_link]
+ while len(to_be_handle_list) > 0:
+ parent = to_be_handle_list.pop(0)
+ if parent not in parent_child_map:
+ continue
+
+ children = parent_child_map[parent]
+ to_be_handle_list.extend(children)
+ bfs_link_list.extend(children)
+ bfs_joint_list = []
+ for link_name in bfs_link_list[1:]:
+ joint_index = [i for i in range(len(self.robot.joints)) if self.robot.joints[i].child == link_name][0]
+ bfs_joint_list.append(self.robot.joints[joint_index])
+
+ # Build tree
+ root = Node(self.base_link, matrix=np.eye(4))
+ for joint in bfs_joint_list:
+ matrix, _ = self._forward_kinematics_joint(joint, 0)
+ parent_node = anytree.search.findall_by_attr(root, value=joint.parent)[0]
+ node = Node(joint.child, parent=parent_node, matrix=matrix)
+ return root
+
+ def update_kinematics(self, configuration):
+ joint_cfg = []
+
+ if isinstance(configuration, dict):
+ for joint in configuration:
+ if isinstance(joint, six.string_types):
+ joint_cfg.append((self._joint_map[joint], configuration[joint]))
+ elif isinstance(joint, Joint):
+ # TODO: Joint is not hashable; so this branch will not succeed
+ joint_cfg.append((joint, configuration[joint]))
+ elif isinstance(configuration, (list, tuple, np.ndarray)):
+ if len(configuration) == len(self.robot.joints):
+ for joint, value in zip(self.robot.joints, configuration):
+ joint_cfg.append((joint, value))
+ elif len(configuration) == self.num_actuated_joints:
+ for joint, value in zip(self._actuated_joints, configuration):
+ joint_cfg.append((joint, value))
+ else:
+ raise ValueError(
+ f"Dimensionality of configuration ({len(configuration)}) doesn't match number of all ({len(self.robot.joints)}) or actuated joints ({self.num_actuated_joints})."
+ )
+ else:
+ raise TypeError("Invalid type for configuration")
+
+ # append all mimic joints in the update
+ for j, q in joint_cfg + [(j, 0.0) for j in self.robot.joints if j.mimic is not None]:
+ matrix, _ = self._forward_kinematics_joint(j, q=q)
+ node = anytree.search.findall_by_attr(self.tree_root, j.child)[0]
+ node.matrix = matrix
+
+ for node in LevelOrderIter(self.tree_root):
+ if node.name == self.base_link:
+ node.global_pose = np.eye(4)
+ else:
+ node.global_pose = node.parent.global_pose @ node.matrix
+
+ def get_link_global_transform(self, link_name):
+ node = anytree.search.findall_by_attr(self.tree_root, link_name)[0]
+
+ return node.global_pose
+
+
+def _add_dummy_joints(robot: Robot, root_link_name: str):
+ # Prepare link and joint properties
+ translation_range = (-5, 5)
+ rotation_range = (-2 * np.pi, 2 * np.pi)
+ joint_types = ["prismatic"] * 3 + ["revolute"] * 3
+ joint_limit = [translation_range] * 3 + [rotation_range] * 3
+ joint_name = DUMMY_JOINT_NAMES.copy()
+ link_name = [f"dummy_{name}_translation_link" for name in "xyz"] + [f"dummy_{name}_rotation_link" for name in "xyz"]
+
+ links = []
+ joints = []
+
+ for i in range(6):
+ inertial = Inertial(
+ mass=0.01, inertia=np.array([[1e-4, 0, 0], [0, 1e-4, 0], [0, 0, 1e-4]]), origin=np.identity(4)
+ )
+ link = Link(name=link_name[i], inertial=inertial)
+ links.append(link)
+
+ joint_axis = np.zeros(3, dtype=int)
+ joint_axis[i % 3] = 1
+ limit = Limit(lower=joint_limit[i][0], upper=joint_limit[i][1], velocity=3.14, effort=10)
+
+ child_name = link_name[i + 1] if i < 5 else root_link_name
+ joint = Joint(
+ name=joint_name[i],
+ type=joint_types[i],
+ parent=link_name[i],
+ child=child_name,
+ origin=np.identity(4),
+ axis=joint_axis,
+ limit=limit,
+ )
+ joints.append(joint)
+
+ robot.joints = joints + robot.joints
+ robot.links = links + robot.links
+
+
+DUMMY_JOINT_NAMES = [f"dummy_{name}_translation_joint" for name in "xyz"] + [
+ f"dummy_{name}_rotation_joint" for name in "xyz"
+]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README.md
new file mode 100644
index 0000000..3f05225
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README.md
@@ -0,0 +1,249 @@
+# LinkerEG Teleoperation Glove Module
+
+LinkerEG teleoperation glove ROS2 driver module, receives glove data via serial port and publishes to ROS2 topics.
+
+## Changelog
+
+### 2026-02-04
+- Fixed dead loop bug in initialization
+- Added support for G20 hand
+
+## Features
+
+- **Publish rate**: 50Hz (all topics unified)
+- Auto scan serial port for glove connection
+- Supports left and right hand data
+- Supports sensor raw data publishing (switchable via topic)
+
+## Control Modes
+
+| motion_type | Mode | Description | Receiver needs robot hand connection |
+|-------------|------|-------------|-------------------------------------|
+| `linkereg2` | SDK Control Mode | Glove data publishes to ROS topics directly, SDK controls robot hand | ❌ Not required |
+| `linkereg1` | Receiver Control Mode | Receiver controls robot hand directly, also publishes data to ROS topics | ✅ Required |
+
+## Usage
+
+### 1. Config File
+
+Edit `config/base_config.yml`:
+
+```yaml
+system:
+ # SDK control mode (receiver doesn't need robot hand connection)
+ motion_type: linkereg2
+
+ # Or receiver control mode (receiver must be connected to robot hand)
+ motion_type: linkereg1
+```
+
+#### Serial Port Permission Password
+
+If auto-fixing serial port permissions is needed, configure `password` under `linkereg` (sudo password).
+Note: Values without quotes in YAML are parsed as numbers, use quotes for strings.
+
+```yaml
+linkereg:
+ port: null
+ password: "123456" # sudo password for fixing serial permissions
+```
+
+### 2. Launch
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+### 3. Enable Debug Print
+
+Edit `config/base_config.yml`:
+
+```yaml
+debug:
+ joint_pub_debug: true
+```
+
+## ROS2 Topics
+
+> **Publish rate**: All topics unified at 50Hz
+
+### Published Topics
+
+| Topic | Message Type | Description | Default State |
+|-------|-------------|-------------|---------------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | Right hand control data | ✅ Enabled |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | Left hand control data | ✅ Enabled |
+| `/cb_right_hand_raw_data` | `sensor_msgs/JointState` | Right hand sensor raw data | ❌ Disabled |
+| `/cb_left_hand_raw_data` | `sensor_msgs/JointState` | Left hand sensor raw data | ❌ Disabled |
+
+### Subscribed Topics
+
+| Topic | Message Type | Description |
+|-------|-------------|-------------|
+| `/cb_hand_setting_cmd` | `std_msgs/String` | Settings command (control raw data on/off) |
+
+## Raw Data Feature
+
+Raw data is **disabled** by default, enable via topic command.
+
+### Enable/Disable Raw Data
+
+```bash
+# Enable raw data publishing
+ros2 topic pub --once /cb_hand_setting_cmd std_msgs/String "data: 'on'"
+
+# Disable raw data publishing
+ros2 topic pub --once /cb_hand_setting_cmd std_msgs/String "data: 'off'"
+
+# Listen to raw data
+ros2 topic echo /cb_left_hand_raw_data
+```
+
+## ROS2 Topic Data Format
+
+### Raw Data Format (15 int32 values)
+
+| Index | Joint Name | Index | Joint Name | Index | Joint Name |
+|-------|------------|-------|------------|-------|------------|
+| 0 | Thumb abduction | 1 | Thumb flexion | 2 | Thumb tip |
+| 3 | Index abduction | 4 | Index flexion | 5 | Index tip |
+| 6 | Middle abduction | 7 | Middle flexion | 8 | Middle tip |
+| 9 | Ring abduction | 10 | Ring flexion | 11 | Ring tip |
+| 12 | Pinky abduction | 13 | Pinky flexion | 14 | Pinky tip |
+
+### Control Data Format
+
+#### L21 (25 joint output)
+
+For robot hand models: L21
+
+| Joint | Joint Name | Description |
+|-------|------------|-------------|
+| joint1 | Thumb flexion | |
+| joint2 | Index flexion | |
+| joint3 | Middle flexion | |
+| joint4 | Ring flexion | |
+| joint5 | Pinky flexion | |
+| joint6 | Thumb abduction | |
+| joint7 | Index abduction | |
+| joint8 | Middle abduction | |
+| joint9 | Ring abduction | |
+| joint10 | Pinky abduction | |
+| joint11 | Thumb roll | |
+| joint12 | Reserved | value 0 |
+| joint13 | Reserved | value 0 |
+| joint14 | Reserved | value 0 |
+| joint15 | Reserved | value 0 |
+| joint16 | Thumb middle | value 0 |
+| joint17 | Reserved | value 0 |
+| joint18 | Reserved | value 0 |
+| joint19 | Reserved | value 0 |
+| joint20 | Reserved | value 0 |
+| joint21 | Thumb tip | |
+| joint22 | Index tip | |
+| joint23 | Middle tip | |
+| joint24 | Ring tip | |
+| joint25 | Pinky tip | |
+
+#### L20/G20 (20 joint output)
+
+For robot hand models: L20, Industrial 20, G20
+
+| Joint | Joint Name | Description |
+|-------|------------|-------------|
+| joint1 | Thumb flexion | |
+| joint2 | Index flexion | |
+| joint3 | Middle flexion | |
+| joint4 | Ring flexion | |
+| joint5 | Pinky flexion | |
+| joint6 | Thumb abduction | |
+| joint7 | Index abduction | |
+| joint8 | Middle abduction | |
+| joint9 | Ring abduction | |
+| joint10 | Pinky abduction | |
+| joint11 | Thumb roll | |
+| joint12 | Reserved | value 0 |
+| joint13 | Reserved | value 0 |
+| joint14 | Reserved | value 0 |
+| joint15 | Reserved | value 0 |
+| joint16 | Thumb tip | |
+| joint17 | Index tip | |
+| joint18 | Middle tip | |
+| joint19 | Ring tip | |
+| joint20 | Pinky tip | |
+
+#### L10 (10 joints)
+
+For robot hand models: L10
+
+| Joint | Joint Name |
+|-------|------------|
+| joint1 | Thumb flexion |
+| joint2 | Thumb abduction |
+| joint3 | Index flexion |
+| joint4 | Middle flexion |
+| joint5 | Ring flexion |
+| joint6 | Pinky flexion |
+| joint7 | Index abduction |
+| joint8 | Ring abduction |
+| joint9 | Pinky abduction |
+| joint10 | Thumb roll |
+
+#### L6 (6 joints)
+
+For robot hand models: L6, O6
+
+| Joint | Joint Name |
+|-------|------------|
+| joint1 | Thumb flexion |
+| joint2 | Thumb abduction |
+| joint3 | Index flexion |
+| joint4 | Middle flexion |
+| joint5 | Ring flexion |
+| joint6 | Pinky flexion |
+
+#### O7 (7 joints)
+
+For robot hand models: O7
+
+| Joint | Joint Name |
+|-------|------------|
+| joint1 | Thumb flexion |
+| joint2 | Thumb abduction |
+| joint3 | Index flexion |
+| joint4 | Middle flexion |
+| joint5 | Ring flexion |
+| joint6 | Pinky flexion |
+| joint7 | Thumb roll |
+
+### Message Format
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # Timestamp
+msg.name # ['joint1', 'joint2', ..., 'jointN']
+msg.position # [0-255, ...] Motor position values
+msg.velocity # [255, ...] Velocity values
+```
+
+## File Structure
+
+```
+motion/linkereg/
+├── __init__.py # Module export
+├── linkeregcore.py # Serial communication core
+├── retarget.py # ROS2 integration
+└── README.md # English documentation
+```
+
+## Notes
+
+1. **Robot hand model**: No need to specify in config file, glove automatically identifies protocol type in data frame
+
+2. **Hardware connection**: Receiver must be connected to host (where the program runs)
+
+3. **Data rate**: Glove pushes data at 50Hz
+
+4. **Mode selection**:
+ - `linkereg2` (SDK control mode): Receiver doesn't need robot hand connection, glove data publishes to ROS topics, upper-layer robot hand SDK subscribes to control robot hand
+ - `linkereg1` (Receiver control mode): Receiver must be connected to real robot hand, receiver directly controls robot hand motion, current SDK is only for collecting glove data.
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README_zh.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README_zh.md
new file mode 100644
index 0000000..97ada4d
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/README_zh.md
@@ -0,0 +1,248 @@
+# LinkerEG 遥操作手套模块
+
+LinkerEG 遥操作手套的 ROS2 驱动模块,通过串口接收手套数据并发布到 ROS2 话题。
+
+## 更新日志
+
+### 2026-02-04
+- 修复了初始化流程中的死循环 bug
+- 增加了对 G20 手的支持
+
+## 特性
+
+- **发布频率**: 50Hz (所有话题统一频率)
+- 自动扫描串口连接手套
+- 支持左右手数据
+- 支持传感器原始数据推送 (可通过话题开关)
+
+## 控制模式
+
+| motion_type | 模式 | 说明 |接收器是否需要连接灵巧手 |
+|-------------|------|------|-------------------|
+| `linkereg2` | SDK控制模式 | 手套数据直接发布到ROS话题,由SDK控制灵巧手 | ❌ 不需要 |
+| `linkereg1` | 接收器控制模式 | 接收器直接控制灵巧手,同时发布数据到ROS话题 | ✅ 必须连接 |
+
+## 使用方法
+
+### 1. 配置文件
+
+修改 `config/base_config.yml`:
+
+```yaml
+system:
+ # SDK控制模式 (接收器不需要连接灵巧手)
+ motion_type: linkereg2
+
+ #或 接收器控制模式 (接收器必须连接灵巧手)
+ motion_type: linkereg1
+```
+
+#### 串口权限密码
+
+如果需要自动修复串口权限,请在 `linkereg` 下配置 `password`(sudo 密码)。
+注意:YAML 里不加引号会被解析成数字,建议加引号以确保是字符串。
+
+```yaml
+linkereg:
+ port: null
+ password: "123456" # sudo密码,用于修复串口权限,如果密码全是数字 需要加引号,比如 "123456"
+```
+
+### 2. 运行
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+### 3. 启用调试打印
+
+修改 `config/base_config.yml`:
+
+```yaml
+debug:
+ joint_pub_debug: true
+```
+
+## ROS2 话题
+
+> **发布频率**: 所有话题统一 50Hz
+
+### 发布话题
+
+| 话题名 | 消息类型 | 说明 | 默认状态 |
+|-------|---------|------|---------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | 右手控制数据 | ✅ 启用 |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | 左手控制数据 | ✅ 启用 |
+| `/cb_right_hand_raw_data` | `sensor_msgs/JointState` | 右手传感器原始数据 | ❌ 禁用 |
+| `/cb_left_hand_raw_data` | `sensor_msgs/JointState` | 左手传感器原始数据 | ❌ 禁用 |
+
+### 订阅话题
+
+| 话题名 | 消息类型 | 说明 |
+|-------|---------|------|
+| `/cb_hand_setting_cmd` | `std_msgs/String` | 设置命令 (控制原始数据开关) |
+
+## 原始数据功能
+
+原始数据默认**禁用**,需要通过话题命令启用。
+
+### 启用/禁用原始数据
+
+```bash
+# 启用原始数据推送
+ros2 topic pub --once /cb_hand_setting_cmd std_msgs/String "data: 'on'"
+
+# 禁用原始数据推送
+ros2 topic pub --once /cb_hand_setting_cmd std_msgs/String "data: 'off'"
+
+# 监听原始数据
+ros2 topic echo /cb_left_hand_raw_data
+```
+
+## ROS2 话题数据格式
+
+### 原始数据格式 (15个int32值)
+
+| 索引 | 关节名称 | 索引 | 关节名称 | 索引 | 关节名称 |
+|------|----------|------|----------|------|----------|
+| 0 | 大拇指横摆 | 1 | 大拇指弯曲 | 2 | 大拇指指尖 |
+| 3 | 食指横摆 | 4 | 食指弯曲 | 5 | 食指指尖 |
+| 6 | 中指横摆 | 7 | 中指弯曲 | 8 | 中指指尖 |
+| 9 | 无名指横摆 | 10 | 无名指弯曲 | 11 | 无名指指尖 |
+| 12 | 小指横摆 | 13 | 小指弯曲 | 14 | 小指指尖 |
+
+### 控制数据格式
+#### L21 (25关节输出)
+
+适用灵巧手型号:L21
+
+| Joint | 关节名称 | 说明 |
+|-------|----------|------|
+| joint1 | 大拇指弯曲 | |
+| joint2 | 食指弯曲 | |
+| joint3 | 中指弯曲 | |
+| joint4 | 无名指弯曲 | |
+| joint5 | 小拇指弯曲 | |
+| joint6 | 大拇指横摆 | |
+| joint7 | 食指横摆 | |
+| joint8 | 中指横摆 | |
+| joint9 | 无名指横摆 | |
+| joint10 | 小拇指横摆 | |
+| joint11 | 大拇指横滚 | |
+| joint12 | 预留 | 值为0 |
+| joint13 | 预留 | 值为0 |
+| joint14 | 预留 | 值为0 |
+| joint15 | 预留 | 值为0 |
+| joint16 | 大拇指中部 | 值为0 |
+| joint17 | 预留 | 值为0 |
+| joint18 | 预留 | 值为0 |
+| joint19 | 预留 | 值为0 |
+| joint20 | 预留 | 值为0 |
+| joint21 | 大拇指指尖 | |
+| joint22 | 食指指尖 | |
+| joint23 | 中指指尖 | |
+| joint24 | 无名指指尖 | |
+| joint25 | 小指指尖 | |
+
+#### L20/G20 (20关节输出)
+
+适用灵巧手型号:L20、工业版20、G20
+
+| Joint | 关节名称 | 说明 |
+|-------|----------|------|
+| joint1 | 拇指弯曲 | |
+| joint2 | 食指弯曲 | |
+| joint3 | 中指弯曲 | |
+| joint4 | 无名指弯曲 | |
+| joint5 | 小指弯曲 | |
+| joint6 | 拇指横摆 | |
+| joint7 | 食指横摆 | |
+| joint8 | 中指横摆 | |
+| joint9 | 无名指横摆 | |
+| joint10 | 小指横摆 | |
+| joint11 | 拇指横滚 | |
+| joint12 | 预留 | 值为0 |
+| joint13 | 预留 | 值为0 |
+| joint14 | 预留 | 值为0 |
+| joint15 | 预留 | 值为0 |
+| joint16 | 拇指指尖 | |
+| joint17 | 食指指尖 | |
+| joint18 | 中指指尖 | |
+| joint19 | 无名指指尖 | |
+| joint20 | 小指指尖 | |
+
+#### L10 (10关节)
+
+适用灵巧手型号:L10
+
+| Joint | 关节名称 |
+|-------|----------|
+| joint1 | 大拇指弯曲 |
+| joint2 | 大拇指横摆 |
+| joint3 | 食指弯曲 |
+| joint4 | 中指弯曲 |
+| joint5 | 无名指弯曲 |
+| joint6 | 小指弯曲 |
+| joint7 | 食指横摆 |
+| joint8 | 无名指横摆 |
+| joint9 | 小指横摆 |
+| joint10 | 大拇指横滚 |
+
+#### L6 (6关节)
+
+适用灵巧手型号:L6、O6
+
+| Joint | 关节名称 |
+|-------|----------|
+| joint1 | 大拇指弯曲 |
+| joint2 | 大拇指横摆 |
+| joint3 | 食指弯曲 |
+| joint4 | 中指弯曲 |
+| joint5 | 无名指弯曲 |
+| joint6 | 小指弯曲 |
+
+#### O7 (7关节)
+
+适用灵巧手型号:O7
+
+| Joint | 关节名称 |
+|-------|----------|
+| joint1 | 大拇指弯曲 |
+| joint2 | 大拇指横摆 |
+| joint3 | 食指弯曲 |
+| joint4 | 中指弯曲 |
+| joint5 | 无名指弯曲 |
+| joint6 | 小指弯曲 |
+| joint7 | 大拇指横滚 |
+
+### 消息格式
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # 时间戳
+msg.name # ['joint1', 'joint2', ..., 'jointN']
+msg.position # [0-255, ...] 电机位置值
+msg.velocity # [255, ...] 速度值
+```
+
+## 文件结构
+
+```
+motion/linkereg/
+├── __init__.py # 模块导出
+├── linkeregcore.py # 串口通讯核心
+├── retarget.py # ROS2 集成
+└── README.md # 本文档
+```
+
+## 注意事项
+
+1. **灵巧手型号**: 无需在配置文件中指定,手套会在数据帧中自动标识协议类型
+
+2. **硬件连接**:接收器必须连接到主机上(当前程序所在主机)
+
+3. **数据频率**: 手套以 50Hz 频率推送数据
+
+4. **模式选择**:
+ - `linkereg2` (SDK控制模式): 接收器不需要连接灵巧手,手套数据通过ROS话题发布,由上层灵巧手SDK订阅对应话题来控制灵巧手
+ - `linkereg1` (接收器控制模式): 接收器必须连接真实灵巧手,接收器直接控制灵巧手运动,当前SDK只是为了采集手套数据。
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/__init__.py
new file mode 100644
index 0000000..0f70c53
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/__init__.py
@@ -0,0 +1,12 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+LinkerEG 遥操作手套模块
+
+通过串口与 LinkerEG 手套通讯,接收映射数据并发布到 ROS2 话题
+"""
+
+from .linkeregcore import LinkerEGSerial, PROTOCOL_MAP
+from .retarget import Retarget
+
+__all__ = ['LinkerEGSerial', 'PROTOCOL_MAP', 'Retarget']
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/linkeregcore.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/linkeregcore.py
new file mode 100644
index 0000000..d2546fb
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkereg/linkeregcore.py
@@ -0,0 +1,911 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+LinkerEG 遥操作手套串口通讯核心模块
+
+协议特点:
+- 波特率: 115200, 8-n-1
+- 帧格式: [0xAA] [CMD] [LEN] [DATA...] [CHECKSUM] [0x55]
+- 校验和: 补码累加和 checksum = ~(cmd + len + sum(data)) + 1
+- 左右手共用同一串口
+"""
+
+import array
+import struct
+import time
+import re
+import os
+import subprocess
+import serial
+import serial.tools.list_ports
+from enum import Enum
+from threading import Thread, Event
+from typing import Optional, Tuple, List, Callable
+
+# 帧常量
+FRAME_HEADER = 0xAA
+FRAME_TAIL = 0x55
+MAX_DATA_SIZE = 128
+BUFFER_SIZE = 512
+
+# 命令类型
+class CmdType:
+ READ_CONTROL_MODE = 0x0B # 读取控制方式
+ RECEIVER_CONTROL = 0x0D # 接收器控制灵巧手
+ SDK_CONTROL = 0x0E # SDK控制灵巧手
+ ENABLE_RAW_DATA = 0x0F # 启用传感器原始数据推送
+ DISABLE_RAW_DATA = 0x10 # 禁用传感器原始数据推送
+ ENABLE_MAPPED_DATA = 0x11 # 启用映射数据推送
+ DISABLE_MAPPED_DATA = 0x12 # 禁用映射数据推送
+ READ_VERSION = 0x14 # 读取版本号
+ RAW_DATA_PUSH = 0x20 # 传感器原始数据推送 (50Hz)
+ MAPPED_DATA_PUSH = 0x21 # 映射数据推送 (50Hz)
+
+# 协议类型映射
+PROTOCOL_MAP = {
+ 0: {'name': 'L20', 'joints': 16},
+ 1: {'name': 'L10', 'joints': 10},
+ 2: {'name': 'L21', 'joints': 16},
+ 3: {'name': 'L6', 'joints': 6},
+ 4: {'name': 'O7', 'joints': 7},
+ 5: {'name': 'G20', 'joints': 16},
+}
+
+# 结果码
+class ResultCode:
+ SUCCESS = 0x00
+ FAILED = 0x01
+ UNKNOWN_CMD = 0xFD
+ FRAME_MODE_DISABLED = 0xFE
+ CHECKSUM_ERROR = 0xFF
+
+
+class FrameParseState(Enum):
+ """帧解析状态机"""
+ HEADER = 0
+ CMD = 1
+ LENGTH = 2
+ DATA = 3
+ CHECKSUM = 4
+ TAIL = 5
+
+
+class FrameParser:
+ """LinkerEG 帧解析器"""
+
+ def __init__(self):
+ self.state = FrameParseState.HEADER
+ self.frame_buf = array.array('B', [0] * (4 + MAX_DATA_SIZE + 2))
+ self.expected_len = 0
+ self.current_pos = 0
+ self.cmd = 0
+ self.data_len = 0
+
+ def reset(self):
+ """重置解析器状态"""
+ self.state = FrameParseState.HEADER
+ self.current_pos = 0
+ self.cmd = 0
+ self.data_len = 0
+
+ @staticmethod
+ def calculate_checksum(cmd: int, data_len: int, data: bytes) -> int:
+ """
+ 计算校验和 (补码累加和)
+ checksum = ~(cmd + len + sum(data)) + 1
+ """
+ total = cmd + data_len
+ for b in data:
+ total += b
+ checksum = (~total + 1) & 0xFF
+ return checksum
+
+ def process_byte(self, byte: int) -> bool:
+ """
+ 处理单个字节,返回是否接收到完整帧
+ """
+ byte = byte & 0xFF
+
+ if self.state == FrameParseState.HEADER:
+ if byte == FRAME_HEADER:
+ self.frame_buf[0] = byte
+ self.current_pos = 1
+ self.state = FrameParseState.CMD
+
+ elif self.state == FrameParseState.CMD:
+ self.cmd = byte
+ self.frame_buf[1] = byte
+ self.current_pos = 2
+ self.state = FrameParseState.LENGTH
+
+ elif self.state == FrameParseState.LENGTH:
+ self.data_len = byte
+ self.frame_buf[2] = byte
+ self.current_pos = 3
+ if byte <= MAX_DATA_SIZE:
+ if byte == 0:
+ self.state = FrameParseState.CHECKSUM
+ else:
+ self.state = FrameParseState.DATA
+ else:
+ self.reset()
+
+ elif self.state == FrameParseState.DATA:
+ self.frame_buf[self.current_pos] = byte
+ self.current_pos += 1
+ if self.current_pos >= 3 + self.data_len:
+ self.state = FrameParseState.CHECKSUM
+
+ elif self.state == FrameParseState.CHECKSUM:
+ self.frame_buf[self.current_pos] = byte
+ # 验证校验和
+ data = bytes(self.frame_buf[3:3 + self.data_len])
+ expected_checksum = self.calculate_checksum(self.cmd, self.data_len, data)
+ if byte == expected_checksum:
+ self.current_pos += 1
+ self.state = FrameParseState.TAIL
+ else:
+ self.reset()
+
+ elif self.state == FrameParseState.TAIL:
+ if byte == FRAME_TAIL:
+ self.frame_buf[self.current_pos] = byte
+ return True
+ else:
+ self.reset()
+
+ return False
+
+ def get_frame_data(self) -> Tuple[int, bytes]:
+ """获取解析后的帧数据 (cmd, data)"""
+ data = bytes(self.frame_buf[3:3 + self.data_len])
+ return self.cmd, data
+
+
+class LinkerEGSerial:
+ """LinkerEG 串口通讯类"""
+
+ # 控制模式
+ MODE_SDK = 'sdk' # SDK控制模式 (linkereg)
+ MODE_RECEIVER = 'receiver' # 接收器控制模式 (linkereg1)
+
+ def __init__(self, port: str = None, baudrate: int = 115200, password: str = '12345678', isdebug: bool = False, mode: str = 'sdk'):
+ """
+ 初始化 LinkerEG 串口通讯
+
+ Args:
+ port: 串口路径,如果为 None 则自动扫描
+ baudrate: 波特率 (默认 115200)
+ password: sudo 密码,用于修复串口权限
+ isdebug: 是否打印调试信息
+ mode: 控制模式 'sdk'=SDK控制模式, 'receiver'=接收器控制模式
+ """
+ self.port = port
+ self.baudrate = baudrate
+ self.mode = mode
+
+
+ print(f"[LinkerEG] 初始化 LinkerEGSerial (mode={self.mode})...", flush=True)
+ self.serial_port: Optional[serial.Serial] = None
+ self.parser = FrameParser()
+ self.running = Event()
+ self.thread: Optional[Thread] = None
+
+ # 版本信息
+ self.version: Optional[str] = None
+ self.connected = False
+ self.initialized = False
+
+ # 数据存储 (左右手) - 控制数据
+ self.right_hand_data: List[int] = []
+ self.left_hand_data: List[int] = []
+ self.right_hand_protocol: int = -1
+ self.left_hand_protocol: int = -1
+
+ # 回调函数 (control_data, protocol)
+ self.on_right_hand_data: Optional[Callable[[List[int], int], None]] = None
+ self.on_left_hand_data: Optional[Callable[[List[int], int], None]] = None
+
+ # 传感器原始数据存储 (左右手) - 15个int32关节值
+ self.right_hand_raw_data: List[int] = []
+ self.left_hand_raw_data: List[int] = []
+
+ # 原始数据回调函数 (raw_data: List[int])
+ # raw_data 为15个int32值: 大拇指横摆、大拇指弯曲、大拇指指尖、食指横摆、食指弯曲、食指指尖...
+ self.on_right_hand_raw_data: Optional[Callable[[List[int]], None]] = None
+ self.on_left_hand_raw_data: Optional[Callable[[List[int]], None]] = None
+
+ # 原始数据推送状态
+ self.raw_data_enabled = False
+
+ # sudo密码 (用于权限修复,从外部传入)
+ self._sudo_password: str = password
+ self.isdebug = isdebug
+
+ # 串口扫描相关
+ self.checked_ports: set = set()
+ self.exclude_ports: set = set()
+
+
+ # 灵巧手控制状态
+ self.hand_control_mode: Optional[str] = None # 'sdk' 或 'receiver'
+
+ def _is_usb_device(self, port_name: str) -> bool:
+ """
+ 判断是否为USB串口设备
+ 支持: /dev/ttyUSB*, /dev/ttyACM* 等USB转串口设备
+ """
+ import re
+ usb_patterns = [
+ r'/dev/ttyUSB\d+',
+ r'/dev/ttyACM\d+',
+ r'/dev/ttyXRUSB\d+',
+ r'/dev/ttyOBC\d+',
+ ]
+
+ for pattern in usb_patterns:
+ if re.match(pattern, port_name):
+ return True
+
+ try:
+ ports = serial.tools.list_ports.comports()
+ for port_info in ports:
+ if port_info.device == port_name:
+ description = (port_info.description or "").lower()
+ if any(keyword in description for keyword in ['usb', 'serial', 'com']):
+ return True
+ if port_info.hwid and 'USB' in port_info.hwid.upper():
+ return True
+ except:
+ pass
+
+ return False
+
+ def scan_serial_ports(self) -> List[str]:
+ """扫描所有可用的USB串口"""
+ ports = serial.tools.list_ports.comports()
+ available_ports = []
+
+ for port in ports:
+ port_device = port.device
+ if not self._is_usb_device(port_device):
+ continue
+ if port_device in self.exclude_ports:
+ if self.isdebug:
+ print(f"[LinkerEG] 跳过排除的串口: {port_device}", flush=True)
+ continue
+ if port_device not in self.checked_ports:
+ available_ports.append(port_device)
+
+ return available_ports
+
+ def _quick_test_port(self, port_name: str, timeout: float = 3.0) -> Tuple[bool, Optional[int]]:
+ """
+ 快速测试串口是否为 LinkerEG 设备
+ 返回: (success, error_code)
+ error_code: None=成功, -1=不存在, -2=权限问题, -3=设备忙, -99=其他错误
+ """
+ try:
+ with serial.Serial(
+ port_name,
+ baudrate=self.baudrate,
+ timeout=0.2,
+ bytesize=serial.EIGHTBITS,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE
+ ) as ser:
+ # 清空缓冲区
+ ser.reset_input_buffer()
+ ser.reset_output_buffer()
+ time.sleep(0.2)
+
+ # 发送 frame_enable (尝试三次,间隔更长)
+ for _ in range(3):
+ ser.write(b'frame_enable\n')
+ time.sleep(0.2)
+
+ # 清空可能的回复
+ if ser.in_waiting > 0:
+ ser.read(ser.in_waiting)
+
+ # 发送 SDK 控制命令 (0x0E) - 这个命令响应更可靠
+ cmd = CmdType.SDK_CONTROL
+ data = b''
+ checksum = FrameParser.calculate_checksum(cmd, 0, data)
+ frame = bytes([FRAME_HEADER, cmd, 0, checksum, FRAME_TAIL])
+ if self.isdebug:
+ print(f"[LinkerEG] 发送SDK控制命令: {frame.hex()}", flush=True)
+ ser.write(frame)
+ time.sleep(0.1)
+
+ # 再发送读取版本命令
+ cmd = CmdType.READ_VERSION
+ checksum = FrameParser.calculate_checksum(cmd, 0, data)
+ frame = bytes([FRAME_HEADER, cmd, 0, checksum, FRAME_TAIL])
+ if self.isdebug:
+ print(f"[LinkerEG] 发送版本查询: {frame.hex()}", flush=True)
+ ser.write(frame)
+
+ # 等待响应
+ start_time = time.time()
+ parser = FrameParser()
+
+ while (time.time() - start_time) < timeout:
+ if ser.in_waiting > 0:
+ chunk = ser.read(ser.in_waiting)
+ if self.isdebug:
+ print(f"[LinkerEG] 收到数据: {chunk.hex()}", flush=True)
+ for byte in chunk:
+ if parser.process_byte(byte):
+ cmd_resp, data_resp = parser.get_frame_data()
+ if self.isdebug:
+ print(f"[LinkerEG] 解析到帧: cmd=0x{cmd_resp:02X}, data={data_resp.hex()}", flush=True)
+ # 收到任何有效响应都说明是 LinkerEG 设备
+ if cmd_resp in (CmdType.READ_VERSION, CmdType.SDK_CONTROL, CmdType.RECEIVER_CONTROL,
+ CmdType.MAPPED_DATA_PUSH, CmdType.RAW_DATA_PUSH,
+ CmdType.ENABLE_MAPPED_DATA, CmdType.DISABLE_MAPPED_DATA):
+ if self.isdebug:
+ print(f"[LinkerEG] 串口 {port_name} 检测到 LinkerEG 设备 (cmd=0x{cmd_resp:02X})", flush=True)
+ return True, None
+ parser.reset()
+ time.sleep(0.01)
+
+ if self.isdebug:
+ print(f"[LinkerEG] 串口 {port_name} 无响应", flush=True)
+ return False, None
+
+ except serial.SerialException as e:
+ error_msg = str(e)
+ if "No such file or directory" in error_msg or "[Errno 2]" in error_msg:
+ return False, -1
+ elif "Permission denied" in error_msg or "[Errno 13]" in error_msg:
+ return False, -2
+ elif "Device or resource busy" in error_msg:
+ return False, -3
+ else:
+ if self.isdebug:
+ print(f"[LinkerEG] 串口测试失败: {e}", flush=True)
+ return False, -99
+ except Exception as e:
+ if self.isdebug:
+ print(f"[LinkerEG] 串口测试异常: {e}", flush=True)
+ return False, -99
+
+ def find_valid_port(self) -> Tuple[Optional[str], Optional[int]]:
+ """
+ 自动扫描并查找有效的 LinkerEG 串口
+ 返回: (port_name, error_code)
+ """
+ print("[LinkerEG] 开始扫描串口...", flush=True)
+
+ available_ports = self.scan_serial_ports()
+ if not available_ports:
+ print("[LinkerEG] 未发现可用的 USB 串口设备", flush=True)
+ return None, -1
+
+ print(f"[LinkerEG] 发现 {len(available_ports)} 个串口: {available_ports}", flush=True)
+
+ for port in available_ports:
+ print(f"[LinkerEG] 正在检测 {port}...", flush=True)
+ success, error_code = self._quick_test_port(port)
+
+ if error_code == -2: # 权限问题
+ print(f"[LinkerEG] 检测到权限问题,尝试修复 {port}...", flush=True)
+ if self._fix_serial_permission(port):
+ # 修复后重试
+ success, error_code = self._quick_test_port(port)
+
+ self.checked_ports.add(port)
+
+ if success:
+ print(f"[LinkerEG] ✓ 找到有效串口: {port}", flush=True)
+ return port, None
+
+ print("[LinkerEG] 未找到 LinkerEG 设备", flush=True)
+ return None, -1
+
+
+ def _fix_serial_permission(self, port_name: str) -> bool:
+ """尝试修复串口权限(使用预设密码)"""
+ try:
+ if not os.path.exists(port_name):
+ print(f"[LinkerEG] 串口设备不存在: {port_name}", flush=True)
+ return False
+
+ password = self._sudo_password
+ if not password:
+ print("[LinkerEG] 未设置 sudo 密码,无法修复权限", flush=True)
+ return False
+
+ # 使用echo传递密码执行chmod
+ command = f'echo "{password}" | sudo -S chmod 666 {port_name}'
+ result = subprocess.run(
+ command,
+ shell=True,
+ capture_output=True,
+ text=True,
+ timeout=10
+ )
+
+ if result.returncode == 0:
+ print(f"[LinkerEG] ✓ 成功修复 {port_name} 权限", flush=True)
+ return True
+ else:
+ print(f"[LinkerEG] ✗ 权限修复失败: {result.stderr.strip()}", flush=True)
+ return False
+
+ except subprocess.TimeoutExpired:
+ print(f"[LinkerEG] 修复权限超时", flush=True)
+ except Exception as e:
+ print(f"[LinkerEG] 修复权限时出错: {e}", flush=True)
+
+ return False
+
+ def open(self, auto_scan: bool = True) -> bool:
+ """
+ 打开串口(带权限检测和自动修复)
+
+ Args:
+ auto_scan: 如果 port 为 None,是否自动扫描
+ """
+ # 如果没有指定串口,自动扫描
+ if self.port is None and auto_scan:
+ port, error_code = self.find_valid_port()
+ if port is None:
+ print("[LinkerEG] 自动扫描未找到有效串口", flush=True)
+ return False
+ self.port = port
+
+ if self.port is None:
+ print("[LinkerEG] 未指定串口", flush=True)
+ return False
+
+ max_retry = 2 # 最多尝试2次 (第一次失败后尝试修复权限再试一次)
+
+ for attempt in range(max_retry):
+ try:
+ self.serial_port = serial.Serial(
+ port=self.port,
+ baudrate=self.baudrate,
+ timeout=0.01,
+ bytesize=serial.EIGHTBITS,
+ parity=serial.PARITY_NONE,
+ stopbits=serial.STOPBITS_ONE
+ )
+ self.connected = True
+ print(f"[LinkerEG] 串口 {self.port} 打开成功 (波特率: {self.baudrate})", flush=True)
+ return True
+
+ except serial.SerialException as e:
+ error_msg = str(e)
+
+ if "No such file or directory" in error_msg or "[Errno 2]" in error_msg:
+ print(f"[LinkerEG] 串口设备不存在: {self.port}", flush=True)
+ print("[LinkerEG] 请检查串口名称或设备是否连接", flush=True)
+ return False
+
+ elif "Permission denied" in error_msg or "[Errno 13]" in error_msg:
+ print(f"[LinkerEG] 权限被拒绝: {self.port}", flush=True)
+
+ if attempt == 0: # 第一次失败,尝试修复权限
+ print("[LinkerEG] 检测到权限问题,尝试修复权限...", flush=True)
+ if self._fix_serial_permission(self.port):
+ print("[LinkerEG] 权限修复成功,重新尝试打开串口...", flush=True)
+ continue # 重试
+ else:
+ print("[LinkerEG] 权限修复失败", flush=True)
+ return False
+ else:
+ print("[LinkerEG] 权限修复后仍无法打开串口", flush=True)
+ return False
+
+ elif "Device or resource busy" in error_msg:
+ print(f"[LinkerEG] 设备忙: {self.port} - 串口可能已被其他程序占用", flush=True)
+ return False
+
+ else:
+ print(f"[LinkerEG] 串口打开失败: {e}", flush=True)
+ return False
+
+ return False
+
+ def close(self):
+ """关闭串口"""
+ self.stop()
+ if self.serial_port and self.serial_port.is_open:
+ self.serial_port.close()
+ self.connected = False
+ print("[LinkerEG] 串口已关闭", flush=True)
+
+ def _build_frame(self, cmd: int, data: bytes = b'') -> bytes:
+ """构建发送帧"""
+ data_len = len(data)
+ checksum = FrameParser.calculate_checksum(cmd, data_len, data)
+ frame = bytes([FRAME_HEADER, cmd, data_len]) + data + bytes([checksum, FRAME_TAIL])
+ return frame
+
+ def _send_frame(self, cmd: int, data: bytes = b'') -> bool:
+ """发送帧"""
+ if not self.serial_port or not self.serial_port.is_open:
+ return False
+ frame = self._build_frame(cmd, data)
+ try:
+ self.serial_port.write(frame)
+ return True
+ except Exception as e:
+ print(f"[LinkerEG] 发送失败: {e}", flush=True)
+ return False
+
+ def enable_frame_mode(self) -> bool:
+ """启用数据帧模式 (发送 'frame_enable\n' 多次)"""
+ if not self.serial_port or not self.serial_port.is_open:
+ return False
+ try:
+ # 清空缓冲区
+ self.serial_port.reset_input_buffer()
+ self.serial_port.reset_output_buffer()
+
+ # 发送 ASCII 字符串 "frame_enable\n" 多次确保可靠
+ for _ in range(3):
+ self.serial_port.write(b'frame_enable\n')
+ time.sleep(0.1)
+ print("[LinkerEG] 已发送 frame_enable 命令", flush=True)
+ return True
+ except Exception as e:
+ print(f"[LinkerEG] 发送 frame_enable 失败: {e}", flush=True)
+ return False
+
+ def sdk_control(self) -> bool:
+ """SDK控制灵巧手 (0x0E) - linkereg2模式"""
+ return self._send_frame(CmdType.SDK_CONTROL)
+
+ def receiver_control(self) -> bool:
+ """接收器控制灵巧手 (0x0D) - linkereg1模式"""
+ return self._send_frame(CmdType.RECEIVER_CONTROL)
+
+ def enable_mapped_data(self) -> bool:
+ """启用映射数据推送 (0x11)"""
+ return self._send_frame(CmdType.ENABLE_MAPPED_DATA)
+
+ def disable_mapped_data(self) -> bool:
+ """禁用映射数据推送 (0x12)"""
+ return self._send_frame(CmdType.DISABLE_MAPPED_DATA)
+
+ def enable_raw_data(self) -> bool:
+ """
+ 启用传感器原始数据推送 (0x0F)
+ 发送: AA 0F 00 F1 55
+ 响应: AA 0F 01 00 [校验和] 55
+ 启用后系统以50Hz频率推送原始传感器数据 (命令类型0x20)
+ """
+ return self._send_frame(CmdType.ENABLE_RAW_DATA)
+
+ def disable_raw_data(self) -> bool:
+ """
+ 禁用传感器原始数据推送 (0x10)
+ 发送: AA 10 00 F0 55
+ 响应: AA 10 01 00 [校验和] 55
+ """
+ return self._send_frame(CmdType.DISABLE_RAW_DATA)
+
+ def read_control_mode(self) -> bool:
+ """
+ 读取控制方式 (0x0B)
+ 发送: AA 0B 00 F5 55
+ 响应: AA 0B 02 00 [0x00:SDK控制, 0x01:接收器控制] [校验和] 55
+ """
+ return self._send_frame(CmdType.READ_CONTROL_MODE)
+
+ def read_version(self) -> bool:
+ """读取版本号 (0x14)"""
+ return self._send_frame(CmdType.READ_VERSION)
+
+ def initialize(self) -> bool:
+ """
+ 完整初始化流程:
+ - SDK控制模式: 发送 0x0E 命令
+ - 接收器控制模式: 发送 0x0D 命令 (需要连接灵巧手)
+ """
+ mode_name = "SDK控制模式" if self.mode == self.MODE_SDK else "接收器控制模式"
+ print(f"[LinkerEG] 开始初始化 ({mode_name})...", flush=True)
+
+ # 1. 启用数据帧模式
+ if not self.enable_frame_mode():
+ print("[LinkerEG] 启用数据帧模式失败", flush=True)
+ return False
+ time.sleep(0.2)
+
+ # 2. 根据模式发送控制命令 (多次发送确保生效)
+ while 1:
+ if self.mode == self.MODE_RECEIVER and self.hand_control_mode != self.MODE_RECEIVER:
+ self.receiver_control()
+ elif self.mode == self.MODE_SDK and self.hand_control_mode != self.MODE_SDK:
+ self.sdk_control()
+ else:
+ print(f"[LinkerEG] 当前控制方式: {self.hand_control_mode} (与期望一致)", flush=True)
+ break
+ time.sleep(0.2)
+ self.read_control_mode()
+
+
+
+
+ # 3. 读取版本号
+ if not self.read_version():
+ print("[LinkerEG] 读取版本号失败", flush=True)
+ return False
+ time.sleep(0.3)
+
+ # 4. 读取当前控制方式
+ if not self.read_control_mode():
+ print("[LinkerEG] 读取控制方式失败", flush=True)
+ return False
+ time.sleep(0.3)
+
+ # 5. 启用映射数据推送
+ if not self.enable_mapped_data():
+ print("[LinkerEG] 启用映射数据推送失败", flush=True)
+ return False
+
+ self.initialized = True
+
+ print(f"[LinkerEG] 初始化完成 ({self.hand_control_mode})", flush=True)
+ return True
+
+ def start(self):
+ """启动接收线程"""
+ if self.thread is not None and self.thread.is_alive():
+ return
+ self.running.set()
+ self.thread = Thread(target=self._receive_loop, daemon=True)
+ self.thread.start()
+ print("[LinkerEG] 接收线程已启动", flush=True)
+
+ def stop(self):
+ """停止接收线程"""
+ self.running.clear()
+ if self.thread is not None:
+ self.thread.join(timeout=1.0)
+ self.thread = None
+ print("[LinkerEG] 接收线程已停止", flush=True)
+
+ def _reorder_joints(self, control_data: List[int], protocol: int) -> List[int]:
+ """
+ 重排关节顺序,使输出与 haocun/linkerforce 保持一致
+
+ Args:
+ control_data: 原始控制数据
+ protocol: 协议类型
+
+ Returns:
+ 重排后的控制数据
+ """
+ if protocol == 2: # L21 (16关节 -> 25关节输出)
+ # 原序 (16关节): [大拇指横摆, 食指横摆, 中指横摆, 无名指横摆, 小指横摆,
+ # 大拇指弯曲, 食指弯曲, 中指弯曲, 无名指弯曲, 小指弯曲,
+ # 大拇指横滚, 大拇指中部, 大拇指指尖, 食指指尖, 中指指尖, 无名指指尖, 小指指尖]
+
+ if len(control_data) >= 16:
+ return [
+ control_data[5], # 大拇指弯曲
+ control_data[6], # 食指弯曲
+ control_data[7], # 中指弯曲
+ control_data[8], # 无名指弯曲
+ control_data[9], # 小拇指弯曲
+ control_data[0], # 大拇指横摆
+ control_data[1], # 食指横摆
+ control_data[2], # 中指横摆
+ control_data[3], # 无名指横摆
+ control_data[4], # 小拇指横摆
+ control_data[15], # 大拇指横滚
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ 0, # 大拇指中部
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ control_data[10], # 大拇指指尖
+ control_data[11], # 食指指尖
+ control_data[12], # 中指指尖
+ control_data[13], # 无名指指尖
+ control_data[14], # 小指指尖
+
+ ]
+
+ elif protocol == 3: # L6/O6 (6关节)
+ # 原序: [大拇指横摆, 大拇指弯曲, 食指, 中指, 无名指, 小指]
+ # 新序: [大拇指弯曲, 大拇指横摆, 食指, 中指, 无名指, 小指]
+ if len(control_data) >= 6:
+ return [
+ control_data[1], # 大拇指弯曲
+ control_data[0], # 大拇指横摆
+ control_data[2], # 食指弯曲
+ control_data[3], # 中指弯曲
+ control_data[4], # 无名指弯曲
+ control_data[5], # 小指弯曲
+ ]
+
+ elif protocol == 4: # O7 (7关节)
+ # 原序: [大拇指横摆, 大拇指弯曲, 食指, 中指, 无名指, 小指, 大拇指旋转]
+ # 新序: [大拇指弯曲, 大拇指横摆, 食指, 中指, 无名指, 小指, 大拇指旋转]
+ if len(control_data) >= 7:
+ return [
+ control_data[1], # 大拇指弯曲
+ control_data[0], # 大拇指横摆
+ control_data[2], # 食指弯曲
+ control_data[3], # 中指弯曲
+ control_data[4], # 无名指弯曲
+ control_data[5], # 小指弯曲
+ control_data[6], # 大拇指旋转
+ ]
+
+ elif protocol == 1: # L10 (10关节)
+ # 新序: [大拇指弯曲, 大拇指横摆, 食指弯曲, 中指弯曲, 无名指弯曲, 小指弯曲, 食指横摆, 无名指横摆, 小指横摆, 大拇指旋转]
+ # 新序: [大拇指弯曲, 大拇指横摆, 食指弯曲, 中指弯曲, 无名指弯曲, 小指弯曲, 食指横摆, 无名指横摆, 小指横摆, 大拇指旋转]
+ if len(control_data) >= 10:
+ return [
+ control_data[4], # 大拇指弯曲
+ control_data[0], # 大拇指横摆
+ control_data[5], # 食指弯曲
+ control_data[6], # 中指弯曲
+ control_data[7], # 无名指弯曲
+ control_data[8], # 小指弯曲
+ control_data[1], # 食指横摆
+ control_data[2], # 无名指横摆
+ control_data[3], # 小指横摆
+ control_data[9], # 大拇指旋转
+ ]
+
+ elif protocol == 0 or protocol == 5: # L20 G20(16关节 -> 20关节输出)
+ # 原序 (16关节): [大拇指横摆, 食指横摆, 中指横摆, 无名指横摆, 小指横摆,
+ # 大拇指弯曲, 食指弯曲, 中指弯曲, 无名指弯曲, 小指弯曲,
+ # 大拇指指尖, 食指指尖, 中指指尖, 无名指指尖, 小指指尖, 大拇指横滚]
+ # 新序 (20关节): [拇指弯曲, 食指弯曲, 中指弯曲, 无名指弯曲, 小指弯曲,
+ # 拇指横摆, 食指横摆, 中指横摆, 无名指横摆, 小指横摆,
+ # 拇指横滚, 预留, 预留, 预留, 预留,
+ # 拇指指尖, 食指指尖, 中指指尖, 无名指指尖, 小指指尖]
+ if len(control_data) >= 16:
+ return [
+ control_data[5], # 拇指弯曲
+ control_data[6], # 食指弯曲
+ control_data[7], # 中指弯曲
+ control_data[8], # 无名指弯曲
+ control_data[9], # 小指弯曲
+ control_data[0], # 拇指横摆
+ control_data[1], # 食指横摆
+ control_data[2], # 中指横摆
+ control_data[3], # 无名指横摆
+ control_data[4], # 小指横摆
+ control_data[15], # 拇指横滚
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ 0, # 预留
+ control_data[10], # 拇指指尖
+ control_data[11], # 食指指尖
+ control_data[12], # 中指指尖
+ control_data[13], # 无名指指尖
+ control_data[14], # 小指指尖
+ ]
+
+ # 其他协议直接透传
+ return control_data
+
+ def _receive_loop(self):
+ """接收数据循环"""
+ while self.running.is_set():
+ try:
+ if self.serial_port and self.serial_port.in_waiting > 0:
+ data = self.serial_port.read(self.serial_port.in_waiting)
+ for byte in data:
+ if self.parser.process_byte(byte):
+ self._handle_frame()
+ self.parser.reset()
+ time.sleep(0.001)
+ except Exception as e:
+ print(f"[LinkerEG] 接收错误: {e}", flush=True)
+ time.sleep(0.01)
+
+ def _handle_frame(self):
+ """处理接收到的完整帧"""
+ cmd, data = self.parser.get_frame_data()
+
+ if cmd == CmdType.READ_CONTROL_MODE:
+ # 响应格式: AA 0B 02 00 [0x00:SDK控制, 0x01:接收器控制] [校验和] 55
+ if len(data) >= 2 and data[0] == ResultCode.SUCCESS:
+ control_mode = data[1]
+ self.hand_control_mode = "sdk" if control_mode == 0x00 else "receiver"
+ elif cmd == CmdType.READ_VERSION:
+ # 响应格式: [结果码] [版本数据...]
+ if len(data) >= 5 and data[0] == ResultCode.SUCCESS:
+ # 版本号格式: 4字节小端序
+ version_bytes = data[1:5]
+ major = version_bytes[0]
+ minor = version_bytes[1]
+ patch = (version_bytes[2] << 8) | version_bytes[3]
+ self.version = f"{major}.{minor}.{patch}"
+ print(f"[LinkerEG] 版本号: {self.version}", flush=True)
+
+ elif cmd == CmdType.MAPPED_DATA_PUSH:
+ # 映射数据帧 (linkereg2 SDK控制模式): [手侧] [协议] [状态] [N字节控制数据]
+ # SDK模式下只有控制数据
+ if len(data) >= 3:
+ hand_side = data[0] # 0=右手, 1=左手
+ protocol = data[1] # 协议类型
+ # data[2] 是状态字节,忽略
+
+ if protocol in PROTOCOL_MAP:
+ num_joints = PROTOCOL_MAP[protocol]['joints']
+ # SDK模式: 只有控制数据,从第3字节开始
+ if len(data) >= 3 + num_joints:
+ control_data = list(data[3:3 + num_joints])
+
+ # 重排关节顺序,与 haocun/linkerforce 保持一致
+ control_data = self._reorder_joints(control_data, protocol)
+
+ if hand_side == 0: # 右手
+ self.right_hand_data = control_data
+ self.right_hand_protocol = protocol
+ if self.on_right_hand_data:
+ self.on_right_hand_data(control_data, protocol)
+ else: # 左手
+ self.left_hand_data = control_data
+ self.left_hand_protocol = protocol
+ if self.on_left_hand_data:
+ self.on_left_hand_data(control_data, protocol)
+
+ elif cmd == CmdType.SDK_CONTROL:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ print("[LinkerEG] SDK控制模式已启用", flush=True)
+
+ elif cmd == CmdType.RECEIVER_CONTROL:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ print("[LinkerEG] 接收器控制模式已启用", flush=True)
+
+ elif cmd == CmdType.ENABLE_MAPPED_DATA:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ print("[LinkerEG] 映射数据推送已启用", flush=True)
+
+ elif cmd == CmdType.DISABLE_MAPPED_DATA:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ print("[LinkerEG] 映射数据推送已禁用", flush=True)
+
+ elif cmd == CmdType.ENABLE_RAW_DATA:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ self.raw_data_enabled = True
+ print("[LinkerEG] 传感器原始数据推送已启用", flush=True)
+
+ elif cmd == CmdType.DISABLE_RAW_DATA:
+ if len(data) >= 1 and data[0] == ResultCode.SUCCESS:
+ self.raw_data_enabled = False
+ print("[LinkerEG] 传感器原始数据推送已禁用", flush=True)
+
+ elif cmd == CmdType.RAW_DATA_PUSH:
+ # 传感器原始数据帧 (0x20): AA 20 3D [1字节手侧] [60字节传感器数据] [校验和] 55
+ # 数据长度: 0x3D = 61字节 (1字节手侧 + 60字节传感器数据)
+ # 传感器数据: 15个int32_t值 (每个4字节,小端序)
+ # 顺序: 大拇指横摆、大拇指弯曲、大拇指指尖、食指横摆、食指弯曲、食指指尖、
+ # 中指横摆、中指弯曲、中指指尖、无名指横摆、无名指弯曲、无名指指尖、
+ # 小指横摆、小指弯曲、小指指尖
+ if len(data) >= 61: # 1字节手侧 + 60字节传感器数据
+ hand_side = data[0] # 0=右手, 1=左手
+ sensor_bytes = data[1:61] # 60字节传感器数据
+
+ # 解析15个int32_t值 (小端序)
+ raw_values = []
+ for i in range(15):
+ offset = i * 4
+ value = struct.unpack(' 读版本 -> 启用映射数据)
+- 接收左右手数据并通过 ROS2 话题发布
+- 支持传感器原始数据推送
+- 支持通过话题动态启用/禁用原始数据
+- 完全不依赖 HandCore
+
+话题列表:
+ 控制数据 (驱动机械手):
+ /cb_right_hand_control_cmd (sensor_msgs/JointState)
+ /cb_left_hand_control_cmd (sensor_msgs/JointState)
+
+ 传感器原始数据:
+ /cb_right_hand_raw_data (sensor_msgs/JointState) - 15个int32关节值
+ /cb_left_hand_raw_data (sensor_msgs/JointState) - 15个int32关节值
+
+ 控制命令:
+ /cb_hand_setting_cmd (std_msgs/String) - 发送 "on" 或 "off"
+"""
+
+import time
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from std_msgs.msg import String
+
+from .linkeregcore import LinkerEGSerial, PROTOCOL_MAP
+
+
+class Retarget:
+ """LinkerEG Retarget 类"""
+
+ def __init__(self,
+ node: Node,
+ port: str = None,
+ baudrate: int = 115200,
+ password: str = 'i',
+ isdebug: bool = False,
+ mode: str = 'sdk'):
+ """
+ 初始化 LinkerEG Retarget
+
+ Args:
+ node: ROS2 节点
+ port: 串口路径,为 None 时自动扫描 (左右手共用同一个串口)
+ baudrate: 波特率 (默认 115200)
+ password: sudo 密码,用于自动修复串口权限
+ isdebug: 是否打印调试信息
+ mode: 控制模式 'sdk'=SDK控制模式, 'receiver'=接收器控制模式
+ """
+ self.node = node
+ self.port = port
+ self.baudrate = baudrate
+ self.password = password
+ self.isdebug = isdebug
+ self.mode = mode
+ self.raw_data_enabled = False # 原始数据推送状态 (通过话题控制)
+ self.running = True
+ self.pubprintcount = 0
+ self.raw_count = 0
+
+ # 创建 LinkerEG 串口通讯对象
+ self.linkereg = LinkerEGSerial(
+ port=port,
+ baudrate=baudrate,
+ password=password,
+ isdebug=isdebug,
+ mode=mode
+ )
+
+ # 设置数据回调
+ self.linkereg.on_right_hand_data = self._on_right_hand_data
+ self.linkereg.on_left_hand_data = self._on_left_hand_data
+
+ # ROS2 发布器 - 控制数据
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ 10
+ )
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ 10
+ )
+
+ # ROS2 发布器 - 传感器原始数据 (默认创建,通过话题控制启用/禁用)
+ self.publisher_r_raw = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_raw_data',
+ 10
+ )
+ self.publisher_l_raw = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_raw_data',
+ 10
+ )
+
+ # ROS2 订阅器 - 设置命令 (用于动态启用/禁用原始数据)
+ self.setting_sub = self.node.create_subscription(
+ String,
+ '/cb_hand_setting_cmd',
+ self._on_setting_cmd,
+ 10
+ )
+
+ # 定时器 - 检查连接状态
+ self.status_timer = self.node.create_timer(1.0, self._status_callback)
+
+ self.node.get_logger().info("LinkerEG Retarget 模块已创建")
+
+ def _on_right_hand_data(self, control_data: list, protocol: int):
+ """右手数据回调 - 直接发布数据"""
+ if not self.running:
+ return
+
+ now = self.node.get_clock().now().to_msg()
+ protocol_info = PROTOCOL_MAP.get(protocol, {'name': 'Unknown', 'joints': len(control_data)})
+
+ # 调试打印
+ if self.isdebug and self.pubprintcount % 50 == 0:
+ self.node.get_logger().info(
+ f"[LinkerEG] 右手 ({protocol_info['name']}): ctrl={control_data}"
+ )
+
+ msg_ctrl = JointState()
+ msg_ctrl.header.stamp = now
+ msg_ctrl.name = [f'joint{i + 1}' for i in range(len(control_data))]
+ msg_ctrl.position = [float(v) for v in control_data]
+ msg_ctrl.velocity = [255.0] * len(control_data)
+ self.publisher_r.publish(msg_ctrl)
+ self.pubprintcount += 1
+
+ def _on_left_hand_data(self, control_data: list, protocol: int):
+ """左手数据回调 - 直接发布数据"""
+ if not self.running:
+ return
+
+ now = self.node.get_clock().now().to_msg()
+ protocol_info = PROTOCOL_MAP.get(protocol, {'name': 'Unknown', 'joints': len(control_data)})
+
+ # 调试打印
+ if self.isdebug and self.pubprintcount % 50 == 0:
+ self.node.get_logger().info(
+ f"[LinkerEG] 左手 ({protocol_info['name']}): ctrl={control_data}"
+ )
+
+ msg_ctrl = JointState()
+ msg_ctrl.header.stamp = now
+ msg_ctrl.name = [f'joint{i + 1}' for i in range(len(control_data))]
+ msg_ctrl.position = [float(v) for v in control_data]
+ msg_ctrl.velocity = [255.0] * len(control_data)
+ self.publisher_l.publish(msg_ctrl)
+
+ def _on_right_hand_raw_data(self, raw_data: list):
+ """右手传感器原始数据回调 - 直接发布数据"""
+ if not self.running or not self.raw_data_enabled:
+ return
+
+ now = self.node.get_clock().now().to_msg()
+ joint_names = [
+ 'thumb_spread', 'thumb_bend', 'thumb_tip',
+ 'index_spread', 'index_bend', 'index_tip',
+ 'middle_spread', 'middle_bend', 'middle_tip',
+ 'ring_spread', 'ring_bend', 'ring_tip',
+ 'pinky_spread', 'pinky_bend', 'pinky_tip'
+ ]
+ msg = JointState()
+ msg.header.stamp = now
+ msg.name = joint_names
+ msg.position = [float(v) for v in raw_data]
+ self.publisher_r_raw.publish(msg)
+ self.raw_count += 1
+
+ def _on_left_hand_raw_data(self, raw_data: list):
+ """左手传感器原始数据回调 - 直接发布数据"""
+ if not self.running or not self.raw_data_enabled:
+ return
+
+ now = self.node.get_clock().now().to_msg()
+ joint_names = [
+ 'thumb_spread', 'thumb_bend', 'thumb_tip',
+ 'index_spread', 'index_bend', 'index_tip',
+ 'middle_spread', 'middle_bend', 'middle_tip',
+ 'ring_spread', 'ring_bend', 'ring_tip',
+ 'pinky_spread', 'pinky_bend', 'pinky_tip'
+ ]
+ msg = JointState()
+ msg.header.stamp = now
+ msg.name = joint_names
+ msg.position = [float(v) for v in raw_data]
+ self.publisher_l_raw.publish(msg)
+
+ def _on_setting_cmd(self, msg: String):
+ """
+ 处理设置命令
+
+ 支持的命令:
+ on - 启用传感器原始数据推送
+ off - 禁用传感器原始数据推送
+ """
+ cmd = msg.data.strip().lower()
+
+ if cmd == 'on':
+ if self.raw_data_enabled:
+ return # 已启用,静默忽略
+
+ # 设置回调
+ self.linkereg.on_right_hand_raw_data = self._on_right_hand_raw_data
+ self.linkereg.on_left_hand_raw_data = self._on_left_hand_raw_data
+
+ # 发送启用命令
+ self.linkereg.enable_raw_data()
+ self.raw_data_enabled = True
+ self.node.get_logger().info("[LinkerEG] 已启用传感器原始数据推送")
+ self.node.get_logger().info("[LinkerEG] 原始数据话题: /cb_right_hand_raw_data, /cb_left_hand_raw_data")
+
+ elif cmd == 'off':
+ if not self.raw_data_enabled:
+ self.node.get_logger().info("[LinkerEG] 原始数据已经禁用")
+ return
+
+ # 发送禁用命令
+ self.linkereg.disable_raw_data()
+ self.raw_data_enabled = False
+
+ # 清除回调
+ self.linkereg.on_right_hand_raw_data = None
+ self.linkereg.on_left_hand_raw_data = None
+
+ self.node.get_logger().info("[LinkerEG] 已禁用传感器原始数据推送")
+
+ else:
+ self.node.get_logger().warn(f"[LinkerEG] 未知命令: {cmd}")
+ self.node.get_logger().info("[LinkerEG] 支持的命令: on, off")
+
+ def _status_callback(self):
+ """定时状态检查回调"""
+ if not self.running:
+ return
+
+ if self.linkereg.version:
+ # 版本号只打印一次
+ pass
+
+ def initialize(self) -> bool:
+ """初始化串口连接和手套"""
+ if self.port:
+ self.node.get_logger().info(f"[LinkerEG] 正在连接指定串口 {self.port}...")
+ else:
+ self.node.get_logger().info("[LinkerEG] 未指定串口,将自动扫描...")
+
+ # 打开串口(如果 port 为 None,会自动扫描)
+ if not self.linkereg.open():
+ self.node.get_logger().error("[LinkerEG] 无法打开串口")
+ return False
+
+ # 更新实际使用的串口
+ self.port = self.linkereg.port
+ self.node.get_logger().info(f"[LinkerEG] 已连接串口: {self.port}")
+
+ # 启动接收线程
+ self.linkereg.start()
+
+ # 执行初始化流程
+ if not self.linkereg.initialize():
+ self.node.get_logger().error("[LinkerEG] 初始化失败")
+ return False
+
+
+
+ # 等待版本号
+ timeout = 2.0
+ start_time = time.time()
+ while self.linkereg.version is None and (time.time() - start_time) < timeout:
+ time.sleep(0.1)
+
+ if self.linkereg.version:
+ self.node.get_logger().info(f"[LinkerEG] 手套版本: {self.linkereg.version}")
+ else:
+ self.node.get_logger().warn("[LinkerEG] 未能获取版本号,但继续运行")
+
+ self.node.get_logger().info("[LinkerEG] 初始化成功,等待数据...")
+ self.node.get_logger().info("[LinkerEG] 控制数据话题: /cb_right_hand_control_cmd, /cb_left_hand_control_cmd")
+ self.node.get_logger().info("[LinkerEG] 原始数据话题: /cb_right_hand_raw_data, /cb_left_hand_raw_data (默认禁用)")
+ self.node.get_logger().info("[LinkerEG] 设置命令话题: /cb_hand_setting_cmd (on/off)")
+ return True
+
+ def process(self):
+ """主处理函数 (阻塞)"""
+ if not self.initialize():
+ self.node.get_logger().error("[LinkerEG] 初始化失败,无法启动")
+ return
+
+ try:
+ # 保持节点运行
+ rclpy.spin(self.node)
+ except KeyboardInterrupt:
+ self.node.get_logger().info("[LinkerEG] 收到退出信号")
+ finally:
+ self.shutdown()
+
+ def shutdown(self):
+ """关闭模块"""
+ self.running = False
+ # 禁用原始数据推送 (如果启用了)
+ if self.raw_data_enabled:
+ self.linkereg.disable_raw_data()
+ self.linkereg.close()
+ self.node.get_logger().info("[LinkerEG] 模块已关闭")
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README.md
new file mode 100644
index 0000000..5b5c395
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README.md
@@ -0,0 +1,420 @@
+# LinkerFFG Robot Hand Driver Module
+
+LinkerFFG (O6/L7/L10/G20/R20/L25) robot hand ROS1/ROS2 driver module, controls robot hands via serial port with real-time data glove mapping.
+
+---
+
+## Quick Start (Step-by-Step Guide)
+
+### Step 1: Install
+
+```bash
+# ROS2
+cd ~/ros2_ws/src
+git clone https://gitee.com/ericbrunt/linkerhand_telop_python.git
+mv linkerhand_telop_python/ros2/src/linkerhand_retarget ./
+rm -rf linkerhand_telop_python
+cd ..
+rosdep install --from-paths src --ignore-src -r -y
+colcon build --symlink-install
+source install/setup.bash
+
+# ROS1
+cd ~/catkin_ws/src
+git clone https://gitee.com/ericbrunt/linkerhand_telop_python.git
+mv linkerhand_telop_python/ros1/src ./
+rm -rf linkerhand_telop_python
+cd ..
+catkin_make install
+source install/setup.bash
+```
+
+### Step 2: Connect Serial Port
+
+Connect LinkerFFG robot hand to your PC via USB, then verify serial port permissions:
+
+```bash
+# Add current user to dialout group (requires re-login)
+sudo usermod -a -G dialout $USER
+
+# List serial devices
+ls -l /dev/ttyUSB*
+```
+
+### Step 3: Configure Robot Hand Model
+
+Edit `config/base_config.yml` to set robot hand model:
+
+```yaml
+system:
+ motion_type: linkerforce # Data glove type: linkerforce (required)
+ robotname_r: l25 # Right hand model: o6 / l7 / l10 / g20 / r20 / l25
+ robotname_l: l25 # Left hand model
+
+serial:
+ auto_scan: false # Enable auto serial scan
+ baudrates: [2000000, 460800, 1000000, 921600] # 2000000 recommended
+ left:
+ port: /dev/ttyUSB1 # Left hand serial port
+ baudrate: 460800 # Wireless: 460800, Wired: 2000000
+ right:
+ port: /dev/ttyUSB0 # Right hand serial port
+ baudrate: 460800 # Wireless: 460800, Wired: 2000000
+```
+
+### Step 4: Launch
+
+**Method 1: Run node directly**
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+**Method 2: Specify serial port (without modifying config file)**
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args \
+ -p ports:='["/dev/ttyUSB0", "/dev/ttyUSB1"]'
+```
+
+### Step 5: Calibration (if needed)
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=auto_calibrate
+```
+
+During calibration, perform three gestures as prompted:
+1. **Open hand** → hold for 5 seconds
+2. **Make fist** → hold for 5 seconds
+3. **O-pose** → hold for 5 seconds
+
+---
+
+## Robot Hand Models
+
+| Model | DOF | Description |
+|-------|-----|-------------|
+| O6 | 6 | 6-DOF basic model |
+| L7 | 7 | 7-DOF (thumb with roll) |
+| L10 | 10 | 10-DOF industrial model |
+| G20 | 20 | 20-DOF industrial model |
+| R20 | 20 | 20-DOF research model |
+| L25 | 25 | 25-DOF full-featured model |
+
+`robotname_r` and `robotname_l` in config must match the actual connected robot hand models.
+
+---
+
+## Serial Connection Details
+
+### Method 1: Command-line Port List (most common)
+
+Suitable when multiple serial ports exist, system auto-detects left/right hand:
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args \
+ -p ports:='["/dev/ttyUSB0", "/dev/ttyUSB1"]'
+```
+
+### Serial Parameters
+
+| Parameter | Description | Default |
+|-----------|-------------|---------|
+| `ports` | Candidate port list | empty (use config file) |
+| `baudrate` | Specified baudrate (overrides config) | use config file |
+| `auto_scan` | Auto scan when preset fails | `false` |
+
+---
+
+## Calibration Details
+
+### Calibration Config
+
+In `config/base_config.yml`:
+
+```yaml
+calibration:
+ show_fist: true # Whether to show fist calibration step
+ fist_extend_ratio: 0.5 # Fist extend ratio (only effective when show_fist=false)
+```
+
+### Start Calibration
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=auto_calibrate
+```
+
+### Calibration Process
+
+1. **Open hand** → hold for 5 seconds (motor value 255)
+2. **O-pose** → hold for 5 seconds (motor middle value)
+
+If `show_fist: true`, a third step appears:
+
+3. **Make fist** → hold for 5 seconds (motor value 0)
+
+### Difference between show_fist=true and show_fist=false
+
+| Item | `show_fist: true` | `show_fist: false` |
+|------|-------------------|-------------------|
+| Calibration steps | Open → O-pose → Fist (3 steps) | Open → O-pose (2 steps) |
+| Fist data source | User actually performs fist gesture | Calculated from O-pose by ratio |
+| Fist formula | N/A | `fist = opose + (opose - original) × fist_extend_ratio` |
+| Mapping precision | Three-segment linear interpolation, most accurate | Two-segment interpolation, relies on extension |
+| fist_extend_ratio | Not used | Controls extension ratio (default 0.5) |
+
+### fist_extend_ratio Details
+
+Only effective when `show_fist: false`, used to calculate fist value from O-pose:
+
+- `fist_extend_ratio = 0.5` (default): O-pose extends 50% toward fist
+- `fist_extend_ratio = 0.0`: fist value = O-pose value (no extension)
+- `fist_extend_ratio = 1.0`: fist value = O-pose + full (O-pose - Open) extension
+- Recommended range: `0.3 ~ 0.7`, adjust based on actual results
+
+### Stability Detection
+
+- Auto-detects gesture stability (variance < 0.03)
+- Requires **5 seconds continuous stability** to complete
+- Resets on instability, no timeout limit
+- Progress bar shows real-time stability duration
+
+### Calibration Data Storage
+
+- Location: `motion/linkerforce/tmp/jointangle_data.tmp`
+- Format: JSON
+- Auto-loads preset sample data on first use
+- Re-calibration overwrites old data
+
+---
+
+## Topic Parameter Control (Runtime Dynamic Adjustment)
+
+Dynamically adjust parameters via `/hand_teleop_param` topic without restarting the node.
+
+### Adjustable Parameters
+
+| Parameter | Description | Example |
+|-----------|-------------|---------|
+| `mapper_debug` | Mapper debug switch | `true` / `false` / `["thumb_rotate"]` |
+| `mapper_exp_factor` | Extrapolation factor | `2.0` (global) or `{"thumb_rotate": 2.0}` (per finger) |
+| `mapper_scale_factor` | Scale factor | `1.5` (global) or `{"index_root_flexion": 1.5}` (per finger) |
+| `force_glove_pose` | Force glove data source | `open` / `fist` / `opose` / `none` |
+
+### Usage Examples
+
+```bash
+# Enable mapper debug for all fingers
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": true}"}'
+
+# Debug specific thumb fingers
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": [\"thumb_rotate\", \"thumb_abduction\"]}"}'
+
+# Disable debug
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": false}"}'
+
+# Adjust global extrapolation factor (higher = faster to target)
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_exp_factor\": 2.0}"}'
+
+# Adjust per-finger extrapolation factor
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_exp_factor\": {\"thumb_rotate\": 2.0, \"index_root_flexion\": 1.5}}"}'
+
+# Adjust global scale factor
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_scale_factor\": 1.5}"}'
+
+# Use calibration data instead of glove data (for testing)
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"opose\"}"}'
+
+# Restore real-time glove data
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"none\"}"}'
+```
+
+### Available Finger Names
+
+| Finger | Names |
+|--------|-------|
+| Thumb | `thumb_rotate`, `thumb_abduction`, `thumb_root_flexion`, `thumb_end_flexion` |
+| Index | `index_roll`, `index_root_flexion`, `index_end_flexion` |
+| Middle | `middle_roll`, `middle_root_flexion`, `middle_end_flexion` |
+| Ring | `ring_roll`, `ring_root_flexion`, `ring_end_flexion` |
+| Pinky | `pinky_roll`, `pinky_root_flexion`, `pinky_end_flexion` |
+
+---
+
+## Mapping Parameters Details
+
+### Extrapolation Factor (exp_factor)
+
+Controls mapping extrapolation speed to target pose:
+- `= 1.0`: Linear extrapolation
+- `> 1.0`: Accelerated (faster to target)
+- `< 1.0`: Decelerated (smoother)
+
+### Scale Factor (scale_factor)
+
+Controls input-to-output mapping ratio:
+- `= 1.0`: 1:1 mapping
+- `> 1.0`: Amplified output range
+- `< 1.0`: Reduced output range
+
+### Force Glove Data Source (force_glove_pose)
+
+Use calibration data instead of real-time glove data for testing:
+
+| Value | Description |
+|-------|-------------|
+| `open` | Use open hand calibration data |
+| `fist` | Use fist calibration data |
+| `opose` | Use O-pose calibration data |
+| `none` | Use real-time glove data (default) |
+
+---
+
+## Configuration Details
+
+See the "Configuration" section in the main README for full config reference. LinkerFFG-specific configs:
+
+### system System Config
+
+| Config | Description | Options |
+|--------|-------------|---------|
+| `motion_type` | Data glove type | `linkerforce` (required) |
+| `robotname_r` | Right hand robot model | `o6`, `l7`, `l10`, `g20`, `r20`, `l25` |
+| `robotname_l` | Left hand robot model | same as above |
+| `retargeting_type` | Retargeting type | `projection` |
+
+### calibration Calibration Config
+
+| Config | Description | Default |
+|--------|-------------|---------|
+| `show_fist` | Show fist calibration step | `true` |
+| `fist_extend_ratio` | Fist extend ratio | `0.5` |
+
+### debug Debug Config
+
+| Config | Description | Default |
+|--------|-------------|---------|
+| `mapper_debug` | Mapper debug switch | `false` |
+| `joint_motor_debug_r` | Right hand joint motor debug | `false` |
+| `joint_motor_debug_l` | Left hand joint motor debug | `false` |
+
+---
+
+## Troubleshooting
+
+### Cannot open serial port
+
+```bash
+# Check serial permissions
+ls -l /dev/ttyUSB*
+# Grant permissions
+sudo chmod 666 /dev/ttyUSB0
+```
+
+### Robot hand not responding (Key Topic Monitoring)
+
+Follow these steps in order:
+
+**Step 1: Check if topics are published**
+
+```bash
+# List all related topics
+ros2 topic list | grep cb_
+
+# Check if data is being output (is the node publishing normally)
+ros2 topic echo /cb_right_hand_control_cmd --once
+ros2 topic echo /cb_left_hand_control_cmd --once
+```
+
+**Step 2: Check glove data topics**
+
+```bash
+# LinkerFFG driver does not publish glove data topics - this step can be skipped
+# To verify data source, check if /cb_right_hand_control_cmd has data output
+ros2 topic echo /cb_right_hand_control_cmd --once
+```
+
+**Step 3: Check LinkerFFG joint control topics**
+
+```bash
+# Check if joint control commands have output
+ros2 topic echo /cb_right_hand_control_cmd --once
+ros2 topic echo /cb_left_hand_control_cmd --once
+
+# Check topic frequency (should be around 50Hz)
+ros2 topic hz /cb_right_hand_control_cmd
+```
+
+**Step 4: Enable debug output**
+
+```bash
+# Enable all debug
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": true}"}'
+
+# Observe terminal output, check if glove data is changing
+# If data doesn't change, glove is not connected or topic is not published
+```
+
+**Step 5: Check calibration data**
+
+```bash
+# Check current calibration data in use
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"opose\"}"}'
+# Observe if robot hand responds
+
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"fist\"}"}'
+# Observe if robot hand grips
+
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"none\"}"}'
+# Restore normal data source
+```
+
+**Step 6: Check serial connection**
+
+1. Verify baudrate matches robot hand settings (wireless: 460800, wired: 2000000)
+2. Try `auto_scan: true` for auto detection
+3. Check robot hand power supply
+
+**Step 7: Check logs**
+
+```bash
+# View node logs
+ros2 run linkerhand_retarget handretarget
+# Observe debug info in terminal output
+```
+
+**Quick Problem Identification**
+
+| Symptom | Possible Cause | Solution |
+|---------|---------------|----------|
+| No topic data | Glove not connected or topic name wrong | Check glove connection, verify topic name |
+| Data always 0 or 255 | Calibration data abnormal | Re-calibrate or delete `tmp/jointangle_data.tmp` |
+| Data fluctuates wildly | Serial signal interference | Check serial cable, use shielded cable |
+| No terminal output | Node didn't start successfully | Check for errors, verify dependencies installed |
+| Topic has data but robot hand doesn't move | Robot hand SDK not receiving commands | Check SDK connection, verify topic is subscribed |
+
+---
+
+## File Structure
+
+```
+motion/linkerforce/
+├── config/ # Hand model configurations
+│ ├── o6_config.py # O6 config
+│ ├── l6_config.py # L6 config
+│ ├── l7_config.py # L7 config
+│ ├── l10_config.py # L10 config
+│ ├── l20_config.py # L20 config
+│ ├── g20_config.py # G20 config
+│ └── o7_config.py # O7 config
+├── hand/ # Robot hand drivers
+│ ├── linkerforce_o6.py
+│ ├── linkerforce_l6.py
+│ ├── linkerforce_l7.py
+│ ├── linkerforce_l10.py
+│ ├── linkerforce_l20.py
+│ └── linkerforce_g20.py
+├── tmp/ # Temp files (calibration data, etc.)
+├── retarget.py # ROS integration
+└── README.md # English documentation
+```
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README_zh.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README_zh.md
new file mode 100644
index 0000000..29bc911
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/README_zh.md
@@ -0,0 +1,407 @@
+# LinkerFFG 机械手驱动模块
+
+LinkerFFG (O6/L7/L10/G20/R20/L25) 机械手的 ROS1/ROS2 驱动模块,通过串口控制机械手,支持数据手套实时映射。
+
+---
+
+## 快速入门
+### 步骤 1:安装
+
+```bash
+# ROS2
+cd ~/ros2_ws/src
+git clone https://gitee.com/ericbrunt/linkerhand_telop_python.git
+mv linkerhand_telop_python/ros2/src/linkerhand_retarget ./
+rm -rf linkerhand_telop_python
+cd ..
+rosdep install --from-paths src --ignore-src -r -y
+colcon build --symlink-install
+source install/setup.bash
+
+# ROS1
+cd ~/catkin_ws/src
+git clone https://gitee.com/ericbrunt/linkerhand_telop_python.git
+mv linkerhand_telop_python/ros1/src ./
+rm -rf linkerhand_telop_python
+cd ..
+catkin_make install
+source install/setup.bash
+```
+
+### 步骤 2:连接串口
+
+将 LinkerFFG 机械手通过 USB 连接到电脑,确认串口权限:
+
+```bash
+# 添加当前用户到 dialout 组(需要重新登录生效)
+sudo usermod -a -G dialout $USER
+
+# 查看串口设备
+ls -l /dev/ttyUSB*
+```
+
+### 步骤 3:配置机械手型号
+
+编辑 `config/base_config.yml`,设置机械手型号:
+
+```yaml
+system:
+ motion_type: linkerforce # 数据手套类型:linkerforce(必须)
+ robotname_r: l25 # 右手机械手型号:o6 / l7 / l10 / g20 / r20 / l25
+ robotname_l: l25 # 左手机械手型号
+
+serial:
+ auto_scan: false # 是否自动扫描串口
+ baudrates: [2000000, 460800, 1000000, 921600] # 波特率列表,2000000 优先
+ left:
+ port: /dev/ttyUSB1 # 左手套接的串口
+ baudrate: 460800 # 无线460800 有线2000000
+ right:
+ port: /dev/ttyUSB0 # 右手套接的串口
+ baudrate: 460800 # 无线460800 有线2000000
+```
+
+### 步骤 4:启动
+
+**方式一:直接运行节点**
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+**方式二:指定串口启动(不修改配置文件)**
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args \
+ -p ports:='["/dev/ttyUSB0", "/dev/ttyUSB1"]'
+```
+
+### 步骤 5:标定(如需要)
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=auto_calibrate
+```
+
+标定时按照提示做三个动作:
+1. **五指张开** → 保持 5 秒
+2. **握紧拳头** → 保持 5 秒
+3. **O 型手势** → 保持 5 秒
+
+---
+
+## 机械手型号说明
+
+| 型号 | 关节数 | 说明 |
+|------|--------|------|
+| O6 | 6 | 6 自由度基础款 |
+| L7 | 7 | 7 自由度(拇指增加横滚) |
+| L10 | 10 | 10 自由度工业款 |
+| G20 | 20 | 20 自由度工业款 |
+| L25 | 25 | 25 自由度全功能款 |
+
+配置中的 `robotname_r` 和 `robotname_l` 必须与实际连接的机械手型号匹配。
+
+---
+
+## 串口参数说明
+
+| 参数 | 说明 | 默认值 |
+|------|------|--------|
+| `ports` | 候选串口列表 | 空(使用配置文件) |
+| `baudrate` | 指定波特率(优先级高于配置) | 使用配置文件 |
+| `auto_scan` | 预设失败后是否自动扫描 | `false` |
+
+---
+
+## 自动标定详解
+
+### 标定配置
+
+在 `config/base_config.yml` 中:
+
+```yaml
+calibration:
+ show_fist: true # 是否显示握拳标定步骤
+ fist_extend_ratio: 0.5 # 握拳延伸比例(仅 show_fist=false 时生效)
+```
+
+### 启动标定
+
+```bash
+ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=auto_calibrate
+```
+
+### 标定流程
+
+1. **五指张开** → 保持 5 秒(电机值 255)
+2. **O 型手势** → 保持 5 秒(电机中间值)
+
+如果 `show_fist: true`,还会出现第三步:
+
+3. **握紧拳头** → 保持 5 秒(电机值 0)
+
+### show_fist=true 与 show_fist=false 的区别
+
+| 对比项 | `show_fist: true` | `show_fist: false` |
+|--------|-------------------|-------------------|
+| 标定步骤 | 张开 → O 型 → 握拳(共 3 步) | 张开 → O 型(共 2 步) |
+| 握拳数据来源 | 用户实际做握拳动作采集 | 从 O 型按比例延伸计算 |
+| 握拳延伸公式 | 无 | `fist = opose + (opose - original) × fist_extend_ratio` |
+| 映射精度 | 三段线性插值,最精确 | 两段插值,依赖延伸估算 |
+| fist_extend_ratio | 不生效 | 控制延伸比例(默认 0.5) |
+
+### fist_extend_ratio 详解
+
+仅在 `show_fist: false` 时生效,用于从 O 型自动计算握拳值:
+
+- `fist_extend_ratio = 0.5`(默认):O 型向握拳方向延伸 50%
+- `fist_extend_ratio = 0.0`:握拳值 = O 型值(无延伸)
+- `fist_extend_ratio = 1.0`:握拳值 = O 型 + (O 型 - 张开) 的全量延伸
+- 推荐值范围:`0.3 ~ 0.7`,需要根据实际效果调整
+
+### 稳定性检测
+
+- 标定时自动检测手势稳定性(方差 < 0.03)
+- 需**连续稳定 5 秒**才完成采集
+- 不稳定时清空重来,无超时限制
+- 终端显示进度条,实时反馈稳定时长
+
+### 标定数据存储
+
+- 存储位置:`motion/linkerforce/tmp/jointangle_data.tmp`
+- 格式:JSON
+- 首次使用自动加载内置样本数据
+- 重新标定会覆盖旧数据
+
+---
+
+## 话题参数控制(运行时动态调整)
+
+通过 `/hand_teleop_param` 话题动态调整运行参数,无需重启节点。
+
+### 可调参数
+
+| 参数 | 说明 | 示例值 |
+|------|------|--------|
+| `mapper_debug` | 映射器调试开关 | `true` / `false` / `["thumb_rotate"]` |
+| `mapper_exp_factor` | 延伸指数因子 | `2.0`(全局)或 `{"thumb_rotate": 2.0}`(单指) |
+| `mapper_scale_factor` | 缩放因子 | `1.5`(全局)或 `{"index_root_flexion": 1.5}`(单指) |
+| `force_glove_pose` | 强制手套数据源 | `open` / `fist` / `opose` / `none` |
+
+### 使用示例
+
+```bash
+# 开启全部手指的映射器调试
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": true}"}'
+
+# 只调试拇指相关手指
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": [\"thumb_rotate\", \"thumb_abduction\"]}"}'
+
+# 关闭调试
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": false}"}'
+
+# 调整全局延伸指数(值越大到达目标越快)
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_exp_factor\": 2.0}"}'
+
+# 调整单指延伸指数
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_exp_factor\": {\"thumb_rotate\": 2.0, \"index_root_flexion\": 1.5}}"}'
+
+# 调整全局缩放因子
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_scale_factor\": 1.5}"}'
+
+# 用标定数据替代手套数据(用于测试)
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"opose\"}"}'
+
+# 恢复实时手套数据
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"none\"}"}'
+```
+
+### 可用手指名称
+
+| 手指 | 参数名称 |
+|------|----------|
+| 拇指 | `thumb_rotate`, `thumb_abduction`, `thumb_root_flexion`, `thumb_end_flexion` |
+| 食指 | `index_roll`, `index_root_flexion`, `index_end_flexion` |
+| 中指 | `middle_roll`, `middle_root_flexion`, `middle_end_flexion` |
+| 无名指 | `ring_roll`, `ring_root_flexion`, `ring_end_flexion` |
+| 小指 | `pinky_roll`, `pinky_root_flexion`, `pinky_end_flexion` |
+
+---
+
+## 映射参数详解
+
+### 延伸指数因子 (exp_factor)
+
+控制映射延伸到目标姿态的速度:
+- `= 1.0`:线性延伸
+- `> 1.0`:加速延伸(更快到达目标)
+- `< 1.0`:减速延伸(更平滑)
+
+### 缩放因子 (scale_factor)
+
+控制输入到输出的映射比例:
+- `= 1.0`:1:1 映射
+- `> 1.0`:放大输出范围
+- `< 1.0`:缩小输出范围
+
+### 强制手套数据源 (force_glove_pose)
+
+用标定数据替代实时手套数据,用于调试和测试:
+
+| 值 | 说明 |
+|-----|------|
+| `open` | 使用五指张开标定数据 |
+| `fist` | 使用握拳标定数据 |
+| `opose` | 使用 O 型手势标定数据 |
+| `none` | 使用实时手套数据(默认) |
+
+---
+
+## 配置文件详解
+
+完整配置项见主 README 的「配置说明」章节。LinkerFFG 驱动专用配置:
+
+### system 系统配置
+
+| 配置项 | 说明 | 可选值 |
+|--------|------|--------|
+| `motion_type` | 数据手套类型 | `linkerforce`(必须) |
+| `robotname_r` | 右手机械手型号 | `o6`, `l7`, `l10`, `g20`, `r20`, `l25` |
+| `robotname_l` | 左手机械手型号 | 同上 |
+| `retargeting_type` | 重定向类型 | `projection` |
+
+### calibration 标定配置
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| `show_fist` | 是否显示握拳标定步骤 | `true` |
+| `fist_extend_ratio` | 握拳延伸比例 | `0.5` |
+
+### debug 调试配置
+
+| 配置项 | 说明 | 默认值 |
+|--------|------|--------|
+| `mapper_debug` | 映射器调试开关 | `false` |
+| `joint_motor_debug_r` | 右手关节电机调试 | `false` |
+| `joint_motor_debug_l` | 左手关节电机调试 | `false` |
+
+---
+
+## 故障排除
+
+### 串口无法打开
+
+```bash
+# 检查串口权限
+ls -l /dev/ttyUSB*
+# 添加权限
+sudo chmod 666 /dev/ttyUSB0
+```
+
+### 机械手无反应(重点监测话题)
+
+按以下顺序逐项检查:
+
+**步骤 1:检查 LinkerFFG 输出话题**
+
+```bash
+# 查看 LinkerFFG 驱动发布的话题
+ros2 topic list | grep cb_
+
+# 查看是否有数据输出(机械手节点是否正常发布数据)
+ros2 topic echo /cb_right_hand_control_cmd --once
+ros2 topic echo /cb_left_hand_control_cmd --once
+```
+
+**步骤 2:检查手套数据话题**
+
+```bash
+# 查看手套数据是否到达(LinkerFFG 驱动不发布此话题,此步骤可跳过)
+# 如需验证数据源,请检查 /cb_right_hand_control_cmd 是否有数据输出
+ros2 topic echo /cb_right_hand_control_cmd --once
+```
+
+**步骤 3:检查 LinkerFFG 关节控制话题**
+
+```bash
+# 查看关节控制指令是否有输出
+ros2 topic echo /cb_right_hand_control_cmd --once
+ros2 topic echo /cb_left_hand_control_cmd --once
+
+# 查看话题频率是否正常(应该 50Hz 左右)
+ros2 topic hz /cb_right_hand_control_cmd
+```
+
+**步骤 4:启用调试打印**
+
+```bash
+# 开启全部调试
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"mapper_debug\": true}"}'
+
+# 查看终端输出,是否有手套数据变化
+# 如果数据不变,说明手套未连接或话题未发布
+```
+
+**步骤 5:检查标定数据**
+
+```bash
+# 查看当前使用的标定数据
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"opose\"}"}'
+# 观察机械手是否有反应
+
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"fist\"}"}'
+# 观察机械手是否握紧
+
+ros2 topic pub --once /hand_teleop_param std_msgs/msg/String '{"data": "{\"force_glove_pose\": \"none\"}"}'
+# 恢复正常数据源
+```
+
+**步骤 6:检查串口连接**
+
+1. 确认波特率配置与机械手一致(无线 460800,有线 2000000)
+2. 尝试使用 `auto_scan: true` 自动检测
+3. 检查机械手电源是否正常
+
+**步骤 7:检查日志**
+
+```bash
+# 查看节点日志
+ros2 run linkerhand_retarget handretarget
+# 观察终端输出的 debug 信息
+```
+
+**常见问题快速定位**
+
+| 现象 | 可能原因 | 解决方法 |
+|------|----------|----------|
+| 话题无数据 | 手套未连接或话题名错误 | 检查手套连接,确认话题名 |
+| 数据一直是 0 或 255 | 标定数据异常 | 重新标定或删除 `tmp/jointangle_data.tmp` |
+| 数据跳变剧烈 | 串口信号干扰 | 检查串口线,使用屏蔽线 |
+| 终端无输出 | 节点未启动成功 | 检查是否报错,检查依赖是否安装 |
+| 话题有数据但机械手不动 | 机械手SDK未收到指令 | 检查机械手SDK连接,确认话题被正确订阅 |
+
+---
+
+## 文件结构
+
+```
+motion/linkerforce/
+├── config/ # 手型配置文件
+│ ├── o6_config.py # O6 手型配置
+│ ├── l6_config.py # L6 手型配置
+│ ├── l7_config.py # L7 手型配置
+│ ├── l10_config.py # L10 手型配置
+│ ├── l20_config.py # L20 手型配置
+│ ├── g20_config.py # G20 手型配置
+│ └── o7_config.py # O7 手型配置
+├── hand/ # 机械手驱动
+│ ├── linkerforce_o6.py
+│ ├── linkerforce_l6.py
+│ ├── linkerforce_l7.py
+│ ├── linkerforce_l10.py
+│ ├── linkerforce_l20.py
+│ └── linkerforce_g20.py
+├── tmp/ # 临时文件(标定数据等)
+├── retarget.py # ROS 集成层
+└── README.md # 本文档
+```
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/g20_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/g20_config.py
new file mode 100644
index 0000000..02a52be
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/g20_config.py
@@ -0,0 +1,473 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ # 含义解释:
+ # robot_idx:URDF关节序列
+
+ # 拇指旋转3个关节的加权系数,人手的0/1/2序列,对应URDF的第1关节(下标0)
+ 'thumb_rotate': {
+ 'name': '拇指旋转',
+ 'joints': [1, 2],
+ 'weights': {
+ 'v1': [1, 0],
+ 'v2': [1, 0]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 1.0
+ }
+ },
+ # 拇指侧摆3个关节的加权系数,人手的0/1/2序列,对应URDF的第2关节(下标1)
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0.7, 0.3, 0],
+ 'v2': [0.7, 0.3, 0]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 1.0
+ }
+ },
+ # 拇指根部弯曲3个关节的加权系数,人手的2/3/4序列,对应URDF的第3关节(下标2)
+ 'thumb_root_flexion': {
+ 'name': '拇指根部弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 2,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ # {
+ # 'trigger_finger': 'thumb_abduction',
+ # 'threshold': 0.3,
+ # 'low_weight_config': {
+ # 'joints': [2, 3, 4],
+ # 'weights': [1, 0, 0],
+ # 'reverse_motion': False
+ # },
+ # 'high_weight_config': {
+ # 'joints': [2, 3, 4],
+ # 'weights': [0.3, 0.0, 0.7],
+ # 'reverse_motion': False
+ # }
+ # },
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 30
+ }
+ },
+ # 拇指指尖弯曲3个关节的加权系数,人手的2/3/4序列,对应URDF的第4关节(下标3)
+ 'thumb_end_flexion': {
+ 'name': '拇指指尖弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 3,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1,
+ 'extended_exp_factor': 50
+ }
+ },
+ # 食指ROLL旋转(侧摆)关节的加权系数,人手的5序列,对应URDF的第4关节(下标3)
+ 'index_roll': {
+ 'name': '食指',
+ 'joints': [5],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ # 食指弯曲(根部弯曲)的加权系数,人身的6/7/8序列,对应URDF的第4关节(下标3)
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 6,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ # 食指弯曲(末端弯曲)的加权系数,人手的6/7/8序列,对应URDF的第4关节(下标3)
+ 'index_end_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 7,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1,
+ 'extended_exp_factor': 30
+ }
+ },
+ # 中指ROLL旋转(侧摆)关节的加权系数,人手的5序列,对应URDF的第4关节(下标3)
+ 'middle_roll': {
+ 'name': '中指',
+ 'joints': [9],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 9,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 1.0
+ }
+ },
+ # 中指弯曲(根部弯曲)的加权系数,人手的10/11/12序列,对应URDF的第6关节(下标5)
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 10,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ # 中指弯曲(末端弯曲)的加权系数,人手的10/11/12序列,对应URDF的第6关节(下标5)
+ 'middle_end_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 11,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1,
+ 'extended_exp_factor': 30
+ }
+ },
+ # 无名指ROLL旋转(侧摆)关节的加权系数,人手的5序列,对应URDF的第4关节(下标3)
+ 'ring_roll': {
+ 'name': '无名指',
+ 'joints': [13],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 13,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ # 无名指弯曲(根部弯曲)的加权系数,人手的14/15/16序列,对应URDF的第8关节(下标7)
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 14,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ # 无名指弯曲(末端弯曲)的加权系数,人手的14/15/16序列,对应URDF的第8关节(下标7)
+ 'ring_end_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 15,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1,
+ 'extended_exp_factor': 30
+ }
+ },
+ # 小指ROLL旋转(侧摆)关节的加权系数,人手的5序列,对应URDF的第4关节(下标3)
+ 'pinky_roll': {
+ 'name': '小指',
+ 'joints': [17],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 17,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ # 小指弯曲(根部弯曲)的加权系数,人手的18/19/20序列,对应URDF的第10关节(下标9)
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 18,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ # 小指弯曲(末端弯曲)的加权系数,人手的18/19/20序列,对应URDF的第10关节(下标9)
+ 'pinky_end_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 19,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1,
+ 'extended_exp_factor': 30
+ }
+ }
+}
+
+# 映射顺序
+MAPPING_ORDER = [
+ 'thumb_rotate', 'thumb_abduction', 'thumb_root_flexion', 'thumb_end_flexion',
+ 'index_roll', 'index_root_flexion', 'index_end_flexion',
+ 'middle_roll', 'middle_root_flexion', 'middle_end_flexion',
+ 'ring_roll', 'ring_root_flexion','ring_end_flexion',
+ 'pinky_roll', 'pinky_root_flexion', 'pinky_end_flexion'
+]
+
+# 三态默认配置
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ 'opose',
+ # 'fist' # 取消注释启用三段映射
+ ],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ # 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.2, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ -0.2, 0.0, 0.0, 0.0,
+ -0.2, 0.0, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ -0.2, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0,
+ 0.2, 0.0, 0.0, 0.0,
+ 0.2, 0.0, 0.0, 0.0
+]
+
+ROBOT_OPOSE_LEFT = [
+ 0.6, 1.2, 0.5, 0.6, 0.0,
+ 0.0, 0.7, 1.08, 0.00,
+ 0.0, 0.7, 1.08, 0.00 ,
+ 0.0, 0.7, 1.08, 0.00,
+ 0.0, 0.7, 1.08, 0.00
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 0.6, 1.2, 0.5, 0.6, 0.0,
+ 0.0, 0.7, 1.08, 0.00,
+ 0.0, 0.7, 1.08, 0.00,
+ 0.0, 0.7, 1.08, 0.00,
+ 0.0, 0.7, 1.08, 0.00
+]
+
+ROBOT_FIST_RIGHT = [
+ 1.39, 1.57, 0.83, 1.25, 1.29,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55
+]
+
+ROBOT_FIST_LEFT = [
+ 1.39, 1.57, 0.83, 1.25, 1.29,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55,
+ 0, 1.22, 1.75, 1.55
+]
+
+# 电机输出约束配置 (20电机)
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 80, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 80, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l10_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l10_config.py
new file mode 100644
index 0000000..db74520
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l10_config.py
@@ -0,0 +1,297 @@
+FINGER_CONFIGS = {
+ 'thumb_rotate': {
+ 'name': '拇指旋转',
+ 'joints': [1, 2],
+ 'weights': {
+ 'v1': [1, 0],
+ 'v2': [1, 0]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 1
+ }
+ },
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0, 0, 1],
+ 'v2': [0, 1, 0]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': False,
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.3,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'thumb_root_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0.2, 0, 0.8],
+ 'v2': [0.6, 0, 0.4]
+ },
+ 'robot_idx': 2,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 2
+ }
+ },
+ 'index_roll': {
+ 'name': '食指',
+ 'joints': [5],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 6,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 9,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'ring_roll': {
+ 'name': '无名指',
+ 'joints': [13],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 12,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 13,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'pinky_roll': {
+ 'name': '小指',
+ 'joints': [17],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 16,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 17,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 30
+ }
+ }
+}
+
+MAPPING_ORDER = [
+ 'thumb_rotate', 'thumb_abduction', 'thumb_root_flexion',
+ 'index_roll','index_root_flexion',
+ 'middle_root_flexion',
+ 'ring_roll', 'ring_root_flexion',
+ 'pinky_roll', 'pinky_root_flexion',
+
+]
+
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ 'opose',
+ # 'fist'
+ ],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ # 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_OPOSE_LEFT = [
+ 0.13, 1.13, 0.28, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0,
+ 0.73, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 0.13, 1.13, 0.28, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0,
+ 0.73, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0,
+ 0.0, 0.73, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.22, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0,
+ -0.22, 0.0, 0.0, 0.0,
+ -0.22, 0.0, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0,
+ -0.22, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0,
+ 0.22, 0.0, 0.0, 0.0,
+ 0.22, 0.0, 0.0, 0.0
+]
+
+ROBOT_FIST_LEFT = [
+ 1.1339, 1.9189, 0.5146, 0.7152, 0.7763,
+ 0, 1.3607, 1.8317, 1.8317,
+ 1.3607, 1.8317, 0.628,
+ 0, 1.3607, 1.8317, 0.628,
+ 0, 1.3607, 1.8317, 0.628
+]
+
+ROBOT_FIST_RIGHT = [
+ 1.1339, 1.9189, 0.5146, 0.7152, 0.7763,
+ 0, 1.3607, 1.8317, 1.8317,
+ 1.3607, 1.8317, 0.628,
+ 0, 1.3607, 1.8317, 0.628,
+ 0, 1.3607, 1.8317, 0.628
+]
+
+# 电机输出约束配置
+# 格式: {'min': 最小值, 'max': 最大值, 'enabled': 是否启用}
+# None 表示不约束该电机
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指弯曲
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 6: 食指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 7: 无名指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 8: 小指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 9: 拇指旋转
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指弯曲
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 6: 食指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 7: 无名指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 8: 小指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 9: 拇指旋转
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l20_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l20_config.py
new file mode 100644
index 0000000..414e2a0
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l20_config.py
@@ -0,0 +1,434 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_rotate': {
+ 'name': '拇指旋转',
+ 'joints': [1, 2],
+ 'weights': {
+ 'v1': [1, 0],
+ 'v2': [1, 0]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 1.5
+ }
+ },
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0.7, 0.3, 0],
+ 'v2': [0.7, 0.3, 0]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'thumb_root_flexion': {
+ 'name': '拇指根部弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 2,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': {
+ 'trigger_finger': 'thumb_abduction',
+ 'threshold': 0.3,
+ 'low_weight_config': {
+ 'joints': [2, 3, 4],
+ 'weights': {'v1': [1, 0, 0], 'v2': [1, 0, 0]},
+ 'reverse_motion': {'v1': False, 'v2': False}
+ },
+ 'high_weight_config': {
+ 'joints': [2, 3, 4],
+ 'weights': {'v1': [0.3, 0.0, 0.7], 'v2': [0.3, 0.0, 0.7]},
+ 'reverse_motion': {'v1': False, 'v2': False}
+ }
+ },
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'thumb_end_flexion': {
+ 'name': '拇指指尖弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 3,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 0.9,
+ 'extended_exp_factor': 1.5
+ }
+ },
+ 'index_roll': {
+ 'name': '食指',
+ 'joints': [5],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 6,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'index_end_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 7,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 0.9,
+ 'extended_exp_factor': 1.5
+ }
+ },
+ 'middle_roll': {
+ 'name': '中指',
+ 'joints': [9],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 9,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 10,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'middle_end_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 11,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 0.9,
+ 'extended_exp_factor': 1.5
+ }
+ },
+ 'ring_roll': {
+ 'name': '无名指',
+ 'joints': [13],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 13,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 14,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'ring_end_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 15,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 0.9,
+ 'extended_exp_factor': 1.5
+ }
+ },
+ 'pinky_roll': {
+ 'name': '小指',
+ 'joints': [17],
+ 'weights': {
+ 'v1': [1],
+ 'v2': [1]
+ },
+ 'robot_idx': 17,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': False,
+ 'scale_factor': 1.0,
+ }
+ },
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1, 0.0, 0],
+ 'v2': [1, 0.0, 0]
+ },
+ 'robot_idx': 18,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'pinky_end_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [0, 0.0, 1],
+ 'v2': [0, 0.0, 1]
+ },
+ 'robot_idx': 19,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 0.9,
+ 'extended_exp_factor': 1.5
+ }
+ }
+}
+
+MAPPING_ORDER = [
+ 'thumb_rotate', 'thumb_abduction', 'thumb_root_flexion', 'thumb_end_flexion',
+ 'index_roll', 'index_root_flexion', 'index_end_flexion',
+ 'middle_roll', 'middle_root_flexion', 'middle_end_flexion',
+ 'ring_roll', 'ring_root_flexion','ring_end_flexion',
+ 'pinky_roll', 'pinky_root_flexion', 'pinky_end_flexion'
+]
+
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ 'opose',
+ 'fist'
+ ],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, -0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -0.15, 0.0, 0.0, 0.0, -0.15, 0.0, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.15, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.15, 0.0, 0.0, 0.0, 0.15, 0.0, 0.0, 0.0
+]
+
+ROBOT_FIST_RIGHT = [
+ 0.5, 1.57, 0.6, 1.2, 1.2,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43
+]
+
+ROBOT_FIST_LEFT = [
+ 0.5, 1.57, 0.6, 1.2, 1.2,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43,
+ 0.18, 1.33, 1.77, 1.43
+]
+
+ROBOT_OPOSE_LEFT = [
+ 0.0, 1.2, 0.3, 0.8, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 0.0, 1.2, 0.3, 0.8, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0, 0.0, 0.65, 1.0, 0.0
+]
+
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': True},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ {'min': 0, 'max': 255, 'enabled': False},
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l6_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l6_config.py
new file mode 100644
index 0000000..9409707
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/l6_config.py
@@ -0,0 +1,209 @@
+FINGER_CONFIGS = {
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0, 0, 1],
+ 'v2': [0, 1, 0]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': False,
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.3,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'thumb_root_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0.2, 0, 0.8],
+ 'v2': [0.6, 0, 0.4]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 2
+ }
+ },
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 3,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 7,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 9,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 5
+ }
+ }
+}
+
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_root_flexion',
+ 'index_root_flexion', 'middle_root_flexion', 'ring_root_flexion', 'pinky_root_flexion'
+]
+
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ # 'opose',
+ 'fist'
+ ],
+ 'state_names': {
+ 'original': '张手',
+ # 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_OPOSE_LEFT = [
+ 1.4, 0.5, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 1.4, 0.5, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0,
+ 0.48, 0.0
+]
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0,
+ 0.0, 0.0
+]
+
+# 握拳姿态 (使用 URDF upper limit)
+ROBOT_FIST_LEFT = [
+ 1.53, 0.73, 0.66,
+ 1.22, 1.08,
+ 1.22, 1.08,
+ 1.22, 1.08,
+ 1.22, 1.08
+]
+
+ROBOT_FIST_RIGHT = [
+ 1.53, 0.73, 0.66,
+ 1.22, 1.08,
+ 1.22, 1.08,
+ 1.22, 1.08,
+ 1.22, 1.08
+]
+
+# 电机输出约束配置
+# 格式: {'min': 最小值, 'max': 最大值, 'enabled': 是否启用}
+# None 表示不约束该电机
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指弯曲
+ {'min': 15, 'max': 255, 'enabled': True}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指弯曲
+ {'min': 18, 'max': 255, 'enabled': True}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o6_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o6_config.py
new file mode 100644
index 0000000..0ee2f65
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o6_config.py
@@ -0,0 +1,187 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0, 0, 1],
+ 'v2': [0, 1, 0]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': False,
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'thumb_root_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0, 0, 1],
+ 'v2': [0.2, 0, 0.8]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1.0, 0.0, 0.0],
+ 'v2': [1.0, 0.0, 0.0]
+ },
+ 'robot_idx': 3,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1.0, 0.0, 0.0],
+ 'v2': [1.0, 0.0, 0.0]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1.0, 0.0, 0.0],
+ 'v2': [1.0, 0.0, 0.0]
+ },
+ 'robot_idx': 7,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1.0, 0.0, 0.0],
+ 'v2': [1.0, 0.0, 0.0]
+ },
+ 'robot_idx': 9,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 10
+ }
+ }
+}
+
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_root_flexion',
+ 'index_root_flexion', 'middle_root_flexion', 'ring_root_flexion', 'pinky_root_flexion'
+]
+
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ # 'opose',
+ 'fist'
+ ],
+ 'state_names': {
+ 'original': '张手',
+ # 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_OPOSE_LEFT = [
+ 1.1, 0.33, 0.0, 0.84, 0.0, 0.84, 0.0, 0.84, 0.0, 0.84, 0.0
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 1.1, 0.33, 0.0, 0.84, 0.0, 0.84, 0.0, 0.84, 0.0, 0.84, 0.0
+]
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+]
+
+ROBOT_FIST_LEFT = [
+ 1.54, 0.52, 0.96, 1.57, 1.4, 1.57, 1.4, 1.57, 1.4, 1.57, 1.4
+]
+
+ROBOT_FIST_RIGHT = [
+ 1.54, 0.52, 0.96, 1.57, 1.4, 1.57, 1.4, 1.57, 1.4, 1.57, 1.4
+]
+
+PLOTGUI_ROBOT_ID = [
+ 0, 1, 2
+]
+
+# 电机输出约束配置 (6电机)
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 1: 拇指根部
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 1: 拇指根部
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 小指
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o7_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o7_config.py
new file mode 100644
index 0000000..d177aa4
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/config/o7_config.py
@@ -0,0 +1,217 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_rotate': {
+ 'name': '拇指旋转',
+ 'joints': [1, 2],
+ 'weights': {
+ 'v1': [0.3, 0.7],
+ 'v2': [0.3, 0.7]
+ },
+ 'robot_idx': 0,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 5
+ }
+ },
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': {
+ 'v1': [0, 0, 1],
+ 'v2': [0, 1, 0]
+ },
+ 'robot_idx': 1,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.3,
+ 'extended_exp_factor': 10
+ }
+ },
+ 'thumb_root_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': {
+ 'v1': [0.2, 0, 0.8],
+ 'v2': [0.6, 0, 0.4]
+ },
+ 'robot_idx': 2,
+ 'type': 'thumb',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 2
+ }
+ },
+ 'index_root_flexion': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 5,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'middle_root_flexion': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 8,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'ring_root_flexion': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 11,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.0,
+ 'extended_exp_factor': 25
+ }
+ },
+ 'pinky_root_flexion': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': {
+ 'v1': [1, 0, 0],
+ 'v2': [1, 0, 0]
+ },
+ 'robot_idx': 14,
+ 'type': 'finger',
+ 'reverse_motion': {
+ 'v1': False,
+ 'v2': False
+ },
+ 'dynamic_weight': None,
+ 'extended_mapping': {
+ 'enabled': True,
+ 'scale_factor': 1.2,
+ 'extended_exp_factor': 30
+ }
+ }
+}
+
+MAPPING_ORDER = [
+ 'thumb_rotate', 'thumb_abduction', 'thumb_root_flexion',
+ 'index_root_flexion', 'middle_root_flexion', 'ring_root_flexion', 'pinky_root_flexion'
+]
+
+MULTI_SEGMENT_CONFIG = {
+ 'states': [
+ 'original',
+ 'opose',
+ # 'fist'
+ ],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ # 'fist': '握拳'
+ }
+}
+MULTI_SEGMENT_CONFIG_FROZEN = tuple(MULTI_SEGMENT_CONFIG['states'])
+
+ROBOT_OPOSE_LEFT = [
+ 0.0, 0.8, 0.5, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7
+]
+
+ROBOT_OPOSE_RIGHT = [
+ 0.0, 0.8, 0.5, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7, 0.0, 0.0, 0.7
+]
+
+ROBOT_ORIGINAL_LEFT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+]
+
+ROBOT_ORIGINAL_RIGHT = [
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0
+]
+
+ROBOT_FIST_LEFT = [
+ 1.0467, 1.2037, 0.4867, 0.6699, 0.6611,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053
+]
+
+ROBOT_FIST_RIGHT = [
+ 1.0467, 1.2037, 0.4867, 0.6699, 0.6611,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053,
+ 1.3275, 1.7915, 0.6053
+]
+
+# 电机输出约束配置
+# 格式: {'min': 最小值, 'max': 最大值, 'enabled': 是否启用}
+MOTOR_CONSTRAINTS = {
+ 'left': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指旋转
+ {'min': 30, 'max': 255, 'enabled': True}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 拇指弯曲
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 6: 小指
+ ],
+ 'right': [
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 0: 拇指旋转
+ {'min': 30, 'max': 255, 'enabled': True}, # motor 1: 拇指侧摆
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 2: 拇指弯曲
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 3: 食指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 4: 中指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 5: 无名指
+ {'min': 0, 'max': 255, 'enabled': False}, # motor 6: 小指
+ ]
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_g20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_g20.py
new file mode 100644
index 0000000..1eec528
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_g20.py
@@ -0,0 +1,521 @@
+"""
+LinkerForce G20 手型映射模块 - ROS2版本
+支持基于标定数据的精确映射
+v2.8.0升级了映射器算法
+"""
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.g20_config import FINGER_CONFIGS, MAPPING_ORDER, ROBOT_OPOSE_RIGHT, ROBOT_OPOSE_LEFT, ROBOT_ORIGINAL_LEFT, ROBOT_ORIGINAL_RIGHT, ROBOT_FIST_LEFT, ROBOT_FIST_RIGHT, MULTI_SEGMENT_CONFIG, MULTI_SEGMENT_CONFIG_FROZEN, MOTOR_CONSTRAINTS
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+def _resolve_version_config(configs: dict, version: str) -> dict:
+ """
+ 解析版本配置,将字典格式的 weights/reverse_motion 转换为具体值
+ """
+ resolved = copy.deepcopy(configs)
+ for finger_name, config in resolved.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ config['weights'] = config['weights'].get(version, config['weights'].get('v2', [0.5, 0, 0.5]))
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ config['reverse_motion'] = config['reverse_motion'].get(version, config['reverse_motion'].get('v2', False))
+ return resolved
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None # 五指张开标定值 (对应255)
+ self.calibrationfistpose = None # 握拳标定值 (对应0)
+ self.calibrationopose = None # O型标定值 (对应中间值)
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def _apply_motor_constraints(self):
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ state_info = self.multi_state_mapper.get_state_info()
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ # print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ arc_value = None
+
+ if arc_value is not None:
+ qpos[16] = self.g_jointpositions_arc[0] = arc_value[0]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[18] = self.g_jointpositions_arc[2] = arc_value[2]
+ qpos[19] = self.g_jointpositions_arc[3] = arc_value[3]
+
+ qpos[0] = self.g_jointpositions_arc[5] = arc_value[5]
+ qpos[1] = self.g_jointpositions_arc[6] = arc_value[6]
+ qpos[2] = self.g_jointpositions_arc[7] = arc_value[7]
+ qpos[3] = self.g_jointpositions_arc[8] = arc_value[8]
+
+ qpos[4] = self.g_jointpositions_arc[17] = arc_value[17]
+ qpos[5] = self.g_jointpositions_arc[18] = arc_value[18]
+ qpos[6] = self.g_jointpositions_arc[19] = arc_value[19]
+ qpos[7] = self.g_jointpositions_arc[4] = arc_value[20]
+
+ qpos[8] = self.g_jointpositions_arc[9] = arc_value[9]
+ qpos[9] = self.g_jointpositions_arc[10] = arc_value[10]
+ qpos[10] = self.g_jointpositions_arc[11] = arc_value[11]
+ qpos[11] = self.g_jointpositions_arc[12] = arc_value[12]
+
+ qpos[12] = self.g_jointpositions_arc[13] = arc_value[13]
+ qpos[13] = self.g_jointpositions_arc[14] = arc_value[14]
+ qpos[14] = self.g_jointpositions_arc[15] = arc_value[15]
+ qpos[15] = self.g_jointpositions_arc[16] = arc_value[16]
+ else:
+ # 手动映射备用
+ qpos[20] = joint_arc[4] * 2.2
+ qpos[17] = joint_arc[2] * -2.5
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self._apply_motor_constraints()
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5
+ self.smooth_positions = [255.0] * length
+ self.max_step = 20
+
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def _apply_motor_constraints(self):
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ state_info = self.multi_state_mapper.get_state_info()
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ # print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ arc_value = None
+
+ if arc_value is not None:
+ qpos[16] = self.g_jointpositions_arc[0] = arc_value[0]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[18] = self.g_jointpositions_arc[2] = arc_value[2]
+ qpos[19] = self.g_jointpositions_arc[3] = arc_value[3]
+
+ qpos[0] = self.g_jointpositions_arc[5] = arc_value[5]
+ qpos[1] = self.g_jointpositions_arc[6] = arc_value[6]
+ qpos[2] = self.g_jointpositions_arc[7] = arc_value[7]
+ qpos[3] = self.g_jointpositions_arc[8] = arc_value[8]
+
+ qpos[4] = self.g_jointpositions_arc[17] = arc_value[17]
+ qpos[5] = self.g_jointpositions_arc[18] = arc_value[18]
+ qpos[6] = self.g_jointpositions_arc[19] = arc_value[19]
+ qpos[7] = self.g_jointpositions_arc[4] = arc_value[20]
+
+ qpos[8] = self.g_jointpositions_arc[9] = arc_value[9]
+ qpos[9] = self.g_jointpositions_arc[10] = arc_value[10]
+ qpos[10] = self.g_jointpositions_arc[11] = arc_value[11]
+ qpos[11] = self.g_jointpositions_arc[12] = arc_value[12]
+
+ qpos[12] = self.g_jointpositions_arc[13] = arc_value[13]
+ qpos[13] = self.g_jointpositions_arc[14] = arc_value[14]
+ qpos[14] = self.g_jointpositions_arc[15] = arc_value[15]
+ qpos[15] = self.g_jointpositions_arc[16] = arc_value[16]
+ else:
+ # 手动映射备用
+ qpos[20] = joint_arc[4] * 2.2
+ qpos[17] = joint_arc[2] * -2.5
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ # print(qpos[4],arc_value[17],self.g_jointpositions[9])
+ self._apply_motor_constraints()
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l10.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l10.py
new file mode 100644
index 0000000..283bbb6
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l10.py
@@ -0,0 +1,387 @@
+"""
+LinkerForce L10 手型映射模块 - ROS2版本
+"""
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.l10_config import FINGER_CONFIGS, MAPPING_ORDER, ROBOT_OPOSE_RIGHT, ROBOT_OPOSE_LEFT, ROBOT_ORIGINAL_RIGHT, ROBOT_ORIGINAL_LEFT, ROBOT_FIST_RIGHT, ROBOT_FIST_LEFT, MULTI_SEGMENT_CONFIG, MULTI_SEGMENT_CONFIG_FROZEN, MOTOR_CONSTRAINTS
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+def _resolve_version_config(configs: dict, version: str) -> dict:
+ """
+ 解析版本配置,将字典格式的 weights/reverse_motion 转换为具体值
+ """
+ resolved = copy.deepcopy(configs)
+ for finger_name, config in resolved.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ config['weights'] = config['weights'].get(version, config['weights'].get('v2', [0.5, 0, 0.5]))
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ config['reverse_motion'] = config['reverse_motion'].get(version, config['reverse_motion'].get('v2', False))
+ return resolved
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5
+ self.smooth_positions = [255.0] * length
+ self.max_step = 20
+
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def _apply_motor_constraints(self):
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[2]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[6]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[9]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[13]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[17]
+ qpos[0] = self.g_jointpositions_arc[6] = arc_value[5]
+ qpos[12] = self.g_jointpositions_arc[7] = arc_value[12]
+ qpos[4] = self.g_jointpositions_arc[8] = arc_value[16]
+ qpos[16] = self.g_jointpositions_arc[9] = arc_value[0]
+ else:
+ # 拇指处理
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[4] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ self._apply_motor_constraints()
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5
+ self.smooth_positions = [255.0] * length
+ self.max_step = 20
+
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def _apply_motor_constraints(self):
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[2]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[6]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[9]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[13]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[17]
+ qpos[0] = self.g_jointpositions_arc[6] = arc_value[5]
+ qpos[12] = self.g_jointpositions_arc[7] = arc_value[12]
+ qpos[4] = self.g_jointpositions_arc[8] = arc_value[16]
+ qpos[16] = self.g_jointpositions_arc[9] = arc_value[0]
+ else:
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+ self._apply_motor_constraints()
+
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l20.py
new file mode 100644
index 0000000..f27162a
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l20.py
@@ -0,0 +1,558 @@
+"""
+LinkerForce L20 手型映射模块 - ROS2版本
+"""
+
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.l20_config import (
+ FINGER_CONFIGS, MAPPING_ORDER, MULTI_SEGMENT_CONFIG,
+ ROBOT_ORIGINAL_LEFT, ROBOT_ORIGINAL_RIGHT,
+ ROBOT_FIST_LEFT, ROBOT_FIST_RIGHT,
+ ROBOT_OPOSE_LEFT, ROBOT_OPOSE_RIGHT,
+ MOTOR_CONSTRAINTS
+)
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None # 五指张开标定值 (对应255)
+ self.calibrationfistpose = None # 握拳标定值 (对应0)
+ self.calibrationopose = None # O型标定值 (对应中间值)
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+
+ # self.robot_fist[0] = -0.2 # 拇指旋转锁死在最大0.2,高于0.2的属于无用区间
+ # self.robot_fist[1] = 1.4 # 拇指侧摆锁死在最大1.2,高于1.2的属于无用区间
+
+ # 这里可以额外对self.robot_fist的非期望值进行修正,
+ # 由于机械手达到最大值,存在非期望值的区域,在这里可以进行修正,
+ # 同样也需要URDF驱动工具包去做这个事情
+
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER, is_debug=is_debug)
+
+ # 设置动态权重配置(v2.8.2新增)
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ # 电机输出约束
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+ # 侧摆部分预处理
+ for i in [5, 9, 13, 17]:
+ self.calibrationoriginal[i] = self.calibrationopose[i] + 0.1
+ self.calibrationfistpose[i] = self.calibrationopose[i] - 0.1
+
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(MULTI_SEGMENT_CONFIG['states'])
+
+ state_info = self.multi_state_mapper.get_state_info()
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_motor_constraints(self, positions):
+ """应用电机输出约束"""
+ if not hasattr(self, 'motor_constraints') or self.motor_constraints is None:
+ return positions
+ result = []
+ for i, pos in enumerate(positions):
+ if i < len(self.motor_constraints) and self.motor_constraints[i].get('enabled', False):
+ result.append(max(self.motor_constraints[i]['min'], min(pos, self.motor_constraints[i]['max'])))
+ else:
+ result.append(pos)
+ return result
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[16] = self.g_jointpositions_arc[0] = arc_value[0]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[18] = self.g_jointpositions_arc[2] = arc_value[2]
+ qpos[19] = self.g_jointpositions_arc[3] = arc_value[3]
+
+ qpos[0] = self.g_jointpositions_arc[5] = arc_value[5]
+ qpos[1] = self.g_jointpositions_arc[6] = arc_value[6]
+
+ qpos[2] = self.g_jointpositions_arc[7] = arc_value[7]
+ qpos[3] = self.g_jointpositions_arc[8] = arc_value[8]
+
+ qpos[4] = self.g_jointpositions_arc[17] = arc_value[17]
+ qpos[5] = self.g_jointpositions_arc[18] = arc_value[18]
+ qpos[6] = self.g_jointpositions_arc[19] = arc_value[19]
+ qpos[7] = self.g_jointpositions_arc[4] = arc_value[20]
+
+ qpos[8] = self.g_jointpositions_arc[9] = arc_value[9]
+ qpos[9] = self.g_jointpositions_arc[10] = arc_value[10]
+ qpos[10] = self.g_jointpositions_arc[11] = arc_value[11]
+ qpos[11] = self.g_jointpositions_arc[12] = arc_value[12]
+
+ qpos[12] = self.g_jointpositions_arc[13] = arc_value[13]
+ qpos[13] = self.g_jointpositions_arc[14] = arc_value[14]
+ qpos[14] = self.g_jointpositions_arc[15] = arc_value[15]
+ qpos[15] = self.g_jointpositions_arc[16] = arc_value[16]
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ # 拇指处理 (与O6相同)
+ qpos[20] = joint_arc[4] * 2.2 # 拇指弯曲
+ qpos[17] = joint_arc[2] * -2.5 # 拇指侧摆
+ # 四指处理
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ # self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ # ========== 应用电机约束 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self.g_jointpositions = self._apply_motor_constraints(self.g_jointpositions)
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None # 五指张开标定值 (对应255)
+ self.calibrationfistpose = None # 握拳标定值 (对应0)
+ self.calibrationopose = None # O型标定值 (对应中间值)
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+
+ self.robot_fist[0] = 0.3 # 拇指旋转锁死在最大0.2,高于0.2的属于无用区间
+ # self.robot_fist[1] = 1.5 # 拇指侧摆锁死在最大1.2,高于1.2的属于无用区间
+
+ # 这里可以额外对self.robot_fist的非期望值进行修正,
+ # 由于机械手达到最大值,存在非期望值的区域,在这里可以进行修正,
+ # 同样也需要URDF驱动工具包去做这个事情
+
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER, is_debug=is_debug)
+
+ # 设置动态权重配置(v2.8.2新增)
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ # 电机输出约束
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+ # 侧摆部分预处理
+ for i in [5, 9, 13, 17]:
+ self.calibrationoriginal[i] = self.calibrationopose[i] - 0.1
+ self.calibrationfistpose[i] = self.calibrationopose[i] + 0.1
+
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(MULTI_SEGMENT_CONFIG['states'])
+
+ state_info = self.multi_state_mapper.get_state_info()
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_motor_constraints(self, positions):
+ """应用电机输出约束"""
+ if not hasattr(self, 'motor_constraints') or self.motor_constraints is None:
+ return positions
+ result = []
+ for i, pos in enumerate(positions):
+ if i < len(self.motor_constraints) and self.motor_constraints[i].get('enabled', False):
+ result.append(max(self.motor_constraints[i]['min'], min(pos, self.motor_constraints[i]['max'])))
+ else:
+ result.append(pos)
+ return result
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_LEFT
+ qpos[16] = self.g_jointpositions_arc[0] = arc_value[0]
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[18] = self.g_jointpositions_arc[2] = arc_value[2]
+ qpos[19] = self.g_jointpositions_arc[3] = arc_value[3]
+
+ qpos[0] = self.g_jointpositions_arc[5] = arc_value[5] * 3
+ qpos[1] = self.g_jointpositions_arc[6] = arc_value[6]
+ qpos[2] = self.g_jointpositions_arc[7] = arc_value[7]
+ qpos[3] = self.g_jointpositions_arc[8] = arc_value[8]
+
+ qpos[4] = self.g_jointpositions_arc[17] = arc_value[17]
+ qpos[5] = self.g_jointpositions_arc[18] = arc_value[18]
+ qpos[6] = self.g_jointpositions_arc[19] = arc_value[19]
+ qpos[7] = self.g_jointpositions_arc[4] = arc_value[20]
+
+ qpos[8] = self.g_jointpositions_arc[9] = arc_value[9]
+ qpos[9] = self.g_jointpositions_arc[10] = arc_value[10]
+ qpos[10] = self.g_jointpositions_arc[11] = arc_value[11]
+ qpos[11] = self.g_jointpositions_arc[12] = arc_value[12]
+
+ qpos[12] = self.g_jointpositions_arc[13] = arc_value[13]
+ qpos[13] = self.g_jointpositions_arc[14] = arc_value[14]
+ qpos[14] = self.g_jointpositions_arc[15] = arc_value[15]
+ qpos[15] = self.g_jointpositions_arc[16] = arc_value[16]
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ # 拇指处理 (与O6相同)
+ qpos[20] = joint_arc[4] * 2.2 # 拇指弯曲
+ qpos[17] = joint_arc[2] * -2.5 # 拇指侧摆
+ # 四指处理
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ # self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ # ========== 应用电机约束 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+ self.g_jointpositions = self._apply_motor_constraints(self.g_jointpositions)
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l6.py
new file mode 100644
index 0000000..866d5a7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l6.py
@@ -0,0 +1,495 @@
+"""
+LinkerForce L6 手型映射模块 - ROS2版本
+与ROS1 L6版本保持一致,支持基于标定数据的精确映射
+v2.8.0升级了映射器算法
+"""
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.l6_config import FINGER_CONFIGS, MAPPING_ORDER, ROBOT_OPOSE_RIGHT, ROBOT_OPOSE_LEFT, ROBOT_ORIGINAL_RIGHT, ROBOT_ORIGINAL_LEFT, ROBOT_FIST_RIGHT, ROBOT_FIST_LEFT, MULTI_SEGMENT_CONFIG, MULTI_SEGMENT_CONFIG_FROZEN, MOTOR_CONSTRAINTS
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+
+def _resolve_version_config(configs: dict, version: str) -> dict:
+ """
+ 解析版本配置,将字典格式的 weights/reverse_motion 转换为具体值
+ """
+ resolved = copy.deepcopy(configs)
+ for finger_name, config in resolved.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ config['weights'] = config['weights'].get(version, config['weights'].get('v2', [0.5, 0, 0.5]))
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ config['reverse_motion'] = config['reverse_motion'].get(version, config['reverse_motion'].get('v2', False))
+ return resolved
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ # 电机约束配置
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # info = self.multi_state_mapper.get_mapping_info()
+ # for finger, data in info.items():
+ # print(f"{finger}: 扩展映射={data['has_extended_mapping']}, "
+ # f"缩放={data['scale_factor']:.1f}, "
+ # f"最大角度={data['max_angle']:.3f}rad")
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None \
+ and self.calibrationfistpose is not None \
+ and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ # 拇指处理 (与O6相同)
+ qpos[20] = joint_arc[4] * 2.2 # 拇指弯曲
+ qpos[17] = joint_arc[2] * -2.5 # 拇指侧摆
+ # 四指处理
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self._apply_motor_constraints()
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def _apply_motor_constraints(self):
+ """对 g_jointpositions (电机值) 应用约束"""
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ # 电机约束配置
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def _to_list(self, data):
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 左手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+
+ # ========== 使用映射器进行精确映射 ==========
+ # for finger, data in info.items():
+ # print(f"{finger}: 扩展映射={data['has_extended_mapping']}, "
+ # f"缩放={data['scale_factor']:.1f}, "
+ # f"最大角度={data['max_angle']:.3f}rad")
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ # 拇指处理 (与O6相同)
+ qpos[20] = joint_arc[4] * 2.2 # 拇指弯曲
+ qpos[17] = joint_arc[2] * -2.5 # 拇指侧摆
+ # 四指处理
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+ self._apply_motor_constraints()
+ # self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def _apply_motor_constraints(self):
+ """对 g_jointpositions (电机值) 应用约束"""
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l7.py
new file mode 100644
index 0000000..a3710c0
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_l7.py
@@ -0,0 +1,434 @@
+"""
+LinkerForce L7 手型映射模块 - ROS2版本
+"""
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.o7_config import FINGER_CONFIGS, MAPPING_ORDER, ROBOT_OPOSE_RIGHT, ROBOT_OPOSE_LEFT, ROBOT_ORIGINAL_RIGHT, ROBOT_ORIGINAL_LEFT, ROBOT_FIST_RIGHT, ROBOT_FIST_LEFT, MULTI_SEGMENT_CONFIG, MULTI_SEGMENT_CONFIG_FROZEN, MOTOR_CONSTRAINTS
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None # 五指张开标定值 (对应255)
+ self.calibrationfistpose = None # 握拳标定值 (对应0)
+ self.calibrationopose = None # O型标定值 (对应中间值)
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+ self.robot_fist[0] = 0.8
+
+ # 这里可以额外对self.robot_fist的非期望值进行修正,
+ # 由于机械手达到最大值,存在非期望值的区域,在这里可以进行修正,
+ # 同样也需要URDF驱动工具包去做这个事情
+
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER, is_debug=is_debug)
+
+ # 设置动态权重配置(v2.8.2新增)
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ # 电机输出约束
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+
+ def initialize_mapper(self) -> bool:
+ """初始化映射器"""
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_motor_constraints(self, positions):
+ """应用电机输出约束"""
+ if not hasattr(self, 'motor_constraints') or self.motor_constraints is None:
+ return positions
+ result = []
+ for i, pos in enumerate(positions):
+ if i < len(self.motor_constraints) and self.motor_constraints[i].get('enabled', False):
+ result.append(max(self.motor_constraints[i]['min'], min(pos, self.motor_constraints[i]['max'])))
+ else:
+ result.append(pos)
+ return result
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[2]
+ qpos[16] = self.g_jointpositions_arc[6] = arc_value[0]
+
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[4]
+
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[5]
+
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[6]
+ else:
+ # 拇指处理
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ # ========== 应用电机约束 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self.g_jointpositions = self._apply_motor_constraints(self.g_jointpositions)
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+# LeftHand 类类似修正
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None # 五指张开标定值 (对应255)
+ self.calibrationfistpose = None # 握拳标定值 (对应0)
+ self.calibrationopose = None # O型标定值 (对应中间值)
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+ self.robot_fist[0] = 0.8
+
+ # 这里可以额外对self.robot_fist的非期望值进行修正,
+ # 由于机械手达到最大值,存在非期望值的区域,在这里可以进行修正,
+ # 同样也需要URDF驱动工具包去做这个事情
+
+
+ # 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER, is_debug=is_debug)
+
+ # 设置动态权重配置(v2.8.2新增)
+ for config_name, config in FINGER_CONFIGS.items():
+ if config.get('dynamic_weight'):
+ self.multi_state_mapper.set_dynamic_weight_config(config_name, config['dynamic_weight'])
+
+ # 电机输出约束
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def initialize_mapper(self) -> bool:
+ """初始化映射器"""
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(list(MULTI_SEGMENT_CONFIG_FROZEN))
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def _apply_motor_constraints(self, positions):
+ """应用电机输出约束"""
+ if not hasattr(self, 'motor_constraints') or self.motor_constraints is None:
+ return positions
+ result = []
+ for i, pos in enumerate(positions):
+ if i < len(self.motor_constraints) and self.motor_constraints[i].get('enabled', False):
+ result.append(max(self.motor_constraints[i]['min'], min(pos, self.motor_constraints[i]['max'])))
+ else:
+ result.append(pos)
+ return result
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[1]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[2]
+ qpos[16] = self.g_jointpositions_arc[6] = arc_value[0]
+
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[4]
+
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[5]
+
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[6]
+ else:
+ # 拇指处理
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ # ========== 应用电机约束 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self.g_jointpositions = self._apply_motor_constraints(self.g_jointpositions)
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_o6.py
new file mode 100644
index 0000000..042d84a
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/hand/linkerforce_o6.py
@@ -0,0 +1,468 @@
+"""
+LinkerForce O6 手型映射模块 - ROS2版本
+与ROS1 O6版本保持一致,支持基于标定数据的精确映射
+v2.8.0升级了映射器算法
+"""
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.o6_config import FINGER_CONFIGS, MAPPING_ORDER, ROBOT_OPOSE_RIGHT, ROBOT_OPOSE_LEFT, ROBOT_ORIGINAL_RIGHT, ROBOT_ORIGINAL_LEFT, ROBOT_FIST_RIGHT, ROBOT_FIST_LEFT, MULTI_SEGMENT_CONFIG, MOTOR_CONSTRAINTS
+from typing import List
+from linkerhand.handcoreex import DynamicWeightMultiStateLinearMapper,MultiStateLinearMapper
+
+
+def _resolve_version_config(configs: dict, version: str) -> dict:
+ """
+ 解析版本配置,将字典格式的 weights/reverse_motion 转换为具体值
+ """
+ resolved = copy.deepcopy(configs)
+ for finger_name, config in resolved.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ config['weights'] = config['weights'].get(version, config['weights'].get('v2', [0.5, 0, 0.5]))
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ config['reverse_motion'] = config['reverse_motion'].get(version, config['reverse_motion'].get('v2', False))
+ return resolved
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ # 目标机械手预设姿势,数值从URDF获取数据集,
+ # 张开手的时候对应最小角度,
+ # 握拳的时候对应最大角度
+ # O型手势的时候,用工具驱动URDF去驱动目标机械手达到期望姿势,也可以调整这些参数使得实物更加达到期望角度
+ # 其他手势也类似,也可以增加多个手势来实现多模态的映射器(后期陆续开发)
+ self.robot_original = ROBOT_ORIGINAL_RIGHT
+ self.robot_opose = ROBOT_OPOSE_RIGHT
+ self.robot_fist = ROBOT_FIST_RIGHT
+
+ # self.robot_fist[0] = 1.5 # 拇指旋转锁死在最大0.2,高于0.2的属于无用区间
+ self.robot_fist[0] = 1.1 # 拇指侧摆锁死在最大1.2,高于1.2的属于无用区间
+ # 这里可以额外对self.robot_fist的非期望值进行修正,
+ # 由于机械手达到最大值,存在非期望值的区域,在这里可以进行修正,
+ # 同样也需要URDF驱动工具包去做这个事情
+
+# 映射器(v2.8.0专属),具体介绍参考l6_config.py文件
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ # 电机约束配置
+ self.motor_constraints = MOTOR_CONSTRAINTS['right']
+
+ def _apply_motor_constraints(self):
+ """对 g_jointpositions (电机值) 应用约束"""
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def initialize_mapper(self) -> bool:
+ """
+ 初始化映射器
+
+ 将三种人手标定数据和三种机械手标定数据加载到映射器中
+ 分别是original,opose,fist
+
+ 人手是glove_前缀,机械手是robot_前缀
+ """
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+ self.multi_state_mapper.set_state_order(MULTI_SEGMENT_CONFIG['states'])
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ """
+ 右手映射 - 基于标定数据和预期机械手动作的映射器完成
+ """
+ qpos = np.zeros(25)
+ # info = self.multi_state_mapper.get_mapping_info()
+ # for finger, data in info.items():
+ # print(f"{finger}: 扩展映射={data['has_extended_mapping']}, "
+ # f"缩放={data['scale_factor']:.1f}, "
+ # f"最大角度={data['max_angle']:.3f}rad")
+ # ========== 使用映射器进行精确映射 ==========
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ # for i in range(20):
+ # self.multi_state_mapper.debug_value[i] = joint_arc[i]
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ # arc_value = ROBOT_OPOSE_RIGHT
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ # ========== 没有标定数据时使用手动映射 ==========
+ else:
+ # 拇指处理 (与O6相同)
+ qpos[20] = joint_arc[4] * 2.2 # 拇指弯曲
+ qpos[17] = joint_arc[2] * -2.5 # 拇指侧摆
+ # 四指处理
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ # ========== 应用平滑滤波 ==========
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ self._apply_motor_constraints()
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6, is_debug: bool = False):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+ self.glove_version = 'v2'
+
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5
+ self.smooth_positions = [255.0] * length
+ self.max_step = 20
+
+ self.robot_original = ROBOT_ORIGINAL_LEFT
+ self.robot_opose = ROBOT_OPOSE_LEFT
+ self.robot_fist = ROBOT_FIST_LEFT
+
+ finger_configs = _resolve_version_config(FINGER_CONFIGS, self.glove_version)
+ self.multi_state_mapper = DynamicWeightMultiStateLinearMapper(finger_configs, MAPPING_ORDER, is_debug=is_debug)
+
+ # 电机约束配置
+ self.motor_constraints = MOTOR_CONSTRAINTS['left']
+
+ def _apply_motor_constraints(self):
+ """对 g_jointpositions (电机值) 应用约束"""
+ for i, constraint in enumerate(self.motor_constraints):
+ if constraint.get('enabled', False):
+ min_val = constraint.get('min', 0)
+ max_val = constraint.get('max', 255)
+ self.g_jointpositions[i] = int(max(min_val, min(max_val, self.g_jointpositions[i])))
+
+ def initialize_mapper(self) -> bool:
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ self.multi_state_mapper.set_state_order(MULTI_SEGMENT_CONFIG['states'])
+
+ def set_glove_version(self, version: str):
+ if not version:
+ return
+
+ major_version = version.split('.')[0]
+ version_key = f'v{major_version}'
+
+ if version_key == self.glove_version:
+ return
+
+ self.glove_version = version_key
+
+ for finger_name, config in FINGER_CONFIGS.items():
+ if 'weights' in config and isinstance(config['weights'], dict):
+ if version_key in config['weights']:
+ self.multi_state_mapper.finger_configs[finger_name]['weights'] = config['weights'][version_key]
+
+ if 'reverse_motion' in config and isinstance(config['reverse_motion'], dict):
+ if version_key in config['reverse_motion']:
+ self.multi_state_mapper.finger_configs[finger_name]['reverse_motion'] = config['reverse_motion'][version_key]
+
+ def _to_list(self, data):
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ print(111)
+ return data.tolist()
+ else:
+ return list(data)
+
+ # V2.8.0 本函数作废
+ # def _linear_map_diff(self, current_diff, fist_diff, extend_ratio=1.2):
+ # """
+ # 基于差值的线性映射到0-255
+
+ # 注意: 传入joint_update的是差值 (当前值 - 张开值)
+
+ # 参数:
+ # current_diff: 当前传感器差值 (当前值 - 张开值)
+ # fist_diff: 握拳时的差值 (握拳值 - 张开值)
+ # extend_ratio: 缩放比例,>1.0 使映射更容易到达0/255边界
+
+ # 映射逻辑:
+ # - 差值为0(张开)→ 255
+ # - 差值为fist_diff(握拳)→ 0
+ # """
+ # if abs(fist_diff) < 0.01:
+ # return 128 # 变化太小,返回中值
+
+ # # 缩小fist_diff使得更容易到达0边界
+ # effective_fist_diff = fist_diff / extend_ratio
+
+ # # 计算比例: 差值0→比例0, 差值fist_diff→比例1
+ # ratio = current_diff / effective_fist_diff
+ # ratio = max(0.0, min(1.0, ratio)) # 限制在0-1之间
+
+ # # 映射: 比例0→255, 比例1→0
+ # return int((1 - ratio) * 255)
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ if self.calibrationoriginal is not None and self.calibrationfistpose is not None and self.calibrationopose is not None:
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ else:
+ qpos[20] = joint_arc[4] * 2.2
+ qpos[17] = joint_arc[2] * -2.5
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+ self._apply_motor_constraints()
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/retarget.py
new file mode 100644
index 0000000..8dac21c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/retarget.py
@@ -0,0 +1,1289 @@
+import time
+import sys
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from std_msgs.msg import String, Int32MultiArray, Header, Float32MultiArray, MultiArrayLayout, MultiArrayDimension
+from pathlib import Path
+
+# 将项目根目录放在最前面
+# 强制使用项目本地的 linkerhand 模块
+_project_root = Path(__file__).absolute().parent.parent.parent
+_project_root_str = str(_project_root)
+
+if _project_root_str in sys.path:
+ sys.path.remove(_project_root_str)
+sys.path.insert(0, _project_root_str)
+
+from linkerhand.linkerforce import ForceSerialReader
+from linkerhand.constants import RobotName, ROBOT_LEN_MAP, HandType
+from linkerhand.handcore import HandCore
+from tqdm import tqdm
+from pathlib import Path
+from colorama import Fore, init
+from datetime import datetime, timedelta
+import threading
+import copy
+import pickle
+import os
+import json
+import sys
+import numpy as np
+import math
+import yaml
+
+
+TMP_FILE_PATH = Path(__file__).parent / "tmp" / "jointangle_data.tmp"
+SAMPLE_FILE_PATH = Path(__file__).parent.parent.parent / "config" / "calibration_sample.yml"
+
+
+class Retarget():
+ def __init__(self,
+ node,
+ righthand: RobotName,
+ lefthand: RobotName,
+ handcore: HandCore,
+ lefthandpubprint: bool,
+ righthandpubprint: bool,
+ calibration: bool = False,
+ auto_detect: bool = True,
+ isgetdebug: bool = True,
+ baseconfig: dict = None,
+ cmd_ports: list = None,
+ cmd_baudrate: int = None,
+ cmd_auto_scan: bool = None):
+ """
+ 初始化 LinkerForce Retarget 模块 (ROS1 版本)
+
+ Args:
+ leftport: 左手串口路径(如 '/dev/ttyUSB0'),auto_detect=True 时可忽略
+ leftbaudrate: 左手波特率
+ rightport: 右手串口路径(如 '/dev/ttyUSB1'),auto_detect=True 时可忽略
+ rightbaudrate: 右手波特率
+ lefthand: 左手机器人类型
+ righthand: 右手机器人类型
+ handcore: HandCore 实例
+ lefthandpubprint: 是否打印左手调试信息
+ righthandpubprint: 是否打印右手调试信息
+ calibration: True=强制标定, False=尝试加载缓存
+ auto_detect: 是否自动检测串口(默认 True)
+ isgetdebug: 是否发布debug测试数据话题(默认False)
+ baseconfig: 基础配置字典
+ """
+
+ self.node = node
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.runing = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+ self.isdebugpub = isgetdebug
+ self.baseconfig = baseconfig or {}
+
+ self.show_fist_calibration = self.baseconfig.get('calibration', {}).get('show_fist', True)
+ self.fist_extend_ratio = self.baseconfig.get('calibration', {}).get('fist_extend_ratio', 0.5)
+
+ # 命令行串口参数(候选列表,系统自动识别左右手)
+ self.cmd_ports = cmd_ports
+ self.cmd_baudrate = cmd_baudrate
+ self.cmd_auto_scan = cmd_auto_scan
+
+ # 根据右手类型初始化
+ mapper_debug = self.baseconfig.get('debug', {}).get('mapper_debug', False)
+
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v3:
+ from .hand.linkerforce_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+
+ elif self.righthandtype == RobotName.o6:
+ from .hand.linkerforce_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+ elif self.righthandtype == RobotName.l6:
+ from .hand.linkerforce_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+ elif self.righthandtype == RobotName.l25 \
+ or self.righthandtype == RobotName.g20:
+ from .hand.linkerforce_g20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+ elif self.righthandtype == RobotName.l20:
+ from .hand.linkerforce_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 \
+ or self.righthandtype == RobotName.l20lite:
+ from .hand.linkerforce_l10 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand], is_debug=mapper_debug)
+ else:
+ print("未正确定义机械左手对象,请检查支持清单列表!")
+
+ # 根据左手类型初始化
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v3:
+ from .hand.linkerforce_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.linkerforce_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.linkerforce_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ elif self.lefthandtype == RobotName.l25 \
+ or self.lefthandtype == RobotName.g20:
+ from .hand.linkerforce_g20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.linkerforce_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 \
+ or self.lefthandtype == RobotName.l20lite:
+ from .hand.linkerforce_l10 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand], is_debug=mapper_debug)
+ else:
+ print("未正确定义机械右手对象,请检查支持清单列表!")
+
+ self.node.get_logger().info(f"[机械手] 左手型号: {self.lefthandtype.name}, 右手型号: {self.righthandtype.name}")
+
+ # ROS2 发布器
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ self.publisher_angle_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_angle_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_angle_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_angle_cmd',
+ self.handcore.hand_numjoints_l)
+
+ # 创建订阅者,订阅/cb_left_hand_matrix_touch话题
+ self.left_touch_subscription = self.node.create_subscription(
+ String,
+ '/cb_left_hand_matrix_touch',
+ self.touch_left_callback,
+ 10 # QoS 队列深度
+ )
+ self.right_touch_subscription = self.node.create_subscription(
+ String,
+ '/cb_right_hand_matrix_touch',
+ self.touch_right_callback,
+ 10 # QoS 队列深度
+ )
+
+
+ if self.isdebugpub:
+ # # ROS1 发布器,触感矩阵转换相关
+ # self.publisher_hand_matrix2int_r = self.node.create_publisher(
+ # '/cb_right_hand_matrix2int',
+ # Int32MultiArray,
+ # self.handcore.hand_numjoints_r)
+
+ # self.publisher_hand_matrix2int_l = self.node.create_publisher(
+ # '/cb_left_hand_matrix2int',
+ # Int32MultiArray,
+ # self.handcore.hand_numjoints_l)
+
+
+ self.publisher_hand_debugdata_r = self.node.create_publisher(
+ Float32MultiArray,
+ '/cb_right_hand_debug',
+ self.handcore.hand_numjoints_r)
+
+
+ self.publisher_hand_debugdata_l = self.node.create_publisher(
+ Float32MultiArray,
+ '/cb_left_hand_debug',
+ self.handcore.hand_numjoints_l)
+
+ # 初始化统计结果
+ self.results = {
+ 'left':{},
+ 'right':{}
+ }
+ self.leftforcesendcount = -1
+ self.rightforcesendcount = -1
+
+ # 状态变量
+ self.pubprintcount = 0
+ self.force_reader_left = None
+ self.force_reader_right = None
+ self.calibration = calibration
+ self.leftport = None
+ self.leftbaudrate = None
+ self.rightport = None
+ self.rightbaudrate = None
+
+ # 强制手套数据源 (none/open/fist/opose)
+ self.force_glove_pose = None
+ self.calibration_cache = None # 缓存标定数据
+
+ # 力数据线程锁
+ self.forcelock = threading.Lock()
+
+ # 自动标定相关变量
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+ self.calibration_in_progress = False
+
+ # ========== 调试:映射层跳变检测 ==========
+ self.debug_enabled = True # 设为 False 关闭调试
+ self.debug_motor_jump_threshold = 20 # 电机值跳变阈值
+ self.debug_last_motor_l = [255] * 6
+ self.debug_last_motor_r = [255] * 6
+ self.debug_last_raw_l = [0.0] * 21
+ self.debug_last_raw_r = [0.0] * 21
+
+
+
+ def touch_left_callback(self, msg):
+ self.process_touch_data(msg.data,'left')
+
+ def touch_right_callback(self, msg):
+ self.process_touch_data(msg.data,'right')
+
+ def process_touch_data(self, json_str, hand_type):
+ with self.forcelock:
+ try:
+ data = json.loads(json_str)
+ self.results[hand_type] = {}
+ # 处理每个手指的矩阵
+ for finger in ['thumb_matrix', 'index_matrix', 'middle_matrix', 'ring_matrix', 'little_matrix']:
+ matrix = np.array(data[finger])
+ # 计算接触面积(非零元素数量)
+ contact_area = np.count_nonzero(matrix)
+ # 计算总接触力
+ total_force = np.sum(matrix)
+ # 计算平均接触力(避免除以零)
+ avg_force = total_force / contact_area if contact_area > 0 else 0
+ max_force = np.max(matrix) * 4 if contact_area > 0 else 0
+ if max_force > 500:
+ max_force = 500
+ self.results[hand_type][finger] = {
+ 'contact_area': contact_area,
+ 'total_force': total_force,
+ 'avg_force': avg_force,
+ 'max_force': max_force
+ }
+
+ except Exception as e:
+ self.node.get_logger().error("Error processing touch data: %s" % str(e))
+ return None
+
+ def linkerforce_init(self):
+ # 从配置读取串口参数
+ serial_config = self.baseconfig.get('serial', {})
+ baudrates = serial_config.get('baudrates', [2000000, 1000000, 921600, 460800])
+ exclude_ports = serial_config.get('exclude_ports', [])
+ serial_debug = serial_config.get('serial_debug', False)
+ config_auto_scan = serial_config.get('auto_scan', False)
+
+ # 自动扫描开关:命令行参数 > 配置文件 > 默认false
+ auto_scan = self.cmd_auto_scan if self.cmd_auto_scan is not None else config_auto_scan
+
+ saved_left = serial_config.get('left', {})
+ saved_right = serial_config.get('right', {})
+
+ # 确定候选端口列表
+ if self.cmd_ports:
+ candidate_ports = self.cmd_ports
+ self.node.get_logger().info(f"使用命令行候选串口: {candidate_ports}")
+ else:
+ candidate_ports = None
+ self.node.get_logger().info(f"使用配置文件串口: 左手={saved_left.get('port')}, 右手={saved_right.get('port')}")
+
+ # 确定波特率
+ if self.cmd_baudrate:
+ baudrates = [self.cmd_baudrate]
+
+ self.node.get_logger().info(f"波特率组合: {baudrates}, 自动扫描={auto_scan}, 调试={serial_debug}")
+
+ # 日志回调函数
+ def serial_logger(level, msg):
+ if level == 'error':
+ self.node.get_logger().error(msg)
+ elif level == 'warn':
+ self.node.get_logger().warn(msg)
+ elif level == 'debug':
+ self.node.get_logger().debug(msg)
+ else:
+ self.node.get_logger().info(msg)
+
+ # 如果提供了候选端口列表,从中自动检测左右手
+ if candidate_ports:
+ left_found, right_found = self._init_from_candidates(
+ candidate_ports, baudrates, exclude_ports, serial_debug, serial_logger
+ )
+ else:
+ # 使用配置文件的预设端口
+ self.force_reader_left = ForceSerialReader(
+ HandType.left,
+ excludelist=exclude_ports,
+ baudrates=baudrates,
+ isdebug=serial_debug,
+ logger=serial_logger
+ )
+ left_found = self._init_hand(
+ self.force_reader_left,
+ 'Left',
+ saved_left.get('port'),
+ saved_left.get('baudrate'),
+ '左手',
+ auto_scan
+ )
+
+ exclude_right = exclude_ports + ([self.leftport] if left_found else [])
+ self.force_reader_right = ForceSerialReader(
+ HandType.right,
+ excludelist=exclude_right,
+ baudrates=baudrates,
+ isdebug=serial_debug,
+ logger=serial_logger
+ )
+ right_found = self._init_hand(
+ self.force_reader_right,
+ 'Right',
+ saved_right.get('port') if saved_right.get('port') not in exclude_right else None,
+ saved_right.get('baudrate'),
+ '右手',
+ auto_scan
+ )
+
+ # 保存检测到的串口配置
+ if left_found or right_found:
+ self._save_serial_to_config(left_found, right_found)
+
+ time.sleep(1)
+
+ # 标定流程
+ if self.calibration is True:
+ self.calibration = "auto_calibrate"
+ self.node.get_logger().info("强制标定模式:将进行自动标定")
+ else:
+ if self._load_from_tmp() is True:
+ self.node.get_logger().info("已加载缓存标定数据,跳过标定流程")
+ self.calibration = -1
+ self.righthand.initialize_mapper()
+ self.lefthand.initialize_mapper()
+ else:
+ self.calibration = "auto_calibrate"
+ self.node.get_logger().info("未找到有效缓存,将进行自动标定")
+
+ def _init_from_candidates(self, candidate_ports, baudrates, exclude_ports, serial_debug, serial_logger):
+ """从候选端口列表中自动检测并初始化左右手"""
+ left_found = False
+ right_found = False
+ detected_ports = {}
+
+ for port in candidate_ports:
+ if port in exclude_ports:
+ continue
+
+ # 检查端口是否存在
+ if not os.path.exists(port):
+ self.node.get_logger().warn(f"端口不存在: {port}")
+ continue
+
+ self.node.get_logger().info(f"检测候选端口: {port}")
+
+ temp_reader = ForceSerialReader(
+ HandType.left,
+ excludelist=[],
+ baudrates=baudrates,
+ isdebug=serial_debug,
+ logger=serial_logger
+ )
+
+ detected = False
+ for baudrate in baudrates:
+ try:
+ if temp_reader.openserial(port=port, baudrate=baudrate):
+ temp_reader.start()
+ time.sleep(0.1)
+ temp_reader.serial_port.write(temp_reader.pack_01_data())
+
+ for _ in range(10):
+ time.sleep(0.1)
+ if temp_reader.handtype:
+ detected_ports[port] = (temp_reader.handtype, baudrate, temp_reader.version)
+ self.node.get_logger().info(f"检测到 {port}: {temp_reader.handtype} @ {baudrate}")
+ detected = True
+ break
+
+ if detected:
+ break
+ except Exception as e:
+ self.node.get_logger().debug(f"端口 {port} @ {baudrate} 检测失败: {e}")
+ finally:
+ temp_reader.stop()
+
+ if not detected:
+ self.node.get_logger().warn(f"端口 {port} 未能识别设备类型")
+
+ # 根据检测到的手型初始化
+ for port, (handtype, baudrate, version) in detected_ports.items():
+ if handtype == 'Left' and not left_found:
+ self.force_reader_left = ForceSerialReader(
+ HandType.left,
+ excludelist=exclude_ports,
+ baudrates=baudrates,
+ isdebug=serial_debug,
+ logger=serial_logger
+ )
+ if self.force_reader_left.openserial(port=port, baudrate=baudrate):
+ self.force_reader_left.start()
+ self.force_reader_left.serial_port.write(self.force_reader_left.pack_01_data())
+ self.leftport = port
+ self.leftbaudrate = baudrate
+ left_found = True
+ self.node.get_logger().info(f"左手已连接: {port} @ {baudrate}, 版本 {version}")
+ if version:
+ self.lefthand.set_glove_version(version)
+ self.node.get_logger().info(f"[手套版本] 左手: v{version.split('.')[0]} ({version})")
+
+ elif handtype == 'Right' and not right_found:
+ self.force_reader_right = ForceSerialReader(
+ HandType.right,
+ excludelist=exclude_ports + ([self.leftport] if left_found else []),
+ baudrates=baudrates,
+ isdebug=serial_debug,
+ logger=serial_logger
+ )
+ if self.force_reader_right.openserial(port=port, baudrate=baudrate):
+ self.force_reader_right.start()
+ self.force_reader_right.serial_port.write(self.force_reader_right.pack_01_data())
+ self.rightport = port
+ self.rightbaudrate = baudrate
+ right_found = True
+ self.node.get_logger().info(f"右手已连接: {port} @ {baudrate}, 版本 {version}")
+ if version:
+ self.righthand.set_glove_version(version)
+ self.node.get_logger().info(f"[手套版本] 右手: v{version.split('.')[0]} ({version})")
+
+ # 初始化未找到的 reader(占位)
+ if not left_found:
+ self.node.get_logger().error("未找到左手力反馈手套")
+ self.force_reader_left = None
+ if not right_found:
+ self.node.get_logger().error("未找到右手力反馈手套")
+ self.force_reader_right = None
+
+ return left_found, right_found
+
+ def _init_hand(self, reader, hand_type, saved_port, saved_baudrate, hand_name, auto_scan=False):
+ """初始化单个手的串口连接"""
+ found = False
+
+ # 尝试预设串口
+ if saved_port and saved_baudrate:
+ self.node.get_logger().info(f"尝试预设{hand_name}串口: {saved_port}")
+ try:
+ if reader.openserial(port=saved_port, baudrate=int(saved_baudrate)):
+ time.sleep(0.3)
+ reader.start()
+ time.sleep(0.3)
+ reader.serial_port.write(reader.pack_01_data())
+ time.sleep(0.3)
+ if reader.handtype == hand_type:
+ self.node.get_logger().info(f"预设{hand_name}串口有效, 版本{reader.version}")
+ if hand_type == 'Left':
+ self.leftport = saved_port
+ self.leftbaudrate = int(saved_baudrate)
+ if reader.version:
+ self.lefthand.set_glove_version(reader.version)
+ self.node.get_logger().info(f"[手套版本] 左手: v{reader.version.split('.')[0]} ({reader.version})")
+ else:
+ self.rightport = saved_port
+ self.rightbaudrate = int(saved_baudrate)
+ if reader.version:
+ self.righthand.set_glove_version(reader.version)
+ self.node.get_logger().info(f"[手套版本] 右手: v{reader.version.split('.')[0]} ({reader.version})")
+ found = True
+ else:
+ reader.stop()
+ self.node.get_logger().warn(f"预设{hand_name}串口类型不匹配")
+ except Exception as e:
+ self.node.get_logger().warn(f"预设{hand_name}串口无效: {e}")
+
+ # 预设无效,根据 auto_scan 决定是否搜索设备
+ if not found:
+ if auto_scan:
+ self.node.get_logger().info(f"搜索{hand_name}力反馈手套...")
+ port, baudrate, errorcode = reader.find_valid_ports(timeout=0.001)
+ if port:
+ if reader.openserial(port=port, baudrate=baudrate):
+ time.sleep(0.3)
+ reader.start()
+ time.sleep(0.3)
+ reader.serial_port.write(reader.pack_01_data())
+ time.sleep(0.3)
+ if reader.handtype == hand_type:
+ self.node.get_logger().info(f"已搜索到{hand_name}力反馈手套, 版本{reader.version}")
+ if hand_type == 'Left':
+ self.leftport = port
+ self.leftbaudrate = baudrate
+ if reader.version:
+ self.lefthand.set_glove_version(reader.version)
+ self.node.get_logger().info(f"[手套版本] 左手: v{reader.version.split('.')[0]} ({reader.version})")
+ else:
+ self.rightport = port
+ self.rightbaudrate = baudrate
+ if reader.version:
+ self.righthand.set_glove_version(reader.version)
+ self.node.get_logger().info(f"[手套版本] 右手: v{reader.version.split('.')[0]} ({reader.version})")
+ found = True
+ else:
+ self.node.get_logger().warn(f"{hand_name}无法正常识别")
+ else:
+ self.node.get_logger().warn(f"未搜索到{hand_name}力反馈手套")
+ else:
+ self.node.get_logger().warn(f"{hand_name}串口未连接(自动扫描已禁用)")
+
+ return found
+
+ def _save_serial_to_config(self, left_found, right_found):
+ """保存检测到的串口配置到 base_config.yml"""
+ config_path = Path(__file__).parent.parent.parent / "config" / "base_config.yml"
+ try:
+ with open(config_path, 'r') as f:
+ config = yaml.safe_load(f)
+
+ if 'serial' not in config:
+ config['serial'] = {}
+ if 'left' not in config['serial']:
+ config['serial']['left'] = {}
+ if 'right' not in config['serial']:
+ config['serial']['right'] = {}
+
+ if left_found:
+ config['serial']['left']['port'] = self.leftport
+ config['serial']['left']['baudrate'] = self.leftbaudrate
+ if right_found:
+ config['serial']['right']['port'] = self.rightport
+ config['serial']['right']['baudrate'] = self.rightbaudrate
+
+ with open(config_path, 'w') as f:
+ yaml.dump(config, f, default_flow_style=False)
+
+ self.node.get_logger().info(f"串口配置已保存: 左手={self.leftport}, 右手={self.rightport}")
+ except Exception as e:
+ self.node.get_logger().error(f"保存串口配置失败: {e}")
+
+ def process_callback(self):
+ if not self.runing:
+ return
+
+ self.pubprintcount += 1
+
+ left_valid = self.force_reader_left and self.force_reader_left.handtype == 'Left'
+ right_valid = self.force_reader_right and self.force_reader_right.handtype == 'Right'
+
+ warn_interval = 150
+
+ if not left_valid and not right_valid and not self.force_glove_pose:
+ if self.pubprintcount % warn_interval == 1:
+ self.node.get_logger().warn("设备未连接: 左手套(未连接), 右手套(未连接)")
+ return
+
+ if left_valid and not right_valid and not self.force_glove_pose:
+ if self.pubprintcount % warn_interval == 1:
+ self.node.get_logger().warn("设备未连接: 右手套(未连接)")
+ elif right_valid and not left_valid and not self.force_glove_pose:
+ if self.pubprintcount % warn_interval == 1:
+ self.node.get_logger().warn("设备未连接: 左手套(未连接)")
+
+
+ if left_valid or self.force_glove_pose:
+ if self.force_glove_pose:
+ left_positions = self._get_forced_positions(self.force_glove_pose, 'left')
+ if left_positions is None:
+ if left_valid:
+ left_positions = copy.deepcopy(self.force_reader_left.poslist)
+ self.node.get_logger().warn(f"强制姿态 {self.force_glove_pose} 数据不存在,使用实际数据")
+ else:
+ self.node.get_logger().warn(f"强制姿态 {self.force_glove_pose} 数据不存在且无设备")
+ return
+ else:
+ left_positions = copy.deepcopy(self.force_reader_left.poslist)
+
+ self.lefthand.joint_update(left_positions)
+ self.lefthand.speed_update()
+ if left_valid:
+ with self.forcelock:
+ if self.results['left']:
+ self.force_reader_left.forcelist = [
+ self.results['left']['thumb_matrix']['max_force'],
+ self.results['left']['index_matrix']['max_force'],
+ self.results['left']['middle_matrix']['max_force'],
+ self.results['left']['ring_matrix']['max_force'],
+ self.results['left']['little_matrix']['max_force']
+ ]
+ self.force_reader_left.serial_port.write(self.force_reader_left.pack_04_data())
+ if self.lefthandpubprint and self.pubprintcount % 5 == 0:
+ print(f"左手位置: {self.lefthand.g_jointpositions}")
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ self.publisher_l.publish(msg_l)
+
+ if self.isdebugpub:
+ msg_debug_l = Float32MultiArray()
+ msg_debug_l.data = [float(num) for num in self.lefthand.multi_state_mapper.debug_value]
+ self.publisher_hand_debugdata_l.publish(msg_debug_l)
+
+ if right_valid or self.force_glove_pose:
+ if self.force_glove_pose:
+ right_positions = self._get_forced_positions(self.force_glove_pose, 'right')
+ if right_positions is None:
+ if right_valid:
+ right_positions = copy.deepcopy(self.force_reader_right.poslist)
+ self.node.get_logger().warn(f"强制姿态 {self.force_glove_pose} 数据不存在,使用实际数据")
+ else:
+ self.node.get_logger().warn(f"强制姿态 {self.force_glove_pose} 数据不存在且无设备")
+ return
+ else:
+ right_positions = copy.deepcopy(self.force_reader_right.poslist)
+
+ self.righthand.joint_update(right_positions)
+ self.righthand.speed_update()
+ if right_valid:
+ with self.forcelock:
+ if self.results['right']:
+ self.force_reader_right.forcelist = [
+ self.results['right']['thumb_matrix']['max_force'],
+ self.results['right']['index_matrix']['max_force'],
+ self.results['right']['middle_matrix']['max_force'],
+ self.results['right']['ring_matrix']['max_force'],
+ self.results['right']['little_matrix']['max_force']
+ ]
+ self.force_reader_right.serial_port.write(self.force_reader_right.pack_04_data())
+ if self.righthandpubprint and self.pubprintcount % 5 == 0:
+ print(f"右手位置: {self.righthand.g_jointpositions}")
+
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ if self.isdebugpub:
+ msg_debug_r = Float32MultiArray()
+ msg_debug_r.data = [float(num) for num in self.righthand.multi_state_mapper.debug_value]
+
+ self.publisher_hand_debugdata_r.publish(msg_debug_r)
+
+ self.pubprintcount += 1
+
+ def _calculate_weighted_average(self, data_list):
+ """
+ 计算加权平均值,后面的数据权重更高
+
+ Args:
+ data_list: 包含多帧数据的列表,每帧是21个关节值的列表
+
+ Returns:
+ 加权平均后的21个关节值列表
+ """
+ if not data_list:
+ return [0.0] * 21
+
+ n = len(data_list)
+ if n == 1:
+ return data_list[0]
+
+ # 生成权重:后面的数据权重更高
+ weights = np.array([i + 1 for i in range(n)], dtype=float)
+ weights = weights / weights.sum()
+
+ # 转换为numpy数组进行计算
+ data_array = np.array(data_list)
+
+ # 加权平均
+ weighted_avg = np.average(data_array, axis=0, weights=weights)
+
+ return weighted_avg.tolist()
+
+ def _check_stability(self, window_size=20, threshold=0.05):
+ """
+ 检测手势稳定性
+
+ Args:
+ window_size: 检测窗口大小(帧数)
+ threshold: 稳定性阈值(关节角度方差)
+
+ Returns:
+ (is_stable, variance): 是否稳定,当前方差
+ """
+ if len(self.calibration_data_left) < window_size:
+ return False, 1.0
+
+ recent_left = self.calibration_data_left[-window_size:]
+ recent_right = self.calibration_data_right[-window_size:]
+
+ var_left = np.var(recent_left, axis=0).mean()
+ var_right = np.var(recent_right, axis=0).mean()
+
+ is_stable = var_left < threshold and var_right < threshold
+ return is_stable, max(var_left, var_right)
+
+ def _calibration_with_progress(self, stability_window=30, stability_threshold=0.03):
+ """
+ 带稳定性检测的标定数据采集
+
+ Args:
+ stability_window: 稳定性检测窗口(帧数)
+ stability_threshold: 稳定性阈值
+ """
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+
+ temp_buffer_left = []
+ temp_buffer_right = []
+
+ stability_samples = []
+ stable_start = None
+ collected_duration = 0
+ target_stable_duration = 5.0
+
+ with tqdm(total=100, desc=f"{Fore.CYAN}标定进度{Fore.RESET}",
+ bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{postfix}]",
+ postfix="") as pbar:
+ while collected_duration < target_stable_duration:
+ left_pos = copy.deepcopy(self.force_reader_left.poslist)
+ right_pos = copy.deepcopy(self.force_reader_right.poslist)
+
+ temp_buffer_left.append(left_pos)
+ temp_buffer_right.append(right_pos)
+
+ if len(temp_buffer_left) > stability_window:
+ temp_buffer_left.pop(0)
+ temp_buffer_right.pop(0)
+
+ if len(temp_buffer_left) < stability_window:
+ pbar.set_postfix_str(f"{Fore.YELLOW}等待数据 {len(temp_buffer_left)}/{stability_window}{Fore.RESET}")
+ pbar.refresh()
+ time.sleep(1.0 / 30)
+ continue
+
+ var_left = np.var(temp_buffer_left, axis=0).mean()
+ var_right = np.var(temp_buffer_right, axis=0).mean()
+
+ drift_left = np.abs(np.array(temp_buffer_left[-1]) - np.array(temp_buffer_left[0])).mean()
+ drift_right = np.abs(np.array(temp_buffer_right[-1]) - np.array(temp_buffer_right[0])).mean()
+
+ variance = max(var_left, var_right)
+ drift = max(drift_left, drift_right)
+
+ is_stable = (variance < stability_threshold and drift < stability_threshold)
+
+ if is_stable:
+ if stable_start is None:
+ stable_start = time.time()
+
+ stability_samples.append((left_pos, right_pos))
+ collected_duration = time.time() - stable_start
+
+ progress = min(100, int(collected_duration / target_stable_duration * 100))
+ pbar.n = progress
+ pbar.last_print_n = progress
+ pbar.set_postfix_str(f"{Fore.GREEN}稳定 {collected_duration:.1f}s var={variance:.3f} drift={drift:.3f}{Fore.RESET}")
+ else:
+ stable_start = None
+ stability_samples = []
+ collected_duration = 0
+ pbar.n = 0
+ pbar.last_print_n = 0
+ pbar.set_postfix_str(f"{Fore.YELLOW}等待稳定 var={variance:.3f} drift={drift:.3f}{Fore.RESET}")
+
+ pbar.refresh()
+ time.sleep(1.0 / 30)
+
+ if len(stability_samples) > 0:
+ self.calibration_data_left = [s[0] for s in stability_samples]
+ self.calibration_data_right = [s[1] for s in stability_samples]
+ print(f"{Fore.GREEN}采集完成,有效样本: {len(stability_samples)} 帧{Fore.RESET}")
+
+ def run_calibration(self):
+ """
+ 执行自动标定流程
+ """
+ # 检查手套连接状态
+ left_valid = self.force_reader_left and self.force_reader_left.handtype == 'Left'
+ right_valid = self.force_reader_right and self.force_reader_right.handtype == 'Right'
+
+ if not left_valid and not right_valid:
+ print(f"\n{Fore.RED}【标定失败】左右手套均未连接,请检查设备连接后重试{Fore.RESET}\n")
+ self.calibration_in_progress = False
+ return False
+
+ self.calibration_in_progress = True
+
+ # ===== 第一步:五指张开标定 (对应255) =====
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+ total_steps = 2 if not self.show_fist_calibration else 3
+ print(f"\n{Fore.GREEN}{'='*50}{Fore.RESET}")
+ print(f"{Fore.GREEN}【标定 1/{total_steps}】请保持五指张开姿势 (对应电机值255){Fore.RESET}")
+ print(f"{Fore.GREEN}{'='*50}{Fore.RESET}\n")
+
+ self._calibration_with_progress(10)
+
+ if len(self.calibration_data_left) > 0:
+ avg_left_open = self._calculate_weighted_average(self.calibration_data_left)
+ avg_right_open = self._calculate_weighted_average(self.calibration_data_right)
+ self.lefthand.calibrationoriginal = avg_left_open
+ self.righthand.calibrationoriginal = avg_right_open
+ else:
+ print(f"\n{Fore.RED}【标定失败】五指张开数据采集失败{Fore.RESET}\n")
+ self.calibration_in_progress = False
+ return False
+
+ # ===== 第二步:O型标定 (对应中间值) =====
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+ print(f"\n{Fore.MAGENTA}{'='*50}{Fore.RESET}")
+ print(f"{Fore.MAGENTA}【标定 2/{total_steps}】请保持O型手势 (对应电机中间值){Fore.RESET}")
+ print(f"{Fore.MAGENTA}{'='*50}{Fore.RESET}\n")
+
+ self._calibration_with_progress(10)
+
+ if len(self.calibration_data_left) > 0:
+ avg_left_opose = self._calculate_weighted_average(self.calibration_data_left)
+ avg_right_opose = self._calculate_weighted_average(self.calibration_data_right)
+ self.lefthand.calibrationopose = avg_left_opose
+ self.righthand.calibrationopose = avg_right_opose
+ else:
+ print(f"\n{Fore.RED}【标定失败】O型手势数据采集失败{Fore.RESET}\n")
+ self.calibration_in_progress = False
+ return False
+
+ # ===== 第三步:握拳标定 (可选) =====
+ if self.show_fist_calibration:
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+ print(f"\n{Fore.YELLOW}{'='*50}{Fore.RESET}")
+ print(f"{Fore.YELLOW}【标定 3/{total_steps}】请握紧拳头 (对应电机值0){Fore.RESET}")
+ print(f"{Fore.YELLOW}{'='*50}{Fore.RESET}\n")
+
+ self._calibration_with_progress(10)
+
+ if len(self.calibration_data_left) > 0:
+ avg_left_fist = self._calculate_weighted_average(self.calibration_data_left)
+ avg_right_fist = self._calculate_weighted_average(self.calibration_data_right)
+ self.lefthand.calibrationfistpose = avg_left_fist
+ self.righthand.calibrationfistpose = avg_right_fist
+ else:
+ print(f"\n{Fore.RED}【标定失败】握拳数据采集失败{Fore.RESET}\n")
+ self.calibration_in_progress = False
+ return False
+ else:
+ self._calculate_fist_from_extension()
+
+ # ===== 保存标定数据 =====
+ if self._save_to_tmp():
+ print(f"\n{Fore.GREEN}{'='*50}{Fore.RESET}")
+ print(f"{Fore.GREEN}【标定完成】{'三个' if self.show_fist_calibration else '两个'}姿势数据已保存{Fore.RESET}")
+ print(f"{Fore.GREEN}{'='*50}{Fore.RESET}\n")
+
+ self.calibration_in_progress = False
+ self.righthand.initialize_mapper()
+ self.lefthand.initialize_mapper()
+ return True
+
+ def _calculate_fist_from_extension(self):
+ """
+ 从 original 和 opose 延伸计算 fist 值
+ fist = opose + (opose - original) * extend_ratio
+ """
+ import numpy as np
+
+ original_l = self.lefthand.calibrationoriginal
+ original_r = self.righthand.calibrationoriginal
+ opose_l = self.lefthand.calibrationopose
+ opose_r = self.righthand.calibrationopose
+
+ if original_l is None or opose_l is None:
+ return
+
+ ratio = self.fist_extend_ratio
+
+ fist_l = []
+ fist_r = []
+ for i in range(len(original_l)):
+ fist_l.append(opose_l[i] + (opose_l[i] - original_l[i]) * ratio)
+ for i in range(len(original_r)):
+ fist_r.append(opose_r[i] + (opose_r[i] - original_r[i]) * ratio)
+
+ self.lefthand.calibrationfistpose = fist_l
+ self.righthand.calibrationfistpose = fist_r
+
+ self.node.get_logger().info(f"[自动计算] 握拳值已从 O型延伸 {ratio*100:.0f}% 生成")
+
+ def _save_to_tmp(self):
+ """
+ 保存标定数据到临时文件 (JSON格式,与ROS2一致)
+ - jointangleoriginal: 五指张开 (对应电机255)
+ - jointanglefist: 握拳 (对应电机0)
+ """
+ # v2.8.6 版本添加标定差异检测
+ def calculate_vector_difference(vec1, vec2):
+ """计算两个向量之间的差异"""
+ if not vec1 or not vec2:
+ return 0
+
+ # 确保向量长度一致
+ min_len = min(len(vec1), len(vec2))
+ if min_len == 0:
+ return 0
+
+ # 计算欧几里得距离作为差异度量
+ squared_diff = 0
+ for i in range(min_len):
+ squared_diff += (vec1[i] - vec2[i]) ** 2
+ return math.sqrt(squared_diff)
+
+ # 检查右手标定数据的差异
+ right_diff_original_fist = calculate_vector_difference(
+ self.righthand.calibrationoriginal,
+ self.righthand.calibrationfistpose
+ )
+
+ # 检查左手标定数据的差异
+ left_diff_original_fist = calculate_vector_difference(
+ self.lefthand.calibrationoriginal,
+ self.lefthand.calibrationfistpose
+ )
+
+ # 设置阈值,根据实际情况调整
+ # 这个阈值表示两个向量之间的最小可接受差异
+ MIN_DIFFERENCE_THRESHOLD = 3.0
+
+ # 检查右手数据是否有效
+ right_valid = False
+ if hasattr(self, 'force_reader_right') and self.force_reader_right.handtype == 'Right':
+ # 检查original和fistpose的差异
+ if right_diff_original_fist > MIN_DIFFERENCE_THRESHOLD:
+ right_valid = True
+ else:
+ self.node.get_logger().error("右手张手和握拳标定数据差异过小,可能未正确标定")
+
+ # 检查左手数据是否有效
+ left_valid = False
+ if hasattr(self, 'force_reader_left') and self.force_reader_left.handtype == 'Left':
+ # 检查original和fistpose的差异
+ if left_diff_original_fist > MIN_DIFFERENCE_THRESHOLD:
+ left_valid = True
+ else:
+ self.node.get_logger().error("左手张手和握拳标定数据差异过小,可能未正确标定")
+
+ # 如果没有有效的新数据,直接返回不保存
+ if not right_valid and not left_valid:
+ self.node.get_logger().error("没有有效的标定数据差异,取消保存")
+ return False
+
+ data = {
+ "timestamp": datetime.now().isoformat(),
+ "jointangleoriginal_r": self.righthand.calibrationoriginal,
+ "jointangleoriginal_l": self.lefthand.calibrationoriginal,
+ "jointanglefist_r": self.righthand.calibrationfistpose,
+ "jointanglefist_l": self.lefthand.calibrationfistpose,
+ "jointangleopose_r": self.righthand.calibrationopose,
+ "jointangleopose_l": self.lefthand.calibrationopose
+ }
+
+ try:
+ TMP_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
+ if TMP_FILE_PATH.exists():
+ content = TMP_FILE_PATH.read_text()
+ historydata = json.loads(content)
+ # 根据有效性更新数据
+ if not right_valid:
+ data["jointangleoriginal_r"] = historydata["jointangleoriginal_r"]
+ data["jointanglefist_r"] = historydata["jointanglefist_r"]
+ data["jointangleopose_r"] = historydata["jointangleopose_r"]
+ # 同时更新 righthand 的标定属性
+ self.righthand.calibrationoriginal = historydata["jointangleoriginal_r"]
+ self.righthand.calibrationfistpose = historydata["jointanglefist_r"]
+ self.righthand.calibrationopose = historydata["jointangleopose_r"]
+ self.node.get_logger().warning("右手未能正确标定,采用上一次的标定内容。")
+ if not left_valid:
+ data["jointangleoriginal_l"] = historydata["jointangleoriginal_l"]
+ data["jointanglefist_l"] = historydata["jointanglefist_l"]
+ data["jointangleopose_l"] = historydata["jointangleopose_l"]
+ # 同时更新 lefthand 的标定属性
+ self.lefthand.calibrationoriginal = historydata["jointangleoriginal_l"]
+ self.lefthand.calibrationfistpose = historydata["jointanglefist_l"]
+ self.lefthand.calibrationopose = historydata["jointangleopose_l"]
+ self.node.get_logger().warning("左手未能正确标定,采用上一次的标定内容。")
+ json_str = json.dumps(data, indent=2)
+ TMP_FILE_PATH.write_text(json_str)
+ self.node.get_logger().info("标定数据保存成功")
+ return True
+ except Exception as e:
+ self.node.get_logger().error(f"保存失败: {e}")
+ return False
+
+ def _load_from_tmp(self):
+ """
+ 从临时文件读取数据 (JSON格式)
+ 如果文件不存在,自动从样本数据(YAML)加载并保存
+ """
+ data = None
+ from_sample = False
+
+ if TMP_FILE_PATH.exists():
+ try:
+ content = TMP_FILE_PATH.read_text()
+ data = json.loads(content)
+ self.node.get_logger().info("加载用户标定数据")
+ except Exception as e:
+ self.node.get_logger().error(f"读取标定数据失败: {e}")
+
+ if data is None and SAMPLE_FILE_PATH.exists():
+ try:
+ with open(SAMPLE_FILE_PATH, 'r') as f:
+ data = yaml.safe_load(f)
+ from_sample = True
+ self.node.get_logger().info("首次使用,加载样本标定数据")
+ except Exception as e:
+ self.node.get_logger().error(f"读取样本数据失败: {e}")
+
+ if data is None:
+ self.node.get_logger().warn("标定数据不存在")
+ return False
+
+ if 'timestamp' not in data or not data['timestamp']:
+ self.node.get_logger().warn("无效的时间戳...")
+ return False
+
+ try:
+ saved_time = datetime.fromisoformat(str(data['timestamp']))
+ current_time = datetime.now()
+ time_diff = current_time - saved_time
+ if time_diff > timedelta(days=30):
+ self.node.get_logger().warn("标定数据已超过30天有效期,建议重新标定...")
+ except:
+ pass
+
+ self.righthand.calibrationoriginal = data.get('jointangleoriginal_r')
+ self.lefthand.calibrationoriginal = data.get('jointangleoriginal_l')
+
+ if data.get('jointanglefist_r'):
+ self.righthand.calibrationfistpose = data['jointanglefist_r']
+ if data.get('jointanglefist_l'):
+ self.lefthand.calibrationfistpose = data['jointanglefist_l']
+
+ if data.get('jointangleopose_r'):
+ self.righthand.calibrationopose = data['jointangleopose_r']
+ if data.get('jointangleopose_l'):
+ self.lefthand.calibrationopose = data['jointangleopose_l']
+
+ if self.lefthand.calibrationfistpose is None or self.righthand.calibrationfistpose is None:
+ if self.lefthand.calibrationoriginal and self.lefthand.calibrationopose:
+ self._calculate_fist_from_extension()
+
+ if from_sample:
+ TMP_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
+ with open(TMP_FILE_PATH, 'w') as f:
+ json.dump(data, f, indent=2)
+ self.node.get_logger().info("样本标定数据已保存到用户标定文件")
+
+ self.node.get_logger().info("标定数据加载成功")
+ return True
+
+ def _get_forced_positions(self, pose_type: str, hand_type: str):
+ """
+ 获取强制姿态的标定数据
+
+ Args:
+ pose_type: 姿态类型 (open/fist/opose)
+ hand_type: 手类型 (left/right)
+
+ Returns:
+ 位置数据列表,如果不存在返回 None
+ """
+ if not TMP_FILE_PATH.exists():
+ return None
+
+ if self.calibration_cache is None:
+ try:
+ content = TMP_FILE_PATH.read_text()
+ self.calibration_cache = json.loads(content)
+ except:
+ return None
+
+ hand = hand_type.lower()
+ pose_map = {
+ 'open': f'jointangleoriginal_{hand[0]}',
+ 'fist': f'jointanglefist_{hand[0]}',
+ 'opose': f'jointangleopose_{hand[0]}'
+ }
+
+ key = pose_map.get(pose_type)
+ if key and self.calibration_cache:
+ return self.calibration_cache.get(key)
+ return None
+
+ def process(self):
+ """主处理函数"""
+ # 初始化串口连接
+ self.linkerforce_init()
+ # 执行标定(如果需要)
+ if self.calibration == "auto_calibrate":
+ if not self.run_calibration():
+ self.node.get_logger().error("标定失败,退出程序")
+ return
+ self.calibration = -1
+ self.node.create_timer(1.0/30, self.process_callback) # 30Hz
+
+ def stop_serial_threads(self):
+ """停止串口线程,在 destroy_node 时调用"""
+ self.runing = False
+ if self.force_reader_left:
+ self.force_reader_left.stop()
+ if self.force_reader_right:
+ self.force_reader_right.stop()
+ self.node.get_logger().info("串口线程已停止")
+
+ def set_mode(self, mode, param=None):
+ """
+ 设置遥操作模式
+
+ Args:
+ mode: 运行模式
+ - 'glove': 使用手套数据
+ - 'fixed_opose': 使用固定O型姿态
+ - 'fixed_fist': 使用固定握拳姿态
+ param: 额外参数 (dict)
+ - serial_debug: bool, 开启串口调试
+ - mapper_debug: bool, 开启映射器调试
+ """
+ if param is None:
+ param = {}
+
+ # 处理串口调试开关
+ if 'serial_debug' in param:
+ debug_enabled = param['serial_debug']
+ if hasattr(self.force_reader_left, 'isdebug'):
+ self.force_reader_left.isdebug = debug_enabled
+ if hasattr(self.force_reader_right, 'isdebug'):
+ self.force_reader_right.isdebug = debug_enabled
+ self.node.get_logger().info(f"串口调试: {'开启' if debug_enabled else '关闭'}")
+
+ # 处理映射器调试开关
+ if 'mapper_debug' in param:
+ mapper_debug_enabled = param['mapper_debug']
+ if hasattr(self.righthand, 'multi_state_mapper') and hasattr(self.righthand.multi_state_mapper, 'set_debug'):
+ self.righthand.multi_state_mapper.set_debug(mapper_debug_enabled)
+ if hasattr(self.lefthand, 'multi_state_mapper') and hasattr(self.lefthand.multi_state_mapper, 'set_debug'):
+ self.lefthand.multi_state_mapper.set_debug(mapper_debug_enabled)
+ if isinstance(mapper_debug_enabled, list):
+ fingers_str = ', '.join(mapper_debug_enabled) if mapper_debug_enabled else '全部'
+ self.node.get_logger().info(f"映射器调试: 开启 (手指: {fingers_str})")
+ else:
+ self.node.get_logger().info(f"映射器调试: {'开启' if mapper_debug_enabled else '关闭'}")
+
+ # 处理强制手套数据源
+ if 'force_glove_pose' in param:
+ pose = param['force_glove_pose']
+ if pose in ['open', 'fist', 'opose', 'none', None]:
+ self.force_glove_pose = pose if pose != 'none' else None
+ if self.force_glove_pose:
+ self.node.get_logger().info(f"强制手套数据源: {self.force_glove_pose}")
+ else:
+ self.node.get_logger().info("强制手套数据源: 关闭,使用实际数据")
+ else:
+ self.node.get_logger().warn(f"无效的强制姿态: {pose},可选: open/fist/opose/none")
+
+ # 处理延伸指数因子
+ if 'mapper_exp_factor' in param:
+ exp_factor = param['mapper_exp_factor']
+ if isinstance(exp_factor, dict):
+ # 指定手指设置
+ for finger, value in exp_factor.items():
+ if hasattr(self.righthand, 'multi_state_mapper') and hasattr(self.righthand.multi_state_mapper, 'exp_factors'):
+ if finger in self.righthand.multi_state_mapper.exp_factors:
+ self.righthand.multi_state_mapper.exp_factors[finger] = value
+ if hasattr(self.lefthand, 'multi_state_mapper') and hasattr(self.lefthand.multi_state_mapper, 'exp_factors'):
+ if finger in self.lefthand.multi_state_mapper.exp_factors:
+ self.lefthand.multi_state_mapper.exp_factors[finger] = value
+ fingers_str = ', '.join([f"{k}:{v}" for k, v in exp_factor.items()])
+ self.node.get_logger().info(f"延伸指数因子(指定): {fingers_str}")
+ else:
+ # 全部手指设置
+ if hasattr(self.righthand, 'multi_state_mapper') and hasattr(self.righthand.multi_state_mapper, 'exp_factors'):
+ for finger in self.righthand.multi_state_mapper.exp_factors:
+ self.righthand.multi_state_mapper.exp_factors[finger] = exp_factor
+ if hasattr(self.lefthand, 'multi_state_mapper') and hasattr(self.lefthand.multi_state_mapper, 'exp_factors'):
+ for finger in self.lefthand.multi_state_mapper.exp_factors:
+ self.lefthand.multi_state_mapper.exp_factors[finger] = exp_factor
+ self.node.get_logger().info(f"延伸指数因子(全部): {exp_factor}")
+
+ # 处理缩放因子
+ if 'mapper_scale_factor' in param:
+ scale_factor = param['mapper_scale_factor']
+ if isinstance(scale_factor, dict):
+ # 指定手指设置
+ for finger, value in scale_factor.items():
+ if hasattr(self.righthand, 'multi_state_mapper') and hasattr(self.righthand.multi_state_mapper, 'scale_factors'):
+ if finger in self.righthand.multi_state_mapper.scale_factors:
+ self.righthand.multi_state_mapper.scale_factors[finger] = value
+ if hasattr(self.lefthand, 'multi_state_mapper') and hasattr(self.lefthand.multi_state_mapper, 'scale_factors'):
+ if finger in self.lefthand.multi_state_mapper.scale_factors:
+ self.lefthand.multi_state_mapper.scale_factors[finger] = value
+ fingers_str = ', '.join([f"{k}:{v}" for k, v in scale_factor.items()])
+ self.node.get_logger().info(f"缩放因子(指定): {fingers_str}")
+ else:
+ # 全部手指设置
+ if hasattr(self.righthand, 'multi_state_mapper') and hasattr(self.righthand.multi_state_mapper, 'scale_factors'):
+ for finger in self.righthand.multi_state_mapper.scale_factors:
+ self.righthand.multi_state_mapper.scale_factors[finger] = scale_factor
+ if hasattr(self.lefthand, 'multi_state_mapper') and hasattr(self.lefthand.multi_state_mapper, 'scale_factors'):
+ for finger in self.lefthand.multi_state_mapper.scale_factors:
+ self.lefthand.multi_state_mapper.scale_factors[finger] = scale_factor
+ self.node.get_logger().info(f"缩放因子(全部): {scale_factor}")
+
+ # 模式切换(仅当 mode 有明确值时)
+ if mode == 'glove':
+ if hasattr(self.righthand, 'use_fixed_pose'):
+ self.righthand.use_fixed_pose = False
+ if hasattr(self.lefthand, 'use_fixed_pose'):
+ self.lefthand.use_fixed_pose = False
+ self.node.get_logger().info("模式切换: 手套数据")
+
+ elif mode == 'fixed_opose':
+ if hasattr(self.righthand, 'use_fixed_pose'):
+ self.righthand.use_fixed_pose = True
+ self.righthand.fixed_pose = self.righthand.robot_opose
+ if hasattr(self.lefthand, 'use_fixed_pose'):
+ self.lefthand.use_fixed_pose = True
+ self.lefthand.fixed_pose = self.lefthand.robot_opose
+ self.node.get_logger().info("模式切换: 固定O型姿态")
+
+ elif mode == 'fixed_fist':
+ if hasattr(self.righthand, 'use_fixed_pose'):
+ self.righthand.use_fixed_pose = True
+ self.righthand.fixed_pose = self.righthand.robot_fist
+ if hasattr(self.lefthand, 'use_fixed_pose'):
+ self.lefthand.use_fixed_pose = True
+ self.lefthand.fixed_pose = self.lefthand.robot_fist
+ self.node.get_logger().info("模式切换: 固定握拳姿态")
+
+ elif mode is not None:
+ self.node.get_logger().warn(f"未知模式: {mode}")
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/tmp/.gitkeep b/src/linkerhand_retarget/linkerhand_retarget/motion/linkerforce/tmp/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README.md
new file mode 100644
index 0000000..52e63d9
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README.md
@@ -0,0 +1,62 @@
+# LinkerMCG Data Glove Module
+
+LinkerMCG data glove ROS2 driver module, receives glove data via UDP and publishes to ROS2 topics.
+
+## Features
+
+- Receives glove data via UDP protocol
+- Supports left and right hand data
+- 50Hz publish rate
+
+## Usage
+
+### 1. Config File
+
+Edit `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: linkermcg
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. Launch
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 Topics
+
+### Published Topics
+
+| Topic | Message Type | Description |
+|-------|-------------|-------------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | Right hand control data |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | Left hand control data |
+
+### Message Format
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # Timestamp
+msg.name # Joint name list
+msg.position # Joint position values
+```
+
+## File Structure
+
+```
+motion/linkermcg/
+├── __init__.py
+├── retarget.py # ROS2 integration
+└── README.md # English documentation
+```
+
+## Notes
+
+1. Ensure UDP port is not occupied
+2. Glove and host must be on the same network
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README_zh.md b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README_zh.md
new file mode 100644
index 0000000..cf4c2af
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/README_zh.md
@@ -0,0 +1,62 @@
+# LinkerMCG 数据手套模块
+
+LinkerMCG 数据手套的 ROS2 驱动模块,通过 UDP 接收手套数据并发布到 ROS2 话题。
+
+## 特性
+
+- 通过 UDP 协议接收手套数据
+- 支持左右手数据
+- 发布频率 50Hz
+
+## 使用方法
+
+### 1. 配置文件
+
+修改 `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: linkermcg
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. 运行
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 话题
+
+### 发布话题
+
+| 话题名 | 消息类型 | 说明 |
+|-------|---------|------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | 右手控制数据 |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | 左手控制数据 |
+
+### 消息格式
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # 时间戳
+msg.name # 关节名称列表
+msg.position # 关节位置值
+```
+
+## 文件结构
+
+```
+motion/linkermcg/
+├── __init__.py
+├── retarget.py # ROS2 集成
+└── README.md # 本文档
+```
+
+## 注意事项
+
+1. 确保 UDP 端口未被占用
+2. 手套与主机需在同一网络
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l10v7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l10v7.py
new file mode 100644
index 0000000..0a349c1
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l10v7.py
@@ -0,0 +1,143 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+ self.g_jointpositions[6] = joint_arc[6] # 食指侧摆
+ self.g_jointpositions[7] = joint_arc[8] # 无名指侧摆
+ self.g_jointpositions[8] = joint_arc[9] # 小指侧摆
+ self.g_jointpositions[9] = joint_arc[10] # 旋转
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+ self.g_jointpositions[6] = joint_arc[6] # 食指侧摆
+ self.g_jointpositions[7] = joint_arc[8] # 无名指侧摆
+ self.g_jointpositions[8] = joint_arc[9] # 小指侧摆
+ self.g_jointpositions[9] = joint_arc[10] # 旋转
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l20.py
new file mode 100644
index 0000000..53785fa
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l20.py
@@ -0,0 +1,123 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc[:20] # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_rimit = 2
+ fast_rimit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_rimit < position_error < fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_rimit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc[:20] # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 2
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l21.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l21.py
new file mode 100644
index 0000000..b2244e6
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l21.py
@@ -0,0 +1,123 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l25.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l25.py
new file mode 100644
index 0000000..53785fa
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l25.py
@@ -0,0 +1,123 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc[:20] # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_rimit = 2
+ fast_rimit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_rimit < position_error < fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_rimit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions = joint_arc[:20] # 完整复制
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 2
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l6.py
new file mode 100644
index 0000000..02105bf
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l6.py
@@ -0,0 +1,161 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+
+
+ # qpos[17] = joint_arc[10] # 旋转
+
+ # qpos[19] = joint_arc[15] # 中部关节
+ # qpos[20] = joint_arc[20] # 远端关节
+
+ # qpos[0] = joint_arc[6] # 食指侧摆
+ # qpos[1] = joint_arc[1] # 食指根部关节
+ # qpos[2] = joint_arc[16] # 食指中部关节
+ # qpos[3] = joint_arc[21] # 食指远端关节
+
+ # qpos[4] = joint_arc[9] # 小指侧摆
+ # qpos[5] = joint_arc[4] # 小指根部关节
+ # qpos[6] = joint_arc[19] # 小指中部关节
+ # qpos[7] = joint_arc[24] # 小指远端关节
+
+ # qpos[8] = joint_arc[7] # 中指侧摆
+ # qpos[9] = joint_arc[2] # 中指根部关节
+ # qpos[10] = joint_arc[17] # 中指中部关节
+ # qpos[11] = joint_arc[22] # 中指远端关节
+
+ # qpos[12] = joint_arc[8] # 无名指侧摆
+ # qpos[13] = joint_arc[3] # 无名指根部关节
+ # qpos[14] = joint_arc[18] # 无名指中部关节
+ # qpos[15] = joint_arc[23] # 无名指远端关节
+ # self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l7.py
new file mode 100644
index 0000000..9261653
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_l7.py
@@ -0,0 +1,136 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+ self.g_jointpositions[6] = joint_arc[10] # 旋转
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+ self.g_jointpositions[6] = joint_arc[10] # 旋转
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_o6.py
new file mode 100644
index 0000000..5ea5083
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/hand/linkermcg_o6.py
@@ -0,0 +1,134 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ self.g_jointpositions[0] = joint_arc[0] # 根部关节
+ self.g_jointpositions[1] = joint_arc[5] # 侧摆
+ self.g_jointpositions[2] = joint_arc[1] # 食指根部关节
+ self.g_jointpositions[3] = joint_arc[2] # 中指根部关节
+ self.g_jointpositions[4] = joint_arc[3] # 无名指根部关节
+ self.g_jointpositions[5] = joint_arc[4] # 小指根部关节
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/retarget.py
new file mode 100644
index 0000000..bbfb120
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/linkermcg/retarget.py
@@ -0,0 +1,174 @@
+import time
+import rclpy
+import sys
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from pathlib import Path
+
+_project_root = Path(__file__).absolute().parent.parent.parent
+_project_root_str = str(_project_root)
+
+if _project_root_str in sys.path:
+ sys.path.remove(_project_root_str)
+sys.path.insert(0, _project_root_str)
+
+
+from linkerhand.linkermcgcore import HaoCunScoketUdp
+from linkerhand.handcore import HandCore
+from linkerhand.constants import RobotName, ROBOT_LEN_MAP
+
+
+
+class Retarget():
+ def __init__(self,node, ip, port, lefthand: RobotName, righthand: RobotName, handcore: HandCore,
+ lefthandpubprint: bool, righthandpubprint: bool):
+ self.node = node
+ self.udp_ip = ip
+ self.udp_port = port
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.running = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+
+ # 根据右手类型初始化
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v3:
+ from .hand.linkermcg_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.o6:
+ from .hand.linkermcg_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l6:
+ from .hand.linkermcg_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l25 \
+ or self.righthandtype == RobotName.g20:
+ from .hand.linkermcg_l25 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.t25:
+ # from .hand.linkermcg_t25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l20:
+ from .hand.linkermcg_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 :
+ from .hand.linkermcg_l10v7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l21:
+ from .hand.linkermcg_l21 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+
+ # 根据LEFT手类型初始化
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v3:
+ from .hand.linkermcg_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.linkermcg_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.linkermcg_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l25 \
+ or self.lefthandtype == RobotName.g20:
+ from .hand.linkermcg_l25 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ # elif self.lefthandtype == RobotName.t25:
+ # from .hand.linkermcg_t25 import LeftHand
+ # self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.linkermcg_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 :
+ from .hand.linkermcg_l10v7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l21:
+ from .hand.linkermcg_l21 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ # 创建ROS定时器,以固定频率处理数据
+ # 参数1: period 周期(秒)
+ # 参数2: callback 回调函数
+ # 参数3: oneshot 是否只执行一次
+ self.timer = self.node.create_timer(1.0/120, self.process_callback) # 120Hz
+ self.pubprintcount = 0
+ self.pubprintcount = 0
+
+ self.pubprintcount = 0
+ self.udp_datacapture = None
+
+ def initialize_udp(self):
+ """初始化UDP连接"""
+ self.udp_datacapture = HaoCunScoketUdp(
+ host=self.udp_ip,
+ port=self.udp_port)
+ if self.udp_datacapture.udp_initial():
+ self.node.get_logger().info("UDP连接初始化成功")
+ self.running = True
+ else:
+ self.node.get_logger().error("UDP连接初始化失败")
+
+ def process_callback(self):
+ if not self.running:
+ return
+
+ mocapdata = self.udp_datacapture.realmocapdata
+ if not mocapdata.is_update:
+ return
+
+ # 处理左右手原始数据
+ self.lefthand.joint_update(mocapdata.jointangle_lHand)
+ self.righthand.joint_update(mocapdata.jointangle_rHand)
+
+ # 速度环节处理
+ self.lefthand.speed_update()
+ self.righthand.speed_update()
+
+ # 调试打印
+ if self.lefthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"左手位置: {self.lefthand.g_jointpositions}")
+ if self.righthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"右手位置: {self.righthand.g_jointpositions}")
+
+ # 发布右手数据
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ # 发布左手数据
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ msg_l.velocity = [float(num) for num in self.lefthand.g_jointvelocity]
+ self.publisher_l.publish(msg_l)
+
+ def process(self):
+ """主处理函数"""
+ self.initialize_udp()
+ try:
+ while rclpy.ok():
+ rclpy.spin_once(self.node, timeout_sec=0.1)
+ except rclpy.ROSInterruptException:
+ pass
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l10v7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l10v7.py
new file mode 100644
index 0000000..fcb7de0
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l10v7.py
@@ -0,0 +1,180 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.3 # 旋转
+ qpos[17] = joint_arc[20] * 2.6 # 侧摆
+ # qpos[18] = joint_arc[2] * 0 # 根部关节
+ # qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[0] * -0.6 # 远端关节
+ # print(qpos[16],qpos[17])
+
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ # qpos[2] = joint_arc[5] * -1
+ # qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ # qpos[6] = joint_arc[17] * -1
+ # qpos[7] = joint_arc[16] * -1
+
+ # qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -0.7
+ # qpos[10] = joint_arc[9] * -1
+ # qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ # qpos[14] = joint_arc[13] * -1
+ # qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 旋转
+ qpos[17] = joint_arc[2] * -1 # 侧摆
+ qpos[18] = joint_arc[2] * 0 # 根部关节
+ qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[1] * -1 # 远端关节
+ # print(qpos[16],qpos[17])
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l20.py
new file mode 100644
index 0000000..2e8790c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l20.py
@@ -0,0 +1,176 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * -1
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_rimit = 2
+ fast_rimit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_rimit < position_error < fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_rimit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = (joint_arc[2] * -1 - 0.3)
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * 0
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * 1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 2
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l21.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l21.py
new file mode 100644
index 0000000..d9ee68e
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l21.py
@@ -0,0 +1,192 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.5 # 侧摆
+ qpos[17] = joint_arc[20] * 3 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l25.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l25.py
new file mode 100644
index 0000000..bc32533
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l25.py
@@ -0,0 +1,202 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2.6 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1.5 # 远端关节
+ if joint_arc[0] > -40 * 3.14 / 180:
+ qpos[18] = 0
+ qpos[19] = 0
+ qpos[20] = 0
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 - 0.4 # 侧摆
+ qpos[17] = joint_arc[20] * 2 - 0.2 # 旋转
+ qpos[18] = joint_arc[2] * -0.2 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+ if joint_arc[0] > -50 * 3.14 / 180:
+ qpos[18] = 0.2
+ qpos[19] = 0
+ qpos[20] = 0
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * 0
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l6.py
new file mode 100644
index 0000000..81715cd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l6.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 1.5 # 旋转
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.8
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.8
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -0.8
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.8 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l7.py
new file mode 100644
index 0000000..7707080
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_l7.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[2] * -0.5 # 旋转
+ qpos[17] = joint_arc[20] * 0.8 # 侧摆
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.5 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_o6.py
new file mode 100644
index 0000000..a6cf885
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/hand/simulator_o6.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.1 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2 # 旋转
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.3 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -11
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/retarget.py
new file mode 100644
index 0000000..0664155
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/simulator/retarget.py
@@ -0,0 +1,232 @@
+
+import time
+import sapien
+import tyro
+import rclpy
+import numpy as np
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from datetime import datetime
+from pathlib import Path
+from loguru import logger
+from sapien.asset import create_dome_envmap
+from sapien.utils import Viewer
+
+from ...linkerhand.handcore import HandCore
+from ...linkerhand.constants import (
+ RetargetingType,
+ DataSource,
+ ROBOT_LEN_MAP,
+ MotionSource,
+ RobotName,
+ HandType,
+ get_default_config_path,
+)
+from ...linkerhand.retargeting_config import RetargetingConfig
+
+LOG_FILE_PATH = "/tmp/b.log"
+
+class Retarget():
+ def __init__(self,node, ip, port, deviceid, lefthand: RobotName, righthand: RobotName, handcore: HandCore,
+ lefthandpubprint: bool, righthandpubprint: bool):
+ self.node = node
+ self.udp_ip = ip
+ self.udp_port = port
+ self.motion_device = deviceid
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.runing = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+
+ # 根据右手类型初始化
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v2:
+ from .hand.simulator_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.o6:
+ from .hand.simulator_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l6:
+ from .hand.simulator_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.l25:
+ # from .hand.simulator_l25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.t25:
+ # from .hand.simulator_t25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l20:
+ from .hand.simulator_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 :
+ from .hand.simulator_l10v7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l21:
+ from .hand.simulator_l21 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+
+ # 根据LEFT手类型初始化
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v2:
+ from .hand.simulator_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.simulator_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.simulator_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l25:
+ from .hand.simulator_l25 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ # elif self.lefthandtype == RobotName.t25:
+ # from .hand.simulator_t25 import LeftHand
+ # self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.simulator_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 :
+ from .hand.simulator_l10v7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l21:
+ from .hand.simulator_l21 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+
+ # ROS2 发布器
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ self.timer = self.node.create_timer(1.0/120, self.process_callback) # 120Hz
+ self.pubprintcount = 0
+ self.udp_datacapture = None
+
+ def sapien_init(self):
+ sapien.render.set_viewer_shader_dir("default")
+ sapien.render.set_camera_shader_dir("default")
+
+ config = RetargetingConfig.load_from_file(self.config_path)
+
+ # Setup
+ scene = sapien.Scene()
+ render_mat = sapien.render.RenderMaterial()
+ render_mat.base_color = [0.06, 0.08, 0.12, 1]
+ render_mat.metallic = 0.0
+ render_mat.roughness = 0.9
+ render_mat.specular = 0.8
+ scene.add_ground(-0.2, render_material=render_mat, render_half_size=[1000, 1000])
+
+ # Lighting
+ scene.add_directional_light(np.array([1, 1, -1]), np.array([3, 3, 3]))
+ scene.add_point_light(np.array([2, 2, 2]), np.array([2, 2, 2]), shadow=False)
+ scene.add_point_light(np.array([2, -2, 2]), np.array([2, 2, 2]), shadow=False)
+ scene.set_environment_map(
+ create_dome_envmap(sky_color=[0.2, 0.2, 0.2], ground_color=[0.2, 0.2, 0.2])
+ )
+ scene.add_area_light_for_ray_tracing(
+ sapien.Pose([2, 1, 2], [0.707, 0, 0.707, 0]), np.array([1, 1, 1]), 5, 5
+ )
+
+ # Camera
+ cam = scene.add_camera(
+ name="Cheese!", width=600, height=600, fovy=1, near=0.1, far=10
+ )
+ cam.set_local_pose(sapien.Pose([0.50, 0, 0.0], [0, 0, 0, -1]))
+
+ self.viewer = Viewer()
+ self.viewer.set_scene(scene)
+ self.viewer.control_window.show_origin_frame = False
+ self.viewer.control_window.move_speed = 0.01
+ self.viewer.control_window.toggle_camera_lines(False)
+ self.viewer.set_camera_pose(cam.get_local_pose())
+
+ # Load robot and set it to a good pose to take picture
+ loader = scene.create_urdf_loader()
+ filepath = Path(config.urdf_path)
+ robot_name = filepath.stem
+ loader.load_multiple_collisions_from_file = True
+ loader.scale = 1.5
+
+ filepath = str(filepath)
+ self.robot = loader.load(filepath)
+ self.robot.set_pose(sapien.Pose([0, 0, -0.13]))
+
+ # Different robot loader may have different orders for joints
+ self.sapien_joint_names = [joint.get_name() for joint in self.robot.get_active_joints()]
+ retargeting_joint_names = self.retargeting.joint_names
+ retargeting_to_sapien = np.array(
+ [retargeting_joint_names.index(name) for name in self.sapien_joint_names]
+ ).astype(int)
+
+ self.qpos = [0] * len(self.sapien_joint_names)
+
+ def process_callback(self):
+ self.viewer.render()
+
+ """定时器回调函数,处理数据并发布"""
+ if not self.udp_datacapture or not self.udp_datacapture.udp_is_onnect():
+ self.node.get_logger().warning("侦测到UDP断开状态,正在重连!")
+ if not self.initialize_udp():
+ self.node.get_logger().error("UDP重连失败")
+ return
+ time.sleep(2)
+ return
+
+ mocapdata = self.udp_datacapture.realmocapdata
+ if not mocapdata.is_update:
+ return
+
+ # 处理左右手原始数据
+ self.lefthand.joint_update(mocapdata.jointangle_lHand)
+ self.righthand.joint_update(mocapdata.jointangle_rHand)
+
+ # 速度环节处理
+ self.lefthand.speed_update()
+ self.righthand.speed_update()
+
+ # self.quick_log(self.lefthand.g_jointpositions[1],LOG_FILE_PATH,"SEND")
+
+ # 调试打印
+ if self.lefthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"左手位置: {self.lefthand.g_jointpositions}")
+ if self.righthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"右手位置: {self.righthand.g_jointpositions}")
+
+ # 发布右手数据
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ # 发布左手数据
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ msg_l.velocity = [float(num) for num in self.lefthand.g_jointvelocity]
+ self.publisher_l.publish(msg_l)
+
+ self.pubprintcount += 1
+
+ def process(self):
+ """主处理函数"""
+ if not self.initialize_udp():
+ self.node.get_logger().error("初始化配置网络失败")
+ return
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README.md b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README.md
new file mode 100644
index 0000000..28259bb
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README.md
@@ -0,0 +1,62 @@
+# UdexReal (LinkerTG) Data Glove Module
+
+UdexReal / LinkerTG data glove ROS2 driver module, receives glove data via UDP and publishes to ROS2 topics.
+
+## Features
+
+- Receives glove data via UDP protocol
+- Supports left and right hand data
+- 50Hz publish rate
+
+## Usage
+
+### 1. Config File
+
+Edit `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: udexreal
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. Launch
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 Topics
+
+### Published Topics
+
+| Topic | Message Type | Description |
+|-------|-------------|-------------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | Right hand control data |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | Left hand control data |
+
+### Message Format
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # Timestamp
+msg.name # Joint name list
+msg.position # Joint position values
+```
+
+## File Structure
+
+```
+motion/udexreal/
+├── __init__.py
+├── retarget.py # ROS2 integration
+└── README.md # English documentation
+```
+
+## Notes
+
+1. Ensure UDP port is not occupied
+2. Glove and host must be on the same network
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README_zh.md b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README_zh.md
new file mode 100644
index 0000000..9c2ecd8
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/README_zh.md
@@ -0,0 +1,62 @@
+# UdexReal (LinkerTG) 数据手套模块
+
+UdexReal / LinkerTG 数据手套的 ROS2 驱动模块,通过 UDP 接收手套数据并发布到 ROS2 话题。
+
+## 特性
+
+- 通过 UDP 协议接收手套数据
+- 支持左右手数据
+- 发布频率 50Hz
+
+## 使用方法
+
+### 1. 配置文件
+
+修改 `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: udexreal
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. 运行
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 话题
+
+### 发布话题
+
+| 话题名 | 消息类型 | 说明 |
+|-------|---------|------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | 右手控制数据 |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | 左手控制数据 |
+
+### 消息格式
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # 时间戳
+msg.name # 关节名称列表
+msg.position # 关节位置值
+```
+
+## 文件结构
+
+```
+motion/udexreal/
+├── __init__.py
+├── retarget.py # ROS2 集成
+└── README.md # 本文档
+```
+
+## 注意事项
+
+1. 确保 UDP 端口未被占用
+2. 手套与主机需在同一网络
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l10v7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l10v7.py
new file mode 100644
index 0000000..832eac6
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l10v7.py
@@ -0,0 +1,180 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.3 # 旋转
+ qpos[17] = joint_arc[20] * 2.6 # 侧摆
+ # qpos[18] = joint_arc[2] * 0 # 根部关节
+ # qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[0] * -0.6 # 远端关节
+ # print(qpos[16],qpos[17])
+
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ # qpos[2] = joint_arc[5] * -1
+ # qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ # qpos[6] = joint_arc[17] * -1
+ # qpos[7] = joint_arc[16] * -1
+
+ # qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -0.7
+ # qpos[10] = joint_arc[9] * -1
+ # qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ # qpos[14] = joint_arc[13] * -1
+ # qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 旋转
+ qpos[17] = joint_arc[2] * -1 # 侧摆
+ qpos[18] = joint_arc[2] * 0 # 根部关节
+ qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[1] * -1 # 远端关节
+ # print(qpos[16],qpos[17])
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l20.py
new file mode 100644
index 0000000..2e8790c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l20.py
@@ -0,0 +1,176 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * -1
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_rimit = 2
+ fast_rimit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_rimit < position_error < fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_rimit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = (joint_arc[2] * -1 - 0.3)
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * 0
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * 1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 2
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l21.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l21.py
new file mode 100644
index 0000000..d9ee68e
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l21.py
@@ -0,0 +1,192 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.5 # 侧摆
+ qpos[17] = joint_arc[20] * 3 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l25.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l25.py
new file mode 100644
index 0000000..bc32533
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l25.py
@@ -0,0 +1,202 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2.6 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1.5 # 远端关节
+ if joint_arc[0] > -40 * 3.14 / 180:
+ qpos[18] = 0
+ qpos[19] = 0
+ qpos[20] = 0
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 - 0.4 # 侧摆
+ qpos[17] = joint_arc[20] * 2 - 0.2 # 旋转
+ qpos[18] = joint_arc[2] * -0.2 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+ if joint_arc[0] > -50 * 3.14 / 180:
+ qpos[18] = 0.2
+ qpos[19] = 0
+ qpos[20] = 0
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * 0
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l6.py
new file mode 100644
index 0000000..81715cd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l6.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 1.5 # 旋转
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.8
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.8
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -0.8
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.8 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l7.py
new file mode 100644
index 0000000..7707080
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_l7.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[2] * -0.5 # 旋转
+ qpos[17] = joint_arc[20] * 0.8 # 侧摆
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.5 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_o6.py
new file mode 100644
index 0000000..a6cf885
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/hand/udexreal_o6.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.1 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2 # 旋转
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.3 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -11
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/retarget.py
new file mode 100644
index 0000000..76a1080
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexreal/retarget.py
@@ -0,0 +1,189 @@
+
+import time
+import sys
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from datetime import datetime
+from pathlib import Path
+
+# 将项目根目录放在最前面
+# 强制使用项目本地的 linkerhand 模块
+_project_root = Path(__file__).absolute().parent.parent.parent
+_project_root_str = str(_project_root)
+
+if _project_root_str in sys.path:
+ sys.path.remove(_project_root_str)
+sys.path.insert(0, _project_root_str)
+from linkerhand.udexrealcore import UdexRealScoketUdp, TimeoutStatus, UdexRealData
+from linkerhand.handcore import HandCore
+
+from linkerhand.constants import RobotName, ROBOT_LEN_MAP
+
+
+LOG_FILE_PATH = "/tmp/b.log"
+
+class Retarget():
+ def __init__(self,node, ip, port, deviceid, lefthand: RobotName, righthand: RobotName, handcore: HandCore,
+ lefthandpubprint: bool, righthandpubprint: bool):
+ self.node = node
+ self.udp_ip = ip
+ self.udp_port = port
+ self.motion_device = deviceid
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.runing = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+
+ # 根据右手类型初始化
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v3:
+ from .hand.udexreal_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.o6:
+ from .hand.udexreal_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l6:
+ from .hand.udexreal_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l25 \
+ or self.righthandtype == RobotName.g20:
+ from .hand.udexreal_l25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.t25:
+ # from .hand.udexreal_t25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l20:
+ from .hand.udexreal_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 :
+ from .hand.udexreal_l10v7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+
+ else:
+ print("未正确定义机械左手对象,请检查支持清单列表!")
+
+ # 根据LEFT手类型初始化
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v3:
+ from .hand.udexreal_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.udexreal_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.udexreal_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l25 \
+ or self.lefthandtype == RobotName.g20:
+ from .hand.udexreal_l25 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ # elif self.lefthandtype == RobotName.t25:
+ # from .hand.udexreal_t25 import LeftHand
+ # self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.udexreal_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 :
+ from .hand.udexreal_l10v7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ else:
+ print("未正确定义机械右手对象,请检查支持清单列表!")
+
+ # ROS2 发布器
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ self.timer = self.node.create_timer(1.0/120, self.process_callback) # 120Hz
+ self.pubprintcount = 0
+ self.udp_datacapture = None
+
+ def initialize_udp(self):
+ """初始化UDP连接"""
+ self.udp_datacapture = UdexRealScoketUdp(
+ host=self.udp_ip,
+ port=self.udp_port,
+ device_id=self.motion_device)
+ self.udp_datacapture.set_timeout_callback(self.on_timeout_callback)
+ self.udp_datacapture.set_data_recovered_callback(self.on_data_recovered_callback)
+ return self.udp_datacapture.udp_initial()
+
+ def on_timeout_callback(self, status: TimeoutStatus):
+ """超时回调函数"""
+ # if status.consecutive_timeout_checks == 1: # 第一次超时
+ # print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
+ # f"警告: 数据接收超时,{status.time_since_last_data:.1f}秒未收到数据")
+ if status.consecutive_timeout_checks % 1 == 0: # 每10次检查打印一次
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
+ f"警告: 数据接收超时,{status.time_since_last_data:.1f}秒未收到数据")
+
+ def on_data_recovered_callback(self):
+ """数据恢复回调函数"""
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 数据连接已恢复")
+
+
+ def process_callback(self):
+ if not self.runing:
+ return
+
+ mocapdata = self.udp_datacapture.realmocapdata
+
+ # 检查数据是否更新
+ if not mocapdata.is_update:
+ return
+
+ # 处理左右手原始数据
+ self.lefthand.joint_update(mocapdata.jointangle_lHand)
+ self.righthand.joint_update(mocapdata.jointangle_rHand)
+
+ # 速度环节处理
+ self.lefthand.speed_update()
+ self.righthand.speed_update()
+
+ # self.quick_log(self.lefthand.g_jointpositions[1],LOG_FILE_PATH,"SEND")
+
+ # 调试打印
+ if self.lefthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"左手位置: {self.lefthand.g_jointpositions}")
+ if self.righthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"右手位置: {self.righthand.g_jointpositions}")
+
+ # 发布右手数据
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ # 发布左手数据
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ msg_l.velocity = [float(num) for num in self.lefthand.g_jointvelocity]
+ self.publisher_l.publish(msg_l)
+
+ self.pubprintcount += 1
+
+ def process(self):
+ """主处理函数"""
+ if not self.initialize_udp():
+ self.node.get_logger().error("初始化配置网络失败")
+ return
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/l6_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/l6_config.py
new file mode 100644
index 0000000..d896fc9
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/l6_config.py
@@ -0,0 +1,61 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': [0.3, 0.4, 0.3],
+ 'robot_idx': 0,
+ 'type': 'thumb'
+ },
+ 'thumb_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': [0.3, 0.0, 0.7],
+ 'robot_idx': 1,
+ 'type': 'thumb'
+ },
+ 'index': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': [0.5, 0.3, 0.2],
+ 'robot_idx': 3,
+ 'type': 'finger'
+ },
+ 'middle': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': [0.5, 0.1, 0.4],
+ 'robot_idx': 5,
+ 'type': 'finger'
+ },
+ 'ring': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': [0.5, 0.1, 0.4],
+ 'robot_idx': 7,
+ 'type': 'finger'
+ },
+ 'pinky': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': [0.5, 0.1, 0.4],
+ 'robot_idx': 9,
+ 'type': 'finger'
+ }
+}
+
+# 映射顺序
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_flexion',
+ 'index', 'middle', 'ring', 'pinky'
+]
+
+# 三态默认配置
+MULTI_SEGMENT_CONFIG = {
+ 'states': ['original', 'opose', 'fist'],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o6_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o6_config.py
new file mode 100644
index 0000000..bdd9a92
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o6_config.py
@@ -0,0 +1,61 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [20],
+ 'weights': [1],
+ 'robot_idx': 0,
+ 'type': 'thumb'
+ },
+ 'thumb_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [0, 1, 2],
+ 'weights': [0.6, 0.3, 0.1],
+ 'robot_idx': 1,
+ 'type': 'thumb'
+ },
+ 'index': {
+ 'name': '食指',
+ 'joints': [4, 5, 6],
+ 'weights': [0.6, 0.3, 0.1],
+ 'robot_idx': 3,
+ 'type': 'finger'
+ },
+ 'middle': {
+ 'name': '中指',
+ 'joints': [8, 9, 10],
+ 'weights': [0.6, 0.3, 0.1],
+ 'robot_idx': 5,
+ 'type': 'finger'
+ },
+ 'ring': {
+ 'name': '无名指',
+ 'joints': [12, 13, 14],
+ 'weights': [0.6, 0.3, 0.1],
+ 'robot_idx': 7,
+ 'type': 'finger'
+ },
+ 'pinky': {
+ 'name': '小指',
+ 'joints': [16, 17, 18],
+ 'weights': [0.6, 0.3, 0.1],
+ 'robot_idx': 9,
+ 'type': 'finger'
+ }
+}
+
+# 映射顺序
+MAPPING_ORDER = [
+ 'thumb_abduction', 'thumb_flexion',
+ 'index', 'middle', 'ring', 'pinky'
+]
+
+# 三态默认配置
+MULTI_SEGMENT_CONFIG = {
+ 'states': ['original', 'opose', 'fist'],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o7_config.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o7_config.py
new file mode 100644
index 0000000..2e0b543
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/config/o7_config.py
@@ -0,0 +1,68 @@
+# 手指配置常量
+FINGER_CONFIGS = {
+ 'thumb_rotate': {
+ 'name': '拇指旋转',
+ 'joints': [1, 2],
+ 'weights': [0.3, 0.7],
+ 'robot_idx': 0,
+ 'type': 'thumb'
+ },
+ 'thumb_abduction': {
+ 'name': '拇指侧摆',
+ 'joints': [0, 1, 2],
+ 'weights': [0.6, 0.1, 0.3],
+ 'robot_idx': 1,
+ 'type': 'thumb'
+ },
+ 'thumb_flexion': {
+ 'name': '拇指弯曲',
+ 'joints': [2, 3, 4],
+ 'weights': [0.3, 0.1, 0.6],
+ 'robot_idx': 2,
+ 'type': 'thumb'
+ },
+ 'index': {
+ 'name': '食指',
+ 'joints': [6, 7, 8],
+ 'weights': [0.5, 0.3, 0.2],
+ 'robot_idx': 5,
+ 'type': 'finger'
+ },
+ 'middle': {
+ 'name': '中指',
+ 'joints': [10, 11, 12],
+ 'weights': [0.5, 0.3, 0.2],
+ 'robot_idx': 8,
+ 'type': 'finger'
+ },
+ 'ring': {
+ 'name': '无名指',
+ 'joints': [14, 15, 16],
+ 'weights': [0.5, 0.3, 0.2],
+ 'robot_idx': 11,
+ 'type': 'finger'
+ },
+ 'pinky': {
+ 'name': '小指',
+ 'joints': [18, 19, 20],
+ 'weights': [0.5, 0.3, 0.2],
+ 'robot_idx': 14,
+ 'type': 'finger'
+ }
+}
+
+# 映射顺序
+MAPPING_ORDER = [
+ 'thumb_rotate', 'thumb_abduction', 'thumb_flexion',
+ 'index', 'middle', 'ring', 'pinky'
+]
+
+# 三态默认配置
+MULTI_SEGMENT_CONFIG = {
+ 'states': ['original', 'opose', 'fist'],
+ 'state_names': {
+ 'original': '张手',
+ 'opose': 'O手势',
+ 'fist': '握拳'
+ }
+}
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l10v7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l10v7.py
new file mode 100644
index 0000000..832eac6
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l10v7.py
@@ -0,0 +1,180 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.3 # 旋转
+ qpos[17] = joint_arc[20] * 2.6 # 侧摆
+ # qpos[18] = joint_arc[2] * 0 # 根部关节
+ # qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[0] * -0.6 # 远端关节
+ # print(qpos[16],qpos[17])
+
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ # qpos[2] = joint_arc[5] * -1
+ # qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ # qpos[6] = joint_arc[17] * -1
+ # qpos[7] = joint_arc[16] * -1
+
+ # qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -0.7
+ # qpos[10] = joint_arc[9] * -1
+ # qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ # qpos[14] = joint_arc[13] * -1
+ # qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 旋转
+ qpos[17] = joint_arc[2] * -1 # 侧摆
+ qpos[18] = joint_arc[2] * 0 # 根部关节
+ qpos[19] = joint_arc[1] * 0 # 中部关节
+ qpos[20] = joint_arc[1] * -1 # 远端关节
+ # print(qpos[16],qpos[17])
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l20.py
new file mode 100644
index 0000000..2e8790c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l20.py
@@ -0,0 +1,176 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * -1
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_rimit = 2
+ fast_rimit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_rimit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_rimit < position_error < fast_rimit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_rimit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = (joint_arc[2] * -1 - 0.3)
+ qpos[17] = joint_arc[20] * 1
+ qpos[18] = joint_arc[1] * -1
+ qpos[19] = joint_arc[0] * 0
+
+ qpos[0] = joint_arc[7] * 1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19] * 1
+
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11] * 1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+
+ qpos[12] = joint_arc[15] * 1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 2
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.8)
+ min_vel = int(self.last_jointvelocity[i] * 0.6)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 10 + 5
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 10 + 10
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 10 + 50
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 10 + 30
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 10 + 10
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity [i] = int(target_vel)
+
+ if self.g_jointvelocity [i] > 255:
+ self.g_jointvelocity [i] = 255
+ self.g_jointvelocity [i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity [i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l21.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l21.py
new file mode 100644
index 0000000..d9ee68e
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l21.py
@@ -0,0 +1,192 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.5 # 侧摆
+ qpos[17] = joint_arc[20] * 3 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.9
+ qpos[2] = joint_arc[5] * -0.9
+ qpos[3] = joint_arc[4] * -1
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ # if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.9
+ qpos[6] = joint_arc[17] * -0.9
+ qpos[7] = joint_arc[16] * -1
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ # if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -0.9
+ qpos[10] = joint_arc[9] * -0.9
+ qpos[11] = joint_arc[8] * -1
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ # if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.9
+ qpos[14] = joint_arc[13] * -0.9
+ qpos[15] = joint_arc[12] * -1
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ # if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l25.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l25.py
new file mode 100644
index 0000000..bc32533
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l25.py
@@ -0,0 +1,202 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 2.6 # 旋转
+ qpos[18] = joint_arc[2] * -0.5 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1.5 # 远端关节
+ if joint_arc[0] > -40 * 3.14 / 180:
+ qpos[18] = 0
+ qpos[19] = 0
+ qpos[20] = 0
+
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -70 * 3.14 / 180: qpos[7] = 0
+
+ qpos[8] = joint_arc[11] * -1
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions[6:4] = [128, 128, 128, 128]
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 - 0.4 # 侧摆
+ qpos[17] = joint_arc[20] * 2 - 0.2 # 旋转
+ qpos[18] = joint_arc[2] * -0.2 # 根部关节
+ qpos[19] = joint_arc[1] * -1 # 中部关节
+ qpos[20] = joint_arc[0] * -1 # 远端关节
+ if joint_arc[0] > -50 * 3.14 / 180:
+ qpos[18] = 0.2
+ qpos[19] = 0
+ qpos[20] = 0
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[2] = 0
+ if joint_arc[6] > -80 * 3.14 / 180: qpos[3] = 0
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[6] = 0
+ if joint_arc[18] > -75 * 3.14 / 180: qpos[7] = 0
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * 0
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[10] = 0
+ if joint_arc[10] > -80 * 3.14 / 180: qpos[11] = 0
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[14] = 0
+ if joint_arc[14] > -80 * 3.14 / 180: qpos[15] = 0
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l6.py
new file mode 100644
index 0000000..81715cd
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l6.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1 # 侧摆
+ qpos[17] = joint_arc[20] * 1.5 # 旋转
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -0.8
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -0.8
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -0.8
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -0.8 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l7.py
new file mode 100644
index 0000000..7707080
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_l7.py
@@ -0,0 +1,181 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[20] * 1.2 # 侧摆
+ qpos[17] = joint_arc[20] * 2.4 # 旋转
+ qpos[18] = joint_arc[2] * -0.3878 # 根部关节
+ qpos[19] = joint_arc[1] * -0.66845 # 中部关节
+ qpos[20] = joint_arc[0] * -0.8 # 远端关节
+
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -0.7
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -0.7
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ qpos[8] = joint_arc[11]
+ qpos[9] = joint_arc[10] * -1.0098
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ qpos[12] = joint_arc[15]
+ qpos[13] = joint_arc[14] * -0.7
+ qpos[14] = joint_arc[13] * -1
+ qpos[15] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[2] * -0.5 # 旋转
+ qpos[17] = joint_arc[20] * 0.8 # 侧摆
+ qpos[18] = joint_arc[2] * -0.9 # 根部关节
+ qpos[19] = joint_arc[1] * -0.8 # 中部关节
+ qpos[20] = joint_arc[0] * -0.5 # 远端关节
+
+ # 食指 index
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+
+ # 小指 little
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+
+ # 中指 middle
+ qpos[8] = joint_arc[11] * -1.0098
+ qpos[9] = joint_arc[10] * -1
+ qpos[10] = joint_arc[9] * -1
+ qpos[11] = joint_arc[8] * -1
+
+ # 无名指 ring
+ qpos[12] = joint_arc[15] * -1
+ qpos[13] = joint_arc[14] * -1 # gen
+ qpos[14] = joint_arc[13] * -1 # zhong
+ qpos[15] = joint_arc[12] * -1
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_o6.py
new file mode 100644
index 0000000..6cc6628
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/hand/udexreal_o6.py
@@ -0,0 +1,256 @@
+import numpy as np
+import copy
+from linkerhand.handcore import HandCore
+from ..config.o6_config import FINGER_CONFIGS, MAPPING_ORDER
+from typing import List
+from linkerhand.handcoreex import MultiStateLinearMapper
+
+
+# 修正 RightHand 类
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationopose = None
+ self.calibrationfistpose = None
+ self.calibrationoriginal = None
+
+ # 机械手预设姿势
+ self.robot_original = handcore.hand_lower_limits_l
+ self.robot_opose = [1.1, 0.37, 0.0, 0.82, 0.0, 0.82, 0.0, 0.82, 0.0, 0.82, 0.0]
+ self.robot_fist = handcore.hand_upper_limits_l
+
+ # 映射器
+ self.multi_state_mapper = MultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER)
+
+ def initialize_mapper(self) -> bool:
+ """初始化映射器"""
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ # 设置状态顺序
+ self.multi_state_mapper.set_state_order(['original', 'opose', 'fist'])
+
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def joint_arc_update(self, joint_arc: List[float]):
+ """映射手套数据到机械手"""
+ qpos = np.zeros(25)
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ # 拇指处理
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+# LeftHand 类类似修正
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.g_jointpositions_arc = [0] * length
+ self.g_jointvelocity_arc = [0] * length
+ self.handstate = [0] * length
+ self.calibrationopose = None
+ self.calibrationfistpose = None
+ self.calibrationoriginal = None
+
+ # 机械手预设姿势
+ self.robot_original = handcore.hand_lower_limits_l
+ self.robot_opose = [1.1, 0.37, 0.0, 0.82, 0.0, 0.82, 0.0, 0.82, 0.0, 0.82, 0.0]
+ self.robot_fist = handcore.hand_upper_limits_l
+
+ # 使用多态映射器(支持更多手势)
+ self.multi_state_mapper = MultiStateLinearMapper(FINGER_CONFIGS, MAPPING_ORDER)
+
+
+ def initialize_mapper(self) -> bool:
+ """初始化映射器"""
+ glove_original = self._to_list(self.calibrationoriginal)
+ glove_fist = self._to_list(self.calibrationfistpose)
+ glove_opose = self._to_list(self.calibrationopose)
+
+ self.multi_state_mapper.add_state('original', glove_original, self.robot_original)
+ self.multi_state_mapper.add_state('opose', glove_opose, self.robot_opose)
+ self.multi_state_mapper.add_state('fist', glove_fist, self.robot_fist)
+
+ glove_pinch = glove_original.copy()
+ glove_pinch[2:5] = [1.5, 1.2, 1.0] # 拇指弯曲明显
+ glove_pinch[0:2] = glove_original[0:2] # 拇指侧摆保持原始
+ robot_pinch = self.robot_original.copy()
+ robot_pinch[1] = 0.8 # 拇指弯曲加大
+ robot_pinch[3] = 0.6 # 食指轻微弯曲
+
+ self.multi_state_mapper.add_state('pinch', glove_pinch, robot_pinch)
+
+ # 设置状态顺序
+ self.multi_state_mapper.set_state_order(['original', 'opose', 'fist'])
+
+
+ def _to_list(self, data):
+ """转换为列表"""
+ if hasattr(data, 'tolist'):
+ return data.tolist()
+ elif isinstance(data, np.ndarray):
+ return data.tolist()
+ else:
+ return list(data)
+
+ def joint_arc_update(self, joint_arc: List[float]):
+ """映射手套数据到机械手"""
+ qpos = np.zeros(25)
+ arc_value = self.multi_state_mapper.map_glove_to_robot(joint_arc)
+ qpos[17] = self.g_jointpositions_arc[1] = arc_value[0]
+ qpos[20] = self.g_jointpositions_arc[0] = arc_value[1]
+ qpos[1] = self.g_jointpositions_arc[2] = arc_value[3]
+ qpos[9] = self.g_jointpositions_arc[3] = arc_value[5]
+ qpos[13] = self.g_jointpositions_arc[4] = arc_value[7]
+ qpos[5] = self.g_jointpositions_arc[5] = arc_value[9]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ # 拇指处理
+ qpos[16] = joint_arc[1] * 1.2
+ qpos[17] = joint_arc[0] * -2 + joint_arc[1] * 1.2
+ qpos[20] = joint_arc[4] * 0.5 + joint_arc[2] * 0.8
+
+ qpos[1] = joint_arc[6] * 0.1 + joint_arc[8] * 0.7
+ qpos[9] = joint_arc[10] * 0.1 + joint_arc[12] * 0.7
+ qpos[13] = joint_arc[14] * 0.1 + joint_arc[16] * 0.7
+ qpos[5] = joint_arc[18] * 0.1 + joint_arc[20] * 0.7
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/retarget.py
new file mode 100644
index 0000000..24a653c
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/udexrealv2t/retarget.py
@@ -0,0 +1,442 @@
+
+import time
+import json
+import copy
+import rclpy
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from datetime import datetime, timedelta
+from tqdm import tqdm
+from pathlib import Path
+from colorama import Fore, init
+from typing import Optional
+
+from ...linkerhand.udexrealcore import UdexRealScoketUdp, TimeoutStatus, UdexRealData
+from ...linkerhand.handcore import HandCore
+
+from ...linkerhand.constants import RobotName, ROBOT_LEN_MAP
+
+
+tmp_file_path = Path(__file__).parent / "tmp" / "jointangle_data.tmp"
+
+
+
+class CalibrationProgress:
+ """校准进度管理器"""
+ def __init__(self, duration: float = 3.0):
+ self.step_started = False
+ self.message = ""
+ self.start_time = 0
+ self.duration = duration
+ self.last_progress = -1
+
+ # 进度条配置
+ self.bar_length = 60 # 增加到60,占更多屏幕
+ self.filled_char = '█'
+ self.empty_char = '░'
+
+ # 状态跟踪
+ self.message_printed = False
+
+ def start_step(self, message: str, duration: Optional[float] = None):
+ """开始新的校准步骤"""
+ # 重置状态
+ self.step_started = True
+ self.message = message
+ self.start_time = time.time()
+ self.last_progress = -1
+ self.message_printed = False
+
+ if duration is not None:
+ self.duration = duration
+
+ # 只在开始时打印一次消息
+ if not self.message_printed:
+ self.message_printed = True
+
+ def update_progress(self) -> tuple[bool, int]:
+ """
+ 更新并显示进度
+
+ Returns:
+ tuple[bool, int]: (是否完成, 当前进度百分比)
+ """
+ if not self.step_started:
+ return False, 0
+
+ elapsed = time.time() - self.start_time
+ progress = min(100, int((elapsed / self.duration) * 100))
+
+ # 只在进度有变化时更新显示
+ if progress != self.last_progress:
+ self.last_progress = progress
+
+ # 计算填充长度
+ filled = int(self.bar_length * progress // 100)
+
+ # 构建进度条
+ if progress < 100:
+ bar = self.filled_char * filled + self.empty_char * (self.bar_length - filled)
+ else:
+ # 100%时显示完整条
+ bar = self.filled_char * self.bar_length
+
+ # 使用 \r 和 \033[K 确保完全覆盖
+ print(f'\r\033[K{self.message} |{bar}| {progress:3d}%', end='', flush=True)
+
+ # 检查是否完成
+ is_completed = elapsed >= self.duration
+ if is_completed:
+ self.step_started = False
+ # 完成后不在这里换行,让调用者控制
+
+ return is_completed, progress
+
+ def get_progress_info(self) -> dict:
+ """获取进度信息"""
+ if not self.step_started:
+ return {"elapsed": 0, "progress": 0, "remaining": 0}
+
+ elapsed = time.time() - self.start_time
+ progress = min(1.0, elapsed / self.duration)
+
+ return {
+ "elapsed": elapsed,
+ "progress": progress,
+ "remaining": max(0, self.duration - elapsed),
+ "progress_percent": int(progress * 100)
+ }
+
+ def reset(self):
+ """重置状态"""
+ self.step_started = False
+ self.message = ""
+ self.start_time = 0
+ self.last_progress = -1
+ self.message_printed = False
+
+ def is_active(self) -> bool:
+ """检查是否正在运行"""
+ return self.step_started
+
+ def stop(self, success: bool = True, final_message: Optional[str] = None):
+ """停止进度条"""
+ if not self.step_started:
+ return
+
+ self.step_started = False
+
+ if success:
+ if final_message:
+ print(f"\r\033[K✅ {final_message}")
+ else:
+ print(f"\r\033[K✅ {self.message} - 完成")
+ else:
+ if final_message:
+ print(f"\r\033[K❌ {final_message}")
+ else:
+ print(f"\r\033[K❌ {self.message} - 失败")
+
+
+class Retarget():
+ def __init__(self,node, ip, port, deviceid, lefthand: RobotName, righthand: RobotName, handcore: HandCore,
+ lefthandpubprint: bool, righthandpubprint: bool,calibration :bool = False):
+ self.node = node
+ self.udp_ip = ip
+ self.udp_port = port
+ self.motion_device = deviceid
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.runing = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+ self.calibration = calibration
+
+ # 根据右手类型初始化
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v3:
+ from .hand.udexreal_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.o6:
+ from .hand.udexreal_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l6:
+ from .hand.udexreal_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.l25:
+ # from .hand.udexreal_l25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ # elif self.righthandtype == RobotName.t25:
+ # from .hand.udexreal_t25 import RightHand
+ # self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l20:
+ from .hand.udexreal_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 :
+ from .hand.udexreal_l10v7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l21:
+ from .hand.udexreal_l21 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+
+ # 根据LEFT手类型初始化
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v3:
+ from .hand.udexreal_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.udexreal_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.udexreal_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l25:
+ from .hand.udexreal_l25 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ # elif self.lefthandtype == RobotName.t25:
+ # from .hand.udexreal_t25 import LeftHand
+ # self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.udexreal_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 :
+ from .hand.udexreal_l10v7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l21:
+ from .hand.udexreal_l21 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+
+ # ROS2 发布器
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ self.timer = self.node.create_timer(1.0/120, self.process_callback) # 120Hz
+ self.pubprintcount = 0
+ self.udp_datacapture = None
+
+ self.calibration_progress = CalibrationProgress(duration=3.0)
+
+ def initialize_udp(self):
+ """初始化UDP连接"""
+ self.udp_datacapture = UdexRealScoketUdp(
+ host=self.udp_ip,
+ port=self.udp_port,
+ device_id=self.motion_device)
+ self.udp_datacapture.set_timeout_callback(self.on_timeout_callback)
+ self.udp_datacapture.set_data_recovered_callback(self.on_data_recovered_callback)
+ return self.udp_datacapture.udp_initial()
+
+ def on_timeout_callback(self, status: TimeoutStatus):
+ """超时回调函数"""
+ # if status.consecutive_timeout_checks == 1: # 第一次超时
+ # print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
+ # f"警告: 数据接收超时,{status.time_since_last_data:.1f}秒未收到数据")
+ if status.consecutive_timeout_checks % 1 == 0: # 每10次检查打印一次
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
+ f"警告: 数据接收超时,{status.time_since_last_data:.1f}秒未收到数据")
+
+ def on_data_recovered_callback(self):
+ """数据恢复回调函数"""
+ print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 数据连接已恢复")
+
+
+ def process_callback(self):
+ if not self.runing:
+ return
+
+ mocapdata = self.udp_datacapture.realmocapdata
+
+ # 检查数据是否更新
+ if not mocapdata.is_update:
+ return
+
+ if self.calibration != -1:
+ step_info = {
+ None: ("请并拢五指...", 0),
+ 0: ("请做握拳姿势...", 1),
+ 1: ("请做O型姿势...", -1)
+ }.get(self.calibration)
+
+ if not step_info:
+ return
+
+ message, next_step = step_info
+
+ # 启动或更新进度条
+ if not self.calibration_progress.is_active():
+ self.calibration_progress.start_step(message)
+
+ # 更新显示
+ is_completed, _ = self.calibration_progress.update_progress()
+
+ if is_completed:
+ print() # 换行
+ if self.calibration is None:
+ # 第一次校准:并拢五指姿势
+ self.righthand.calibrationoriginal = mocapdata.jointangle_rHand.copy()
+ self.lefthand.calibrationoriginal = mocapdata.jointangle_lHand.copy()
+ self.calibration = 0
+
+ elif self.calibration == 0:
+ # 第二次校准:握拳姿势
+ self.righthand.calibrationfistpose = mocapdata.jointangle_rHand.copy()
+ self.lefthand.calibrationfistpose = mocapdata.jointangle_lHand.copy()
+ self.calibration = 1
+
+ elif self.calibration == 1:
+ # 第三次校准:O型姿势
+ self.righthand.calibrationopose = mocapdata.jointangle_rHand.copy()
+ self.lefthand.calibrationopose = mocapdata.jointangle_lHand.copy()
+ self.calibration = -1
+
+ # 完成所有校准步骤
+ self._save_to_tmp()
+ self.righthand.initialize_mapper()
+ self.lefthand.initialize_mapper()
+ print("✅ 校准完成!")
+
+ # 重置进度条,准备下一步或结束
+ self.calibration_progress.reset()
+
+ return
+
+ # 处理左右手原始数据
+ self.lefthand.joint_arc_update(mocapdata.jointangle_lHand)
+ self.righthand.joint_arc_update(mocapdata.jointangle_rHand)
+
+ # 速度环节处理
+ self.lefthand.speed_update()
+ self.righthand.speed_update()
+
+ # 调试打印
+ if self.lefthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"左手位置: {self.lefthand.g_jointpositions}")
+ if self.righthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"右手位置: {self.righthand.g_jointpositions}")
+
+ # 发布右手数据
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ # 发布左手数据
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ msg_l.velocity = [float(num) for num in self.lefthand.g_jointvelocity]
+ self.publisher_l.publish(msg_l)
+
+ self.pubprintcount += 1
+
+ def _save_to_tmp(self):
+ """
+ 保存 jointangle_r 数据和时间戳到临时文件
+ """
+ newdata = {
+ "timestamp": datetime.now().isoformat(), # 当前时间
+ "jointangleoriginal_r": self.righthand.calibrationoriginal,
+ "jointangleoriginal_l": self.lefthand.calibrationoriginal,
+ "jointanglefist_r": self.righthand.calibrationfistpose,
+ "jointanglefist_l": self.lefthand.calibrationfistpose,
+ "jointangleopose_r": self.righthand.calibrationopose,
+ "jointangleopose_l": self.lefthand.calibrationopose
+ }
+ try:
+ tmp_file_path.parent.mkdir(parents=True, exist_ok=True)
+ if tmp_file_path.exists():
+ content = tmp_file_path.read_text()
+ historydata = json.loads(content)
+ # if self.force_reader_left.handtype != 'Left':
+ # # 左手数据不存在
+ # newdata["jointangleoriginal_l"] = historydata["jointangleoriginal_l"]
+ # newdata["jointanglefist_l"] = historydata["jointanglefist_l"]
+ # newdata["jointangleopose_l"] = historydata["jointangleopose_l"]
+ # if self.force_reader_right.handtype != 'Right':
+ # newdata["jointangleoriginal_r"] = historydata["jointangleoriginal_r"]
+ # newdata["jointanglefist_r"] = historydata["jointanglefist_r"]
+ # newdata["jointangleopose_r"] = historydata["jointangleopose_r"]
+ json_str = json.dumps(newdata, indent=2)
+ tmp_file_path.write_text(json_str)
+ print("保存成功")
+ return True
+ except Exception as e:
+ print(f"保存失败: {e}")
+ return False
+
+ def _load_from_tmp(self):
+ """
+ 从临时文件读取数据,检查时间戳有效性
+ - 如果文件不存在、时间戳为空或超过1小时,返回 False
+ - 否则返回 jointangle_r 数据
+ """
+ if not tmp_file_path.exists():
+ print("文件不存在")
+ return False
+ try:
+ content = tmp_file_path.read_text()
+ data = json.loads(content)
+ except Exception as e:
+ print(f"读取失败: {e}")
+ return False
+
+ # 检查时间戳
+ if 'timestamp' not in data or not data['timestamp']:
+ print("无效的时间戳...")
+ return False
+
+ # 检查是否超过8小时
+ saved_time = datetime.fromisoformat(data['timestamp'])
+ current_time = datetime.now()
+
+ # 计算时间差
+ time_diff = current_time - saved_time
+ if time_diff > timedelta(hours=8):
+ print("标定数据有效期不足8小时,暂不进行标定流程...")
+ try:
+ self.righthand.calibrationoriginal = data['jointangleoriginal_r']
+ self.lefthand.calibrationoriginal = data['jointangleoriginal_l']
+ self.righthand.calibrationfistpose = data['jointanglefist_r']
+ self.lefthand.calibrationfistpose = data['jointanglefist_l']
+ self.righthand.calibrationopose = data['jointangleopose_r']
+ self.lefthand.calibrationopose = data['jointangleopose_l']
+ except Exception as e:
+ print(f"标定数据异常: {e}")
+ return False
+ return True
+
+
+ def process(self):
+ """主处理函数"""
+ if not self.initialize_udp():
+ self.node.get_logger().error("初始化配置网络失败")
+ return
+
+ if self.calibration is True:
+ self.calibration = None
+ else:
+ if self._load_from_tmp() is True:
+ print("跳过标定流程")
+ self.calibration = -1
+ self.righthand.initialize_mapper()
+ self.lefthand.initialize_mapper()
+ else:
+ self.calibration = None
+
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README.md b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README.md
new file mode 100644
index 0000000..c3ade4f
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README.md
@@ -0,0 +1,63 @@
+# VTR-DYN Data Glove Module
+
+VTR-DYN data glove ROS2 driver module, receives glove data via UDP and publishes to ROS2 topics.
+
+## Features
+
+- Receives glove data via UDP protocol
+- Supports left and right hand data
+- 50Hz publish rate
+
+## Usage
+
+### 1. Config File
+
+Edit `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: vtrdyn
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. Launch
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 Topics
+
+### Published Topics
+
+| Topic | Message Type | Description |
+|-------|-------------|-------------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | Right hand control data |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | Left hand control data |
+
+### Message Format
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # Timestamp
+msg.name # Joint name list
+msg.position # Joint position values
+```
+
+## File Structure
+
+```
+motion/vtrdyn/
+├── __init__.py
+├── vtrdyncore.py # UDP communication core
+├── retarget.py # ROS2 integration
+└── README.md # English documentation
+```
+
+## Notes
+
+1. Ensure UDP port is not occupied
+2. Glove and host must be on the same network
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README_zh.md b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README_zh.md
new file mode 100644
index 0000000..541d383
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/README_zh.md
@@ -0,0 +1,63 @@
+# VTR-DYN 数据手套模块
+
+VTR-DYN 数据手套的 ROS2 驱动模块,通过 UDP 接收手套数据并发布到 ROS2 话题。
+
+## 特性
+
+- 通过 UDP 协议接收手套数据
+- 支持左右手数据
+- 发布频率 50Hz
+
+## 使用方法
+
+### 1. 配置文件
+
+修改 `config/base_config.yml`:
+
+```yaml
+system:
+ motion_type: vtrdyn
+
+udp:
+ ip: "0.0.0.0"
+ port: 8888
+```
+
+### 2. 运行
+
+```bash
+ros2 run linkerhand_retarget handretarget
+```
+
+## ROS2 话题
+
+### 发布话题
+
+| 话题名 | 消息类型 | 说明 |
+|-------|---------|------|
+| `/cb_right_hand_control_cmd` | `sensor_msgs/JointState` | 右手控制数据 |
+| `/cb_left_hand_control_cmd` | `sensor_msgs/JointState` | 左手控制数据 |
+
+### 消息格式
+
+```python
+# sensor_msgs/JointState
+msg.header.stamp # 时间戳
+msg.name # 关节名称列表
+msg.position # 关节位置值
+```
+
+## 文件结构
+
+```
+motion/vtrdyn/
+├── __init__.py
+├── vtrdyncore.py # UDP 通讯核心
+├── retarget.py # ROS2 集成
+└── README.md # 本文档
+```
+
+## 注意事项
+
+1. 确保 UDP 端口未被占用
+2. 手套与主机需在同一网络
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/__init__.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l10v7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l10v7.py
new file mode 100644
index 0000000..05deae2
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l10v7.py
@@ -0,0 +1,179 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ # if joint_arc[0] * 1.2 > 0.5:
+ # qpos[15] = joint_arc[0] * 0.8
+ # else:
+ qpos[16] = joint_arc[0] * 0.4
+ qpos[17] = joint_arc[1] * 0.8
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=10):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.6
+ qpos[17] = joint_arc[1] * 0.6
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ self.g_jointvelocity[i] = 255
+ # lastpos = self.last_jointpositions[i]
+ # position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ # position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ # slow_limit = 4
+ # fast_limit = 10
+ # max_vel = int(self.last_jointvelocity[i] * 2)
+ # mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ # min_vel = int(self.last_jointvelocity[i] * 0.5)
+ # target_vel = self.last_jointvelocity[i]
+ # if self.handstate[i] == 0: # stop
+ # if 0 < position_error:
+ # target_vel = position_error * 5 + 30
+ # self.handstate[i] = 1
+ # elif self.handstate[i] == 1: # slow
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 50
+ # if target_vel > mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 2
+ # elif position_error == 0:
+ # self.handstate[i] = 0
+ # target_vel = position_error * 5 + 100
+ # else:
+ # target_vel = position_error * 5 + 100
+ # else: # fast
+ # if position_error >= fast_limit:
+ # target_vel = position_error * 5 + 90
+ # if target_vel > max_vel:
+ # target_vel = max_vel
+ # elif slow_limit < position_error < fast_limit:
+ # target_vel = position_error * 5 + 60
+ # if target_vel < mid_vel:
+ # target_vel = mid_vel
+ # self.handstate[i] = 3
+ # elif 0 < position_error <= slow_limit:
+ # target_vel = position_error * 5 + 40
+ # if target_vel < min_vel:
+ # target_vel = min_vel
+ # self.handstate[i] = 1
+ # self.g_jointvelocity[i] = int(target_vel * 1)
+ # if self.g_jointvelocity[i] > 255:
+ # self.g_jointvelocity[i] = 255
+ # self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ # self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l20.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l20.py
new file mode 100644
index 0000000..6fd6e82
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l20.py
@@ -0,0 +1,177 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.5
+ qpos[17] = joint_arc[1] * 0.4
+ qpos[18] = joint_arc[2] * 1.5
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=20):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.3
+ qpos[17] = joint_arc[1] * 0.8
+ qpos[18] = joint_arc[2] * 0.5
+ qpos[19] = joint_arc[4] * 1
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7] * 0.7
+ qpos[2] = joint_arc[8] * 0.6
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22] * 0.7
+ qpos[6] = joint_arc[23] * 0.6
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12] * 0.7
+ qpos[10] = joint_arc[13] * 0.6
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17] * 0.7
+ qpos[14] = joint_arc[18] * 0.6
+ qpos[15] = joint_arc[19]
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l21.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l21.py
new file mode 100644
index 0000000..c857659
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l21.py
@@ -0,0 +1,173 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 0.5
+ qpos[18] = joint_arc[2] * 0.5
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -1.2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 0.6
+ qpos[18] = joint_arc[2] * 0.5
+ qpos[19] = joint_arc[4]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7] *1.1
+ qpos[2] = joint_arc[8] *1.1
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -1.2
+ qpos[5] = joint_arc[22] *1.1
+ qpos[6] = joint_arc[23] *1.1
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12] *1.1
+ qpos[10] = joint_arc[13] *1.1
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17] *1.1
+ qpos[14] = joint_arc[18] *1.1
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l25.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l25.py
new file mode 100644
index 0000000..c857659
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l25.py
@@ -0,0 +1,173 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 0.5
+ qpos[18] = joint_arc[2] * 0.5
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -1.2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=25):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 0.6
+ qpos[18] = joint_arc[2] * 0.5
+ qpos[19] = joint_arc[4]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7] *1.1
+ qpos[2] = joint_arc[8] *1.1
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -1.2
+ qpos[5] = joint_arc[22] *1.1
+ qpos[6] = joint_arc[23] *1.1
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12] *1.1
+ qpos[10] = joint_arc[13] *1.1
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17] *1.1
+ qpos[14] = joint_arc[18] *1.1
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ slow_limit = 1
+ fast_limit = 5
+ max_vel = int(self.last_jointvelocity[i] * 1.5)
+ min_vel = int(self.last_jointvelocity[i] * 0.995)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 3 + 50
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 3 + 100
+ if target_vel > max_vel:
+ target_vel = max_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ else:
+ target_vel = position_error * 3 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 200
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 3 + 200
+ if target_vel < min_vel:
+ target_vel = min_vel
+ else:
+ target_vel = min_vel
+ if min_vel < 150:
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+ self.g_jointvelocity[6] = 255
+ self.g_jointvelocity[7] = 255
+ self.g_jointvelocity[8] = 255
+ self.g_jointvelocity[9] = 255
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l6.py
new file mode 100644
index 0000000..d4458df
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l6.py
@@ -0,0 +1,177 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.8
+ qpos[17] = joint_arc[1] * 0.5
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.6
+ qpos[17] = joint_arc[1] * 0.4
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l7.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l7.py
new file mode 100644
index 0000000..56b4bc3
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_l7.py
@@ -0,0 +1,175 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.8
+ qpos[17] = joint_arc[1] * 0.5
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=7):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0] * 0.6
+ qpos[17] = joint_arc[1] * 0.4
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
\ No newline at end of file
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_o6.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_o6.py
new file mode 100644
index 0000000..acf50d7
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/hand/vtrdyn_o6.py
@@ -0,0 +1,209 @@
+import numpy as np
+from linkerhand.handcore import HandCore
+
+
+class RightHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ # ========== 平滑滤波参数 ==========
+ self.smooth_enabled = True
+ self.smooth_alpha = 0.5 # 平滑系数:越小越平滑,范围 0.05-0.3
+ self.smooth_positions = [255.0] * length # 平滑后的位置(浮点)
+ self.max_step = 20 # 每帧最大变化量,防止跳变
+
+ def _apply_smooth(self, raw_positions):
+ """
+ 对电机输出应用平滑滤波,防止跳变
+
+ 使用指数移动平均(EMA) + 最大步长限制
+ """
+ if not self.smooth_enabled:
+ return raw_positions
+
+ smoothed = []
+ for i, raw in enumerate(raw_positions):
+ # 指数移动平均
+ target = self.smooth_alpha * raw + (1 - self.smooth_alpha) * self.smooth_positions[i]
+
+ # 最大步长限制,防止大幅跳变
+ diff = target - self.smooth_positions[i]
+ if abs(diff) > self.max_step:
+ target = self.smooth_positions[i] + (self.max_step if diff > 0 else -self.max_step)
+
+ self.smooth_positions[i] = target
+ smoothed.append(int(round(target)))
+
+ return smoothed
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 1.5
+ qpos[19] = joint_arc[3] * 0.7
+ qpos[20] = joint_arc[4] * 0.7
+
+ qpos[0] = joint_arc[5] * -1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * 2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * 1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+ if self.g_jointpositions[1] < 70:
+ self.g_jointpositions[1] = 70
+ self.g_jointpositions = self._apply_smooth(self.g_jointpositions)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
+
+
+class LeftHand:
+ def __init__(self, handcore: HandCore, length=6):
+ self.handcore = handcore
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[16] = joint_arc[0]
+ qpos[17] = joint_arc[1] * 1.5
+ qpos[18] = joint_arc[2]
+ qpos[19] = joint_arc[3]
+ qpos[20] = joint_arc[4]
+
+ qpos[0] = joint_arc[5] * 1.2
+ qpos[1] = joint_arc[7]
+ qpos[2] = joint_arc[8]
+ qpos[3] = joint_arc[9]
+
+ qpos[4] = joint_arc[20] * -2
+ qpos[5] = joint_arc[22]
+ qpos[6] = joint_arc[23]
+ qpos[7] = joint_arc[24]
+
+ qpos[8] = joint_arc[10]
+ qpos[9] = joint_arc[12]
+ qpos[10] = joint_arc[13]
+ qpos[11] = joint_arc[14]
+
+ qpos[12] = joint_arc[15] * -1.5
+ qpos[13] = joint_arc[17]
+ qpos[14] = joint_arc[18]
+ qpos[15] = joint_arc[19]
+
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ for i in range(len(self.g_jointpositions)):
+ lastpos = self.last_jointpositions[i]
+ position_error = int(abs(self.g_jointpositions[i] - lastpos))
+ position_derict = 1 if self.g_jointpositions[i] - lastpos > 0 else -1
+ slow_limit = 4
+ fast_limit = 10
+ max_vel = int(self.last_jointvelocity[i] * 2)
+ mid_vel = int(self.last_jointvelocity[i] * 0.7)
+ min_vel = int(self.last_jointvelocity[i] * 0.5)
+ target_vel = self.last_jointvelocity[i]
+ if self.handstate[i] == 0: # stop
+ if 0 < position_error:
+ target_vel = position_error * 5 + 30
+ self.handstate[i] = 1
+ elif self.handstate[i] == 1: # slow
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 50
+ if target_vel > mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 2
+ elif position_error == 0:
+ self.handstate[i] = 0
+ target_vel = position_error * 5 + 100
+ else:
+ target_vel = position_error * 5 + 100
+ else: # fast
+ if position_error >= fast_limit:
+ target_vel = position_error * 5 + 90
+ if target_vel > max_vel:
+ target_vel = max_vel
+ elif slow_limit < position_error < fast_limit:
+ target_vel = position_error * 5 + 60
+ if target_vel < mid_vel:
+ target_vel = mid_vel
+ self.handstate[i] = 3
+ elif 0 < position_error <= slow_limit:
+ target_vel = position_error * 5 + 40
+ if target_vel < min_vel:
+ target_vel = min_vel
+ self.handstate[i] = 1
+ self.g_jointvelocity[i] = int(target_vel * 1)
+ if self.g_jointvelocity[i] > 255:
+ self.g_jointvelocity[i] = 255
+ self.g_jointvelocity[i] = 255
+ self.last_jointvelocity[i] = self.g_jointvelocity[i]
+ self.last_jointpositions[i] = self.g_jointpositions[i]
diff --git a/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/retarget.py b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/retarget.py
new file mode 100644
index 0000000..6e3ce39
--- /dev/null
+++ b/src/linkerhand_retarget/linkerhand_retarget/motion/vtrdyn/retarget.py
@@ -0,0 +1,194 @@
+import time
+import rclpy
+import sys
+from rclpy.node import Node
+from sensor_msgs.msg import JointState
+from pathlib import Path
+
+_project_root = Path(__file__).absolute().parent.parent.parent
+_project_root_str = str(_project_root)
+
+if _project_root_str in sys.path:
+ sys.path.remove(_project_root_str)
+sys.path.insert(0, _project_root_str)
+
+from linkerhand.vtrdyncore import VtrdynSocketUdp, MocapData
+from linkerhand.constants import RobotName, ROBOT_LEN_MAP
+from linkerhand.handcore import HandCore
+
+
+class Retarget:
+ def __init__(self,
+ node,
+ ip,
+ port,
+ lefthand: RobotName,
+ righthand: RobotName,
+ handcore: HandCore,
+ lefthandpubprint: bool,
+ righthandpubprint: bool,
+ calibration=None):
+ self.node = node
+ self.udp_ip = ip
+ self.udp_port = port
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.handcore = handcore
+ self.runing = True
+ self.lefthandpubprint = lefthandpubprint
+ self.righthandpubprint = righthandpubprint
+ if self.righthandtype == RobotName.o7 \
+ or self.righthandtype == RobotName.l7 \
+ or self.righthandtype == RobotName.o7v1 \
+ or self.righthandtype == RobotName.o7v3:
+ from .hand.vtrdyn_l7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.o6:
+ from .hand.vtrdyn_o6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l6:
+ from .hand.vtrdyn_l6 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l25 \
+ or self.righthandtype == RobotName.g20:
+ from .hand.vtrdyn_l25 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l20:
+ from .hand.vtrdyn_l20 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+ elif self.righthandtype == RobotName.l10 \
+ or self.righthandtype == RobotName.l10v7 :
+ from .hand.vtrdyn_l10v7 import RightHand
+ self.righthand = RightHand(handcore, length=ROBOT_LEN_MAP[righthand])
+
+
+ if self.lefthandtype == RobotName.o7 \
+ or self.lefthandtype == RobotName.l7 \
+ or self.lefthandtype == RobotName.o7v1 \
+ or self.lefthandtype == RobotName.o7v3:
+ from .hand.vtrdyn_l7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.o6:
+ from .hand.vtrdyn_o6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l6:
+ from .hand.vtrdyn_l6 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l25 \
+ or self.lefthandtype == RobotName.g20:
+ from .hand.vtrdyn_l25 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l20:
+ from .hand.vtrdyn_l20 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+ elif self.lefthandtype == RobotName.l10 \
+ or self.lefthandtype == RobotName.l10v7 :
+ from .hand.vtrdyn_l10v7 import LeftHand
+ self.lefthand = LeftHand(handcore, length=ROBOT_LEN_MAP[lefthand])
+
+ self.publisher_r = self.node.create_publisher(
+ JointState,
+ '/cb_right_hand_control_cmd',
+ self.handcore.hand_numjoints_r)
+
+ self.publisher_l = self.node.create_publisher(
+ JointState,
+ '/cb_left_hand_control_cmd',
+ self.handcore.hand_numjoints_l)
+
+ self.timer = self.node.create_timer(1.0/120, self.process_callback) # 120Hz
+ self.pubprintcount = 0
+ self.pubprintcount = 0
+ self.udp_datacapture,self.dstAddr = None,None
+
+ def initialize_udp(self):
+ """初始化UDP连接"""
+ self.node.get_logger().info(f"正在初始化UDP连接 -> IP: {self.udp_ip}, 端口: {self.udp_port}")
+ self.udp_datacapture = VtrdynSocketUdp()
+ if self.udp_datacapture.udp_initial(2223):
+ self.dstAddr = self.udp_datacapture.udp_getsockaddr(self.udp_ip, self.udp_port)
+ self.node.get_logger().info(f"UDP连接初始化成功 -> 目标地址: {self.udp_ip}:{self.udp_port}")
+ self.udp_datacapture.udp_send_request_connect(self.dstAddr)
+ return True
+ else:
+ self.node.get_logger().error("UDP连接初始化失败!")
+ return False
+
+
+ def process_callback(self):
+ if not self.runing:
+ return
+
+ """定时器回调函数,处理数据并发布"""
+ if not self.udp_datacapture.udp_is_onnect():
+ self.node.get_logger().warning("侦测到UDP断开状态,正在重连!")
+ if self.udp_datacapture.udp_initial(2223):
+ if self.udp_datacapture.udp_send_request_connect(self.dstAddr):
+ self.node.get_logger().info("与服务器建立链路,启动接收线程......")
+ else:
+ self.node.get_logger().info("未与服务器正常通讯,请检查通讯连接......")
+ time.sleep(2)
+ return
+ else:
+ self.node.get_logger().error("UDP重连初始化失败!")
+ time.sleep(2)
+ return
+
+ mocapdata = MocapData()
+ self.udp_datacapture.udp_recv_mocap_data(mocapdata) # 接收数据
+ if not mocapdata.is_update:
+ return
+
+ right_hand_pose, left_hand_pose = self.handcore.generate_position(
+ mocapdata.quaternion_rHand,
+ mocapdata.quaternion_lHand)
+ qpos_r = self.handcore.projection_process(right_hand_pose)
+ qpos_l = self.handcore.projection_process(left_hand_pose)
+ qpos_r[0] = qpos_r[0] - 0.11
+ qpos_r[1] = qpos_r[1] - 0.09
+ qpos_r[2] = qpos_r[2] - 0.25
+ qpos_r[3] = qpos_r[3] - 0.12
+ qpos_r[4] = qpos_r[4] - 0.08
+ qpos_l[0] = qpos_l[0] - 0.15
+ qpos_l[1] = qpos_l[1] - 0.21
+ qpos_l[2] = qpos_l[2] - 0.15
+ qpos_l[3] = qpos_l[3] - 0.21
+ qpos_l[4] = qpos_l[4] - 0.11
+
+ # 处理左右手原始数据+重定向
+ self.lefthand.joint_update(qpos_l)
+ self.righthand.joint_update(qpos_r)
+ # 速度环节处理
+ self.lefthand.speed_update()
+ self.righthand.speed_update()
+
+ # 调试打印
+ if self.lefthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"左手位置: {self.lefthand.g_jointpositions}")
+ if self.righthandpubprint and self.pubprintcount % 1 == 0:
+ self.node.get_logger().info(f"右手位置: {self.righthand.g_jointpositions}")
+
+ # 发布右手数据
+ msg_r = JointState()
+ msg_r.header.stamp = self.node.get_clock().now().to_msg()
+ msg_r.name = [f'joint{i + 1}' for i in range(len(self.righthand.g_jointpositions))]
+ msg_r.position = [float(num) for num in self.righthand.g_jointpositions]
+ msg_r.velocity = [float(num) for num in self.righthand.g_jointvelocity]
+ self.publisher_r.publish(msg_r)
+
+ # 发布左手数据
+ msg_l = JointState()
+ msg_l.header.stamp = self.node.get_clock().now().to_msg()
+ msg_l.name = [f'joint{i + 1}' for i in range(len(self.lefthand.g_jointpositions))]
+ msg_l.position = [float(num) for num in self.lefthand.g_jointpositions]
+ msg_l.velocity = [float(num) for num in self.lefthand.g_jointvelocity]
+ self.publisher_l.publish(msg_l)
+
+ self.pubprintcount += 1
+
+ def process(self):
+ """主处理函数"""
+ if not self.initialize_udp():
+ self.get_logger().error("初始化配置网络失败")
+ return
+
diff --git a/src/linkerhand_retarget/package.xml b/src/linkerhand_retarget/package.xml
new file mode 100644
index 0000000..cfb575b
--- /dev/null
+++ b/src/linkerhand_retarget/package.xml
@@ -0,0 +1,18 @@
+
+
+
+ linkerhand_retarget
+ 2.11.7
+ ROS2 SDK for Linker Hand Teleoperation
+ Linker Robotics
+ MIT
+ ament_copyright
+ ament_flake8
+ ament_pep257
+ python3-pytest
+
+ rclpy
+
+ ament_python
+
+
diff --git a/src/linkerhand_retarget/resource/linkerhand_retarget b/src/linkerhand_retarget/resource/linkerhand_retarget
new file mode 100644
index 0000000..e69de29
diff --git a/src/linkerhand_retarget/setup.cfg b/src/linkerhand_retarget/setup.cfg
new file mode 100644
index 0000000..60b4918
--- /dev/null
+++ b/src/linkerhand_retarget/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script_dir=$base/lib/linkerhand_retarget
+[install]
+install_scripts=$base/lib/linkerhand_retarget
diff --git a/src/linkerhand_retarget/setup.py b/src/linkerhand_retarget/setup.py
new file mode 100644
index 0000000..1184add
--- /dev/null
+++ b/src/linkerhand_retarget/setup.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import os
+from glob import glob
+from setuptools import find_packages, setup
+
+package_name = 'linkerhand_retarget'
+
+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')),
+]
+
+rescoure_dir = 'resource'
+for dirpath, dirnames, filenames in os.walk(rescoure_dir):
+ share_path = os.path.relpath(dirpath,rescoure_dir)
+ for filename in filenames:
+ file_path = os.path.join(dirpath,filename)
+ data_files.append((os.path.join('share',package_name,share_path),[file_path]))
+
+setup(
+ name=package_name,
+ version='2.11.4',
+ packages=find_packages(include=[package_name, f"{package_name}.*"]),
+ data_files=data_files,
+ install_requires=[
+ 'setuptools',
+ 'numpy',
+ 'scipy>=1.10.0',
+ 'PyYAML',
+ 'transforms3d',
+ 'anytree',
+ 'loguru',
+ 'trimesh',
+ 'tqdm',
+ 'colorama',
+ 'lxml',
+ ],
+ zip_safe=True,
+ maintainer='Linker Robotics',
+ maintainer_email='support@linker-robotics.com',
+ description='ROS2 SDK for Linker Hand Teleoperation',
+ license='MIT',
+ entry_points={
+ 'console_scripts': [
+ 'handretarget = linkerhand_retarget.handretarget:main',
+ ],
+ },
+)
diff --git a/src/linkerhand_retarget/tests/integration/TEST_L6_VERSION_MAPPING.md b/src/linkerhand_retarget/tests/integration/TEST_L6_VERSION_MAPPING.md
new file mode 100644
index 0000000..83c76d5
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/TEST_L6_VERSION_MAPPING.md
@@ -0,0 +1,36 @@
+# L6 版本区分与延伸映射优化测试报告
+
+**测试日期:** 2026-03-24
+**测试版本:** v2.11.7
+**测试环境:** ROS2 Foxy, LinkerForce L6 左手/右手, 手套版本 v1 (1.2.12)
+
+## 更新内容
+
+1. v1/v2 版本区分支持(weights, reverse_motion 字典格式)
+2. 延伸映射只在 ['original', 'opose'] 模式触发
+3. 移除自动拟合功能
+4. 添加 MULTI_SEGMENT_CONFIG_FROZEN 配置冻结
+5. 更新 README 标定配置建议
+6. 修复标定结束后历史数据加载问题
+7. 添加电机输出约束功能 (MOTOR_CONSTRAINTS)
+8. 优化延伸映射参数
+9. 更新标定样本数据
+
+## 测试内容
+
+| 功能 | 状态 |
+|------|------|
+| v1/v2 版本区分 | ✅ 通过 |
+| 延伸映射触发条件 | ✅ 通过 |
+| 配置冻结机制 | ✅ 通过 |
+| 两段标定+延伸 (open + opose) | ✅ 通过 |
+| 两段标定直连 (open + fist) | ✅ 通过 |
+| 标定历史数据加载 | ✅ 通过 |
+| 左手测试 | ✅ 通过 |
+| 右手测试 | ✅ 通过 |
+| 电机输出约束 | ✅ 通过 |
+| ROS1 同步 | ✅ 通过 |
+
+**结论:** v2.11.7 版本功能正常,可以发布。
+
+**Gitee 分支:** https://gitee.com/ericbrunt/linkerhand_telop_python/tree/fix-linker-bot/linkerhand_telop_python%2310
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/integration/__init__.py b/src/linkerhand_retarget/tests/integration/__init__.py
new file mode 100644
index 0000000..b5508b0
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/__init__.py
@@ -0,0 +1 @@
+# Integration tests for linkerhand_retarget
diff --git a/src/linkerhand_retarget/tests/integration/test_config.py b/src/linkerhand_retarget/tests/integration/test_config.py
new file mode 100644
index 0000000..46aae9c
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_config.py
@@ -0,0 +1,24 @@
+import pytest
+import os
+from pathlib import Path
+from linkerhand.config import HandConfig
+
+
+class TestHandConfig:
+ @pytest.fixture
+ def config_path(self):
+ test_dir = Path(__file__).parent.parent.parent / "linkerhand_retarget"
+ return str(test_dir)
+
+ @pytest.fixture
+ def robot_dir(self):
+ test_dir = Path(__file__).parent.parent.parent / "linkerhand_retarget" / "assets" / "robots"
+ return str(test_dir)
+
+ @pytest.mark.skipif(not Path(__file__).parent.parent.parent.joinpath("linkerhand_retarget/assets").exists(), reason="Assets directory not found")
+ def test_hand_config_initialization(self, config_path, robot_dir):
+ config = HandConfig(robot_dir, config_path)
+ assert config.handconfig is not None
+ assert config.baseconfig is not None
+ assert config.retagetconfig is not None
+ assert config.modelconfig is not None
diff --git a/src/linkerhand_retarget/tests/integration/test_linkerforce.py b/src/linkerhand_retarget/tests/integration/test_linkerforce.py
new file mode 100644
index 0000000..ddc7349
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_linkerforce.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python3
+"""LinkerForce 完整集成测试 - 只通过公共接口访问,避免线程竞争"""
+import time
+import sys
+import statistics
+sys.path.insert(0, '/home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget')
+
+from linkerhand_retarget.linkerhand.linkerforce import ForceSerialReader
+from linkerhand_retarget.linkerhand.constants import HandType
+
+
+class LinkerForceIntegrationTest:
+ def __init__(self):
+ self.reader = None
+ self.port = None
+ self.baudrate = None
+ self.test_results = {}
+
+ def setup(self):
+ """初始化设备连接"""
+ print("=" * 60)
+ print("LinkerForce 完整集成测试")
+ print("=" * 60)
+
+ self.reader = ForceSerialReader(HandType.left, isdebug=False)
+
+ print("\n[初始化] 扫描设备...")
+ self.port, self.baudrate, _ = self.reader.find_valid_ports(timeout=3)
+
+ if self.port is None:
+ self.port = "/dev/ttyUSB0"
+ self.baudrate = 2000000
+
+ print(f"打开设备: {self.port} @ {self.baudrate}")
+ result = self.reader.openserial(self.port, self.baudrate)
+ if not result:
+ print("打开失败")
+ return False
+
+ self.reader.start()
+ self.reader.serial_port.write(self.reader.pack_01_data())
+ time.sleep(1)
+
+ print(f"设备信息: handtype={self.reader.handtype}, version={self.reader.version}")
+ return True
+
+ def teardown(self):
+ """关闭设备"""
+ if self.reader:
+ try:
+ self.reader.stop()
+ except:
+ pass
+ print("\n设备已关闭")
+
+ def test_device_info(self):
+ """测试1: 设备信息"""
+ print("\n" + "-" * 40)
+ print("[测试1] 设备信息")
+ print("-" * 40)
+
+ self.reader.serial_port.write(self.reader.pack_01_data())
+ time.sleep(0.5)
+
+ print(f" handtype: {self.reader.handtype}")
+ print(f" version: {self.reader.version}")
+ print(f" connflag: {self.reader.connflag}")
+
+ result = self.reader.handtype is not None and self.reader.version is not None
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_position_data_stability(self, duration=5):
+ """测试2: 位置数据稳定性"""
+ print("\n" + "-" * 40)
+ print(f"[测试2] 位置数据稳定性 ({duration}秒)")
+ print("-" * 40)
+
+ samples = []
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ self.reader.serial_port.write(self.reader.pack_03_data())
+ time.sleep(0.5)
+ if self.reader.poslist and len(self.reader.poslist) > 0:
+ samples.append(list(self.reader.poslist[:10]))
+
+ if len(samples) < 2:
+ print(" ⚠️ 样本不足")
+ return True
+
+ num_channels = len(samples[0])
+ stds = []
+ for ch in range(num_channels):
+ channel_data = [s[ch] for s in samples]
+ std = statistics.stdev(channel_data)
+ stds.append(std)
+
+ avg_std = statistics.mean(stds)
+ max_std = max(stds)
+
+ print(f" 采样数: {len(samples)}")
+ print(f" 通道数: {num_channels}")
+ print(f" 平均标准差: {avg_std:.6f} rad ({avg_std * 57.3:.4f}°)")
+ print(f" 最大标准差: {max_std:.6f} rad ({max_std * 57.3:.4f}°)")
+
+ result = max_std < 0.1
+ print(f" {'✅ 数据稳定' if result else '⚠️ 数据波动较大'}")
+ return result
+
+ def test_packet_statistics(self, duration=10):
+ """测试3: 数据包统计 - 通过时间戳计算"""
+ print("\n" + "-" * 40)
+ print(f"[测试3] 数据包统计 ({duration}秒)")
+ print("-" * 40)
+
+ sent_packets = 0
+ initial_count = self.reader.receive_count
+ last_poslist = None
+ data_changes = 0
+
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ self.reader.serial_port.write(self.reader.pack_03_data())
+ sent_packets += 1
+ time.sleep(0.1)
+
+ if self.reader.poslist:
+ if last_poslist is not None and self.reader.poslist != last_poslist:
+ data_changes += 1
+ last_poslist = list(self.reader.poslist)
+
+ time.sleep(0.4)
+
+ received_packets = self.reader.receive_count - initial_count
+ response_rate = (received_packets / sent_packets * 100) if sent_packets > 0 else 0
+
+ print(f" 发送请求: {sent_packets}")
+ print(f" 接收帧数: {received_packets}")
+ print(f" 数据变化: {data_changes}")
+ print(f" 响应率: {response_rate:.1f}%")
+
+ result = response_rate >= 80
+ print(f" {'✅ 响应率良好' if result else '⚠️ 响应率较低'}")
+ return result
+
+ def test_response_interval(self, duration=5):
+ """测试4: 响应间隔测试 - 通过时间戳列表计算"""
+ print("\n" + "-" * 40)
+ print(f"[测试4] 响应间隔测试 ({duration}秒)")
+ print("-" * 40)
+
+ # 清空时间戳列表
+ self.reader.receive_times = []
+
+ # 持续发送请求并收集数据
+ start_time = time.time()
+ while time.time() - start_time < duration:
+ self.reader.serial_port.write(self.reader.pack_03_data())
+ time.sleep(0.05) # 50ms间隔
+
+ # 从时间戳列表计算帧间隔
+ times = self.reader.receive_times
+
+ if len(times) >= 2:
+ intervals = []
+ for i in range(1, len(times)):
+ interval = (times[i] - times[i-1]) * 1000 # 转换为ms
+ intervals.append(interval)
+
+ avg_interval = statistics.mean(intervals)
+ min_interval = min(intervals)
+ max_interval = max(intervals)
+
+ # 计算帧率
+ fps = len(times) / duration
+
+ print(f" 接收帧数: {len(times)}")
+ print(f" 平均帧间隔: {avg_interval:.2f} ms")
+ print(f" 最小帧间隔: {min_interval:.2f} ms")
+ print(f" 最大帧间隔: {max_interval:.2f} ms")
+ print(f" 帧率: {fps:.1f} Hz")
+
+ result = avg_interval < 100
+ print(f" {'✅ 响应间隔良好' if result else '⚠️ 响应间隔较长'}")
+ return result
+ else:
+ print(f" 接收帧数: {len(times)}")
+ print(" ⚠️ 响应不足")
+ return True
+
+ def test_continuous_read(self, duration=15):
+ """测试5: 连续读取稳定性"""
+ print("\n" + "-" * 40)
+ print(f"[测试5] 连续读取稳定性 ({duration}秒)")
+ print("-" * 40)
+
+ initial_count = self.reader.receive_count
+ receive_times = []
+
+ start_time = time.time()
+ last_count = initial_count
+
+ while time.time() - start_time < duration:
+ self.reader.serial_port.write(self.reader.pack_03_data())
+ time.sleep(0.5)
+
+ current_count = self.reader.receive_count
+ if current_count > last_count:
+ receive_times.append(time.time())
+ last_count = current_count
+
+ total_received = self.reader.receive_count - initial_count
+
+ # 计算帧间隔
+ if len(receive_times) >= 2:
+ frame_intervals = []
+ for i in range(1, len(receive_times)):
+ interval = (receive_times[i] - receive_times[i-1]) * 1000
+ frame_intervals.append(interval)
+
+ avg_interval = statistics.mean(frame_intervals)
+ print(f" 接收帧数: {total_received}")
+ print(f" 平均帧间隔: {avg_interval:.2f} ms")
+ print(f" 帧率: {1000/avg_interval:.1f} Hz")
+
+ result = True
+ print(" ✅ 连续读取稳定")
+ return result
+ else:
+ print(f" 接收帧数: {total_received}")
+ print(" ⚠️ 接收数据不足")
+ return True
+
+ def test_all_protocols(self):
+ """测试6: 所有协议"""
+ print("\n" + "-" * 40)
+ print("[测试6] 所有协议测试")
+ print("-" * 40)
+
+ results = {}
+
+ # 0x01 - 设备信息
+ self.reader.serial_port.write(self.reader.pack_01_data())
+ time.sleep(0.5)
+ results['0x01'] = self.reader.handtype is not None
+ print(f" 0x01 (设备信息): {'✅' if results['0x01'] else '❌'}")
+
+ # 0x02 - 控制
+ self.reader.serial_port.write(self.reader.pack_02_data(1))
+ time.sleep(0.3)
+ results['0x02'] = True
+ print(f" 0x02 (控制命令): ✅")
+
+ # 0x03 - 位置数据
+ self.reader.serial_port.write(self.reader.pack_03_data())
+ time.sleep(0.3)
+ results['0x03'] = len(self.reader.poslist) > 0
+ print(f" 0x03 (位置数据): {'✅' if results['0x03'] else '❌'} ({len(self.reader.poslist)} floats)")
+
+ # 0x04 - 力数据
+ self.reader.serial_port.write(self.reader.pack_04_data())
+ time.sleep(0.3)
+ results['0x04'] = True
+ print(f" 0x04 (力数据): ✅")
+
+ # 0xA4 - 力发送
+ test_force = [100.0] * 5
+ self.reader.serial_port.write(self.reader.pack_A4_data(test_force))
+ time.sleep(0.3)
+ results['0xA4'] = True
+ print(f" 0xA4 (力发送): ✅")
+
+ # 0xA7 - 力发送变体
+ self.reader.serial_port.write(self.reader.pack_A7_data(test_force))
+ time.sleep(0.3)
+ results['0xA7'] = True
+ print(f" 0xA7 (力发送变体): ✅")
+
+ passed = sum(1 for v in results.values() if v)
+ print(f"\n 协议通过: {passed}/{len(results)}")
+ return passed == len(results)
+
+ def generate_report(self):
+ """生成测试报告"""
+ print("\n" + "=" * 60)
+ print("测试报告")
+ print("=" * 60)
+
+ print("\n## 测试环境")
+ print(f"- 串口: {self.port}")
+ print(f"- 波特率: {self.baudrate}")
+ print(f"- 设备类型: {self.reader.handtype if self.reader else 'N/A'}")
+ print(f"- 固件版本: {self.reader.version if self.reader else 'N/A'}")
+ print(f"- 总接收帧数: {self.reader.receive_count if self.reader else 'N/A'}")
+
+ print("\n## 测试结果")
+ passed = sum(1 for r in self.test_results.values() if r)
+ total = len(self.test_results)
+ print(f"- 通过: {passed}/{total}")
+
+ for name, result in self.test_results.items():
+ status = "✅ 通过" if result else "❌ 失败"
+ print(f" - {name}: {status}")
+
+ print("\n" + "=" * 60)
+
+ def run_all_tests(self):
+ """运行所有测试"""
+ if not self.setup():
+ return
+
+ try:
+ self.test_results['测试1-设备信息'] = self.test_device_info()
+ self.test_results['测试2-数据稳定性'] = self.test_position_data_stability(duration=5)
+ self.test_results['测试3-数据包统计'] = self.test_packet_statistics(duration=10)
+ self.test_results['测试4-响应间隔'] = self.test_response_interval(duration=5)
+ self.test_results['测试5-连续读取'] = self.test_continuous_read(duration=15)
+ self.test_results['测试6-协议测试'] = self.test_all_protocols()
+ except Exception as e:
+ print(f"\n测试中断: {e}")
+ finally:
+ self.teardown()
+
+ self.generate_report()
+
+
+if __name__ == "__main__":
+ test = LinkerForceIntegrationTest()
+ test.run_all_tests()
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/integration/test_linkerforce_retarget.py b/src/linkerhand_retarget/tests/integration/test_linkerforce_retarget.py
new file mode 100644
index 0000000..ff70059
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_linkerforce_retarget.py
@@ -0,0 +1,601 @@
+#!/usr/bin/env python3
+"""LinkerForce Retarget 集成测试 - 无ROS依赖版本"""
+import time
+import sys
+import copy
+import json
+import math
+import threading
+import statistics
+import numpy as np
+from pathlib import Path
+from datetime import datetime, timedelta
+
+sys.path.insert(0, '/home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget')
+
+from linkerhand_retarget.linkerhand.linkerforce import ForceSerialReader
+from linkerhand_retarget.linkerhand.constants import HandType, RobotName, ROBOT_LEN_MAP
+
+
+class MockHandCore:
+ """简化的HandCore用于测试"""
+ def __init__(self, num_joints=25):
+ self.hand_numjoints_r = num_joints
+ self.hand_numjoints_l = num_joints
+ self.hand_lower_limits_r = [-1.57] * num_joints
+ self.hand_upper_limits_r = [1.57] * num_joints
+ self.hand_lower_limits_l = [-1.57] * num_joints
+ self.hand_upper_limits_l = [1.57] * num_joints
+ self.dataminvalue_r = [0] * num_joints
+ self.datamaxvalue_r = [255] * num_joints
+ self.dataminvalue_l = [0] * num_joints
+ self.datamaxvalue_l = [255] * num_joints
+ self.sourcedataindex_r = list(range(num_joints))
+ self.sourcedataindex_l = list(range(num_joints))
+ self.urdfdataindex_r = list(range(num_joints))
+ self.urdfdataindex_l = list(range(num_joints))
+
+ def trans_to_motor_right(self, qpos):
+ result = [255] * len(qpos)
+ for i, val in enumerate(qpos):
+ val = max(-1.57, min(1.57, val))
+ result[i] = int((val + 1.57) / 3.14 * 255)
+ return result
+
+ def trans_to_motor_left(self, qpos):
+ return self.trans_to_motor_right(qpos)
+
+
+class RightHand:
+ """简化版右手"""
+ def __init__(self, handcore, length=25):
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+ self.handcore = handcore
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[15] = joint_arc[3] * -2.5
+ qpos[16] = joint_arc[20] * -2.6
+ qpos[17] = joint_arc[2] * -0.2
+ qpos[18] = joint_arc[1] * -1.5
+ qpos[19] = joint_arc[0] * -1.5
+ qpos[0] = joint_arc[7]
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if len(joint_arc) > 18:
+ qpos[4] = joint_arc[19]
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if len(joint_arc) > 10:
+ qpos[20] = joint_arc[11]
+ qpos[8] = joint_arc[10] * -1
+ qpos[9] = joint_arc[9] * -1
+ qpos[10] = joint_arc[8] * -1
+ if len(joint_arc) > 14:
+ qpos[11] = joint_arc[15]
+ qpos[12] = joint_arc[14] * -1
+ qpos[13] = joint_arc[13] * -1
+ qpos[14] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_right(qpos)
+
+ def speed_update(self):
+ pass
+
+
+class LeftHand:
+ """简化版左手"""
+ def __init__(self, handcore, length=25):
+ self.g_jointpositions = [255] * length
+ self.g_jointvelocity = [255] * length
+ self.last_jointpositions = [255] * length
+ self.last_jointvelocity = [255] * length
+ self.handstate = [0] * length
+ self.handcore = handcore
+ self.calibrationoriginal = None
+ self.calibrationfistpose = None
+ self.calibrationopose = None
+
+ def joint_update(self, joint_arc):
+ qpos = np.zeros(25)
+ qpos[15] = joint_arc[3] * 2.5
+ qpos[16] = joint_arc[20] * 2.6
+ qpos[17] = joint_arc[2] * 0.2
+ qpos[18] = joint_arc[1] * 1.5
+ qpos[19] = joint_arc[0] * 1.5
+ qpos[0] = joint_arc[7] * -1
+ qpos[1] = joint_arc[6] * -1
+ qpos[2] = joint_arc[5] * -1
+ qpos[3] = joint_arc[4] * -1
+ if len(joint_arc) > 18:
+ qpos[4] = joint_arc[19] * -1
+ qpos[5] = joint_arc[18] * -1
+ qpos[6] = joint_arc[17] * -1
+ qpos[7] = joint_arc[16] * -1
+ if len(joint_arc) > 10:
+ qpos[20] = joint_arc[11] * 0
+ qpos[8] = joint_arc[10] * -2
+ qpos[9] = joint_arc[9] * -1
+ qpos[10] = joint_arc[8] * -1
+ if len(joint_arc) > 14:
+ qpos[11] = joint_arc[15] * -1
+ qpos[12] = joint_arc[14] * -1
+ qpos[13] = joint_arc[13] * -1
+ qpos[14] = joint_arc[12] * -1
+ self.g_jointpositions = self.handcore.trans_to_motor_left(qpos)
+
+ def speed_update(self):
+ pass
+
+
+class RetargetCore:
+ """Retarget核心逻辑 - 无ROS依赖"""
+
+ def __init__(self,
+ lefthand: RobotName = RobotName.l21,
+ righthand: RobotName = RobotName.l21,
+ calibration: bool = False):
+ self.lefthandtype = lefthand
+ self.righthandtype = righthand
+ self.calibration = calibration
+
+ self.force_reader_left = None
+ self.force_reader_right = None
+ self.forcelock = threading.Lock()
+
+ self.handcore = MockHandCore(num_joints=ROBOT_LEN_MAP[lefthand])
+ self.lefthand = LeftHand(handcore=self.handcore, length=ROBOT_LEN_MAP[lefthand])
+ self.righthand = RightHand(handcore=self.handcore, length=ROBOT_LEN_MAP[righthand])
+
+ self.results = {'left': {}, 'right': {}}
+ self.leftport = None
+ self.rightport = None
+ self.leftbaudrate = None
+ self.rightbaudrate = None
+
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+
+ self.running = False
+ self._thread = None
+
+ self.receive_times_left = []
+ self.receive_times_right = []
+
+ def linkerforce_init(self):
+ """初始化串口连接"""
+ exclude_list = []
+ baudrates = [2000000, 1000000, 921600, 460800]
+
+ print("\n[左手] 扫描设备...")
+ self.force_reader_left = ForceSerialReader(
+ HandType.left,
+ excludelist=exclude_list,
+ baudrates=baudrates,
+ isdebug=False
+ )
+ self.leftport, self.leftbaudrate, errorcode = self.force_reader_left.find_valid_ports(timeout=3)
+
+ if self.leftport:
+ if self.force_reader_left.openserial(port=self.leftport, baudrate=self.leftbaudrate):
+ self.force_reader_left.start()
+ time.sleep(0.1)
+ self.force_reader_left.serial_port.write(self.force_reader_left.pack_01_data())
+ time.sleep(0.5)
+ if self.force_reader_left.handtype == 'Left':
+ print(f"[左手] 已连接: {self.leftport} @ {self.leftbaudrate}, 版本: {self.force_reader_left.version}")
+ else:
+ print(f"[左手] 设备类型不匹配: {self.force_reader_left.handtype}")
+ exclude_list.append(self.leftport)
+ else:
+ print("[左手] 未找到设备")
+
+ print("\n[右手] 扫描设备...")
+ self.force_reader_right = ForceSerialReader(
+ HandType.right,
+ excludelist=exclude_list,
+ baudrates=baudrates,
+ isdebug=False
+ )
+ self.rightport, self.rightbaudrate, errorcode = self.force_reader_right.find_valid_ports(timeout=3)
+
+ if self.rightport:
+ if self.force_reader_right.openserial(port=self.rightport, baudrate=self.rightbaudrate):
+ self.force_reader_right.start()
+ time.sleep(0.1)
+ self.force_reader_right.serial_port.write(self.force_reader_right.pack_01_data())
+ time.sleep(0.5)
+ if self.force_reader_right.handtype == 'Right':
+ print(f"[右手] 已连接: {self.rightport} @ {self.rightbaudrate}, 版本: {self.force_reader_right.version}")
+ else:
+ print(f"[右手] 设备类型不匹配: {self.force_reader_right.handtype}")
+ else:
+ print("[右手] 未找到设备")
+
+ if self.calibration:
+ self.run_calibration()
+
+ def _calculate_weighted_average(self, data_list):
+ """计算加权平均值"""
+ if not data_list:
+ return [0.0] * 21
+
+ n = len(data_list)
+ if n == 1:
+ return data_list[0]
+
+ weights = [i + 1 for i in range(n)]
+ total_weight = sum(weights)
+
+ result = [0.0] * len(data_list[0])
+ for i, data in enumerate(data_list):
+ w = weights[i] / total_weight
+ for j in range(len(result)):
+ result[j] += data[j] * w
+
+ return result
+
+ def run_calibration(self, duration_per_pose=5):
+ """执行标定流程"""
+ print("\n" + "=" * 50)
+ print("开始标定流程")
+ print("=" * 50)
+
+ if self.force_reader_left and self.force_reader_left.handtype != 'Left' and \
+ self.force_reader_right and self.force_reader_right.handtype != 'Right':
+ print("无可用设备,跳过标定")
+ return False
+
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+
+ print(f"\n[标定 1/3] 请保持五指张开姿势 (对应电机值255)")
+ self._collect_calibration_data(duration_per_pose)
+ if self.calibration_data_left:
+ self.lefthand.calibrationoriginal = self._calculate_weighted_average(self.calibration_data_left)
+ if self.calibration_data_right:
+ self.righthand.calibrationoriginal = self._calculate_weighted_average(self.calibration_data_right)
+
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+
+ print(f"\n[标定 2/3] 请握紧拳头 (对应电机值0)")
+ self._collect_calibration_data(duration_per_pose)
+ if self.calibration_data_left:
+ self.lefthand.calibrationfistpose = self._calculate_weighted_average(self.calibration_data_left)
+ if self.calibration_data_right:
+ self.righthand.calibrationfistpose = self._calculate_weighted_average(self.calibration_data_right)
+
+ self.calibration_data_left = []
+ self.calibration_data_right = []
+
+ print(f"\n[标定 3/3] 请保持O型手势 (对应电机中间值)")
+ self._collect_calibration_data(duration_per_pose)
+ if self.calibration_data_left:
+ self.lefthand.calibrationopose = self._calculate_weighted_average(self.calibration_data_left)
+ if self.calibration_data_right:
+ self.righthand.calibrationopose = self._calculate_weighted_average(self.calibration_data_right)
+
+ print("\n标定完成")
+ return True
+
+ def _collect_calibration_data(self, duration):
+ """采集标定数据"""
+ prepare_time = duration * 0.4
+ collect_time = duration * 0.6
+
+ print(f" 准备阶段: {prepare_time:.1f}s")
+ time.sleep(prepare_time)
+
+ print(f" 采集阶段: {collect_time:.1f}s")
+ start = time.time()
+ while time.time() - start < collect_time:
+ if self.force_reader_left and self.force_reader_left.handtype == 'Left':
+ self.calibration_data_left.append(copy.deepcopy(self.force_reader_left.poslist))
+ if self.force_reader_right and self.force_reader_right.handtype == 'Right':
+ self.calibration_data_right.append(copy.deepcopy(self.force_reader_right.poslist))
+ time.sleep(0.05)
+
+ print(f" 采集样本: 左手 {len(self.calibration_data_left)}, 右手 {len(self.calibration_data_right)}")
+
+ def process_once(self):
+ """单次数据处理"""
+ if self.force_reader_left and self.force_reader_left.handtype == 'Left':
+ try:
+ self.force_reader_left.serial_port.write(self.force_reader_left.pack_03_data())
+ except:
+ pass
+ left_positions = copy.deepcopy(self.force_reader_left.poslist)
+ self.lefthand.joint_update(left_positions)
+ self.lefthand.speed_update()
+
+ if self.force_reader_left.receive_times:
+ self.receive_times_left = self.force_reader_left.receive_times[-100:]
+
+ if self.force_reader_right and self.force_reader_right.handtype == 'Right':
+ try:
+ self.force_reader_right.serial_port.write(self.force_reader_right.pack_03_data())
+ except:
+ pass
+ right_positions = copy.deepcopy(self.force_reader_right.poslist)
+ self.righthand.joint_update(right_positions)
+ self.righthand.speed_update()
+
+ if self.force_reader_right.receive_times:
+ self.receive_times_right = self.force_reader_right.receive_times[-100:]
+
+ def _process_loop(self, rate_hz=30):
+ """处理循环"""
+ interval = 1.0 / rate_hz
+ while self.running:
+ self.process_once()
+ time.sleep(interval)
+
+ def start_processing(self, rate_hz=30):
+ """启动处理线程"""
+ self.running = True
+ self._thread = threading.Thread(target=self._process_loop, args=(rate_hz,), daemon=True)
+ self._thread.start()
+
+ def stop_processing(self):
+ """停止处理"""
+ self.running = False
+ if self._thread:
+ self._thread.join(timeout=2)
+
+ def stop(self):
+ """停止所有连接"""
+ self.stop_processing()
+ if self.force_reader_left:
+ try:
+ self.force_reader_left.stop()
+ except:
+ pass
+ if self.force_reader_right:
+ try:
+ self.force_reader_right.stop()
+ except:
+ pass
+
+ def get_stats(self):
+ """获取统计数据"""
+ stats = {
+ 'left': {
+ 'connected': self.force_reader_left and self.force_reader_left.handtype == 'Left',
+ 'port': self.leftport,
+ 'baudrate': self.leftbaudrate,
+ 'version': self.force_reader_left.version if self.force_reader_left else None,
+ 'receive_count': self.force_reader_left.receive_count if self.force_reader_left else 0,
+ 'motor_positions': self.lefthand.g_jointpositions[:6] if self.lefthand else []
+ },
+ 'right': {
+ 'connected': self.force_reader_right and self.force_reader_right.handtype == 'Right',
+ 'port': self.rightport,
+ 'baudrate': self.rightbaudrate,
+ 'version': self.force_reader_right.version if self.force_reader_right else None,
+ 'receive_count': self.force_reader_right.receive_count if self.force_reader_right else 0,
+ 'motor_positions': self.righthand.g_jointpositions[:6] if self.righthand else []
+ }
+ }
+ return stats
+
+
+class RetargetIntegrationTest:
+ """Retarget集成测试"""
+
+ def __init__(self):
+ self.retarget = None
+ self.test_results = {}
+
+ def setup(self):
+ """初始化"""
+ print("=" * 60)
+ print("LinkerForce Retarget 集成测试 (无ROS)")
+ print("=" * 60)
+
+ self.retarget = RetargetCore(
+ lefthand=RobotName.l21,
+ righthand=RobotName.l21,
+ calibration=False
+ )
+ self.retarget.linkerforce_init()
+ return True
+
+ def teardown(self):
+ """清理"""
+ if self.retarget:
+ self.retarget.stop()
+ print("\n设备已关闭")
+
+ def test_device_connection(self):
+ """测试1: 设备连接"""
+ print("\n" + "-" * 40)
+ print("[测试1] 设备连接")
+ print("-" * 40)
+
+ stats = self.retarget.get_stats()
+
+ left_ok = stats['left']['connected']
+ right_ok = stats['right']['connected']
+
+ print(f" 左手: {'✅ 已连接' if left_ok else '❌ 未连接'}")
+ if left_ok:
+ print(f" 端口: {stats['left']['port']} @ {stats['left']['baudrate']}")
+ print(f" 版本: {stats['left']['version']}")
+
+ print(f" 右手: {'✅ 已连接' if right_ok else '❌ 未连接'}")
+ if right_ok:
+ print(f" 端口: {stats['right']['port']} @ {stats['right']['baudrate']}")
+ print(f" 版本: {stats['right']['version']}")
+
+ result = left_ok or right_ok
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_joint_mapping(self, duration=5):
+ """测试2: 关节映射"""
+ print("\n" + "-" * 40)
+ print(f"[测试2] 关节映射 ({duration}s)")
+ print("-" * 40)
+
+ self.retarget.start_processing(rate_hz=30)
+ time.sleep(duration)
+ self.retarget.stop_processing()
+
+ stats = self.retarget.get_stats()
+
+ if stats['left']['connected']:
+ motor_pos = stats['left']['motor_positions']
+ print(f" 左手电机位置: {motor_pos}")
+ valid = all(0 <= p <= 255 for p in motor_pos)
+ print(f" 左手映射有效性: {'✅' if valid else '❌'}")
+
+ if stats['right']['connected']:
+ motor_pos = stats['right']['motor_positions']
+ print(f" 右手电机位置: {motor_pos}")
+ valid = all(0 <= p <= 255 for p in motor_pos)
+ print(f" 右手映射有效性: {'✅' if valid else '❌'}")
+
+ result = True
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_data_rate(self, duration=10):
+ """测试3: 数据帧率"""
+ print("\n" + "-" * 40)
+ print(f"[测试3] 数据帧率 ({duration}s)")
+ print("-" * 40)
+
+ self.retarget.start_processing(rate_hz=30)
+
+ initial_left = self.retarget.force_reader_left.receive_count if self.retarget.force_reader_left else 0
+ initial_right = self.retarget.force_reader_right.receive_count if self.retarget.force_reader_right else 0
+
+ time.sleep(duration)
+
+ self.retarget.stop_processing()
+
+ final_left = self.retarget.force_reader_left.receive_count if self.retarget.force_reader_left else 0
+ final_right = self.retarget.force_reader_right.receive_count if self.retarget.force_reader_right else 0
+
+ left_frames = final_left - initial_left
+ right_frames = final_right - initial_right
+
+ left_fps = left_frames / duration if left_frames > 0 else 0
+ right_fps = right_frames / duration if right_frames > 0 else 0
+
+ if left_frames > 0:
+ print(f" 左手: {left_frames} 帧, {left_fps:.1f} Hz")
+ if right_frames > 0:
+ print(f" 右手: {right_frames} 帧, {right_fps:.1f} Hz")
+
+ result = left_fps >= 5 or right_fps >= 5
+ print(f" {'✅ 通过' if result else '❌ 帧率过低'}")
+ return result
+
+ def test_frame_interval(self, duration=5):
+ """测试4: 帧间隔分析"""
+ print("\n" + "-" * 40)
+ print(f"[测试4] 帧间隔分析 ({duration}s)")
+ print("-" * 40)
+
+ self.retarget.start_processing(rate_hz=30)
+ time.sleep(duration)
+ self.retarget.stop_processing()
+
+ stats = self.retarget.get_stats()
+
+ if self.retarget.receive_times_left and len(self.retarget.receive_times_left) >= 2:
+ times = self.retarget.receive_times_left
+ intervals = [(times[i] - times[i-1]) * 1000 for i in range(1, len(times))]
+ avg = statistics.mean(intervals)
+ std = statistics.stdev(intervals) if len(intervals) > 1 else 0
+ print(f" 左手帧间隔: 平均 {avg:.2f}ms, 标准差 {std:.2f}ms")
+
+ if self.retarget.receive_times_right and len(self.retarget.receive_times_right) >= 2:
+ times = self.retarget.receive_times_right
+ intervals = [(times[i] - times[i-1]) * 1000 for i in range(1, len(times))]
+ avg = statistics.mean(intervals)
+ std = statistics.stdev(intervals) if len(intervals) > 1 else 0
+ print(f" 右手帧间隔: 平均 {avg:.2f}ms, 标准差 {std:.2f}ms")
+
+ result = True
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_calibration(self, duration_per_pose=3):
+ """测试5: 标定流程"""
+ print("\n" + "-" * 40)
+ print("[测试5] 标定流程")
+ print("-" * 40)
+
+ if not self.retarget.force_reader_left and not self.retarget.force_reader_right:
+ print(" ⚠️ 无设备,跳过")
+ return True
+
+ print(" 开始标定 (每个姿势 3 秒)...")
+ result = self.retarget.run_calibration(duration_per_pose=duration_per_pose)
+
+ if result:
+ print(f" 左手张开数据: {self.retarget.lefthand.calibrationoriginal[:5] if self.retarget.lefthand.calibrationoriginal else 'N/A'}...")
+ print(f" 左手握拳数据: {self.retarget.lefthand.calibrationfistpose[:5] if self.retarget.lefthand.calibrationfistpose else 'N/A'}...")
+
+ print(f" {'✅ 标定完成' if result else '❌ 标定失败'}")
+ return result
+
+ def generate_report(self):
+ """生成报告"""
+ print("\n" + "=" * 60)
+ print("测试报告")
+ print("=" * 60)
+
+ stats = self.retarget.get_stats() if self.retarget else {}
+
+ print("\n## 设备状态")
+ if stats.get('left', {}).get('connected'):
+ print(f"- 左手: {stats['left']['port']} @ {stats['left']['baudrate']}, 版本 {stats['left']['version']}")
+ if stats.get('right', {}).get('connected'):
+ print(f"- 右手: {stats['right']['port']} @ {stats['right']['baudrate']}, 版本 {stats['right']['version']}")
+
+ print("\n## 测试结果")
+ passed = sum(1 for r in self.test_results.values() if r)
+ total = len(self.test_results)
+ print(f"- 通过: {passed}/{total}")
+
+ for name, result in self.test_results.items():
+ print(f" - {name}: {'✅' if result else '❌'}")
+
+ print("\n" + "=" * 60)
+
+ def run_all_tests(self):
+ """运行所有测试"""
+ if not self.setup():
+ return
+
+ try:
+ self.test_results['测试1-设备连接'] = self.test_device_connection()
+ self.test_results['测试2-关节映射'] = self.test_joint_mapping(duration=5)
+ self.test_results['测试3-数据帧率'] = self.test_data_rate(duration=10)
+ self.test_results['测试4-帧间隔'] = self.test_frame_interval(duration=5)
+ # self.test_results['测试5-标定流程'] = self.test_calibration(duration_per_pose=3)
+ except Exception as e:
+ print(f"\n测试中断: {e}")
+ import traceback
+ traceback.print_exc()
+ finally:
+ self.teardown()
+
+ self.generate_report()
+
+
+if __name__ == "__main__":
+ test = RetargetIntegrationTest()
+ test.run_all_tests()
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/integration/test_linkermcg.py b/src/linkerhand_retarget/tests/integration/test_linkermcg.py
new file mode 100644
index 0000000..3af0758
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_linkermcg.py
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+"""LinkerMCG 集成测试 - UDP 客户端"""
+import time
+import sys
+sys.path.insert(0, '/home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget')
+
+from linkerhand_retarget.linkerhand.linkermcgcore import HaoCunScoketUdp, HaoCunData
+
+
+class LinkerMCGIntegrationTest:
+ def __init__(self):
+ self.client = None
+ self.test_results = {}
+
+ def setup(self, host='192.168.1.23', port=8888):
+ print("=" * 60)
+ print("LinkerMCG 集成测试")
+ print("=" * 60)
+
+ print(f"\n[初始化] 连接 UDP {host}:{port}...")
+
+ self.client = HaoCunScoketUdp(host=host, port=port)
+ result = self.client.udp_initial()
+
+ if result:
+ print(f"✅ UDP 初始化成功")
+ return True
+ else:
+ print(f"❌ UDP 初始化失败")
+ return False
+
+ def teardown(self):
+ if self.client:
+ self.client.udp_close()
+ print("\n设备已关闭")
+
+ def test_connection(self):
+ print("\n" + "-" * 40)
+ print("[测试1] 连接状态")
+ print("-" * 40)
+
+ is_connect = self.client.udp_is_connect()
+ print(f" is_connected: {is_connect}")
+
+ print(f" {'✅ 通过' if is_connect else '❌ 失败'}")
+ return is_connect
+
+ def test_receive_data(self, duration=5):
+ print("\n" + "-" * 40)
+ print(f"[测试2] 数据接收 ({duration}秒)")
+ print("-" * 40)
+
+ frame_count = 0
+ last_frame = 0
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(0.5)
+ current_frame = self.client.realmocapdata.frame_index
+ if current_frame > last_frame:
+ frame_count += current_frame - last_frame
+ last_frame = current_frame
+ print(f" 接收帧: {current_frame}")
+
+ print(f" 总帧数: {frame_count}")
+ print(f" 帧率: {frame_count / duration:.1f} Hz")
+
+ result = frame_count > 0
+ print(f" {'✅ 通过' if result else '❌ 无数据'}")
+ return result
+
+ def test_data_content(self):
+ print("\n" + "-" * 40)
+ print("[测试3] 数据内容")
+ print("-" * 40)
+
+ time.sleep(1)
+
+ data = self.client.realmocapdata
+ print(f" frame_index: {data.frame_index}")
+ print(f" is_update: {data.is_update}")
+
+ r_hand = data.jointangle_rHand
+ l_hand = data.jointangle_lHand
+
+ print(f" 右手关节数: {len(r_hand)}")
+ print(f" 左手关节数: {len(l_hand)}")
+
+ r_nonzero = any(v != 0.0 for v in r_hand)
+ l_nonzero = any(v != 0.0 for v in l_hand)
+
+ if r_nonzero:
+ print(f" 右手关节示例: {[f'{v:.2f}' for v in r_hand[:5]]}")
+ if l_nonzero:
+ print(f" 左手关节示例: {[f'{v:.2f}' for v in l_hand[:5]]}")
+
+ result = len(r_hand) == 25 and len(l_hand) == 25
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_continuous_read(self, duration=10):
+ print("\n" + "-" * 40)
+ print(f"[测试4] 连续读取 ({duration}秒)")
+ print("-" * 40)
+
+ frames = []
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(1)
+ frame = self.client.realmocapdata.frame_index
+ frames.append(frame)
+ print(f" {int(time.time() - start_time)}s: frame={frame}")
+
+ if len(frames) >= 2:
+ frame_diff = frames[-1] - frames[0]
+ avg_fps = frame_diff / (duration - 1) if duration > 1 else 0
+ print(f" 平均帧率: {avg_fps:.1f} Hz")
+
+ result = len(frames) > 0
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def generate_report(self):
+ print("\n" + "=" * 60)
+ print("测试报告")
+ print("=" * 60)
+
+ print("\n## 测试结果")
+ passed = sum(1 for r in self.test_results.values() if r)
+ total = len(self.test_results)
+ print(f"- 通过: {passed}/{total}")
+
+ for name, result in self.test_results.items():
+ status = "✅ 通过" if result else "❌ 失败"
+ print(f" - {name}: {status}")
+
+ print("\n" + "=" * 60)
+
+ def run_all_tests(self, host='192.168.1.23', port=8888):
+ if not self.setup(host, port):
+ return
+
+ try:
+ self.test_results['测试1-连接状态'] = self.test_connection()
+ self.test_results['测试2-数据接收'] = self.test_receive_data(duration=5)
+ self.test_results['测试3-数据内容'] = self.test_data_content()
+ self.test_results['测试4-连续读取'] = self.test_continuous_read(duration=10)
+ except Exception as e:
+ print(f"\n测试中断: {e}")
+ finally:
+ self.teardown()
+
+ self.generate_report()
+
+
+if __name__ == "__main__":
+ test = LinkerMCGIntegrationTest()
+ test.run_all_tests(host='192.168.11.88', port=9000)
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/integration/test_udexreal.py b/src/linkerhand_retarget/tests/integration/test_udexreal.py
new file mode 100644
index 0000000..9984234
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_udexreal.py
@@ -0,0 +1,191 @@
+#!/usr/bin/env python3
+"""UdexReal 集成测试 - UDP 设备连接"""
+import time
+import sys
+sys.path.insert(0, '/home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget')
+
+from linkerhand_retarget.linkerhand.udexrealcore import UdexRealScoketUdp, UdexRealData
+
+
+class UdexRealIntegrationTest:
+ def __init__(self):
+ self.client = None
+ self.test_results = {}
+
+ def setup(self, host='0.0.0.0', port=8888):
+ """初始化设备连接"""
+ print("=" * 60)
+ print("UdexReal 集成测试")
+ print("=" * 60)
+
+ print(f"\n[初始化] 连接 UDP {host}:{port}...")
+
+ self.client = UdexRealScoketUdp(host=host, port=port)
+ result = self.client.udp_initial()
+
+ if result:
+ print(f"✅ UDP 初始化成功")
+ return True
+ else:
+ print(f"❌ UDP 初始化失败")
+ return False
+
+ def teardown(self):
+ """关闭设备"""
+ if self.client:
+ self.client.udp_close()
+ print("\n设备已关闭")
+
+ def test_connection(self):
+ """测试1: 连接状态"""
+ print("\n" + "-" * 40)
+ print("[测试1] 连接状态")
+ print("-" * 40)
+
+ status = self.client.get_connection_status()
+ print(f" is_connected: {status['is_connected']}")
+ print(f" is_data_timeout: {status['is_data_timeout']}")
+
+ result = status['is_connected']
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_receive_data(self, duration=5):
+ """测试2: 数据接收"""
+ print("\n" + "-" * 40)
+ print(f"[测试2] 数据接收 ({duration}秒)")
+ print("-" * 40)
+
+ frame_count = 0
+ last_frame = 0
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(0.5)
+ current_frame = self.client.realmocapdata.frame_index
+ if current_frame > last_frame:
+ frame_count += current_frame - last_frame
+ last_frame = current_frame
+ print(f" 接收帧: {current_frame}")
+
+ print(f" 总帧数: {frame_count}")
+ print(f" 帧率: {frame_count / duration:.1f} Hz")
+
+ result = frame_count > 0
+ print(f" {'✅ 通过' if result else '❌ 无数据'}")
+ return result
+
+ def test_data_content(self):
+ """测试3: 数据内容"""
+ print("\n" + "-" * 40)
+ print("[测试3] 数据内容")
+ print("-" * 40)
+
+ time.sleep(1)
+
+ data = self.client.realmocapdata
+ print(f" frame_index: {data.frame_index}")
+ print(f" frequency: {data.frequency}")
+ print(f" is_update: {data.is_update}")
+
+ # 检查关节数据
+ r_hand = data.jointangle_rHand
+ l_hand = data.jointangle_lHand
+
+ print(f" 右手关节数: {len(r_hand)}")
+ print(f" 左手关节数: {len(l_hand)}")
+
+ # 检查是否有非零数据
+ r_nonzero = any(v != 0.0 for v in r_hand)
+ l_nonzero = any(v != 0.0 for v in l_hand)
+
+ if r_nonzero:
+ print(f" 右手关节示例: {r_hand[:5]}")
+ if l_nonzero:
+ print(f" 左手关节示例: {l_hand[:5]}")
+
+ result = len(r_hand) == 24 and len(l_hand) == 24
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_timeout_detection(self, timeout=2):
+ """测试4: 超时检测"""
+ print("\n" + "-" * 40)
+ print(f"[测试4] 超时检测")
+ print("-" * 40)
+
+ timeout_status = self.client.check_timeout()
+ print(f" is_timeout: {timeout_status.is_timeout}")
+ print(f" time_since_last_data: {timeout_status.time_since_last_data:.3f}s")
+ print(f" timeout_threshold: {timeout_status.timeout_threshold}s")
+
+ result = True # 功能存在即通过
+ print(f" ✅ 超时检测功能正常")
+ return result
+
+ def test_continuous_read(self, duration=10):
+ """测试5: 连续读取"""
+ print("\n" + "-" * 40)
+ print(f"[测试5] 连续读取 ({duration}秒)")
+ print("-" * 40)
+
+ frames = []
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(1)
+ frame = self.client.realmocapdata.frame_index
+ frames.append(frame)
+ timeout_status = self.client.check_timeout()
+ status = "⏰ 超时" if timeout_status.is_timeout else "✓"
+ print(f" {int(time.time() - start_time)}s: frame={frame} {status}")
+
+ # 计算帧率
+ if len(frames) >= 2:
+ frame_diff = frames[-1] - frames[0]
+ avg_fps = frame_diff / (duration - 1) if duration > 1 else 0
+ print(f" 平均帧率: {avg_fps:.1f} Hz")
+
+ result = len(frames) > 0
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def generate_report(self):
+ """生成测试报告"""
+ print("\n" + "=" * 60)
+ print("测试报告")
+ print("=" * 60)
+
+ print("\n## 测试结果")
+ passed = sum(1 for r in self.test_results.values() if r)
+ total = len(self.test_results)
+ print(f"- 通过: {passed}/{total}")
+
+ for name, result in self.test_results.items():
+ status = "✅ 通过" if result else "❌ 失败"
+ print(f" - {name}: {status}")
+
+ print("\n" + "=" * 60)
+
+ def run_all_tests(self, host='0.0.0.0', port=8888):
+ """运行所有测试"""
+ if not self.setup(host, port):
+ return
+
+ try:
+ self.test_results['测试1-连接状态'] = self.test_connection()
+ self.test_results['测试2-数据接收'] = self.test_receive_data(duration=5)
+ self.test_results['测试3-数据内容'] = self.test_data_content()
+ self.test_results['测试4-超时检测'] = self.test_timeout_detection()
+ self.test_results['测试5-连续读取'] = self.test_continuous_read(duration=10)
+ except Exception as e:
+ print(f"\n测试中断: {e}")
+ finally:
+ self.teardown()
+
+ self.generate_report()
+
+
+if __name__ == "__main__":
+ test = UdexRealIntegrationTest()
+ test.run_all_tests(host='0.0.0.0', port=8888)
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/integration/test_vtrdyn.py b/src/linkerhand_retarget/tests/integration/test_vtrdyn.py
new file mode 100644
index 0000000..dc38491
--- /dev/null
+++ b/src/linkerhand_retarget/tests/integration/test_vtrdyn.py
@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+"""VtrDyn 集成测试 - UDP 设备"""
+import time
+import sys
+sys.path.insert(0, '/home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget')
+
+from linkerhand_retarget.linkerhand.vtrdyncore import VtrdynSocketUdp, MocapData
+
+
+class VtrDynIntegrationTest:
+ def __init__(self):
+ self.client = None
+ self.test_results = {}
+
+ def setup(self, local_port=7000, remote_ip='192.168.11.88', remote_port=7000):
+ print("=" * 60)
+ print("VtrDyn 集成测试")
+ print("=" * 60)
+
+ print(f"\n[初始化] 本地端口 {local_port}, 远程 {remote_ip}:{remote_port}...")
+
+ self.client = VtrdynSocketUdp(debug=True)
+ result = self.client.udp_initial(local_port)
+
+ if not result:
+ print(f"❌ UDP 初始化失败")
+ return False
+
+ print(f"✅ UDP 初始化成功")
+
+ dst_addr = (remote_ip, remote_port)
+ conn_result = self.client.udp_send_request_connect(dst_addr)
+
+ if conn_result:
+ print(f"✅ 连接成功")
+ else:
+ print(f"⚠️ 发送连接请求,等待数据...")
+
+ return True
+
+ def teardown(self):
+ if self.client:
+ self.client.udp_close(('192.168.11.88', 7000))
+ print("\n设备已关闭")
+
+ def test_connection(self):
+ print("\n" + "-" * 40)
+ print("[测试1] 连接状态")
+ print("-" * 40)
+
+ is_connect = self.client.udp_is_onnect()
+ print(f" is_connected: {is_connect}")
+
+ print(f" {'✅ 通过' if is_connect else '❌ 失败'}")
+ return is_connect
+
+ def test_receive_data(self, duration=5):
+ print("\n" + "-" * 40)
+ print(f"[测试2] 数据接收 ({duration}秒)")
+ print("-" * 40)
+
+ frame_count = 0
+ last_frame = 0
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(0.5)
+ current_frame = self.client.mocap_data_realtime.frame_index
+ if current_frame > last_frame:
+ frame_count += current_frame - last_frame
+ last_frame = current_frame
+ print(f" 接收帧: {current_frame}")
+
+ print(f" 总帧数: {frame_count}")
+ print(f" 帧率: {frame_count / duration:.1f} Hz")
+
+ result = frame_count > 0
+ print(f" {'✅ 通过' if result else '❌ 无数据'}")
+ return result
+
+ def test_data_content(self):
+ print("\n" + "-" * 40)
+ print("[测试3] 数据内容")
+ print("-" * 40)
+
+ time.sleep(1)
+
+ data = self.client.mocap_data_realtime
+ print(f" frame_index: {data.frame_index}")
+ print(f" frequency: {data.frequency}")
+ print(f" is_update: {data.is_update}")
+
+ print(f" 身体节点数: {len(data.position_body)}")
+ print(f" 右手节点数: {len(data.position_rHand)}")
+ print(f" 左手节点数: {len(data.position_lHand)}")
+
+ body_pos = data.position_body[0] if data.position_body else [0,0,0]
+ print(f" 身体位置示例: {[f'{v:.3f}' for v in body_pos]}")
+
+ result = len(data.position_body) == 23
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def test_continuous_read(self, duration=10):
+ print("\n" + "-" * 40)
+ print(f"[测试4] 连续读取 ({duration}秒)")
+ print("-" * 40)
+
+ frames = []
+ start_time = time.time()
+
+ while time.time() - start_time < duration:
+ time.sleep(1)
+ frame = self.client.mocap_data_realtime.frame_index
+ frames.append(frame)
+ print(f" {int(time.time() - start_time)}s: frame={frame}")
+
+ if len(frames) >= 2:
+ frame_diff = frames[-1] - frames[0]
+ avg_fps = frame_diff / (duration - 1) if duration > 1 else 0
+ print(f" 平均帧率: {avg_fps:.1f} Hz")
+
+ result = len(frames) > 0
+ print(f" {'✅ 通过' if result else '❌ 失败'}")
+ return result
+
+ def generate_report(self):
+ print("\n" + "=" * 60)
+ print("测试报告")
+ print("=" * 60)
+
+ print("\n## 测试结果")
+ passed = sum(1 for r in self.test_results.values() if r)
+ total = len(self.test_results)
+ print(f"- 通过: {passed}/{total}")
+
+ for name, result in self.test_results.items():
+ status = "✅ 通过" if result else "❌ 失败"
+ print(f" - {name}: {status}")
+
+ print("\n" + "=" * 60)
+
+ def run_all_tests(self, local_port=7000, remote_ip='192.168.11.88', remote_port=7000):
+ if not self.setup(local_port, remote_ip, remote_port):
+ return
+
+ try:
+ self.test_results['测试1-连接状态'] = self.test_connection()
+ self.test_results['测试2-数据接收'] = self.test_receive_data(duration=5)
+ self.test_results['测试3-数据内容'] = self.test_data_content()
+ self.test_results['测试4-连续读取'] = self.test_continuous_read(duration=10)
+ except Exception as e:
+ print(f"\n测试中断: {e}")
+ finally:
+ self.teardown()
+
+ self.generate_report()
+
+
+if __name__ == "__main__":
+ test = VtrDynIntegrationTest()
+ test.run_all_tests(local_port=7000, remote_ip='192.168.11.88', remote_port=7000)
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/TEST_LINKERFORCE.md b/src/linkerhand_retarget/tests/unit/TEST_LINKERFORCE.md
new file mode 100644
index 0000000..39ba7b4
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/TEST_LINKERFORCE.md
@@ -0,0 +1,188 @@
+# LinkerForce 集成测试报告
+
+测试日期: 2026-03-03
+测试设备: LinkerForce 左手数据手套
+测试结果: 6/6 通过
+
+---
+
+## 启动命令
+
+```bash
+cd /home/linker-brunt/project/linkerhand_telop_sdk/old_git/new/ros2/src/linkerhand_retarget
+python3 tests/integration/test_linkerforce.py
+```
+
+前提条件:
+- LinkerForce 设备已连接到 `/dev/ttyUSB0`
+- 波特率: 2000000
+
+---
+
+## 测试环境
+
+- 串口: /dev/ttyUSB0
+- 波特率: 2000000
+- 设备类型: Left (左手)
+- 固件版本: 1.2.12
+- 总接收帧数: 1266
+
+---
+
+## 测试结果
+
+| 测试项 | 结果 | 关键指标 |
+|--------|------|----------|
+| 设备信息 | 通过 | handtype=Left, version=1.2.12 |
+| 数据稳定性 | 通过 | 标准差 0.00007 rad (0.004°) |
+| 数据包统计 | 通过 | 315帧/10秒, 数据变化率 95% |
+| 响应间隔 | 通过 | 平均 30.82ms, 帧率 20Hz |
+| 连续读取 | 通过 | 485帧/15秒, 稳定无断开 |
+| 协议测试 | 通过 | 6/6 协议通过 |
+
+---
+
+## 详细数据
+
+### 1. 设备信息测试
+
+```
+handtype: Left
+version: 1.2.12
+connflag: True
+```
+
+### 2. 位置数据稳定性测试 (5秒)
+
+- 采样数: 10
+- 通道数: 10
+- 平均标准差: 0.000071 rad (0.0041°)
+- 最大标准差: 0.000254 rad (0.0145°)
+
+数据稳定性良好。
+
+### 3. 数据包统计测试 (10秒)
+
+- 发送请求: 20
+- 接收帧数: 315
+- 数据变化: 19
+- 帧率: ~31.5 Hz
+
+设备持续输出数据。
+
+### 4. 响应间隔测试 (5秒)
+
+- 接收帧数: 100
+- 平均帧间隔: 30.82 ms
+- 最小帧间隔: 12.11 ms
+- 最大帧间隔: 42.87 ms
+- 帧率: 20.0 Hz
+
+响应及时稳定。
+
+### 5. 连续读取稳定性测试 (15秒)
+
+- 接收帧数: 485
+- 平均帧间隔: 500.68 ms
+- 帧率: 2.0 Hz (受测试间隔限制)
+- 设备断开: 0 次
+
+稳定运行,无断开无错误。
+
+### 6. 协议测试
+
+| 协议 | 功能 | 状态 |
+|------|------|------|
+| 0x01 | 设备信息 | 通过 |
+| 0x02 | 控制命令 | 通过 |
+| 0x03 | 位置数据 | 通过 (21 floats) |
+| 0x04 | 力数据 | 通过 |
+| 0xA4 | 力发送 | 通过 |
+| 0xA7 | 力发送变体 | 通过 |
+
+---
+
+## 协议说明
+
+### 支持的协议命令
+
+| 命令码 | 功能 | 方向 | 说明 |
+|--------|------|------|------|
+| 0x01 | 设备信息 | 主机→设备 | 查询设备类型和版本 |
+| 0x02 | 控制命令 | 主机→设备 | 发送控制参数 |
+| 0x03 | 位置数据 | 设备→主机 | 返回21个关节角度(float) |
+| 0x04 | 力数据 | 设备→主机 | 返回力传感器数据(int16) |
+| 0xA4 | 力发送 | 主机→设备 | 发送力反馈数据 |
+| 0xA7 | 力发送变体 | 主机→设备 | 发送力反馈数据(备用) |
+
+### 数据格式
+
+位置数据 (0x03):
+- 格式: 21个 float (小端序)
+- 单位: 弧度
+- 更新频率: ~20-30 Hz
+
+力数据 (0x04):
+- 格式: 5个 int16
+- 单位: 原始ADC值
+
+---
+
+## 测试结论
+
+设备工作正常:
+- 通信稳定: 无断开、无错误
+- 数据精确: 角度标准差 < 0.015°
+- 响应及时: 平均帧间隔 30ms, 帧率 20Hz
+- 协议完整: 6个协议全部通过
+
+---
+
+## 测试文件结构
+
+```
+tests/
+├── TEST_LINKERFORCE.md # 本测试报告
+├── unit/
+│ ├── test_linkerforce.py # LinkerForce 单元测试
+│ ├── test_filter.py # 滤波器测试
+│ ├── test_constants.py # 常量测试
+│ ├── test_handcore.py # HandCore测试
+│ ├── test_handcoreex.py # HandCoreEx测试
+│ ├── test_linkermcgcore.py # LinkerMCG测试
+│ ├── test_sensenovacore.py # SenseNova测试
+│ ├── test_udexrealcore.py # UdexReal测试
+│ ├── test_utils.py # 工具函数测试
+│ └── test_vtrdyncore.py # VtrDyn测试
+└── integration/
+ ├── test_linkerforce.py # LinkerForce 集成测试
+ └── test_config.py # 测试配置
+```
+
+### LinkerForce 测试文件
+
+单元测试 `tests/unit/test_linkerforce.py`:
+- CircularBuffer 测试 (7项)
+- FrameParser 测试 (5项)
+- 常量测试 (4项)
+
+集成测试 `tests/integration/test_linkerforce.py`:
+- 设备信息测试
+- 数据稳定性测试
+- 数据包统计测试
+- 响应间隔测试
+- 连续读取测试
+- 协议测试
+
+## 运行测试
+
+```bash
+# 进入测试目录
+cd ros2/src/linkerhand_retarget
+
+# 运行单元测试
+PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest tests/unit/ -v
+
+# 运行集成测试 (需连接设备)
+python3 tests/integration/test_linkerforce.py
+```
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/TEST_LINKERMCG.md b/src/linkerhand_retarget/tests/unit/TEST_LINKERMCG.md
new file mode 100644
index 0000000..addd793
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/TEST_LINKERMCG.md
@@ -0,0 +1,76 @@
+# LinkerMCG 集成测试报告
+
+**测试日期**: 2026-03-04
+**测试设备**: LinkerMCG 数据手套
+**测试结果**: ✅ 3/4 通过
+
+---
+
+## 测试环境
+
+| 项目 | 值 |
+|------|-----|
+| 协议 | UDP 客户端 |
+| 目标地址 | 192.168.11.88 |
+| 端口 | 9000 |
+| 关节数 | 25/手 |
+
+---
+
+## 测试结果总览
+
+| 测试项 | 结果 | 说明 |
+|--------|------|------|
+| 连接状态 | ✅ | UDP 连接成功 |
+| 数据接收 | ⚠️ | 启动延迟约6秒 | UDP软件手动启动
+| 数据内容 | ✅ | 数据结构正确 |
+| 连续读取 | ✅ | 帧率 14.8 Hz |
+
+---
+
+## 详细测试数据
+
+### 1. 连接状态测试
+
+```
+is_connected: True
+```
+
+### 2. 数据接收测试 (5秒)
+
+前5秒无数据,数据在第6秒后开始到达。
+
+### 3. 数据内容测试
+
+| 指标 | 值 |
+|------|-----|
+| 右手关节数 | 25 |
+| 左手关节数 | 25 |
+
+### 4. 连续读取测试 (10秒)
+
+| 指标 | 值 |
+|------|-----|
+| 最终帧数 | 133 |
+| 平均帧率 | 14.8 Hz |
+
+---
+
+## 测试结论
+
+1. **网络连接**: 正常
+2. **数据传输**: 正常,启动有延迟
+3. **帧率**: 14.8 Hz
+
+---
+
+## 运行测试
+
+```bash
+cd ros2/src/linkerhand_retarget
+python3 tests/integration/test_linkermcg.py
+```
+
+---
+
+**报告生成时间**: 2026-03-04
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/TEST_UDEXREAL.md b/src/linkerhand_retarget/tests/unit/TEST_UDEXREAL.md
new file mode 100644
index 0000000..82b5054
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/TEST_UDEXREAL.md
@@ -0,0 +1,121 @@
+# UdexReal 集成测试报告
+
+**测试日期**: 2026-03-03
+**测试设备**: UdexReal 动捕设备
+**测试结果**: ✅ 5/5 通过
+
+---
+
+## 测试环境
+
+| 项目 | 值 |
+|------|-----|
+| 协议 | UDP |
+| 本地地址 | 0.0.0.0 |
+| 端口 | 8888 |
+| 帧率 | ~124 Hz |
+
+---
+
+## 测试结果总览
+
+| 测试项 | 结果 | 关键指标 |
+|--------|------|----------|
+| 连接状态 | ✅ | UDP 连接成功 |
+| 数据接收 | ✅ | 619帧/5秒, 123.8 Hz |
+| 数据内容 | ✅ | 24个关节/手 |
+| 超时检测 | ✅ | 功能正常 |
+| 连续读取 | ✅ | 123.7 Hz, 无断开 |
+
+---
+
+## 详细测试数据
+
+### 1. 连接状态测试
+
+```
+is_connected: True
+is_data_timeout: False
+```
+
+### 2. 数据接收测试 (5秒)
+
+| 指标 | 值 |
+|------|-----|
+| 接收帧数 | 619 |
+| 帧率 | 123.8 Hz |
+
+### 3. 数据内容测试
+
+| 指标 | 值 |
+|------|-----|
+| frame_index | 741 |
+| 右手关节数 | 24 |
+| 左手关节数 | 24 |
+| 左手关节示例 | [0.0, -0.82, 0.17, 0.44, -0.23] |
+
+### 4. 超时检测测试
+
+| 指标 | 值 |
+|------|-----|
+| is_timeout | False |
+| time_since_last_data | 0.008s |
+| timeout_threshold | 1.0s |
+
+### 5. 连续读取测试 (10秒)
+
+| 指标 | 值 |
+|------|-----|
+| 平均帧率 | 123.7 Hz |
+| 超时次数 | 0 |
+
+---
+
+## 数据质量评估
+
+### 通信性能
+
+| 指标 | 值 | 评估 |
+|------|-----|------|
+| 帧率 | 123.7 Hz | 优秀 |
+| 连接稳定性 | 无断开 | ✅ |
+| 超时次数 | 0 | ✅ |
+
+### 数据格式
+
+**关节数据:**
+- 关节数: 24/手
+- 数据类型: float (弧度)
+- 更新频率: ~124 Hz
+
+---
+
+## 测试结论
+
+### 总体评价
+
+✅ **设备工作正常**
+
+1. **连接稳定**: UDP 连接正常
+2. **帧率高**: 124 Hz
+3. **数据完整**: 24个关节数据
+4. **超时检测**: 功能正常
+
+---
+
+## 运行测试
+
+```bash
+# 进入测试目录
+cd ros2/src/linkerhand_retarget
+
+# 运行单元测试
+PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest tests/unit/test_udexrealcore.py -v
+
+# 运行集成测试 (需连接设备)
+python3 tests/integration/test_udexreal.py
+```
+
+---
+
+**报告生成时间**: 2026-03-03
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/TEST_VTRDYN.md b/src/linkerhand_retarget/tests/unit/TEST_VTRDYN.md
new file mode 100644
index 0000000..4aa946a
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/TEST_VTRDYN.md
@@ -0,0 +1,97 @@
+# VtrDyn 集成测试报告
+
+**测试日期**: 2026-03-04
+**测试设备**: VtrDyn 动捕设备
+**测试结果**: ✅ 4/4 通过
+
+---
+
+## 测试环境
+
+| 项目 | 值 |
+|------|-----|
+| 协议 | UDP |
+| 本地端口 | 7000 |
+| 远程地址 | 192.168.11.88:7000 |
+
+---
+
+## 测试结果总览
+
+| 测试项 | 结果 | 说明 |
+|--------|------|------|
+| 连接状态 | ✅ | UDP 连接成功 |
+| 数据接收 | ✅ | 帧率 27.8 Hz |
+| 数据内容 | ✅ | 数据结构正确 |
+| 连续读取 | ✅ | 持续接收数据 |
+
+---
+
+## 详细测试数据
+
+### 1. 连接状态测试
+
+```
+is_connected: True
+Connection established
+```
+
+### 2. 数据接收测试 (5秒)
+
+| 指标 | 值 |
+|------|-----|
+| 接收帧数 | 139 |
+| 帧率 | 27.8 Hz |
+
+### 3. 数据内容测试
+
+| 指标 | 值 |
+|------|-----|
+| frame_index | 166 |
+| frequency | 60 |
+| 身体节点数 | 23 |
+| 右手节点数 | 20 |
+| 左手节点数 | 20 |
+
+### 4. 连续读取测试 (10秒)
+
+| 指标 | 值 |
+|------|-----|
+| 最终帧数 | 221 |
+
+---
+
+## 数据格式
+
+**节点配置:**
+- 身体节点: 23
+- 右手节点: 20
+- 左手节点: 20
+
+**数据类型:**
+- 位置 (position)
+- 四元数 (quaternion)
+- 陀螺仪 (gyr)
+- 加速度计 (acc)
+- 速度 (velocity)
+
+---
+
+## 测试结论
+
+1. 连接正常
+2. 数据接收正常
+3. 帧率稳定
+
+---
+
+## 运行测试
+
+```bash
+cd ros2/src/linkerhand_retarget
+python3 tests/integration/test_vtrdyn.py
+```
+
+---
+
+**报告生成时间**: 2026-03-04
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/__init__.py b/src/linkerhand_retarget/tests/unit/__init__.py
new file mode 100644
index 0000000..322f1e0
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/__init__.py
@@ -0,0 +1 @@
+# Unit tests for linkerhand_retarget
diff --git a/src/linkerhand_retarget/tests/unit/plot_mapping_curve.py b/src/linkerhand_retarget/tests/unit/plot_mapping_curve.py
new file mode 100644
index 0000000..427c7a0
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/plot_mapping_curve.py
@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Mapping Curve Visualization Script
+Plot open → opose → fist sensor and motor value curves for a single DOF
+
+Usage:
+ python3 plot_mapping_curve.py
+
+ joint_index: 0-19 (robot_idx)
+
+ Joint mapping:
+ 0: Thumb Rotate
+ 1: Thumb Abduction
+ 2: Thumb Root Flexion
+ 3: Thumb End Flexion
+ 5: Index Roll
+ 6: Index Root Flexion
+ 7: Index End Flexion
+ 9: Middle Roll
+ 10: Middle Root Flexion
+ 11: Middle End Flexion
+ 13: Ring Roll
+ 14: Ring Root Flexion
+ 15: Ring End Flexion
+ 17: Pinky Roll
+ 18: Pinky Root Flexion
+ 19: Pinky End Flexion
+
+Example:
+ python3 plot_mapping_curve.py 6 # Index Root Flexion
+"""
+
+import sys
+import json
+import numpy as np
+import matplotlib.pyplot as plt
+from pathlib import Path
+
+# Joint name mapping
+JOINT_NAMES = {
+ 0: 'Thumb Rotate',
+ 1: 'Thumb Abduction',
+ 2: 'Thumb Root Flexion',
+ 3: 'Thumb End Flexion',
+ 5: 'Index Roll',
+ 6: 'Index Root Flexion',
+ 7: 'Index End Flexion',
+ 9: 'Middle Roll',
+ 10: 'Middle Root Flexion',
+ 11: 'Middle End Flexion',
+ 13: 'Ring Roll',
+ 14: 'Ring Root Flexion',
+ 15: 'Ring End Flexion',
+ 17: 'Pinky Roll',
+ 18: 'Pinky Root Flexion',
+ 19: 'Pinky End Flexion',
+}
+
+# Sensor index mapping (sensor array index for each robot_idx)
+SENSOR_MAP = {
+ 0: 1, # Thumb Rotate -> sensor[1]
+ 1: 0, # Thumb Abduction -> sensor[0]
+ 2: 2, # Thumb Root Flexion -> sensor[2]
+ 3: 4, # Thumb End Flexion -> sensor[4]
+ 5: 5, # Index Roll -> sensor[5]
+ 6: 6, # Index Root Flexion -> sensor[6]
+ 7: 8, # Index End Flexion -> sensor[8]
+ 9: 9, # Middle Roll -> sensor[9]
+ 10: 10, # Middle Root Flexion -> sensor[10]
+ 11: 12, # Middle End Flexion -> sensor[12]
+ 13: 12, # Ring Roll -> sensor[12] (shared with middle)
+ 14: 14, # Ring Root Flexion -> sensor[14]
+ 15: 16, # Ring End Flexion -> sensor[16]
+ 17: 17, # Pinky Roll -> sensor[17]
+ 18: 18, # Pinky Root Flexion -> sensor[18]
+ 19: 20, # Pinky End Flexion -> sensor[20]
+}
+
+# exp_factor for each joint
+EXP_FACTORS = {
+ 0: 1, # Thumb Rotate
+ 1: 1, # Thumb Abduction
+ 2: 5, # Thumb Root Flexion
+ 3: 7, # Thumb End Flexion
+ 5: 1, # Index Roll
+ 6: 4, # Index Root Flexion
+ 7: 3, # Index End Flexion
+ 9: 1, # Middle Roll
+ 10: 5, # Middle Root Flexion
+ 11: 10, # Middle End Flexion
+ 13: 1, # Ring Roll
+ 14: 5, # Ring Root Flexion
+ 15: 18, # Ring End Flexion
+ 17: 1, # Pinky Roll
+ 18: 5, # Pinky Root Flexion
+ 19: 8, # Pinky End Flexion
+}
+
+TMP_FILE = Path(__file__).resolve().parent.parent.parent / "linkerhand_retarget" / "motion" / "linkerforce" / "tmp" / "jointangle_data.tmp"
+
+MOTOR_OPEN = 255
+MOTOR_OPOSE = 128
+MOTOR_FIST = 0
+
+def map_value(sensor_val, sensor_open, sensor_opose, sensor_fist, exp_factor, debug=False):
+ if abs(sensor_opose - sensor_open) < 1e-6:
+ normalized = 0.5
+ else:
+ normalized = (sensor_val - sensor_open) / (sensor_opose - sensor_open)
+
+ if normalized <= 0:
+ return MOTOR_OPEN, normalized
+ elif normalized <= 1:
+ return MOTOR_OPEN + normalized * (MOTOR_OPOSE - MOTOR_OPEN), normalized
+ else:
+ normalized_fist = (sensor_fist - sensor_open) / (sensor_opose - sensor_open) if abs(sensor_opose - sensor_open) > 1e-6 else 1.5
+ t_max = normalized_fist - 1.0
+ t = min(normalized - 1.0, t_max)
+ slope = MOTOR_FIST - MOTOR_OPOSE
+ S1 = MOTOR_OPOSE - MOTOR_OPEN
+ k = slope / (t_max * S1) - 1
+ ratio = t / t_max
+ extension = slope * (ratio + k * ratio ** exp_factor) / (1 + k)
+ result = MOTOR_OPOSE + extension
+ if debug:
+ print(f"normalized_fist={normalized_fist:.4f}, t_max={t_max:.4f}, slope={slope}, S1={S1}, k={k:.4f}, exp_factor={exp_factor}")
+ return max(MOTOR_FIST, result), normalized
+
+def generate_curve_data(sensor_open, sensor_opose, sensor_fist, exp_factor, steps=200):
+ sensor_min = min(sensor_open, sensor_fist) - 0.1
+ sensor_max = max(sensor_open, sensor_opose, sensor_fist) + 0.2
+
+ sensor_values = np.linspace(sensor_min, sensor_max, steps)
+ motor_values = []
+ normalized_values = []
+
+ for s in sensor_values:
+ motor, normalized = map_value(s, sensor_open, sensor_opose, sensor_fist, exp_factor)
+ motor_values.append(motor)
+ normalized_values.append(normalized)
+
+ return sensor_values, motor_values, normalized_values
+
+def main():
+ if len(sys.argv) < 2:
+ print(__doc__)
+ return
+
+ try:
+ joint_idx = int(sys.argv[1])
+ except ValueError:
+ print(f"Error: joint_index must be an integer")
+ print(__doc__)
+ return
+
+ if joint_idx not in JOINT_NAMES:
+ print(f"Error: joint_index {joint_idx} not found")
+ print("Valid indices:", sorted(JOINT_NAMES.keys()))
+ return
+
+ sensor_idx = SENSOR_MAP.get(joint_idx, joint_idx)
+ exp_factor = EXP_FACTORS.get(joint_idx, 1)
+ joint_name = JOINT_NAMES[joint_idx]
+
+ # Load calibration data
+ with open(TMP_FILE) as f:
+ data = json.load(f)
+
+ open_r = data['jointangleoriginal_r']
+ opose_r = data['jointangleopose_r']
+ fist_r = data['jointanglefist_r']
+
+ if sensor_idx >= len(open_r):
+ print(f"Error: sensor_idx {sensor_idx} out of range")
+ return
+
+ sensor_open = open_r[sensor_idx]
+ sensor_opose = opose_r[sensor_idx]
+ sensor_fist = fist_r[sensor_idx]
+
+ # Generate curve data
+ sensor_vals, motor_vals, normalized_vals = generate_curve_data(sensor_open, sensor_opose, sensor_fist, exp_factor)
+
+ # Print parameters
+ map_value(sensor_opose + 0.01, sensor_open, sensor_opose, sensor_fist, exp_factor, debug=True)
+
+ # Verify motor at normalized=1.2
+ sensor_1_2 = sensor_open + 1.2 * (sensor_opose - sensor_open)
+ motor_1_2, _ = map_value(sensor_1_2, sensor_open, sensor_opose, sensor_fist, exp_factor)
+ print(f"motor at normalized=1.2: {motor_1_2:.1f}")
+
+ # Create plot
+ fig, ax = plt.subplots(figsize=(10, 6))
+ ax2 = ax.twinx()
+
+ line1, = ax.plot(normalized_vals, motor_vals, color='#4ECDC4', linewidth=2.5, label='Motor Value')
+ line2, = ax2.plot(normalized_vals, sensor_vals, color='gray', linewidth=1.5, linestyle='--', label='Sensor Value')
+
+ ax.axvline(x=0, color='green', linestyle=':', alpha=0.7, linewidth=1.5, label='open (normalized=0)')
+ ax.axvline(x=1, color='orange', linestyle=':', alpha=0.7, linewidth=1.5, label='opose (normalized=1)')
+
+ normalized_fist = (sensor_fist - sensor_open) / (sensor_opose - sensor_open) if abs(sensor_opose - sensor_open) > 1e-6 else 0.5
+ if normalized_fist > 1:
+ ax.axvline(x=normalized_fist, color='red', linestyle=':', alpha=0.7, linewidth=1.5, label=f'fist (normalized={normalized_fist:.2f})')
+
+ ax.axhline(y=MOTOR_OPEN, color='green', linestyle=':', alpha=0.3)
+ ax.axhline(y=MOTOR_OPOSE, color='orange', linestyle=':', alpha=0.3)
+ ax.axhline(y=MOTOR_FIST, color='red', linestyle=':', alpha=0.3)
+
+ ax.scatter([0, 1], [MOTOR_OPEN, MOTOR_OPOSE], color='black', s=80, zorder=5)
+ if normalized_fist > 1:
+ motor_at_fist, _ = map_value(sensor_fist, sensor_open, sensor_opose, sensor_fist, exp_factor)
+ ax.scatter([normalized_fist], [motor_at_fist], color='red', s=100, zorder=5, marker='*')
+
+ ax.set_xlabel('Normalized Sensor Value', fontsize=11)
+ ax.set_ylabel('Motor Value', fontsize=11)
+ ax2.set_ylabel('Sensor Raw Value', fontsize=11)
+ ax.set_title(f'{joint_name} (robot_idx={joint_idx}, exp_factor={exp_factor})', fontsize=13, fontweight='bold')
+
+ lines = [line1, line2]
+ labels = [l.get_label() for l in lines]
+ ax.legend(lines, labels, loc='upper right', fontsize=9)
+
+ ax.grid(True, alpha=0.3)
+ ax.set_ylim(-20, 280)
+
+ plt.tight_layout()
+
+ output_path = Path(__file__).parent / "images" / f"mapping_curve_joint_{joint_idx}.png"
+ plt.savefig(output_path, dpi=150, bbox_inches='tight')
+ print(f"Chart saved to: {output_path}")
+ plt.close()
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/test_constants.py b/src/linkerhand_retarget/tests/unit/test_constants.py
new file mode 100644
index 0000000..f6cc48c
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_constants.py
@@ -0,0 +1,99 @@
+import pytest
+from linkerhand_retarget.linkerhand.constants import (
+ RobotName,
+ RetargetingType,
+ HandType,
+ DataSource,
+ MotionSource,
+ ROBOT_NAME_MAP,
+ ROBOT_LEN_MAP,
+ OPERATOR2MANO,
+ OPERATOR2MANO_RIGHT,
+ OPERATOR2MANO_LEFT,
+ get_default_config_path,
+)
+
+
+class TestRobotName:
+ def test_enum_values(self):
+ assert RobotName.o7.value is not None
+ assert RobotName.l6.value is not None
+ assert RobotName.l20.value is not None
+
+ def test_robot_names_list(self):
+ from linkerhand_retarget.linkerhand.constants import ROBOT_NAMES
+ assert len(ROBOT_NAMES) > 0
+ assert RobotName.o7 in ROBOT_NAMES
+
+
+class TestRetargetingType:
+ def test_enum_values(self):
+ assert RetargetingType.vector is not None
+ assert RetargetingType.position is not None
+ assert RetargetingType.dexpilot is not None
+ assert RetargetingType.projection is not None
+
+
+class TestHandType:
+ def test_enum_values(self):
+ assert HandType.right is not None
+ assert HandType.left is not None
+
+
+class TestDataSource:
+ def test_enum_values(self):
+ assert DataSource.motion is not None
+ assert DataSource.video is not None
+ assert DataSource.vr is not None
+
+
+class TestMotionSource:
+ def test_enum_values(self):
+ assert MotionSource.vtrdyn is not None
+ assert MotionSource.udexreal is not None
+ assert MotionSource.linkerforce is not None
+
+
+class TestRobotNameMap:
+ def test_robot_name_map(self):
+ assert ROBOT_NAME_MAP[RobotName.o7] == "linker_hand_o7"
+ assert ROBOT_NAME_MAP[RobotName.l6] == "linker_hand_l6"
+ assert ROBOT_NAME_MAP[RobotName.l20] == "linker_hand_l20"
+ assert ROBOT_NAME_MAP[RobotName.l25] == "linker_hand_l25"
+
+ def test_robot_len_map(self):
+ assert ROBOT_LEN_MAP[RobotName.o7] == 7
+ assert ROBOT_LEN_MAP[RobotName.l6] == 6
+ assert ROBOT_LEN_MAP[RobotName.l20] == 20
+ assert ROBOT_LEN_MAP[RobotName.l25] == 25
+
+
+class TestOperatorToMano:
+ def test_operator2mano_right(self):
+ assert OPERATOR2MANO[HandType.right].shape == (3, 3)
+ assert (OPERATOR2MANO[HandType.right] == OPERATOR2MANO_RIGHT).all()
+
+ def test_operator2mano_left(self):
+ assert OPERATOR2MANO[HandType.left].shape == (3, 3)
+ assert (OPERATOR2MANO[HandType.left] == OPERATOR2MANO_LEFT).all()
+
+ def test_operator2mano_right_values(self):
+ expected = [
+ [0, 0, -1],
+ [-1, 0, 0],
+ [0, 1, 0],
+ ]
+ assert (OPERATOR2MANO_RIGHT == expected).all()
+
+
+class TestGetDefaultConfigPath:
+ def test_get_config_path_teleop(self):
+ path = get_default_config_path(RobotName.l6, RetargetingType.vector, HandType.right)
+ assert path is not None
+ assert "teleop" in str(path)
+ assert "l6" in str(path).lower()
+
+ def test_get_config_path_offline(self):
+ path = get_default_config_path(RobotName.l6, RetargetingType.position, HandType.right)
+ assert path is not None
+ assert "offline" in str(path)
diff --git a/src/linkerhand_retarget/tests/unit/test_filter.py b/src/linkerhand_retarget/tests/unit/test_filter.py
new file mode 100644
index 0000000..e7b3f27
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_filter.py
@@ -0,0 +1,224 @@
+import pytest
+import numpy as np
+from linkerhand_retarget.linkerhand.filter import (
+ LCFilter,
+ MultiChannelLCFilter,
+ AdaptiveLCFilter,
+ KalmanFilter,
+ MultiChannelKalmanFilter,
+ AdaptiveKalmanFilter,
+ SavitzkyGolayFilter,
+ MultiChannelSavitzkyGolayFilter,
+ AdaptiveSavitzkyGolayFilter,
+ apply_lc_filter,
+)
+
+
+class TestLCFilter:
+ def test_initialization(self):
+ f = LCFilter(alpha=0.5, initial_value=1.0)
+ assert f.alpha == 0.5
+ assert f.filtered_value == 1.0
+
+ def test_invalid_alpha(self):
+ with pytest.raises(ValueError):
+ LCFilter(alpha=0)
+ with pytest.raises(ValueError):
+ LCFilter(alpha=1.5)
+
+ def test_update(self):
+ f = LCFilter(alpha=0.5)
+ result = f.update(10.0)
+ assert result == 5.0 # 0.5 * 10 + 0.5 * 0
+
+ def test_update_chain(self):
+ f = LCFilter(alpha=0.5)
+ f.update(10.0) # 5.0
+ result = f.update(20.0) # 0.5 * 20 + 0.5 * 5 = 12.5
+ assert result == 12.5
+
+ def test_update_array(self):
+ f = LCFilter(alpha=0.5)
+ result = f.update_array([10.0, 20.0, 30.0])
+ assert len(result) == 3
+
+ def test_reset(self):
+ f = LCFilter(alpha=0.5, initial_value=5.0)
+ f.update(10.0)
+ f.reset(initial_value=0.0)
+ assert f.filtered_value == 0.0
+ assert len(f.history_raw) == 0
+
+
+class TestMultiChannelLCFilter:
+ def test_initialization(self):
+ f = MultiChannelLCFilter(num_channels=3, alpha=0.5)
+ assert f.num_channels == 3
+ assert len(f.filters) == 3
+
+ def test_invalid_channels(self):
+ with pytest.raises(ValueError):
+ MultiChannelLCFilter(num_channels=3, initial_values=[1.0, 2.0])
+
+ def test_update(self):
+ f = MultiChannelLCFilter(num_channels=3, alpha=0.5)
+ result = f.update([10.0, 20.0, 30.0])
+ assert result == [5.0, 10.0, 15.0]
+
+ def test_update_channel(self):
+ f = MultiChannelLCFilter(num_channels=3, alpha=0.5)
+ result = f.update_channel(1, 20.0)
+ assert result == 10.0
+
+ def test_invalid_channel_index(self):
+ f = MultiChannelLCFilter(num_channels=3)
+ with pytest.raises(ValueError):
+ f.update_channel(5, 10.0)
+
+
+class TestAdaptiveLCFilter:
+ def test_initialization(self):
+ f = AdaptiveLCFilter(alpha_min=0.05, alpha_max=0.3)
+ assert f.alpha_min == 0.05
+ assert f.alpha_max == 0.3
+
+ def test_adaptive_update_fast_change(self):
+ f = AdaptiveLCFilter(alpha_min=0.05, alpha_max=0.3, change_threshold=0.1)
+ f.update(0.0) # initial
+ result = f.update(10.0) # large change, should use alpha_max
+ assert f.alpha == 0.3
+
+ def test_adaptive_update_slow_change(self):
+ f = AdaptiveLCFilter(alpha_min=0.05, alpha_max=0.3, change_threshold=0.1)
+ f.update(0.0) # initial
+ f.update(0.01) # small change
+ result = f.update(0.02) # small change
+ assert f.alpha == 0.05
+
+
+class TestKalmanFilter:
+ def test_initialization(self):
+ kf = KalmanFilter(process_variance=1e-5, measurement_variance=0.1)
+ assert kf.process_variance == 1e-5
+ assert kf.measurement_variance == 0.1
+
+ def test_update(self):
+ kf = KalmanFilter(process_variance=1e-5, measurement_variance=0.1)
+ result = kf.update(10.0)
+ assert result > 0 and result < 10.0
+
+ def test_update_batch(self):
+ kf = KalmanFilter(process_variance=1e-5, measurement_variance=0.1)
+ result = kf.update_batch([10.0, 20.0, 30.0])
+ assert len(result) == 3
+
+ def test_reset(self):
+ kf = KalmanFilter(process_variance=1e-5, measurement_variance=0.1)
+ kf.update(10.0)
+ kf.reset(initial_value=0.0)
+ assert kf.x_hat == 0.0
+
+
+class TestMultiChannelKalmanFilter:
+ def test_initialization(self):
+ mkf = MultiChannelKalmanFilter(num_channels=3)
+ assert mkf.num_channels == 3
+ assert len(mkf.filters) == 3
+
+ def test_update(self):
+ mkf = MultiChannelKalmanFilter(num_channels=3)
+ result = mkf.update([10.0, 20.0, 30.0])
+ assert len(result) == 3
+
+ def test_update_channel(self):
+ mkf = MultiChannelKalmanFilter(num_channels=3)
+ result = mkf.update_channel(1, 20.0)
+ assert result > 0
+
+ def test_invalid_channel(self):
+ mkf = MultiChannelKalmanFilter(num_channels=3)
+ with pytest.raises(ValueError):
+ mkf.update_channel(5, 10.0)
+
+
+class TestAdaptiveKalmanFilter:
+ def test_initialization(self):
+ akf = AdaptiveKalmanFilter(
+ min_process_variance=1e-6,
+ max_process_variance=1e-3,
+ )
+ assert akf.min_process_variance == 1e-6
+ assert akf.max_process_variance == 1e-3
+
+
+class TestSavitzkyGolayFilter:
+ def test_initialization(self):
+ sgf = SavitzkyGolayFilter(window_length=7, polyorder=2)
+ assert sgf.window_length == 7
+ assert sgf.polyorder == 2
+
+ def test_invalid_window_length(self):
+ with pytest.raises(ValueError):
+ SavitzkyGolayFilter(window_length=6) # even number
+
+ def test_window_less_than_polyorder(self):
+ with pytest.raises(ValueError):
+ SavitzkyGolayFilter(window_length=3, polyorder=4)
+
+ def test_update(self):
+ sgf = SavitzkyGolayFilter(window_length=7, polyorder=2)
+ result = sgf.update(10.0)
+ assert isinstance(result, float)
+
+ def test_buffer_not_full(self):
+ sgf = SavitzkyGolayFilter(window_length=7, polyorder=2)
+ for i in range(3):
+ result = sgf.update(float(i))
+ assert result == float(i) # returns original when buffer not full
+
+ def test_reset(self):
+ sgf = SavitzkyGolayFilter(window_length=7, polyorder=2)
+ sgf.update(10.0)
+ sgf.reset()
+ assert len(sgf.buffer) == 0
+
+
+class TestMultiChannelSavitzkyGolayFilter:
+ def test_initialization(self):
+ msgf = MultiChannelSavitzkyGolayFilter(num_channels=3)
+ assert msgf.num_channels == 3
+
+ def test_update(self):
+ msgf = MultiChannelSavitzkyGolayFilter(num_channels=3)
+ result = msgf.update([10.0, 20.0, 30.0])
+ assert len(result) == 3
+
+
+class TestAdaptiveSavitzkyGolayFilter:
+ def test_initialization(self):
+ asgf = AdaptiveSavitzkyGolayFilter(min_window=5, max_window=13)
+ assert asgf.min_window == 5
+ assert asgf.max_window == 13
+
+ def test_update(self):
+ asgf = AdaptiveSavitzkyGolayFilter(min_window=5, max_window=13)
+ result = asgf.update(10.0)
+ assert isinstance(result, float)
+
+
+class TestApplyLCFilter:
+ def test_empty_list(self):
+ result = apply_lc_filter([])
+ assert result == []
+
+ def test_single_value(self):
+ result = apply_lc_filter([5.0], alpha=0.5)
+ assert result == [5.0]
+
+ def test_multiple_values(self):
+ result = apply_lc_filter([10.0, 20.0, 30.0], alpha=0.5)
+ assert len(result) == 3
+
+ def test_invalid_alpha(self):
+ with pytest.raises(ValueError):
+ apply_lc_filter([1.0, 2.0], alpha=0)
diff --git a/src/linkerhand_retarget/tests/unit/test_handcore.py b/src/linkerhand_retarget/tests/unit/test_handcore.py
new file mode 100644
index 0000000..4fc479a
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_handcore.py
@@ -0,0 +1,157 @@
+import pytest
+import numpy as np
+from unittest.mock import Mock, MagicMock
+from linkerhand_retarget.linkerhand.handcore import HandCore, KalmanFilter, MultiTargetKalman
+
+
+class MockJoint:
+ def __init__(self, joint_type="revolute", lower=-1.0, upper=1.0):
+ self.type = joint_type
+ self.limit = MagicMock()
+ self.limit.lower = lower
+ self.limit.upper = upper
+
+
+class MockRobot:
+ def __init__(self, joints):
+ self.joint_map = joints
+
+
+class TestHandCoreGetJointLimits:
+ def test_revolute_joints(self):
+ joints = {
+ 'joint1': MockJoint("revolute", -1.57, 1.57),
+ 'joint2': MockJoint("revolute", -0.5, 0.5),
+ }
+ robot = MockRobot(joints)
+ lower, upper, ranges = HandCore.get_joint_limits(robot)
+
+ assert len(lower) == 2
+
+ def test_prismatic_joint(self):
+ joints = {
+ 'prismatic_joint': MockJoint("prismatic", -0.5, 0.5),
+ }
+ robot = MockRobot(joints)
+ lower, upper, ranges = HandCore.get_joint_limits(robot)
+
+ assert lower[0] == -0.5
+ assert upper[0] == 0.5
+ assert ranges[0] == 1.0
+
+ def test_fixed_joint_skipped(self):
+ joints = {
+ 'fixed_joint': MockJoint("fixed", -1.0, 1.0),
+ 'revolute_joint': MockJoint("revolute", -1.0, 1.0),
+ }
+ robot = MockRobot(joints)
+ lower, upper, ranges = HandCore.get_joint_limits(robot)
+
+ assert len(lower) == 1
+
+ def test_joint_without_limit(self):
+ joints = {
+ 'revolute_no_limit': Mock(),
+ }
+ joints['revolute_no_limit'].type = "revolute"
+ joints['revolute_no_limit'].limit = None
+
+ robot = MockRobot(joints)
+ lower, upper, ranges = HandCore.get_joint_limits(robot)
+
+ assert lower[0] == -3.1415926535
+ assert upper[0] == 3.1415926535
+
+
+class TestHandCoreProjectionProcess:
+ def test_projection_process_returns_30_values(self):
+ hand_position = np.random.rand(25, 3)
+ result = HandCore.projection_process(hand_position)
+
+ assert len(result) == 30
+
+ def test_projection_process_returns_list(self):
+ hand_position = np.ones((25, 3)) * 0.1
+ result = HandCore.projection_process(hand_position)
+
+ assert len(result) == 30
+ assert isinstance(result[0], float)
+
+
+class TestKalmanFilter:
+ def test_initialization(self):
+ kf = KalmanFilter(process_variance=0.01, measurement_variance=0.1, estimated_error=1.0, initial_value=0.0)
+ assert kf.process_variance == 0.01
+ assert kf.measurement_variance == 0.1
+ assert kf.estimated_error == 1.0
+ assert kf.current_estimate == 0.0
+
+ def test_update_first_measurement(self):
+ kf = KalmanFilter(process_variance=0.01, measurement_variance=0.1, estimated_error=1.0, initial_value=0.0)
+ result = kf.update(10.0)
+
+ assert 0.0 < result < 10.0
+
+ def test_update_convergence(self):
+ kf = KalmanFilter(process_variance=0.001, measurement_variance=0.01, estimated_error=1.0, initial_value=0.0)
+
+ results = []
+ for _ in range(100):
+ results.append(kf.update(10.0))
+
+ assert abs(results[-1] - 10.0) < 0.5
+
+ def test_update_with_known_measurement(self):
+ kf = KalmanFilter(process_variance=0.01, measurement_variance=0.1, estimated_error=1.0, initial_value=5.0)
+ result = kf.update(5.0)
+
+ assert result == 5.0
+
+ def test_estimated_error_decreases(self):
+ kf = KalmanFilter(process_variance=0.01, measurement_variance=0.1, estimated_error=1.0, initial_value=0.0)
+
+ initial_error = kf.estimated_error
+ kf.update(10.0)
+
+ assert kf.estimated_error < initial_error
+
+
+class TestMultiTargetKalman:
+ def test_initialization(self):
+ mtkf = MultiTargetKalman(num_targets=5)
+
+ assert mtkf.num_targets == 5
+ assert len(mtkf.kalman_filters) == 5
+ assert len(mtkf.smoothed_data) == 5
+
+ def test_initialization_custom_params(self):
+ mtkf = MultiTargetKalman(
+ num_targets=3,
+ process_variance=0.001,
+ measurement_variance=0.01,
+ estimated_error=0.5,
+ initial_value=100.0
+ )
+
+ assert mtkf.num_targets == 3
+ assert len(mtkf.kalman_filters) == 3
+
+ def test_update_single_target(self):
+ mtkf = MultiTargetKalman(num_targets=5)
+
+ result = mtkf.update(10.0, index=2)
+
+ assert isinstance(result, float)
+
+ def test_update_all_targets(self):
+ mtkf = MultiTargetKalman(num_targets=3)
+
+ for i in range(3):
+ result = mtkf.update(float(i * 10), index=i)
+ assert isinstance(result, float)
+
+ def test_smoothed_data_initialized(self):
+ mtkf = MultiTargetKalman(num_targets=2)
+
+ assert len(mtkf.smoothed_data) == 2
+ assert isinstance(mtkf.smoothed_data, list)
diff --git a/src/linkerhand_retarget/tests/unit/test_handcoreex.py b/src/linkerhand_retarget/tests/unit/test_handcoreex.py
new file mode 100644
index 0000000..d68af96
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_handcoreex.py
@@ -0,0 +1,158 @@
+import pytest
+import numpy as np
+from linkerhand_retarget.linkerhand.handcoreex import MultiStateLinearMapper
+
+
+FINGER_CONFIGS_TEST = {
+ 'thumb': {
+ 'name': 'thumb',
+ 'joints': [0, 1, 2],
+ 'weights': [0.2, 0.3, 0.5],
+ 'robot_idx': 0,
+ 'reverse_motion': False,
+ },
+ 'index': {
+ 'name': 'index',
+ 'joints': [3, 4, 5],
+ 'weights': [0.3, 0.3, 0.4],
+ 'robot_idx': 1,
+ 'reverse_motion': False,
+ },
+}
+
+MAPPING_ORDER_TEST = ['thumb', 'index']
+
+
+class TestMultiStateLinearMapper:
+ def test_initialization(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ assert mapper.finger_configs == FINGER_CONFIGS_TEST
+ assert mapper.mapping_order == MAPPING_ORDER_TEST
+ assert len(mapper.glove_states) == 0
+ assert len(mapper.robot_states) == 0
+
+ def test_add_state(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ glove_angles = [0.0] * 21
+ robot_angles = [0.0] * 6
+
+ mapper.add_state('original', glove_angles, robot_angles)
+
+ assert 'original' in mapper.glove_states
+ assert 'original' in mapper.robot_states
+ assert np.array_equal(mapper.glove_states['original'], glove_angles)
+
+ def test_add_state_with_list(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ glove_angles = [1.0] * 21
+ robot_angles = [0.5] * 6
+
+ mapper.add_state('fist', glove_angles, robot_angles)
+
+ assert 'fist' in mapper.glove_states
+
+ def test_remove_state(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ glove_angles = [0.0] * 21
+ robot_angles = [0.0] * 6
+
+ mapper.add_state('original', glove_angles, robot_angles)
+ mapper.remove_state('original')
+
+ assert 'original' not in mapper.glove_states
+
+ def test_set_state_order(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+
+ mapper.set_state_order(['original', 'fist'])
+
+ assert mapper.state_order == ['original', 'fist']
+
+ def test_set_state_order_invalid_state(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+
+ with pytest.raises(ValueError):
+ mapper.set_state_order(['original', 'nonexistent'])
+
+ def test_map_glove_to_robot_requires_two_states(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+
+ with pytest.raises(ValueError, match="请至少设置两个状态"):
+ mapper.map_glove_to_robot([0.0] * 21)
+
+ def test_map_glove_to_robot_with_original(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+ mapper.set_state_order(['original', 'fist'])
+
+ result = mapper.map_glove_to_robot([0.5] * 21)
+
+ assert isinstance(result, np.ndarray)
+
+ def test_map_glove_to_robot_returns_array(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+ mapper.set_state_order(['original', 'fist'])
+
+ result = mapper.map_glove_to_robot([0.5] * 21)
+
+ assert isinstance(result, np.ndarray)
+ assert len(result) == 6
+
+ def test_map_glove_to_robot_with_numpy_array(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+ mapper.set_state_order(['original', 'fist'])
+
+ result = mapper.map_glove_to_robot(np.array([0.5] * 21))
+
+ assert isinstance(result, np.ndarray)
+
+ def test_map_glove_to_robot_full_extension(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+ mapper.set_state_order(['original', 'fist'])
+
+ result = mapper.map_glove_to_robot([0.0] * 21)
+
+ assert isinstance(result, np.ndarray)
+
+ def test_map_glove_to_robot_full_flexion(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+ mapper.set_state_order(['original', 'fist'])
+
+ result = mapper.map_glove_to_robot([1.0] * 21)
+
+ assert isinstance(result, np.ndarray)
+
+
+class TestMultiStateLinearMapperEdgeCases:
+ def test_empty_glove_states(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ assert len(mapper.glove_states) == 0
+
+ def test_debug_value_initialized(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ assert len(mapper.debug_value) == 20
+
+ def test_history_initialized(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ assert len(mapper.raw_history) == 0
+ assert len(mapper.filtered_history) == 0
+
+ def test_multiple_states(self):
+ mapper = MultiStateLinearMapper(FINGER_CONFIGS_TEST, MAPPING_ORDER_TEST)
+ mapper.add_state('original', [0.0] * 21, [0.0] * 6)
+ mapper.add_state('opose', [0.5] * 21, [0.5] * 6)
+ mapper.add_state('fist', [1.0] * 21, [1.0] * 6)
+
+ assert len(mapper.glove_states) == 3
diff --git a/src/linkerhand_retarget/tests/unit/test_linkerforce.py b/src/linkerhand_retarget/tests/unit/test_linkerforce.py
new file mode 100644
index 0000000..00382bb
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_linkerforce.py
@@ -0,0 +1,119 @@
+import pytest
+import array
+from linkerhand_retarget.linkerhand.linkerforce import CircularBuffer, FrameParser, FrameParseState, BUFFER_SIZE, FRAME_HEADER
+
+
+class TestCircularBuffer:
+ def test_initialization(self):
+ buf = CircularBuffer()
+ assert buf.data_len == 0
+ assert buf.read_pos == 0
+ assert buf.write_pos == 0
+
+ def test_write_single_byte(self):
+ buf = CircularBuffer()
+ buf.write([0x5D])
+ assert buf.data_len == 1
+ assert buf.read_pos == 0
+ assert buf.write_pos == 1
+
+ def test_read_byte(self):
+ buf = CircularBuffer()
+ buf.write([0x5D, 0x01])
+ byte = buf.read_byte()
+ assert byte == 0x5D
+ assert buf.data_len == 1
+
+ def test_read_empty_buffer(self):
+ buf = CircularBuffer()
+ byte = buf.read_byte()
+ assert byte is None
+
+ def test_write_multiple_bytes(self):
+ buf = CircularBuffer()
+ data = [0x01, 0x02, 0x03, 0x04, 0x05]
+ buf.write(data)
+ assert buf.data_len == 5
+
+ def test_read_write_sequence(self):
+ buf = CircularBuffer()
+ buf.write([10, 20, 30])
+ assert buf.read_byte() == 10
+ assert buf.read_byte() == 20
+ assert buf.read_byte() == 30
+ assert buf.read_byte() is None
+
+ def test_buffer_wrap_around(self):
+ buf = CircularBuffer()
+ for i in range(BUFFER_SIZE + 10):
+ buf.write([i % 256])
+ assert buf.data_len == BUFFER_SIZE
+
+
+class TestFrameParser:
+ def test_initialization(self):
+ parser = FrameParser()
+ assert parser.state == FrameParseState.HEADER
+ assert parser.expected_len == 0
+ assert parser.current_pos == 0
+
+ def test_reset(self):
+ parser = FrameParser()
+ parser.state = FrameParseState.DATA
+ parser.current_pos = 5
+ parser.reset()
+ assert parser.state == FrameParseState.HEADER
+ assert parser.current_pos == 0
+
+ def test_process_byte_finds_header(self):
+ parser = FrameParser()
+ result = parser.process_byte(FRAME_HEADER)
+ assert parser.state == FrameParseState.CMD
+
+ def test_process_byte_accumulates_data(self):
+ parser = FrameParser()
+ parser.process_byte(FRAME_HEADER)
+ parser.process_byte(0x01)
+ parser.process_byte(0x03)
+ for i in range(3):
+ parser.process_byte(i)
+ parser.process_byte(0)
+
+ def test_state_transitions(self):
+ parser = FrameParser()
+ assert parser.state == FrameParseState.HEADER
+
+ parser.process_byte(FRAME_HEADER)
+ assert parser.state == FrameParseState.CMD
+
+ parser.process_byte(0x01)
+ assert parser.state == FrameParseState.LENGTH
+
+ parser.process_byte(0x02)
+ assert parser.state == FrameParseState.DATA
+
+ def test_process_multiple_frames(self):
+ parser = FrameParser()
+ frame1 = [FRAME_HEADER, 0x01, 0x02, 0xAA, 0xBB]
+ for byte in frame1:
+ parser.process_byte(byte)
+
+ parser.reset()
+ frame2 = [FRAME_HEADER, 0x02, 0x01, 0xCC]
+ for byte in frame2:
+ parser.process_byte(byte)
+
+
+class TestConstants:
+ def test_buffer_size(self):
+ assert BUFFER_SIZE == 1024
+
+ def test_frame_header(self):
+ assert FRAME_HEADER == 0x5D
+
+ def test_frame_parse_states(self):
+ assert FrameParseState.HEADER.value == 0
+ assert FrameParseState.CMD.value == 1
+ assert FrameParseState.LENGTH.value == 2
+ assert FrameParseState.DATA.value == 3
+ assert FrameParseState.CHECKSUM.value == 4
diff --git a/src/linkerhand_retarget/tests/unit/test_linkerforce_improved.py b/src/linkerhand_retarget/tests/unit/test_linkerforce_improved.py
new file mode 100644
index 0000000..1ffcc94
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_linkerforce_improved.py
@@ -0,0 +1,387 @@
+"""
+LinkerForce 改进版单元测试
+"""
+import unittest
+import threading
+import time
+import queue
+from unittest.mock import Mock, MagicMock, patch
+import serial
+
+from linkerhand.linkerforce_improved import (
+ ForceSerialReader,
+ SerialConfig,
+ DeviceInfo,
+ FrameParser,
+ FrameParseState
+)
+from linkerhand.constants import HandType
+
+
+class TestSerialConfig(unittest.TestCase):
+ """测试配置类"""
+
+ def test_default_config(self):
+ """测试默认配置"""
+ config = SerialConfig()
+ self.assertEqual(config.baudrates, [2000000, 1000000, 921600, 460800])
+ self.assertEqual(config.timeout, 0.001)
+ self.assertTrue(config.auto_reconnect)
+ self.assertEqual(config.max_reconnect_attempts, 5)
+
+ def test_custom_config(self):
+ """测试自定义配置"""
+ config = SerialConfig(
+ baudrates=[921600],
+ auto_reconnect=False,
+ max_reconnect_attempts=10
+ )
+ self.assertEqual(config.baudrates, [921600])
+ self.assertFalse(config.auto_reconnect)
+ self.assertEqual(config.max_reconnect_attempts, 10)
+
+
+class TestDeviceInfo(unittest.TestCase):
+ """测试设备信息类"""
+
+ def test_device_info_creation(self):
+ """测试设备信息创建"""
+ info = DeviceInfo(
+ port="/dev/ttyUSB0",
+ baudrate=2000000,
+ handtype="Right",
+ version="1.0.0"
+ )
+ self.assertEqual(info.port, "/dev/ttyUSB0")
+ self.assertEqual(info.baudrate, 2000000)
+ self.assertEqual(info.handtype, "Right")
+ self.assertEqual(info.version, "1.0.0")
+
+ def test_device_info_optional_fields(self):
+ """测试可选字段"""
+ info = DeviceInfo(port="/dev/ttyUSB0", baudrate=2000000)
+ self.assertIsNone(info.handtype)
+ self.assertIsNone(info.version)
+
+
+class TestFrameParser(unittest.TestCase):
+ """测试帧解析器"""
+
+ def setUp(self):
+ self.parser = FrameParser()
+
+ def test_initial_state(self):
+ """测试初始状态"""
+ self.assertEqual(self.parser.state, FrameParseState.HEADER)
+ self.assertEqual(self.parser.current_pos, 0)
+ self.assertEqual(self.parser.checksum, 0)
+
+ def test_reset(self):
+ """测试重置"""
+ self.parser.state = FrameParseState.DATA
+ self.parser.current_pos = 10
+ self.parser.checksum = 100
+
+ self.parser.reset()
+
+ self.assertEqual(self.parser.state, FrameParseState.HEADER)
+ self.assertEqual(self.parser.current_pos, 0)
+ self.assertEqual(self.parser.checksum, 0)
+
+ def test_parse_valid_frame(self):
+ """测试解析有效帧"""
+ # 构造一个有效帧: 0x5D, 0x01, 0x00, checksum
+ frame_data = bytes([0x5D, 0x01, 0x00, 0x5E]) # checksum = 0x5D + 0x01 + 0x00 = 0x5E
+
+ result = False
+ for byte in frame_data:
+ if self.parser.process_byte(byte):
+ result = True
+
+ self.assertTrue(result)
+ self.assertEqual(self.parser.frame_buf[0], 0x5D)
+ self.assertEqual(self.parser.frame_buf[1], 0x01)
+ self.assertEqual(self.parser.frame_buf[2], 0x00)
+
+
+class TestForceSerialReader(unittest.TestCase):
+ """测试主类"""
+
+ def setUp(self):
+ self.config = SerialConfig(
+ baudrates=[2000000],
+ auto_reconnect=False
+ )
+ self.reader = ForceSerialReader(
+ hand_type=HandType.right,
+ config=self.config,
+ debug=False
+ )
+
+ def tearDown(self):
+ if self.reader.is_connected:
+ self.reader.stop()
+
+ def test_initial_state(self):
+ """测试初始状态"""
+ self.assertEqual(self.reader.poslist, [0.0] * 21)
+ self.assertEqual(self.reader.forcelist, [0.0] * 5)
+ self.assertIsNone(self.reader.handtype)
+ self.assertIsNone(self.reader.version)
+ self.assertFalse(self.reader.is_connected)
+
+ def test_thread_safe_access(self):
+ """测试线程安全访问"""
+ results = []
+ errors = []
+
+ def write_data():
+ for i in range(100):
+ with self.reader._lock:
+ self.reader._poslist = [float(i)] * 21
+ time.sleep(0.001)
+
+ def read_data():
+ for _ in range(100):
+ try:
+ data = self.reader.poslist
+ results.append(len(data))
+ except Exception as e:
+ errors.append(e)
+ time.sleep(0.001)
+
+ writer = threading.Thread(target=write_data)
+ readers = [threading.Thread(target=read_data) for _ in range(5)]
+
+ writer.start()
+ for r in readers:
+ r.start()
+
+ writer.join()
+ for r in readers:
+ r.join()
+
+ self.assertEqual(len(errors), 0)
+ self.assertEqual(len(results), 500)
+ for result in results:
+ self.assertEqual(result, 21)
+
+ def test_callback_registration(self):
+ """测试回调注册"""
+ disconnect_called = []
+ reconnect_called = []
+
+ self.reader.set_disconnect_callback(lambda: disconnect_called.append(True))
+ self.reader.set_reconnect_callback(lambda: reconnect_called.append(True))
+
+ self.assertIsNotNone(self.reader._on_disconnect)
+ self.assertIsNotNone(self.reader._on_reconnect)
+
+ def test_context_manager(self):
+ """测试上下文管理器"""
+ with patch.object(self.reader, 'start') as mock_start, \
+ patch.object(self.reader, 'stop') as mock_stop:
+
+ with self.reader:
+ mock_start.assert_called_once()
+
+ mock_stop.assert_called_once()
+
+ def test_pack_data(self):
+ """测试数据打包"""
+ pack_01 = self.reader._pack_01_data()
+ self.assertEqual(pack_01[0], 0x5D) # header
+ self.assertEqual(pack_01[1], 0x01) # cmd
+ self.assertEqual(pack_01[2], 0x00) # length
+
+ pack_03 = self.reader._pack_03_data()
+ self.assertEqual(pack_03[0], 0x5D)
+ self.assertEqual(pack_03[1], 0x03)
+
+ float_data = [1.0, 2.0, 3.0]
+ pack_A7 = self.reader._pack_A7_data(float_data)
+ self.assertEqual(pack_A7[0], 0x5D)
+ self.assertEqual(pack_A7[1], 0xA7)
+
+ def test_handle_version_frame(self):
+ """测试版本帧处理"""
+ # 模拟版本帧数据: value=10001, status_code=1
+ import struct
+ value = 10001 # version 1.0.1
+ status_code = 1 # right hand
+ frame_data = struct.pack('h', force) for force in forces)
+
+ self.reader._handle_force_frame(frame_data)
+
+ realforcelist = self.reader.realforcelist
+ self.assertEqual(len(realforcelist), 3)
+ self.assertEqual(realforcelist, forces)
+
+ def test_disconnect_handler(self):
+ """测试断开连接处理"""
+ disconnect_called = []
+ self.reader.set_disconnect_callback(lambda: disconnect_called.append(True))
+
+ self.reader._connected = True
+ self.reader._handle_disconnect()
+
+ self.assertFalse(self.reader._connected)
+ self.assertEqual(len(disconnect_called), 1)
+
+ def test_reconnect_disabled(self):
+ """测试重连禁用"""
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(HandType.right, config=config)
+
+ reader._device_info = DeviceInfo(port="/dev/ttyUSB0", baudrate=2000000)
+
+ # 不应该重连
+ reader._attempt_reconnect()
+
+ self.assertFalse(reader._connected)
+
+
+class TestForceSerialReaderIntegration(unittest.TestCase):
+ """集成测试"""
+
+ def setUp(self):
+ self.config = SerialConfig(
+ baudrates=[2000000],
+ auto_reconnect=False
+ )
+
+ @patch('serial.Serial')
+ def test_scan_and_connect(self, mock_serial):
+ """测试扫描和连接流程"""
+ # 模拟串口设备
+ mock_port = Mock()
+ mock_port.device = "/dev/ttyUSB0"
+ mock_port.description = "USB Serial"
+ mock_port.hwid = "USB VID:PID"
+
+ with patch('serial.tools.list_ports.comports', return_value=[mock_port]):
+ reader = ForceSerialReader(
+ hand_type=HandType.right,
+ config=self.config,
+ debug=True
+ )
+
+ # 检查是否识别为USB设备
+ is_usb = reader._is_usb_device("/dev/ttyUSB0")
+ self.assertTrue(is_usb)
+
+ def test_concurrent_read_write(self):
+ """测试并发读写"""
+ reader = ForceSerialReader(HandType.right, config=self.config)
+
+ read_count = [0]
+ write_count = [0]
+
+ def writer():
+ for i in range(100):
+ with reader._lock:
+ reader._poslist = [float(i)] * 21
+ write_count[0] += 1
+ time.sleep(0.0001)
+
+ def reader_thread():
+ for _ in range(100):
+ data = reader.poslist
+ self.assertEqual(len(data), 21)
+ read_count[0] += 1
+ time.sleep(0.0001)
+
+ threads = [
+ threading.Thread(target=writer),
+ threading.Thread(target=reader_thread),
+ threading.Thread(target=reader_thread)
+ ]
+
+ for t in threads:
+ t.start()
+ for t in threads:
+ t.join()
+
+ self.assertEqual(write_count[0], 100)
+ self.assertEqual(read_count[0], 200)
+
+
+class TestForceSerialReaderMockSerial(unittest.TestCase):
+ """模拟串口测试"""
+
+ def setUp(self):
+ self.config = SerialConfig(
+ baudrates=[2000000],
+ auto_reconnect=False
+ )
+
+ @patch('serial.Serial')
+ def test_open_serial_success(self, mock_serial_class):
+ """测试成功打开串口"""
+ mock_serial_instance = Mock()
+ mock_serial_instance.is_open = True
+ mock_serial_class.return_value = mock_serial_instance
+
+ reader = ForceSerialReader(HandType.right, config=self.config)
+ result = reader.open_serial("/dev/ttyUSB0", 2000000)
+
+ self.assertTrue(result)
+ mock_serial_class.assert_called_once()
+
+ @patch('serial.Serial')
+ def test_open_serial_failure(self, mock_serial_class):
+ """测试打开串口失败"""
+ mock_serial_class.side_effect = serial.SerialException("Permission denied")
+
+ reader = ForceSerialReader(HandType.right, config=self.config)
+ result = reader.open_serial("/dev/ttyUSB0", 2000000)
+
+ self.assertFalse(result)
+
+ @patch('serial.Serial')
+ def test_close_serial(self, mock_serial_class):
+ """测试关闭串口"""
+ mock_serial_instance = Mock()
+ mock_serial_instance.is_open = True
+ mock_serial_class.return_value = mock_serial_instance
+
+ reader = ForceSerialReader(HandType.right, config=self.config)
+ reader.open_serial("/dev/ttyUSB0", 2000000)
+ reader.close_serial()
+
+ mock_serial_instance.close.assert_called_once()
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/test_linkerforce_improved_standalone.py b/src/linkerhand_retarget/tests/unit/test_linkerforce_improved_standalone.py
new file mode 100644
index 0000000..d6ff3ab
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_linkerforce_improved_standalone.py
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+"""
+LinkerForce 改进版验证测试
+独立运行,不依赖pytest
+"""
+import sys
+import os
+import threading
+import time
+import struct
+
+# 添加路径 - 从当前文件位置向上查找
+current_dir = os.path.dirname(os.path.abspath(__file__))
+linkerhand_retarget_dir = os.path.dirname(os.path.dirname(current_dir))
+sys.path.insert(0, linkerhand_retarget_dir)
+
+# 直接从文件导入
+import importlib.util
+spec = importlib.util.spec_from_file_location(
+ "linkerforce_improved",
+ os.path.join(linkerhand_retarget_dir, "linkerhand_retarget/linkerhand/linkerforce_improved.py")
+)
+linkerforce_improved = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(linkerforce_improved)
+
+ForceSerialReader = linkerforce_improved.ForceSerialReader
+SerialConfig = linkerforce_improved.SerialConfig
+DeviceInfo = linkerforce_improved.DeviceInfo
+FrameParser = linkerforce_improved.FrameParser
+FrameParseState = linkerforce_improved.FrameParseState
+
+# 导入常量
+spec2 = importlib.util.spec_from_file_location(
+ "constants",
+ os.path.join(linkerhand_retarget_dir, "linkerhand_retarget/linkerhand/constants.py")
+)
+constants = importlib.util.module_from_spec(spec2)
+spec2.loader.exec_module(constants)
+
+HandType = constants.HandType
+import numpy as np
+
+
+class TestRunner:
+ def __init__(self):
+ self.tests_passed = 0
+ self.tests_failed = 0
+ self.errors = []
+
+ def test(self, name, func):
+ try:
+ func()
+ print(f"✓ {name}")
+ self.tests_passed += 1
+ except Exception as e:
+ print(f"✗ {name}: {e}")
+ self.tests_failed += 1
+ self.errors.append((name, str(e)))
+
+ def report(self):
+ print(f"\n{'='*60}")
+ print(f"测试结果: {self.tests_passed} 通过, {self.tests_failed} 失败")
+ if self.errors:
+ print("\n失败详情:")
+ for name, error in self.errors:
+ print(f" - {name}: {error}")
+ print(f"{'='*60}\n")
+
+
+def test_serial_config():
+ runner = TestRunner()
+
+ def test_default():
+ config = SerialConfig()
+ assert config.baudrates == [2000000, 1000000, 921600, 460800]
+ assert config.timeout == 0.001
+ assert config.auto_reconnect == True
+
+ def test_custom():
+ config = SerialConfig(
+ baudrates=[921600],
+ auto_reconnect=False,
+ max_reconnect_attempts=10
+ )
+ assert config.baudrates == [921600]
+ assert config.auto_reconnect == False
+ assert config.max_reconnect_attempts == 10
+
+ runner.test("默认配置", test_default)
+ runner.test("自定义配置", test_custom)
+ return runner
+
+
+def test_device_info():
+ runner = TestRunner()
+
+ def test_creation():
+ info = DeviceInfo(
+ port="/dev/ttyUSB0",
+ baudrate=2000000,
+ handtype="Right",
+ version="1.0.0"
+ )
+ assert info.port == "/dev/ttyUSB0"
+ assert info.baudrate == 2000000
+ assert info.handtype == "Right"
+ assert info.version == "1.0.0"
+
+ def test_optional():
+ info = DeviceInfo(port="/dev/ttyUSB0", baudrate=2000000)
+ assert info.handtype is None
+ assert info.version is None
+
+ runner.test("设备信息创建", test_creation)
+ runner.test("设备信息可选字段", test_optional)
+ return runner
+
+
+def test_frame_parser():
+ runner = TestRunner()
+
+ def test_initial():
+ parser = FrameParser()
+ assert parser.state == FrameParseState.HEADER
+ assert parser.current_pos == 0
+ assert parser.checksum == 0
+
+ def test_reset():
+ parser = FrameParser()
+ parser.state = FrameParseState.DATA
+ parser.current_pos = 10
+ parser.checksum = 100
+ parser.reset()
+ assert parser.state == FrameParseState.HEADER
+ assert parser.current_pos == 0
+ assert parser.checksum == 0
+
+ def test_parse_valid():
+ parser = FrameParser()
+ # 构造帧: 0x5D, 0x01, 0x00, checksum
+ frame_data = bytes([0x5D, 0x01, 0x00, 0x5E])
+ result = False
+ for byte in frame_data:
+ if parser.process_byte(byte):
+ result = True
+ assert result == True
+ assert parser.frame_buf[0] == 0x5D
+ assert parser.frame_buf[1] == 0x01
+
+ runner.test("帧解析器初始状态", test_initial)
+ runner.test("帧解析器重置", test_reset)
+ runner.test("帧解析器解析有效帧", test_parse_valid)
+ return runner
+
+
+def test_force_reader():
+ runner = TestRunner()
+
+ def test_initial():
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(hand_type=HandType.right, config=config)
+ assert reader.poslist == [0.0] * 21
+ assert reader.forcelist == [0.0] * 5
+ assert reader.handtype is None
+ assert reader.version is None
+ assert reader.is_connected == False
+
+ def test_thread_safe():
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(hand_type=HandType.right, config=config)
+
+ errors = []
+
+ def write_data():
+ for i in range(100):
+ with reader._lock:
+ reader._poslist = [float(i)] * 21
+ time.sleep(0.001)
+
+ def read_data():
+ for _ in range(100):
+ try:
+ data = reader.poslist
+ assert len(data) == 21
+ except Exception as e:
+ errors.append(e)
+ time.sleep(0.001)
+
+ writer = threading.Thread(target=write_data)
+ readers = [threading.Thread(target=read_data) for _ in range(3)]
+
+ writer.start()
+ for r in readers:
+ r.start()
+
+ writer.join()
+ for r in readers:
+ r.join()
+
+ assert len(errors) == 0
+
+ def test_version_frame():
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(hand_type=HandType.right, config=config)
+
+ # 模拟版本帧
+ value = 10001
+ status_code = 1
+ frame_data = struct.pack('h', force) for force in forces)
+
+ reader._handle_force_frame(frame_data)
+
+ realforcelist = reader.realforcelist
+ assert len(realforcelist) == 3
+ assert realforcelist == forces
+
+ def test_pack_data():
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(hand_type=HandType.right, config=config)
+
+ pack_01 = reader._pack_01_data()
+ assert pack_01[0] == 0x5D
+ assert pack_01[1] == 0x01
+ assert pack_01[2] == 0x00
+
+ pack_03 = reader._pack_03_data()
+ assert pack_03[0] == 0x5D
+ assert pack_03[1] == 0x03
+
+ runner.test("ForceReader初始状态", test_initial)
+ runner.test("ForceReader线程安全", test_thread_safe)
+ runner.test("ForceReader版本帧处理", test_version_frame)
+ runner.test("ForceReader位置帧处理", test_position_frame)
+ runner.test("ForceReader力数据帧处理", test_force_frame)
+ runner.test("ForceReader数据打包", test_pack_data)
+ return runner
+
+
+def test_concurrent_access():
+ """并发访问压力测试"""
+ runner = TestRunner()
+
+ def test_concurrent():
+ config = SerialConfig(auto_reconnect=False)
+ reader = ForceSerialReader(hand_type=HandType.right, config=config)
+
+ read_count = [0]
+ write_count = [0]
+
+ def writer():
+ for i in range(100):
+ with reader._lock:
+ reader._poslist = [float(i)] * 21
+ write_count[0] += 1
+ time.sleep(0.0001)
+
+ def reader_thread():
+ for _ in range(100):
+ data = reader.poslist
+ assert len(data) == 21
+ read_count[0] += 1
+ time.sleep(0.0001)
+
+ threads = [
+ threading.Thread(target=writer),
+ threading.Thread(target=reader_thread),
+ threading.Thread(target=reader_thread)
+ ]
+
+ for t in threads:
+ t.start()
+ for t in threads:
+ t.join()
+
+ assert write_count[0] == 100
+ assert read_count[0] == 200
+
+ runner.test("并发访问压力测试", test_concurrent)
+ return runner
+
+
+def main():
+ print("=" * 60)
+ print("LinkerForce 改进版验证测试")
+ print("=" * 60)
+ print()
+
+ # 运行所有测试
+ results = []
+
+ print("【配置类测试】")
+ results.append(test_serial_config())
+ print()
+
+ print("【设备信息测试】")
+ results.append(test_device_info())
+ print()
+
+ print("【帧解析器测试】")
+ results.append(test_frame_parser())
+ print()
+
+ print("【ForceReader测试】")
+ results.append(test_force_reader())
+ print()
+
+ print("【并发访问测试】")
+ results.append(test_concurrent_access())
+ print()
+
+ # 汇总结果
+ print("=" * 60)
+ print("总体测试报告")
+ print("=" * 60)
+
+ total_passed = sum(r.tests_passed for r in results)
+ total_failed = sum(r.tests_failed for r in results)
+
+ print(f"总通过: {total_passed}")
+ print(f"总失败: {total_failed}")
+ print(f"成功率: {total_passed / (total_passed + total_failed) * 100:.1f}%")
+
+ if total_failed == 0:
+ print("\n✓ 所有测试通过!")
+ return 0
+ else:
+ print("\n✗ 存在失败的测试")
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main())
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/test_linkermcgcore.py b/src/linkerhand_retarget/tests/unit/test_linkermcgcore.py
new file mode 100644
index 0000000..671ecab
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_linkermcgcore.py
@@ -0,0 +1,52 @@
+import pytest
+from linkerhand_retarget.linkerhand.linkermcgcore import HaoCunData, HaoCunScoketUdp, HandData, NODES_HAND, LOG_FILE_PATH
+
+
+class TestHaoCunData:
+ def test_initialization(self):
+ data = HaoCunData()
+ assert data.is_update == False
+ assert data.frame_index == 0
+ assert data.frequency == 0
+
+ def test_jointangle_arrays_length(self):
+ data = HaoCunData()
+ assert len(data.jointangle_rHand) == NODES_HAND
+ assert len(data.jointangle_lHand) == NODES_HAND
+
+
+class TestHaoCunScoketUdp:
+ def test_initialization_default(self):
+ udp = HaoCunScoketUdp()
+ assert udp.socket_udp is None
+ assert udp.isconnect == False
+
+ def test_initialization_custom_params(self):
+ udp = HaoCunScoketUdp(host='192.168.1.1', port=8000, buffer_size=4096)
+ assert udp.udp_thread is None
+ assert udp.udp_running == False
+
+ def test_is_use_face_blendshapes_default_false(self):
+ udp = HaoCunScoketUdp()
+ assert udp.is_use_face_blend_shapes_arkit == False
+
+
+class TestHandData:
+ def test_hand_data_creation(self):
+ data = HandData(
+ pitch=[0]*5,
+ side=[0]*5,
+ roll=[0]*5,
+ two_pitch=[0]*5,
+ end_pitch=[0]*5
+ )
+ assert len(data.pitch) == 5
+ assert len(data.side) == 5
+
+
+class TestConstants:
+ def test_nodes_hand_value(self):
+ assert NODES_HAND == 25
+
+ def test_log_file_path(self):
+ assert LOG_FILE_PATH == "/tmp/a.log"
diff --git a/src/linkerhand_retarget/tests/unit/test_mapping_curve.py b/src/linkerhand_retarget/tests/unit/test_mapping_curve.py
new file mode 100644
index 0000000..5bcc822
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_mapping_curve.py
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Mapping Curve Test Script
+Test open → opose → fist interpolation for specified joints
+
+Usage:
+ python3 test_mapping_curve.py [joint_index]
+
+ joint_index: 0-20 (sensor index), default: all end joints
+
+ Joint mapping:
+ 2: Thumb Root Flexion
+ 4: Thumb End Flexion
+ 6: Index Root Flexion
+ 8: Index End Flexion
+ 10: Middle Root Flexion
+ 12: Middle End Flexion
+ 14: Ring Root Flexion
+ 16: Ring End Flexion
+ 18: Pinky Root Flexion
+ 20: Pinky End Flexion
+
+Example:
+ python3 test_mapping_curve.py 6 # Index Root Flexion
+ python3 test_mapping_curve.py # All end joints
+"""
+
+import sys
+import json
+import numpy as np
+from pathlib import Path
+
+TMP_FILE = Path(__file__).resolve().parent.parent.parent / "linkerhand_retarget" / "motion" / "linkerforce" / "tmp" / "jointangle_data.tmp"
+with open(TMP_FILE) as f:
+ data = json.load(f)
+
+open_r = data['jointangleoriginal_r']
+opose_r = data['jointangleopose_r']
+fist_r = data['jointanglefist_r']
+
+JOINT_NAMES = {
+ 2: '拇指根部', 4: '拇指末端',
+ 6: '食指根部', 8: '食指末端',
+ 10: '中指根部', 12: '中指末端',
+ 14: '无名指根部', 16: '无名指末端',
+ 18: '小指根部', 20: '小指末端',
+}
+
+EXP_FACTORS = {
+ 2: 5, 4: 7,
+ 6: 10, 8: 3,
+ 10: 5, 12: 10,
+ 14: 5, 16: 18,
+ 18: 5, 20: 8,
+}
+
+MOTOR_OPEN = 255
+MOTOR_OPOSE = 128
+MOTOR_FIST = 0
+
+def interpolate(a, b, t):
+ return a + (b - a) * t
+
+def map_value(sensor_val, sensor_open, sensor_opose, sensor_fist, exp_factor):
+ if abs(sensor_opose - sensor_open) < 1e-6:
+ normalized = 0.5
+ else:
+ normalized = (sensor_val - sensor_open) / (sensor_opose - sensor_open)
+
+ if normalized <= 0:
+ return MOTOR_OPEN, normalized
+ elif normalized <= 1:
+ return MOTOR_OPEN + normalized * (MOTOR_OPOSE - MOTOR_OPEN), normalized
+ else:
+ exceed_amount = normalized - 1.0
+ slope = MOTOR_FIST - MOTOR_OPOSE
+ if exp_factor == 1.0:
+ extension = exceed_amount * slope
+ else:
+ linear_extension = exceed_amount * slope
+ exp_multiplier = 1.0 + (exp_factor - 1.0) * exceed_amount
+ extension = linear_extension * exp_multiplier
+ result = MOTOR_OPOSE + extension
+ return max(MOTOR_FIST, result), normalized
+
+def test_joint(idx):
+ name = JOINT_NAMES.get(idx, f'关节{idx}')
+ exp_factor = EXP_FACTORS.get(idx, 5)
+
+ sensor_open = open_r[idx]
+ sensor_opose = opose_r[idx]
+ sensor_fist = fist_r[idx]
+
+ normalized_fist = (sensor_fist - sensor_open) / (sensor_opose - sensor_open) if abs(sensor_opose - sensor_open) > 1e-6 else 0.5
+
+ print(f"【{name}】 (传感器索引 {idx}, exp_factor={exp_factor})")
+ print("-" * 75)
+ print(f"传感器值: open={sensor_open:.4f}, opose={sensor_opose:.4f}, fist={sensor_fist:.4f}")
+ print(f"normalized_fist={normalized_fist:.4f}")
+ print()
+ print(f"{'阶段':<12} {'插值t':<8} {'传感器值':<12} {'normalized':<12} {'电机值':<10} {'说明'}")
+ print("-" * 75)
+
+ for t in np.arange(0, 1.1, 0.1):
+ sensor_val = interpolate(sensor_open, sensor_opose, t)
+ motor_val, normalized = map_value(sensor_val, sensor_open, sensor_opose, sensor_fist, exp_factor)
+ print(f"open→opose {t:<8.1f} {sensor_val:<12.4f} {normalized:<12.4f} {motor_val:<10.1f}")
+
+ print()
+
+ for t in np.arange(0, 1.1, 0.1):
+ sensor_val = interpolate(sensor_opose, sensor_fist, t)
+ motor_val, normalized = map_value(sensor_val, sensor_open, sensor_opose, sensor_fist, exp_factor)
+ phase = "延伸" if t > 0 else "opose"
+ print(f"opose→fist {t:<8.1f} {sensor_val:<12.4f} {normalized:<12.4f} {motor_val:<10.1f} {phase}")
+
+ print()
+
+def main():
+ if len(sys.argv) > 1:
+ try:
+ idx = int(sys.argv[1])
+ if idx not in JOINT_NAMES:
+ print(f"错误: 不支持关节索引 {idx}")
+ print("有效索引:", sorted(JOINT_NAMES.keys()))
+ return
+ print("=" * 80)
+ print(f"映射曲线测试 - 关节 {idx}")
+ print("=" * 80)
+ print()
+ test_joint(idx)
+ except ValueError:
+ print(__doc__)
+ else:
+ print("=" * 80)
+ print("映射曲线测试 - 所有末端关节")
+ print("=" * 80)
+ print()
+ for idx in [4, 8, 12, 16, 20]:
+ test_joint(idx)
+
+ print("=" * 80)
+ print("测试完成")
+ print("=" * 80)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/src/linkerhand_retarget/tests/unit/test_sensenovacore.py b/src/linkerhand_retarget/tests/unit/test_sensenovacore.py
new file mode 100644
index 0000000..b2b6e5f
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_sensenovacore.py
@@ -0,0 +1,7 @@
+import pytest
+from linkerhand_retarget.linkerhand.sensenovacore import NODES_HAND
+
+
+class TestSensenovaConstants:
+ def test_nodes_hand_value(self):
+ assert NODES_HAND == 30
diff --git a/src/linkerhand_retarget/tests/unit/test_udexrealcore.py b/src/linkerhand_retarget/tests/unit/test_udexrealcore.py
new file mode 100644
index 0000000..34f0f61
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_udexrealcore.py
@@ -0,0 +1,96 @@
+import pytest
+from linkerhand_retarget.linkerhand.udexrealcore import (
+ UdexRealData, MotionData, Bone, Parameter, DeviceData,
+ NODES_HAND, NO_DATA_TIMEOUT
+)
+
+
+class TestUdexRealData:
+ def test_initialization(self):
+ data = UdexRealData()
+ assert data.is_update == False
+ assert data.frame_index == 0
+ assert data.frequency == 0
+
+ def test_jointangle_arrays_length(self):
+ data = UdexRealData()
+ assert len(data.jointangle_rHand) == NODES_HAND
+ assert len(data.jointangle_lHand) == NODES_HAND
+
+ def test_jointderict_arrays_length(self):
+ data = UdexRealData()
+ assert len(data.jointderict_rHand) == NODES_HAND
+ assert len(data.jointderict_lHand) == NODES_HAND
+
+ def test_jointderict_default_values(self):
+ data = UdexRealData()
+ assert all(v == 1 for v in data.jointderict_rHand)
+ assert all(v == 1 for v in data.jointderict_lHand)
+
+ def test_timeout_attributes(self):
+ data = UdexRealData()
+ assert data.last_data_time == 0.0
+ assert data.is_data_timeout == False
+
+
+class TestBone:
+ def test_bone_creation(self):
+ bone = Bone(
+ Name="test_bone",
+ Parent=1,
+ Location=[0.0, 0.0, 0.0],
+ Rotation=[0.0, 0.0, 0.0, 1.0],
+ Scale=[1.0, 1.0, 1.0]
+ )
+ assert bone.Name == "test_bone"
+ assert bone.Parent == 1
+ assert bone.Location == [0.0, 0.0, 0.0]
+
+
+class TestParameter:
+ def test_parameter_creation(self):
+ param = Parameter(Name="test_param", Value=1.0)
+ assert param.Name == "test_param"
+ assert param.Value == 1.0
+
+ def test_parameter_int_value(self):
+ param = Parameter(Name="int_param", Value=10)
+ assert param.Value == 10
+
+ def test_parameter_bool_value(self):
+ param = Parameter(Name="bool_param", Value=True)
+ assert param.Value == True
+
+
+class TestDeviceData:
+ def test_device_data_creation(self):
+ bone = Bone(Name="bone1", Parent=0, Location=[0,0,0], Rotation=[0,0,0,1], Scale=[1,1,1])
+ param = Parameter(Name="param1", Value=1.0)
+ device = DeviceData(Bones=[bone], Parameter=[param])
+
+ assert len(device.Bones) == 1
+ assert len(device.Parameter) == 1
+
+
+class TestMotionData:
+ def test_initialization_empty(self):
+ motion = MotionData({})
+ assert motion.devices == {}
+
+ def test_get_device_not_found(self):
+ motion = MotionData({})
+ result = motion.get_device("nonexistent")
+ assert result is None
+
+ def test_list_sequence_params_empty(self):
+ motion = MotionData({})
+ with pytest.raises(ValueError):
+ motion.list_sequence_params("nonexistent", "prefix")
+
+
+class TestConstants:
+ def test_nodes_hand_value(self):
+ assert NODES_HAND == 24
+
+ def test_no_data_timeout_value(self):
+ assert NO_DATA_TIMEOUT == 1.0
diff --git a/src/linkerhand_retarget/tests/unit/test_utils.py b/src/linkerhand_retarget/tests/unit/test_utils.py
new file mode 100644
index 0000000..7c5f8e0
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_utils.py
@@ -0,0 +1,331 @@
+import pytest
+import numpy as np
+import tempfile
+import os
+import yaml
+from linkerhand_retarget.linkerhand.utils import (
+ DataSource,
+ read_yaml,
+ extract_dataset_folder_last_two_digits,
+ translate_wrist_to_origin,
+ apply_pose_matrix,
+ inverse_transformation,
+ trans_xyzwori_to_wxyzori,
+ trans_wxyzori_to_xyzwori,
+ scale_value,
+ is_within_range,
+ extend_line,
+ poseture_to_matrix,
+ cal_distance,
+ change_list,
+ quaternion_conjugate,
+ quaternion_norm_squared,
+ quaternion_inverse,
+ quaternion_multiply,
+ unitydata_to_worldspacedata,
+ get_quaternion_relative,
+ get_child_quaternion,
+ rotate_matrix_x,
+ rotate_matrix_y,
+ rotate_matrix_z,
+ rotate_quaternion,
+ cubic_model,
+)
+
+
+class TestDataSource:
+ def test_enum_values(self):
+ assert DataSource.motion.value == 1
+ assert DataSource.video.value == 2
+ assert DataSource.vr.value == 3
+
+ def test_enum_names(self):
+ assert DataSource.motion.name == "motion"
+ assert DataSource.video.name == "video"
+ assert DataSource.vr.name == "vr"
+
+
+class TestReadYaml:
+ def test_read_valid_yaml(self):
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
+ yaml.dump({'key': 'value', 'number': 42}, f)
+ f.flush()
+ config = read_yaml(f.name)
+ assert config['key'] == 'value'
+ assert config['number'] == 42
+ os.unlink(f.name)
+
+ def test_read_nested_yaml(self):
+ data = {'a': {'b': {'c': 1}}}
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
+ yaml.dump(data, f)
+ f.flush()
+ config = read_yaml(f.name)
+ assert config['a']['b']['c'] == 1
+ os.unlink(f.name)
+
+
+class TestExtractDatasetFolderLastTwoDigits:
+ def test_valid_two_digits(self):
+ assert extract_dataset_folder_last_two_digits("folder23") == 23
+ assert extract_dataset_folder_last_two_digits("data99") == 99
+
+ def test_single_digit(self):
+ assert extract_dataset_folder_last_two_digits("folder05") == 5
+
+ def test_no_digits(self):
+ assert extract_dataset_folder_last_two_digits("folder") == -1
+ assert extract_dataset_folder_last_two_digits("abc") == -1
+
+
+class TestTranslateWristToOrigin:
+ def test_basic_translation(self):
+ joint_positions = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
+ result = translate_wrist_to_origin(joint_positions)
+ assert np.allclose(result[0], [0.0, 0.0, 0.0])
+ assert np.allclose(result[1], [3.0, 3.0, 3.0])
+ assert np.allclose(result[2], [6.0, 6.0, 6.0])
+
+ def test_single_point(self):
+ joint_positions = np.array([[1.0, 2.0, 3.0]])
+ result = translate_wrist_to_origin(joint_positions)
+ assert np.allclose(result, [[0.0, 0.0, 0.0]])
+
+
+class TestApplyPoseMatrix:
+ def test_identity_matrix(self):
+ joint_positions = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
+ pose_matrix = np.eye(4)
+ result = apply_pose_matrix(joint_positions, pose_matrix)
+ assert np.allclose(result, joint_positions)
+
+ def test_translation_matrix(self):
+ joint_positions = np.array([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]])
+ pose_matrix = np.eye(4)
+ pose_matrix[:3, 3] = [10.0, 20.0, 30.0]
+ result = apply_pose_matrix(joint_positions, pose_matrix)
+ assert np.allclose(result[0], [11.0, 20.0, 30.0])
+ assert np.allclose(result[1], [10.0, 21.0, 30.0])
+
+
+class TestInverseTransformation:
+ def test_identity(self):
+ matrix = np.eye(4)
+ result = inverse_transformation(matrix)
+ assert np.allclose(result, np.eye(4))
+
+ def test_translation_only(self):
+ matrix = np.eye(4)
+ matrix[:3, 3] = [1.0, 2.0, 3.0]
+ result = inverse_transformation(matrix)
+ assert np.allclose(result[:3, 3], [-1.0, -2.0, -3.0])
+
+ def test_rotation_only(self):
+ matrix = np.eye(4)
+ matrix[:3, :3] = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
+ result = inverse_transformation(matrix)
+ assert np.allclose(result[:3, :3], matrix[:3, :3].T)
+
+
+class TestQuaternionConversions:
+ def test_trans_xyzwori_to_wxyzori(self):
+ ori_xyzw = [0.1, 0.2, 0.3, 0.4]
+ result = trans_xyzwori_to_wxyzori(ori_xyzw)
+ assert result == (0.4, 0.1, 0.2, 0.3)
+
+ def test_trans_wxyzori_to_xyzwori(self):
+ ori_wxyz = [0.4, 0.1, 0.2, 0.3]
+ result = trans_wxyzori_to_xyzwori(ori_wxyz)
+ assert result == (0.1, 0.2, 0.3, 0.4)
+
+ def test_quaternion_roundtrip(self):
+ original = [0.1, 0.2, 0.3, 0.4]
+ wxyz = trans_xyzwori_to_wxyzori(original)
+ back = trans_wxyzori_to_xyzwori(wxyz)
+ assert np.allclose(back, original)
+
+
+class TestScaleValue:
+ def test_identity_scale(self):
+ result = scale_value(5.0, 0.0, 10.0, 0.0, 10.0)
+ assert result == 5.0
+
+ def test_range_conversion(self):
+ result = scale_value(5.0, 0.0, 10.0, 0.0, 100.0)
+ assert result == 50.0
+
+ def test_negative_range(self):
+ result = scale_value(5.0, 0.0, 10.0, -100.0, 0.0)
+ assert result == -50.0
+
+ def test_out_of_bounds(self):
+ result = scale_value(15.0, 0.0, 10.0, 0.0, 100.0)
+ assert result == 150.0
+
+
+class TestIsWithinRange:
+ def test_within_bounds(self):
+ assert is_within_range(5.0, 0.0, 10.0) == 5.0
+
+ def test_above_max(self):
+ assert is_within_range(15.0, 0.0, 10.0) == 10.0
+
+ def test_below_min(self):
+ assert is_within_range(-5.0, 0.0, 10.0) == 0.0
+
+
+class TestExtendLine:
+ def test_extend_positive(self):
+ point1 = [0.0, 0.0, 0.0]
+ point2 = [1.0, 0.0, 0.0]
+ result = extend_line(point1, point2, 1.0)
+ assert np.allclose(result, [2.0, 0.0, 0.0])
+
+ def test_extend_negative(self):
+ point1 = [0.0, 0.0, 0.0]
+ point2 = [1.0, 0.0, 0.0]
+ result = extend_line(point1, point2, -0.5)
+ assert np.allclose(result, [0.5, 0.0, 0.0])
+
+
+class TestPosetureToMatrix:
+ def test_identity_rotation(self):
+ position = [1.0, 2.0, 3.0]
+ ori = [0.0, 0.0, 0.0, 1.0]
+ matrix = poseture_to_matrix(position, ori)
+ assert np.allclose(matrix[:3, 3], position)
+
+ def test_180_degree_rotation(self):
+ position = [0.0, 0.0, 0.0]
+ ori = [1.0, 0.0, 0.0, 0.0]
+ matrix = poseture_to_matrix(position, ori)
+ assert np.allclose(matrix[:3, 3], position)
+
+
+class TestCalDistance:
+ def test_same_point(self):
+ assert cal_distance([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) == 0.0
+
+ def test_unit_distance(self):
+ assert cal_distance([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]) == 1.0
+
+ def test_3d_distance(self):
+ result = cal_distance([0.0, 0.0, 0.0], [1.0, 2.0, 2.0])
+ assert np.isclose(result, 3.0)
+
+
+class TestChangeList:
+ def test_none_conversion(self):
+ input_list = ['None', '1', '2']
+ result = change_list(input_list)
+ assert result == [None, '1', '2']
+
+ def test_no_none(self):
+ input_list = ['1', '2', '3']
+ result = change_list(input_list)
+ assert result == ['1', '2', '3']
+
+ def test_all_none(self):
+ input_list = ['None', 'None']
+ result = change_list(input_list)
+ assert result == [None, None]
+
+
+class TestQuaternionOperations:
+ def test_quaternion_conjugate(self):
+ q = [1.0, 2.0, 3.0, 4.0]
+ result = quaternion_conjugate(q)
+ assert np.allclose(result, [-1.0, -2.0, -3.0, 4.0])
+
+ def test_quaternion_norm_squared(self):
+ q = [1.0, 2.0, 2.0, 2.0]
+ result = quaternion_norm_squared(q)
+ assert result == 13.0
+
+ def test_quaternion_inverse(self):
+ q = [0.0, 0.0, 0.0, 1.0]
+ result = quaternion_inverse(q)
+ assert np.allclose(result, [0.0, 0.0, 0.0, 1.0])
+
+ def test_quaternion_multiply_identity(self):
+ q = [0.0, 0.0, 0.0, 1.0]
+ result = quaternion_multiply(q, q)
+ assert np.allclose(result, q)
+
+ def test_quaternion_multiply_rotation(self):
+ q1 = [0.0, 0.0, 0.0, 1.0]
+ q2 = [0.0, 0.0, 0.707, 0.707]
+ result = quaternion_multiply(q1, q2)
+ assert np.allclose(result, q2, atol=0.01)
+
+
+class TestUnitydataToWorldspacedata:
+ def test_basic_conversion(self):
+ positions = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
+ result = unitydata_to_worldspacedata(positions)
+ assert result == [[1.0, 3.0, 2.0], [4.0, 6.0, 5.0]]
+
+
+class TestQuaternionRelative:
+ def test_identity_orientation(self):
+ ori = [0.0, 0.0, 0.0, 1.0]
+ targetori = [0.0, 0.0, 0.0, 1.0]
+ result = get_quaternion_relative(ori, targetori)
+ assert np.allclose(result, [0.0, 0.0, 0.0, 1.0], atol=0.01)
+
+
+class TestGetChildQuaternion:
+ def test_identity_combination(self):
+ ori = [0.0, 0.0, 0.0, 1.0]
+ ori_relative = [0.0, 0.0, 0.0, 1.0]
+ result = get_child_quaternion(ori, ori_relative)
+ assert np.allclose(result, [0.0, 0.0, 0.0, 1.0], atol=0.01)
+
+
+class TestRotateMatrix:
+ def test_rotate_matrix_x_90(self):
+ result = rotate_matrix_x(np.pi / 2)
+ expected = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
+ assert np.allclose(result, expected)
+
+ def test_rotate_matrix_y_90(self):
+ result = rotate_matrix_y(np.pi / 2)
+ expected = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
+ assert np.allclose(result, expected)
+
+ def test_rotate_matrix_z_90(self):
+ result = rotate_matrix_z(np.pi / 2)
+ expected = np.array([[0, -1, 0], [1, 0, 0], [0, 0, 1]])
+ assert np.allclose(result, expected)
+
+ def test_rotate_matrix_identity(self):
+ assert np.allclose(rotate_matrix_x(0), np.eye(3))
+ assert np.allclose(rotate_matrix_y(0), np.eye(3))
+ assert np.allclose(rotate_matrix_z(0), np.eye(3))
+
+
+class TestRotateQuaternion:
+ def test_no_rotation(self):
+ original_quat = [0.0, 0.0, 0.0, 1.0]
+ result = rotate_quaternion(original_quat, 0, 0, 0)
+ assert np.allclose(result, original_quat, atol=0.01)
+
+ def test_180_degree_roll(self):
+ original_quat = [0.0, 0.0, 0.0, 1.0]
+ result = rotate_quaternion(original_quat, 180, 0, 0)
+ assert np.allclose(result, [1.0, 0.0, 0.0, 0.0], atol=0.01)
+
+
+class TestCubicModel:
+ def test_basic_evaluation(self):
+ result = cubic_model(1.0, 1.0, 1.0, 1.0, 1.0)
+ assert result == 4.0
+
+ def test_zero_coefficients(self):
+ result = cubic_model(2.0, 0.0, 0.0, 0.0, 5.0)
+ assert result == 5.0
+
+ def test_array_input(self):
+ x = np.array([0.0, 1.0, 2.0])
+ result = cubic_model(x, 1.0, 0.0, 0.0, 0.0)
+ assert np.allclose(result, [0.0, 1.0, 8.0])
diff --git a/src/linkerhand_retarget/tests/unit/test_vtrdyncore.py b/src/linkerhand_retarget/tests/unit/test_vtrdyncore.py
new file mode 100644
index 0000000..3cdc6e2
--- /dev/null
+++ b/src/linkerhand_retarget/tests/unit/test_vtrdyncore.py
@@ -0,0 +1,76 @@
+import pytest
+from linkerhand_retarget.linkerhand.vtrdyncore import MocapData, VtrdynSocketUdp, NODES_BODY, NODES_HAND, NODES_FACEBS_ARKIT, NODES_FACEBS_AUDIO
+
+
+class TestMocapData:
+ def test_initialization(self):
+ mocap = MocapData()
+ assert mocap.is_update == False
+ assert mocap.frame_index == 0
+ assert mocap.frequency == 0
+
+ def test_body_arrays_length(self):
+ mocap = MocapData()
+ assert len(mocap.sensor_state_body) == NODES_BODY
+ assert len(mocap.position_body) == NODES_BODY
+ assert len(mocap.quaternion_body) == NODES_BODY
+ assert len(mocap.gyr_body) == NODES_BODY
+ assert len(mocap.acc_body) == NODES_BODY
+ assert len(mocap.velocity_body) == NODES_BODY
+
+ def test_hand_arrays_length(self):
+ mocap = MocapData()
+ assert len(mocap.sensor_state_r_hand) == NODES_HAND
+ assert len(mocap.position_rHand) == NODES_HAND
+ assert len(mocap.quaternion_rHand) == NODES_HAND
+
+ def test_face_blendshapes_length(self):
+ mocap = MocapData()
+ assert len(mocap.face_blend_shapes_arkit) == NODES_FACEBS_ARKIT
+ assert len(mocap.face_blend_shapes_audio) == NODES_FACEBS_AUDIO
+
+ def test_eyeball_quaternion_length(self):
+ mocap = MocapData()
+ assert len(mocap.local_quat_right_eyeball) == 4
+ assert len(mocap.local_quat_left_eyeball) == 4
+
+
+class TestVtrdynSocketUdp:
+ def test_initialization(self):
+ udp = VtrdynSocketUdp()
+ assert udp.socket_udp is None
+ assert udp.isconnect == False
+
+ def test_initialization_with_debug(self):
+ udp = VtrdynSocketUdp(debug=True)
+ assert udp.debug == True
+
+ def test_mocap_data_initialized(self):
+ udp = VtrdynSocketUdp()
+ assert udp.mocap_data_realtime is not None
+
+ def test_data_lock_initialized(self):
+ udp = VtrdynSocketUdp()
+ assert udp.data_lock is not None
+
+ def test_send_running_default_false(self):
+ udp = VtrdynSocketUdp()
+ assert udp.send_running == False
+
+ def test_thread_initialized(self):
+ udp = VtrdynSocketUdp()
+ assert udp.send_thread is None
+
+
+class TestConstants:
+ def test_nodes_body_value(self):
+ assert NODES_BODY == 23
+
+ def test_nodes_hand_value(self):
+ assert NODES_HAND == 20
+
+ def test_nodes_facebs_arkit_value(self):
+ assert NODES_FACEBS_ARKIT == 52
+
+ def test_nodes_facebs_audio_value(self):
+ assert NODES_FACEBS_AUDIO == 26
diff --git a/src/requirements.txt b/src/requirements.txt
new file mode 100644
index 0000000..4e77d45
--- /dev/null
+++ b/src/requirements.txt
@@ -0,0 +1,14 @@
+anytree
+colorama
+loguru
+lxml
+numpy
+pyqtgraph
+pyserial
+PyYAML
+scipy>=1.10.0
+six
+tqdm
+transforms3d
+trimesh
+tyro
\ No newline at end of file
diff --git a/startup_linkerforce.sh b/startup_linkerforce.sh
new file mode 100644
index 0000000..7ecbe48
--- /dev/null
+++ b/startup_linkerforce.sh
@@ -0,0 +1,15 @@
+#!/bin/bash
+# LinkerForce 手套遥操作启动脚本
+
+# 设置串口权限
+sudo chmod 666 /dev/ttyUSB0
+sudo chmod 666 /dev/ttyUSB1
+
+# source ROS 工作空间(请输入正确的目录)
+source /home/linker-brunt/project/linkerhand_telop_sdk/v2/ros2/install/setup.bash
+
+# 启动程序(强制标定)
+# ros2 run linkerhand_retarget handretarget --ros-args -p calibration:=True
+
+# 无标定启动
+ros2 run linkerhand_retarget handretarget
\ No newline at end of file