From 9477f25a5117e66f7673fc1a3424d5a2c6cc0f3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=90=89=E7=92=83=E6=9C=88=E5=85=89?=
<15630071+llyg777@user.noreply.gitee.com>
Date: Wed, 13 Aug 2025 12:36:04 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9D=E5=A7=8B=E5=8C=96=E9=A1=B9?=
=?UTF-8?q?=E7=9B=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0=E7=94=B5=E6=9C=BA=E6=8E=A7?=
=?UTF-8?q?=E5=88=B6=E3=80=81CAN=E9=80=9A=E4=BF=A1=E3=80=81QT=E7=95=8C?=
=?UTF-8?q?=E9=9D=A2=E7=AD=89=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/.gitignore | 3 +
.idea/5dof.iml | 8 +
.../inspectionProfiles/profiles_settings.xml | 6 +
.idea/misc.xml | 7 +
.idea/modules.xml | 8 +
5dof_motor_main.py | 226 +++++++
5dof_motor_test.py | 245 +++++++
5dof_test_ctrl_motor.py | 80 +++
DM_CAN/DM_CAN.py | 624 ++++++++++++++++++
DM_CAN/DM_Motor_Test.py | 114 ++++
DM_CAN/__pycache__/DM_CAN.cpython-39.pyc | Bin 0 -> 20252 bytes
calculate/__pycache__/ik.cpython-39.pyc | Bin 0 -> 1816 bytes
.../__pycache__/trajectory.cpython-39.pyc | Bin 0 -> 2209 bytes
calculate/angle_A.txt | 1 +
calculate/angle_B.txt | 1 +
calculate/calculate_angle.py | 179 +++++
calculate/fk.py | 40 ++
calculate/ik.py | 69 ++
calculate/test_ik.py | 64 ++
calculate/traj_fk.py | 62 ++
calculate/traj_main.py | 71 ++
calculate/trajectory.py | 62 ++
.../__pycache__/can_main.cpython-38.pyc | Bin 0 -> 2930 bytes
.../motor_control_can_all.cpython-38.pyc | Bin 0 -> 4798 bytes
can_test_motor/can_main.py | 93 +++
can_test_motor/can_run_demo.py | 15 +
can_test_motor/motor_control_can_all.py | 174 +++++
main_limit.py | 187 ++++++
.../__pycache__/can_main.cpython-38.pyc | Bin 0 -> 3368 bytes
motor_control/can_main.py | 93 +++
motor_control/can_run_demo.py | 13 +
motor_control/can_test.py | 105 +++
motor_control/contral.py | 134 ++++
motor_control/motor_control_can_all.py | 170 +++++
motor_control/motor_control_usb_all.py | 212 ++++++
motor_control/motor_test_mostimprotant.py | 134 ++++
motor_control/test.py | 9 +
qiege_motor.py | 183 +++++
qt/DM_CAN/DM_CAN.py | 624 ++++++++++++++++++
qt/DM_CAN/DM_Motor_Test.py | 114 ++++
qt/__pycache__/untitled.cpython-39.pyc | Bin 0 -> 1604 bytes
qt/calculate/__pycache__/ik.cpython-39.pyc | Bin 0 -> 1819 bytes
.../__pycache__/trajectory.cpython-39.pyc | Bin 0 -> 2207 bytes
qt/calculate/calculate_angle.py | 138 ++++
qt/calculate/fk.py | 40 ++
qt/calculate/ik.py | 69 ++
qt/calculate/test_ik.py | 64 ++
qt/calculate/traj_fk.py | 62 ++
qt/calculate/traj_main.py | 71 ++
qt/calculate/trajectory.py | 62 ++
qt/can_test_motor/can_main.py | 93 +++
qt/can_test_motor/can_run_demo.py | 15 +
qt/can_test_motor/motor_control_can_all.py | 174 +++++
qt/qt_main.py | 146 ++++
qt/qt_motor_main.py | 305 +++++++++
qt/untitled.py | 46 ++
qt/untitled.ui | 91 +++
test1.py | 160 +++++
test_chuankou.py | 55 ++
test_duoji.py | 95 +++
60 files changed, 5816 insertions(+)
create mode 100644 .idea/.gitignore
create mode 100644 .idea/5dof.iml
create mode 100644 .idea/inspectionProfiles/profiles_settings.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/modules.xml
create mode 100644 5dof_motor_main.py
create mode 100644 5dof_motor_test.py
create mode 100644 5dof_test_ctrl_motor.py
create mode 100644 DM_CAN/DM_CAN.py
create mode 100644 DM_CAN/DM_Motor_Test.py
create mode 100644 DM_CAN/__pycache__/DM_CAN.cpython-39.pyc
create mode 100644 calculate/__pycache__/ik.cpython-39.pyc
create mode 100644 calculate/__pycache__/trajectory.cpython-39.pyc
create mode 100644 calculate/angle_A.txt
create mode 100644 calculate/angle_B.txt
create mode 100644 calculate/calculate_angle.py
create mode 100644 calculate/fk.py
create mode 100644 calculate/ik.py
create mode 100644 calculate/test_ik.py
create mode 100644 calculate/traj_fk.py
create mode 100644 calculate/traj_main.py
create mode 100644 calculate/trajectory.py
create mode 100644 can_test_motor/__pycache__/can_main.cpython-38.pyc
create mode 100644 can_test_motor/__pycache__/motor_control_can_all.cpython-38.pyc
create mode 100644 can_test_motor/can_main.py
create mode 100644 can_test_motor/can_run_demo.py
create mode 100644 can_test_motor/motor_control_can_all.py
create mode 100644 main_limit.py
create mode 100644 motor_control/__pycache__/can_main.cpython-38.pyc
create mode 100644 motor_control/can_main.py
create mode 100644 motor_control/can_run_demo.py
create mode 100644 motor_control/can_test.py
create mode 100644 motor_control/contral.py
create mode 100644 motor_control/motor_control_can_all.py
create mode 100644 motor_control/motor_control_usb_all.py
create mode 100644 motor_control/motor_test_mostimprotant.py
create mode 100644 motor_control/test.py
create mode 100644 qiege_motor.py
create mode 100644 qt/DM_CAN/DM_CAN.py
create mode 100644 qt/DM_CAN/DM_Motor_Test.py
create mode 100644 qt/__pycache__/untitled.cpython-39.pyc
create mode 100644 qt/calculate/__pycache__/ik.cpython-39.pyc
create mode 100644 qt/calculate/__pycache__/trajectory.cpython-39.pyc
create mode 100644 qt/calculate/calculate_angle.py
create mode 100644 qt/calculate/fk.py
create mode 100644 qt/calculate/ik.py
create mode 100644 qt/calculate/test_ik.py
create mode 100644 qt/calculate/traj_fk.py
create mode 100644 qt/calculate/traj_main.py
create mode 100644 qt/calculate/trajectory.py
create mode 100644 qt/can_test_motor/can_main.py
create mode 100644 qt/can_test_motor/can_run_demo.py
create mode 100644 qt/can_test_motor/motor_control_can_all.py
create mode 100644 qt/qt_main.py
create mode 100644 qt/qt_motor_main.py
create mode 100644 qt/untitled.py
create mode 100644 qt/untitled.ui
create mode 100644 test1.py
create mode 100644 test_chuankou.py
create mode 100644 test_duoji.py
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..359bb53
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
diff --git a/.idea/5dof.iml b/.idea/5dof.iml
new file mode 100644
index 0000000..ef778ac
--- /dev/null
+++ b/.idea/5dof.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..ed483f3
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..398f280
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/5dof_motor_main.py b/5dof_motor_main.py
new file mode 100644
index 0000000..f618612
--- /dev/null
+++ b/5dof_motor_main.py
@@ -0,0 +1,226 @@
+import time
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.animation import FuncAnimation
+
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+# ------------------------ 调试开关 ------------------------
+DEBUG_MODE = True # <<< True: 只播动画;False: 控制真实电机
+
+# 导入运动学和轨迹函数
+from calculate.ik import inverseF
+from calculate.trajectory import (
+ circle_trajectory,
+ line_trajectory,
+ ellipse_trajectory,
+ square_trajectory,
+ triangle_trajectory
+)
+
+# -------------------- 只在非 Debug 模式导入 DM_CAN --------------------
+if not DEBUG_MODE:
+ from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type, DM_variable
+ import serial
+ from can_test_motor import motor_control_can_all
+else:
+ print("【DEBUG MODE】: 已启用调试模式,不连接真实硬件。")
+
+# ------------------------ 机械臂参数 ------------------------
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250 # 右侧基座偏移
+
+# ------------------------ DM4310 配置 ------------------------
+MOTOR1_ID = 0x01
+MOTOR2_ID = 0x04
+CAN_SERIAL_PORT = '/dev/ttyACM0'
+BAUD_RATE = 921600
+KP = 5.0
+KD = 1.0
+
+# ------------------------ 轮趣科技电机配置 ------------------------
+MOTOR3_ID = 0x05
+
+# 全局变量
+motor1 = motor2 = motor3 = motor_control = None
+current_theta1 = 0.0
+current_theta4 = 0.0
+x_list = y_list = [] # 全局轨迹点
+line = None # 动画线对象
+ani = None # 动画对象
+
+
+# ------------------------ 初始化电机 ------------------------
+def init_motors():
+ global motor1, motor2, motor_control
+
+ if DEBUG_MODE:
+ print("【DEBUG】跳过电机初始化")
+ motor1 = motor2 = type('Motor', (), {'id': 0})()
+ motor_control = type('MotorControl', (), {
+ 'enable': lambda x: True,
+ 'disable': lambda x: None,
+ 'controlMIT': lambda m, kp, kd, pos, vel, torq: None
+ })()
+ return motor1, motor2, motor_control
+
+ try:
+ can_serial = serial.Serial(CAN_SERIAL_PORT, BAUD_RATE, timeout=0.5)
+ print(f"CAN 串口 {CAN_SERIAL_PORT} 打开成功")
+ except Exception as e:
+ print(f"无法打开串口: {e}")
+ exit(1)
+
+ motor_control = MotorControl(can_serial)
+ motor1 = Motor(DM_Motor_Type.DM4310, MOTOR1_ID, 0x11)
+ motor2 = Motor(DM_Motor_Type.DM4310, MOTOR2_ID, 0x15)
+
+ motor_control.addMotor(motor1)
+ motor_control.addMotor(motor2)
+ motor_control.switchControlMode(motor1, Control_Type.MIT)
+ motor_control.switchControlMode(motor2, Control_Type.MIT)
+ time.sleep(0.1)
+ motor_control.save_motor_param(motor1)
+ motor_control.save_motor_param(motor2)
+ motor_control.enable(motor1)
+ motor_control.enable(motor2)
+ print("电机已使能。")
+ return motor1, motor2, motor_control
+
+
+# ------------------------ MIT 控制函数 ------------------------
+def control_two_motors_mit(theta1, theta4):
+ global current_theta1, current_theta4
+
+ pos1 = np.degrees(theta1)
+ pos4 = np.degrees(theta4)
+ vel = 0.0
+
+ if not DEBUG_MODE:
+ motor_control.controlMIT(motor1, KP, KD, pos1, vel, 0.0)
+ motor_control.controlMIT(motor2, KP, KD, pos4, vel, 0.0)
+ else:
+ print(f"[DEBUG] 模拟控制 -> Motor1: {pos1:.2f}°, Motor2: {pos4:.2f}°")
+
+ current_theta1 = theta1
+ current_theta4 = theta4
+
+
+# ------------------------ 动画帧绘制函数 ------------------------
+def draw_frame(i):
+ global line, x_list, y_list
+
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+
+ # 左臂第一杆末端
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ # 右臂第一杆末端(注意 L0 偏移)
+ x4 = L0 + L4 * np.cos(theta4)
+ y4 = L4 * np.sin(theta4)
+
+ # 五连杆点序列:基座左 → 左臂末端 → 末端执行器 → 右臂末端 → 基座右
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+
+ line.set_data(x_coords, y_coords)
+
+ # 发送控制指令(在循环中同步)
+ control_two_motors_mit(theta1, theta4)
+
+ except Exception as e:
+ print(f"第 {i} 帧逆解失败: {e}")
+ line.set_data([], [])
+
+ return line,
+
+
+# ------------------------ 运行轨迹 + 动画 ------------------------
+def run_trajectory_with_animation(trajectory_func, **params):
+ """
+ 生成轨迹并播放动画,同时发送控制指令
+ """
+ global x_list, y_list, line, ani
+
+ print(f"生成轨迹: {trajectory_func.__name__}")
+ x_list, y_list = trajectory_func(**params)
+ print(f"轨迹点数: {len(x_list)}")
+
+ # --- 设置绘图 ---
+ fig, ax = plt.subplots(figsize=(10, 8))
+ ax.set_xlim(-200, L0 + 200) # 根据 L0 调整
+ ax.set_ylim(0, 500)
+ ax.set_aspect('equal')
+ ax.grid(True, linestyle='--', alpha=0.6)
+ ax.set_title(f"五连杆机械臂运动仿真 - 轨迹: {trajectory_func.__name__}", fontsize=14)
+
+ # 绘制目标轨迹(虚线)
+ ax.plot(x_list, y_list, 'b--', alpha=0.5, label='目标轨迹')
+ line, = ax.plot([], [], 'r-o', linewidth=3, markersize=8, label='机械臂结构')
+ ax.legend()
+
+ # --- 创建动画 ---
+ ani = FuncAnimation(
+ fig, draw_frame,
+ frames=len(x_list),
+ interval=50, # 每帧间隔(ms)
+ repeat=False, #循环播放开关
+ blit=True
+ )
+
+ plt.show() # <<< 阻塞显示,确保窗口不黑不闪退
+ print("动画结束。")
+
+
+# ------------------------ 主函数 ------------------------
+if __name__ == "__main__":
+ # === 在这里设置轨迹类型和参数 ===
+
+ '''trajectory_config = {
+ 'func': ellipse_trajectory,
+ 'params': {
+ 'center': (100, 200),
+ 'rx': 50,
+ 'ry': 25,
+ 'num_points': 100
+ }
+ }'''
+
+
+ # 可选其他轨迹:
+ trajectory_config = {
+ 'func': line_trajectory,
+ 'params': {'start': (125, 300), 'end': (125, 400)}
+ }
+ #trajectory_config = {
+ # 'func': circle_trajectory,
+ # 'params': {'center': (100, 300), 'radius': 40}
+ # }
+
+ # 初始化电机
+ try:
+ motor1, motor2, motor_control = init_motors()
+
+ # 运行带动画的轨迹
+ run_trajectory_with_animation(trajectory_config['func'], **trajectory_config['params'])
+
+ except KeyboardInterrupt:
+ print("\n【用户中断】")
+ except Exception as e:
+ print(f"程序异常: {e}")
+ finally:
+ if not DEBUG_MODE:
+ motor_control.disable(motor1)
+ motor_control.disable(motor2)
+ print("电机已停机。")
+ else:
+ print("【DEBUG】程序结束")
\ No newline at end of file
diff --git a/5dof_motor_test.py b/5dof_motor_test.py
new file mode 100644
index 0000000..0f585e8
--- /dev/null
+++ b/5dof_motor_test.py
@@ -0,0 +1,245 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/08/04
+# @Author : hx
+# @File : 5dof_motor_test.py (优化版:切割控制由外部模块接管)
+'''
+
+import time
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.animation import FuncAnimation
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong']
+plt.rcParams['axes.unicode_minus'] = False
+
+# ------------------------ 调试开关 ------------------------
+DEBUG_MODE = True # <<< True: 只播动画;False: 控制真实电机
+
+# 导入运动学和轨迹函数
+from calculate.ik import inverseF
+from calculate.trajectory import (
+ line_trajectory
+)
+
+# -------------------- 只在非 Debug 模式导入 DM_CAN 和 轮趣模块 --------------------
+if not DEBUG_MODE:
+ from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type
+ import serial
+
+ # 注意:不再导入 can_test_motor 或轮趣控制模块
+else:
+ print("【DEBUG MODE】: 已启用调试模式,不连接硬件。")
+
+# ------------------------ 机械臂参数 ------------------------
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250 # 右侧基座偏移
+
+# ------------------------ DM4310 配置 ------------------------
+MOTOR1_ID = 0x01
+MOTOR2_ID = 0x04
+CAN_SERIAL_PORT = '/dev/ttyACM0'
+BAUD_RATE = 921600
+KP = 5.0
+KD = 1.0
+
+# ------------------------ 切割控制模式常量 ------------------------
+CUTTING_MODE = False # <<< 控制标志:False=不切割,True=触发切割(调用 qiege_motor.control())
+CUTTING_AREA_MIN_Y = 280 # Y 值大于此开启切割(用于直线等轨迹)
+
+# ------------------------ 全局变量 ------------------------
+motor1 = motor2 = motor_control = None
+current_theta1 = 0.0
+current_theta4 = 0.0
+x_list = y_list = [] # 全局轨迹点
+line = None # 动画线对象
+ani = None # 动画对象
+
+
+# ------------------------ 初始化电机 ------------------------
+def init_motors():
+ global motor1, motor2, motor_control
+
+ if DEBUG_MODE:
+ print("【DEBUG】跳过 DM4310 电机初始化")
+ motor1 = motor2 = type('Motor', (), {'id': 0})()
+ motor_control = type('MotorControl', (), {
+ 'enable': lambda x: True,
+ 'disable': lambda x: None,
+ 'controlMIT': lambda m, kp, kd, pos, vel, torq: None
+ })()
+ return motor1, motor2, motor_control
+
+ try:
+ can_serial = serial.Serial(CAN_SERIAL_PORT, BAUD_RATE, timeout=0.5)
+ print(f"CAN 串口 {CAN_SERIAL_PORT} 打开成功")
+ except Exception as e:
+ print(f"无法打开串口: {e}")
+ exit(1)
+
+ motor_control = MotorControl(can_serial)
+ motor1 = Motor(DM_Motor_Type.DM4310, MOTOR1_ID, 0x11)
+ motor2 = Motor(DM_Motor_Type.DM4310, MOTOR2_ID, 0x14)
+
+ motor_control.addMotor(motor1)
+ motor_control.addMotor(motor2)
+ motor_control.switchControlMode(motor1, Control_Type.MIT)
+ motor_control.switchControlMode(motor2, Control_Type.MIT)
+ time.sleep(0.1)
+ motor_control.save_motor_param(motor1)
+ motor_control.save_motor_param(motor2)
+ motor_control.enable(motor1)
+ motor_control.enable(motor2)
+ print("DM4310 电机已使能。")
+ return motor1, motor2, motor_control
+
+
+# ------------------------ MIT 控制函数(DM4310)------------------------
+def control_two_motors_mit(theta1, theta4):
+ global current_theta1, current_theta4
+
+ pos1 = np.degrees(theta1)
+ pos4 = np.degrees(theta4)
+ vel = 0.5 # 可根据轨迹速度调整
+
+ if not DEBUG_MODE:
+ motor_control.controlMIT(motor1, KP, KD, pos1, vel, 0.0)
+ motor_control.controlMIT(motor2, KP, KD, pos4, vel, 0.0)
+ else:
+ print(f"[DEBUG] DM4310 -> M1: {pos1:.2f}°, M2: {pos4:.2f}°")
+
+ current_theta1 = theta1
+ current_theta4 = theta4
+
+
+# ------------------------ 动画帧绘制函数 ------------------------
+def draw_frame(i):
+ global line, x_list, y_list, CUTTING_MODE
+
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+
+ # 左臂第一杆末端
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ # 右臂第一杆末端
+ x4 = L0 + L4 * np.cos(theta4)
+ y4 = L4 * np.sin(theta4)
+
+ # 五连杆结构点
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+ line.set_data(x_coords, y_coords)
+
+ # --- 控制两个 DM4310 电机 ---
+ control_two_motors_mit(theta1, theta4)
+
+ # --- 切割控制逻辑:判断是否进入切割区域 ---
+ in_cutting_zone = (y > CUTTING_AREA_MIN_Y) # 例如直线轨迹
+
+ # ✅ 仅当进入切割区且当前未开启切割时,触发外部控制
+ if in_cutting_zone and not CUTTING_MODE:
+ print(f"[切割控制] 进入切割区 (Y={y:.1f}),触发 qiege_motor.control(True)")
+ # 调用你后续集成的切割控制函数
+ try:
+ from qiege_motor import control
+ control(True) # 启动切割
+ except Exception as e:
+ print(f"[错误] 调用 qiege_motor.control(True) 失败: {e}")
+ CUTTING_MODE = True # 标记已开启切割
+
+ # ✅ 当离开切割区且当前正在切割时,可选择关闭(按需)
+ elif not in_cutting_zone and CUTTING_MODE:
+ print(f"[切割控制] 离开切割区 (Y={y:.1f}),可调用 control(False) 关闭")
+ # 可选:自动关闭切割
+ # try:
+ # from qiege_motor import control
+ # control(False)
+ # except Exception as e:
+ # print(f"[错误] 调用 qiege_motor.control(False) 失败: {e}")
+ # CUTTING_MODE = False
+
+ except Exception as e:
+ print(f"第 {i} 帧逆解失败: {e}")
+ line.set_data([], [])
+
+ return line,
+
+
+# ------------------------ 运行轨迹 + 动画 ------------------------
+def run_trajectory_with_animation(trajectory_func, **params):
+ """
+ 生成轨迹并播放动画,同时发送控制指令
+ """
+ global x_list, y_list, line, ani
+ print(f"生成轨迹: {trajectory_func.__name__}")
+ x_list, y_list = trajectory_func(**params)
+ print(f"轨迹点数: {len(x_list)}")
+
+ fig, ax = plt.subplots(figsize=(10, 8))
+ ax.set_xlim(-200, L0 + 200)
+ ax.set_ylim(0, 500)
+ ax.set_aspect('equal')
+ ax.grid(True, linestyle='--', alpha=0.6)
+ ax.set_title(f"五连杆机械臂运动仿真 - 轨迹: {trajectory_func.__name__}", fontsize=14)
+
+ # 绘制目标轨迹(虚线)
+ ax.plot(x_list, y_list, 'b--', alpha=0.5, label='目标轨迹')
+ line, = ax.plot([], [], 'r-o', linewidth=3, markersize=8, label='机械臂结构')
+ ax.legend()
+
+ ani = FuncAnimation(
+ fig, draw_frame,
+ frames=len(x_list),
+ interval=50,
+ repeat=False,
+ blit=True
+ )
+
+ plt.show()
+ print("动画结束。")
+
+
+# ------------------------ 主函数 ------------------------
+if __name__ == "__main__":
+ # === 设置轨迹类型和参数 ===
+ trajectory_config = {
+ 'func': line_trajectory,
+ 'params': {'start': (125, 250), 'end': (125, 400)}
+ }
+
+ try:
+ motor1, motor2, motor_control = init_motors()
+ # 启动轨迹动画与控制
+ run_trajectory_with_animation(trajectory_config['func'], **trajectory_config['params'])
+
+ except KeyboardInterrupt:
+ print("\n【用户中断】")
+ except Exception as e:
+ print(f"程序异常: {e}")
+ finally:
+ # ✅ 程序结束时,确保关闭切割(如果已开启)
+ if CUTTING_MODE:
+ print("[切割控制] 程序结束,尝试关闭切割...")
+ try:
+ from qiege_motor import control
+ control(False)
+ except Exception as e:
+ print(f"[错误] 调用 qiege_motor.control(False) 失败: {e}")
+ CUTTING_MODE = False
+
+ # 停用 DM4310 电机
+ if not DEBUG_MODE and motor_control is not None:
+ motor_control.disable(motor1)
+ motor_control.disable(motor2)
+ print("所有电机已停机。")
+ else:
+ print("【DEBUG】程序结束")
\ No newline at end of file
diff --git a/5dof_test_ctrl_motor.py b/5dof_test_ctrl_motor.py
new file mode 100644
index 0000000..73fbb12
--- /dev/null
+++ b/5dof_test_ctrl_motor.py
@@ -0,0 +1,80 @@
+import numpy as np
+from calculate.fk import forwardF
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# ========== 主程序开始 ==========
+# 输入参数
+l1 = 250
+l2 = 300
+l3 = 300
+l4 = 250
+l5 = 250
+
+hd = np.pi / 180
+u1 = 120 * hd
+u4 = 60 * hd
+
+omega1 = 500
+omega4 = 500
+alpha1 = 0
+alpha4 = 0
+
+# 计算正向运动学
+xc, yc, u2, u3, omega2, omega3, alpha2, alpha3 = forwardF(u1, u4, omega1, omega4, l1, l2, l3, l4, l5, alpha1, alpha4)
+
+# 存储中间变量
+theta2 = [u2]
+theta3 = [u3]
+omega2_list = [omega2]
+omega3_list = [omega3]
+alpha2_list = [alpha2]
+alpha3_list = [alpha3]
+xcd = [xc]
+ycd = [yc]
+
+# 绘图部分
+fig = plt.figure(figsize=(8, 8))
+ax = fig.add_subplot(111)
+ax.set_title('并联SCARA')
+ax.set_xlabel('mm')
+ax.set_ylabel('mm')
+ax.set_xlim(-200, 600)
+ax.set_ylim(-200, 600)
+ax.grid(True)
+
+x = [0] * 5
+y = [0] * 5
+
+# 基础点
+x[0] = 0
+y[0] = 0
+
+# 第一个连杆末端
+x[1] = l1 * np.cos(u1)
+y[1] = l1 * np.sin(u1)
+
+# 末端执行器位置
+x[2] = xcd[0]
+y[2] = ycd[0]
+
+# 第四个连杆末端
+x[3] = l4 * np.cos(u4) + l5
+y[3] = l4 * np.sin(u4)
+
+# 第五个点(第二个电机位置)
+x[4] = l5
+y[4] = 0
+
+# 绘制结构线和关键点
+ax.plot(x, y, 'k-', linewidth=2)
+ax.plot(x[0], y[0], 'or') # 基础点
+ax.plot(x[1], y[1], 'or') # 第一连杆末端
+ax.plot(x[2], y[2], 'og') # 末端执行器
+ax.plot(x[3], y[3], 'om') # 第四连杆末端
+ax.plot(x[4], y[4], 'oc') # 第二个电机
+
+plt.show()
\ No newline at end of file
diff --git a/DM_CAN/DM_CAN.py b/DM_CAN/DM_CAN.py
new file mode 100644
index 0000000..d548f7f
--- /dev/null
+++ b/DM_CAN/DM_CAN.py
@@ -0,0 +1,624 @@
+from time import sleep
+import numpy as np
+from enum import IntEnum
+from struct import unpack
+from struct import pack
+
+
+class Motor:
+ def __init__(self, MotorType, SlaveID, MasterID):
+ """
+ define Motor object 定义电机对象
+ :param MotorType: Motor type 电机类型
+ :param SlaveID: CANID 电机ID
+ :param MasterID: MasterID 主机ID 建议不要设为0
+ """
+ self.Pd = float(0)
+ self.Vd = float(0)
+ self.state_q = float(0)
+ self.state_dq = float(0)
+ self.state_tau = float(0)
+ self.SlaveID = SlaveID
+ self.MasterID = MasterID
+ self.MotorType = MotorType
+ self.isEnable = False
+ self.NowControlMode = Control_Type.MIT
+ self.temp_param_dict = {}
+
+ def recv_data(self, q: float, dq: float, tau: float):
+ self.state_q = q
+ self.state_dq = dq
+ self.state_tau = tau
+
+ def getPosition(self):
+ """
+ get the position of the motor 获取电机位置
+ :return: the position of the motor 电机位置
+ """
+ return self.state_q
+
+ def getVelocity(self):
+ """
+ get the velocity of the motor 获取电机速度
+ :return: the velocity of the motor 电机速度
+ """
+ return self.state_dq
+
+ def getTorque(self):
+ """
+ get the torque of the motor 获取电机力矩
+ :return: the torque of the motor 电机力矩
+ """
+ return self.state_tau
+
+ def getParam(self, RID):
+ """
+ get the parameter of the motor 获取电机内部的参数,需要提前读取
+ :param RID: DM_variable 电机参数
+ :return: the parameter of the motor 电机参数
+ """
+ if RID in self.temp_param_dict:
+ return self.temp_param_dict[RID]
+ else:
+ return None
+
+
+class MotorControl:
+ send_data_frame = np.array(
+ [0x55, 0xAA, 0x1e, 0x03, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0x00, 0x08, 0x00,
+ 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x00], np.uint8)
+ # 4310 4310_48 4340 4340_48
+ Limit_Param = [[12.5, 30, 10], [12.5, 50, 10], [12.5, 8, 28], [12.5, 10, 28],
+ # 6006 8006 8009 10010L 10010
+ [12.5, 45, 20], [12.5, 45, 40], [12.5, 45, 54], [12.5, 25, 200], [12.5, 20, 200],
+ # H3510 DMG62150 DMH6220
+ [12.5 , 280 , 1],[12.5 , 45 , 10],[12.5 , 45 , 10]]
+
+ def __init__(self, serial_device):
+ """
+ define MotorControl object 定义电机控制对象
+ :param serial_device: serial object 串口对象
+ """
+ self.serial_ = serial_device
+ self.motors_map = dict()
+ self.data_save = bytes() # save data
+ if self.serial_.is_open: # open the serial port
+ print("Serial port is open")
+ serial_device.close()
+ self.serial_.open()
+
+ def controlMIT(self, DM_Motor, kp: float, kd: float, q: float, dq: float, tau: float):
+ """
+ MIT Control Mode Function 达妙电机MIT控制模式函数
+ :param DM_Motor: Motor object 电机对象
+ :param kp: kp
+ :param kd: kd
+ :param q: position 期望位置
+ :param dq: velocity 期望速度
+ :param tau: torque 期望力矩
+ :return: None
+ """
+ if DM_Motor.SlaveID not in self.motors_map:
+ print("controlMIT ERROR : Motor ID not found")
+ return
+ kp_uint = float_to_uint(kp, 0, 500, 12)
+ kd_uint = float_to_uint(kd, 0, 5, 12)
+ MotorType = DM_Motor.MotorType
+ Q_MAX = self.Limit_Param[MotorType][0]
+ DQ_MAX = self.Limit_Param[MotorType][1]
+ TAU_MAX = self.Limit_Param[MotorType][2]
+ q_uint = float_to_uint(q, -Q_MAX, Q_MAX, 16)
+ dq_uint = float_to_uint(dq, -DQ_MAX, DQ_MAX, 12)
+ tau_uint = float_to_uint(tau, -TAU_MAX, TAU_MAX, 12)
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ data_buf[0] = (q_uint >> 8) & 0xff
+ data_buf[1] = q_uint & 0xff
+ data_buf[2] = dq_uint >> 4
+ data_buf[3] = ((dq_uint & 0xf) << 4) | ((kp_uint >> 8) & 0xf)
+ data_buf[4] = kp_uint & 0xff
+ data_buf[5] = kd_uint >> 4
+ data_buf[6] = ((kd_uint & 0xf) << 4) | ((tau_uint >> 8) & 0xf)
+ data_buf[7] = tau_uint & 0xff
+ self.__send_data(DM_Motor.SlaveID, data_buf)
+ self.recv() # receive the data from serial port
+
+ def control_delay(self, DM_Motor, kp: float, kd: float, q: float, dq: float, tau: float, delay: float):
+ """
+ MIT Control Mode Function with delay 达妙电机MIT控制模式函数带延迟
+ :param DM_Motor: Motor object 电机对象
+ :param kp: kp
+ :param kd: kd
+ :param q: position 期望位置
+ :param dq: velocity 期望速度
+ :param tau: torque 期望力矩
+ :param delay: delay time 延迟时间 单位秒
+ """
+ self.controlMIT(DM_Motor, kp, kd, q, dq, tau)
+ sleep(delay)
+
+ def control_Pos_Vel(self, Motor, P_desired: float, V_desired: float):
+ """
+ control the motor in position and velocity control mode 电机位置速度控制模式
+ :param Motor: Motor object 电机对象
+ :param P_desired: desired position 期望位置
+ :param V_desired: desired velocity 期望速度
+ :return: None
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("Control Pos_Vel Error : Motor ID not found")
+ return
+ motorid = 0x100 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ P_desired_uint8s = float_to_uint8s(P_desired)
+ V_desired_uint8s = float_to_uint8s(V_desired)
+ data_buf[0:4] = P_desired_uint8s
+ data_buf[4:8] = V_desired_uint8s
+ self.__send_data(motorid, data_buf)
+ # time.sleep(0.001)
+ self.recv() # receive the data from serial port
+
+ def control_Vel(self, Motor, Vel_desired):
+ """
+ control the motor in velocity control mode 电机速度控制模式
+ :param Motor: Motor object 电机对象
+ :param Vel_desired: desired velocity 期望速度
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("control_VEL ERROR : Motor ID not found")
+ return
+ motorid = 0x200 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ Vel_desired_uint8s = float_to_uint8s(Vel_desired)
+ data_buf[0:4] = Vel_desired_uint8s
+ self.__send_data(motorid, data_buf)
+ self.recv() # receive the data from serial port
+
+ def control_pos_force(self, Motor, Pos_des: float, Vel_des, i_des):
+ """
+ control the motor in EMIT control mode 电机力位混合模式
+ :param Pos_des: desired position rad 期望位置 单位为rad
+ :param Vel_des: desired velocity rad/s 期望速度 为放大100倍
+ :param i_des: desired current rang 0-10000 期望电流标幺值放大10000倍
+ 电流标幺值:实际电流值除以最大电流值,最大电流见上电打印
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("control_pos_vel ERROR : Motor ID not found")
+ return
+ motorid = 0x300 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ Pos_desired_uint8s = float_to_uint8s(Pos_des)
+ data_buf[0:4] = Pos_desired_uint8s
+ Vel_uint = np.uint16(Vel_des)
+ ides_uint = np.uint16(i_des)
+ data_buf[4] = Vel_uint & 0xff
+ data_buf[5] = Vel_uint >> 8
+ data_buf[6] = ides_uint & 0xff
+ data_buf[7] = ides_uint >> 8
+ self.__send_data(motorid, data_buf)
+ self.recv() # receive the data from serial port
+
+ def enable(self, Motor):
+ """
+ enable motor 使能电机
+ 最好在上电后几秒后再使能电机
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFC))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def enable_old(self, Motor ,ControlMode):
+ """
+ enable motor old firmware 使能电机旧版本固件,这个是为了旧版本电机固件的兼容性
+ 可恶的旧版本固件使能需要加上偏移量
+ 最好在上电后几秒后再使能电机
+ :param Motor: Motor object 电机对象
+ """
+ data_buf = np.array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc], np.uint8)
+ enable_id = ((int(ControlMode)-1) << 2) + Motor.SlaveID
+ self.__send_data(enable_id, data_buf)
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def disable(self, Motor):
+ """
+ disable motor 失能电机
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFD))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def set_zero_position(self, Motor):
+ """
+ set the zero position of the motor 设置电机0位
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFE))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def recv(self):
+ # 把上次没有解析完的剩下的也放进来
+ data_recv = b''.join([self.data_save, self.serial_.read_all()])
+ packets = self.__extract_packets(data_recv)
+ for packet in packets:
+ data = packet[7:15]
+ CANID = (packet[6] << 24) | (packet[5] << 16) | (packet[4] << 8) | packet[3]
+ CMD = packet[1]
+ self.__process_packet(data, CANID, CMD)
+
+ def recv_set_param_data(self):
+ data_recv = self.serial_.read_all()
+ packets = self.__extract_packets(data_recv)
+ for packet in packets:
+ data = packet[7:15]
+ CANID = (packet[6] << 24) | (packet[5] << 16) | (packet[4] << 8) | packet[3]
+ CMD = packet[1]
+ self.__process_set_param_packet(data, CANID, CMD)
+
+ def __process_packet(self, data, CANID, CMD):
+ if CMD == 0x11:
+ if CANID != 0x00:
+ if CANID in self.motors_map:
+ q_uint = np.uint16((np.uint16(data[1]) << 8) | data[2])
+ dq_uint = np.uint16((np.uint16(data[3]) << 4) | (data[4] >> 4))
+ tau_uint = np.uint16(((data[4] & 0xf) << 8) | data[5])
+ MotorType_recv = self.motors_map[CANID].MotorType
+ Q_MAX = self.Limit_Param[MotorType_recv][0]
+ DQ_MAX = self.Limit_Param[MotorType_recv][1]
+ TAU_MAX = self.Limit_Param[MotorType_recv][2]
+ recv_q = uint_to_float(q_uint, -Q_MAX, Q_MAX, 16)
+ recv_dq = uint_to_float(dq_uint, -DQ_MAX, DQ_MAX, 12)
+ recv_tau = uint_to_float(tau_uint, -TAU_MAX, TAU_MAX, 12)
+ self.motors_map[CANID].recv_data(recv_q, recv_dq, recv_tau)
+ else:
+ MasterID=data[0] & 0x0f
+ if MasterID in self.motors_map:
+ q_uint = np.uint16((np.uint16(data[1]) << 8) | data[2])
+ dq_uint = np.uint16((np.uint16(data[3]) << 4) | (data[4] >> 4))
+ tau_uint = np.uint16(((data[4] & 0xf) << 8) | data[5])
+ MotorType_recv = self.motors_map[MasterID].MotorType
+ Q_MAX = self.Limit_Param[MotorType_recv][0]
+ DQ_MAX = self.Limit_Param[MotorType_recv][1]
+ TAU_MAX = self.Limit_Param[MotorType_recv][2]
+ recv_q = uint_to_float(q_uint, -Q_MAX, Q_MAX, 16)
+ recv_dq = uint_to_float(dq_uint, -DQ_MAX, DQ_MAX, 12)
+ recv_tau = uint_to_float(tau_uint, -TAU_MAX, TAU_MAX, 12)
+ self.motors_map[MasterID].recv_data(recv_q, recv_dq, recv_tau)
+
+
+ def __process_set_param_packet(self, data, CANID, CMD):
+ if CMD == 0x11 and (data[2] == 0x33 or data[2] == 0x55):
+ masterid=CANID
+ slaveId = ((data[1] << 8) | data[0])
+ if CANID==0x00: #防止有人把MasterID设为0稳一手
+ masterid=slaveId
+
+ if masterid not in self.motors_map:
+ if slaveId not in self.motors_map:
+ return
+ else:
+ masterid=slaveId
+
+ RID = data[3]
+ # 读取参数得到的数据
+ if is_in_ranges(RID):
+ #uint32类型
+ num = uint8s_to_uint32(data[4], data[5], data[6], data[7])
+ self.motors_map[masterid].temp_param_dict[RID] = num
+
+ else:
+ #float类型
+ num = uint8s_to_float(data[4], data[5], data[6], data[7])
+ self.motors_map[masterid].temp_param_dict[RID] = num
+
+
+ def addMotor(self, Motor):
+ """
+ add motor to the motor control object 添加电机到电机控制对象
+ :param Motor: Motor object 电机对象
+ """
+ self.motors_map[Motor.SlaveID] = Motor
+ if Motor.MasterID != 0:
+ self.motors_map[Motor.MasterID] = Motor
+ return True
+
+ def __control_cmd(self, Motor, cmd: np.uint8):
+ data_buf = np.array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, cmd], np.uint8)
+ self.__send_data(Motor.SlaveID, data_buf)
+
+ def __send_data(self, motor_id, data):
+ """
+ send data to the motor 发送数据到电机
+ :param motor_id:
+ :param data:
+ :return:
+ """
+ self.send_data_frame[13] = motor_id & 0xff
+ self.send_data_frame[14] = (motor_id >> 8)& 0xff #id high 8 bits
+ self.send_data_frame[21:29] = data
+ self.serial_.write(bytes(self.send_data_frame.T))
+
+ def __read_RID_param(self, Motor, RID):
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0x33, np.uint8(RID), 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.__send_data(0x7FF, data_buf)
+
+ def __write_motor_param(self, Motor, RID, data):
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0x55, np.uint8(RID), 0x00, 0x00, 0x00, 0x00], np.uint8)
+ if not is_in_ranges(RID):
+ # data is float
+ data_buf[4:8] = float_to_uint8s(data)
+ else:
+ # data is int
+ data_buf[4:8] = data_to_uint8s(int(data))
+ self.__send_data(0x7FF, data_buf)
+
+ def switchControlMode(self, Motor, ControlMode):
+ """
+ switch the control mode of the motor 切换电机控制模式
+ :param Motor: Motor object 电机对象
+ :param ControlMode: Control_Type 电机控制模式 example:MIT:Control_Type.MIT MIT模式
+ """
+ max_retries = 20
+ retry_interval = 0.1 #retry times
+ RID = 10
+ self.__write_motor_param(Motor, RID, np.uint8(ControlMode))
+ for _ in range(max_retries):
+ sleep(retry_interval)
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map:
+ if RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ if abs(self.motors_map[Motor.SlaveID].temp_param_dict[RID] - ControlMode) < 0.1:
+ return True
+ else:
+ return False
+ return False
+
+ def save_motor_param(self, Motor):
+ """
+ save the all parameter to flash 保存所有电机参数
+ :param Motor: Motor object 电机对象
+ :return:
+ """
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.disable(Motor) # before save disable the motor
+ self.__send_data(0x7FF, data_buf)
+ sleep(0.001)
+
+ def change_limit_param(self, Motor_Type, PMAX, VMAX, TMAX):
+ """
+ change the PMAX VMAX TMAX of the motor 改变电机的PMAX VMAX TMAX
+ :param Motor_Type:
+ :param PMAX: 电机的PMAX
+ :param VMAX: 电机的VMAX
+ :param TMAX: 电机的TMAX
+ :return:
+ """
+ self.Limit_Param[Motor_Type][0] = PMAX
+ self.Limit_Param[Motor_Type][1] = VMAX
+ self.Limit_Param[Motor_Type][2] = TMAX
+
+ def refresh_motor_status(self,Motor):
+ """
+ get the motor status 获得电机状态
+ """
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8) & 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.__send_data(0x7FF, data_buf)
+ self.recv() # receive the data from serial port
+
+ def change_motor_param(self, Motor, RID, data):
+ """
+ change the RID of the motor 改变电机的参数
+ :param Motor: Motor object 电机对象
+ :param RID: DM_variable 电机参数
+ :param data: 电机参数的值
+ :return: True or False ,True means success, False means fail
+ """
+ max_retries = 20
+ retry_interval = 0.05 #retry times
+
+ self.__write_motor_param(Motor, RID, data)
+ for _ in range(max_retries):
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map and RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ if abs(self.motors_map[Motor.SlaveID].temp_param_dict[RID] - data) < 0.1:
+ return True
+ else:
+ return False
+ sleep(retry_interval)
+ return False
+
+ def read_motor_param(self, Motor, RID):
+ """
+ read only the RID of the motor 读取电机的内部信息例如 版本号等
+ :param Motor: Motor object 电机对象
+ :param RID: DM_variable 电机参数
+ :return: 电机参数的值
+ """
+ max_retries = 20
+ retry_interval = 0.05 #retry times
+ self.__read_RID_param(Motor, RID)
+ for _ in range(max_retries):
+ sleep(retry_interval)
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map:
+ if RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ return self.motors_map[Motor.SlaveID].temp_param_dict[RID]
+ return None
+
+ # -------------------------------------------------
+ # Extract packets from the serial data
+ def __extract_packets(self, data):
+ frames = []
+ header = 0xAA
+ tail = 0x55
+ frame_length = 16
+ i = 0
+ remainder_pos = 0
+
+ while i <= len(data) - frame_length:
+ if data[i] == header and data[i + frame_length - 1] == tail:
+ frame = data[i:i + frame_length]
+ frames.append(frame)
+ i += frame_length
+ remainder_pos = i
+ else:
+ i += 1
+ self.data_save = data[remainder_pos:]
+ return frames
+
+
+def LIMIT_MIN_MAX(x, min, max):
+ if x <= min:
+ x = min
+ elif x > max:
+ x = max
+
+
+def float_to_uint(x: float, x_min: float, x_max: float, bits):
+ LIMIT_MIN_MAX(x, x_min, x_max)
+ span = x_max - x_min
+ data_norm = (x - x_min) / span
+ return np.uint16(data_norm * ((1 << bits) - 1))
+
+
+def uint_to_float(x: np.uint16, min: float, max: float, bits):
+ span = max - min
+ data_norm = float(x) / ((1 << bits) - 1)
+ temp = data_norm * span + min
+ return np.float32(temp)
+
+
+def float_to_uint8s(value):
+ # Pack the float into 4 bytes
+ packed = pack('f', value)
+ # Unpack the bytes into four uint8 values
+ return unpack('4B', packed)
+
+
+def data_to_uint8s(value):
+ # Check if the value is within the range of uint32
+ if isinstance(value, int) and (0 <= value <= 0xFFFFFFFF):
+ # Pack the uint32 into 4 bytes
+ packed = pack('I', value)
+ else:
+ raise ValueError("Value must be an integer within the range of uint32")
+
+ # Unpack the bytes into four uint8 values
+ return unpack('4B', packed)
+
+
+def is_in_ranges(number):
+ """
+ check if the number is in the range of uint32
+ :param number:
+ :return:
+ """
+ if (7 <= number <= 10) or (13 <= number <= 16) or (35 <= number <= 36):
+ return True
+ return False
+
+
+def uint8s_to_uint32(byte1, byte2, byte3, byte4):
+ # Pack the four uint8 values into a single uint32 value in little-endian order
+ packed = pack('<4B', byte1, byte2, byte3, byte4)
+ # Unpack the packed bytes into a uint32 value
+ return unpack('iZ+GA6dGZhp2%$zuNR1^JAtcKzmOu~?XkgF*2^+-Vn;7*4~in0Bt~`;>j8RXw{~k&TiI;Nf8KvKwM!3lZIx5mO{%uM
z-gtk%?>_o51ClmQD&5uJdE9f(J?A^$>wM>&&UJUE41AJzeDl!J-!P2-&P4OaMq&$o
z<(~s6Lz$z-nCY4(Od-`QmXUohTB=_;#YN@iK%YY^L5+pF2qK9B(E2FeQE*ndeuU;2&MbfVs#zD
z1!{>}ig2OoSIZDCQp?r#2p6jr>IQ_@sg>$RgiG+wv|5FCt}4VoVPx-=tO}y;{=!
zNZ+h(LHZU+FH_cDBeSNCF&i1viS4?WM^vL=%&xiCq3LVm8A(%Qk-^
z%pU9|Ka5-WZNsnpMSu#X>6BSDvSt*sqF5}7*-Dd$C=)
za;2bIE{vrwy>k)$K%Lsf3s=sczx2*?S6)AP<@^Vi-nlRkRo5m4wPPsL3
zJwBRcz8BlIbD!HYH2!#yyK7t(yxvM-Y$7Y@$*N+$lCeFzTo@hJ-RPO_Bko~O71kd*wtn{PH|yVfb-kJ#A1SWCTa6E|AKaA%qJm)E#BtBbW{ag_C7bO)
z#qtsWBbhS8Z^HDyjycsM{a+Ur&%`|Qh-YEgF%0Cv(Wzd)lhN}$r!V|4
z&Y$oDD^-mV^dnC%D3LGo&H^)A}_uF
z+~xPrM-?~eLS<5yHeI9ky!A4sVBwtUkG*ZMo#@Z*pew7;&oWush)*r>huZY?A2Zma
zh0*bRv2y(CgRP(VLG8lp^AC3Z+WuhM>S3@l+siSC_Ic${_Xi!hZQh}VPe0~RkD`&N
z$KkwU7LQC8u0GJ(Gp}6!!MUhf%dE~<*B@qEEew{5UE>frkGT3jSQMiRU
zDD3COMH>xdaE5fvTKy5>~N)4**2yaq5AoCyexYV-7
z8?~q2n&0*;7a+ezv#M~kn1}u8C&NZAz4OPl)4$hNaca?CsRL6%SJK6DdVHc#8p?FQ
zIP}}{Y+7tyN=%1xb}TpH#Sa{>6v|!)t(R;WwyT>gmb0wr#V2&JRPo~Z(eZM@vzZqN
zJ-551b4|iWT4^6Q+eqt~U
zVwS5Wbd51$9yN7;1#1@04`-tit6EA{ET`=0BvLV?5-N_AgF5zf2WrKu9m^oZmKoEj
zD3wGC3yI*FgH#u4+tpO3QB7eTXis+|9j|sG-6`$$Af2doA>AeE-b!L73E>ElS4oUG
zXRYbJYPagV-%wp!jB1~XZ8nhh<4MGORQLVNcO>4k#ZcY%8P%lJK&3HVP^TMl
zC#aKmrA{B7^#*koAa2Zb2)6ppTBO~XR7AN8C$|EX5#_?>PlpssgFn4vvXmFQ
z^U4Pw)LwtdSBfa*)9&mK&(_{MU3>Cr(oF?sMUEFT!$w%r4LWE
zH*{ESN~aI2_Ut3bjjZDI?3o|To_VF+2KLu+ibW%H!wBZ<7TD+ia=G
z0vj4H6{0>(-IDj$PDJ4LJ$oMAlMcs?HC<`Ek{%wPEU6(8rp3Pl%Ij3ibrQ5lt2zy;
zI!%jnPDA2;L`+2NuUL_*>v}JMzLhcR{yLc_)9EkDC`0vU0K9HlIc6*4*-7xXtlPZK
zM~Y)ub&1j9StZDYoYuMHUYtel_4@sZ4Co@8Ef-2k7CfHKwM}M$Zwoo$jBCnM{aJ$6
zrIVXHJRz?|L^ZuX{`u^#t-s+V2K_L(Z|fKQj3ZLWOR6B^V1E28M?}_v$>BEfJ+213lhjSd+<#N>@oY~yAZGo-xU$;F{hTcNU279nZ#8`=EYTEG@MzIh{ZlLzsmAiYxf_
zp0J=Nl8D0xDsf{bNiq#IM#I)|CRa8SX)xXc>{wd;*=CE2PD)^79FK^o4IEc^uN?OMZM
zGp|ml(+{K{z&T6HSI1v1P~H16@jK4)c+d5
zOODZPR8)FDQf^;_C6TH3mfgN+{6S8;bnOkvI~doqXvg={IlhQZnG40(qfqU#rdG^L
z@wu_9$dI^m2st_VAzl+vT1cJrwh5Zj
zAmnKB!>3KO`|GD#5K74SAD8~7)`T;%fBPfXvOJ1(*|byHmw*6hHSuzC0^H7~F6b3QUu!Pz6A*b{F$i>;W->yCXly6~2L>h?008b4fnF!s(I;YYt
z#&DqOu#bR8?d=59+Gyu!=y^rq_2sZ7pVF~Mabfm5AJkrZB-rdCu{FssGWE(YP7Z4wo-rd-pjS~-><*)grARk
z^_O0|bn*9R&zwM)qD*)Rn{%$baq`kT&+zHfzf*gzu}GO(8`3LKm&LHJ%{!1(?Yu)R
zEMi*U&)5!v2LUpvYfEkYTPTEmIhfXW-qR$pQw=uM2T;UK2D9VF1+&fU7|cJ7g-~=m
zEX_vUjw0qkqV3XqLGzf0u|Ze(F(%
zSnxVPHB#Ows~+z@I#&fP+C*t&7@5*Q0MvX`_Y-5}ABX{=uB&5Y0eX~WZ?d6VrPwm7K|vQX6z~?{j^oJ28}Q6nWhUCF4-q6xKts2lnvKw9AXJ>S;Ug}(a{-G
zaJZ_DAf8%=yy-Y{J5eVQ2rl|eVy1EAX71Q$x`zH2y5K8ay`8Zg0QG*xL?v%!Y>;3r
z!L0=3%S^(vN5+dKeJ^u10(g!tBK^S_}=^b;EK}|Q|N8pCXGcPVjM7-FxUF{o)A_o2zz4ATi))FXV2TXiB
z+1TG`55&SczToOJG=VlSGf)m^4Ve{d&a6O1M6&{CxUywd;)o@JnSfJVIYAsQ^I%pm
zKQb$!CyTSvFSF7)Bvw#kLUfr}zV?LJG7Fl#q>lo4w_XLyq)&111o+m+Sfx!2ERqu!
zjiSW&|3Nt)pJLiX`1DWqG~*NQ&F~$p*y4eqhc#k={|QwI0yPg?&zt(^*mJNB$Mi3F
zutfI4|2GXD{;DST8P1x5TvGwL4}D-P5TGZ2$**7LH0`PoooQ?1muSp*oT7
zLOK!bc`)4_zGWs+?W}fHyUBv3ze7LpCgp4~rhBSAEOXQ#9-2n=PdVzIf0j|>EzP42
z#7UH>2y4{V9F(YyKN^8|bNI3cWX#7#=5j<^tXIIRRXx}4K$
z;~cjyn{Ab6Hbz-yuMH7GDKl-}Y_5bbqANXgqA#(yQkd__)D10~x4rci-g5Agh_`6L
z_`Ie6GN82BVXK@D;`U(+`4*Ik@DqgTVC9qwR+E+SCU~c+X;3g0`#^ZH^v4!P@{LTqoje
zZ_~GG9dyDqBaAHW8wBe^p;v227Cf1F+v>$%vv3IE8A)=k{szH*f>w>}c4xBznzQKS
zgB%k(XF!y+?TWM%Y#9wHfR$u4Y5PYWbrMmm$+rHQD7o11AWery9bQAzFex9cZPVw-#Rx%0Jw1He!w?)64{uGwLpR`ada_~W{W6Lg
zu>a4RFbwF0$23U{D+L@XmoG@r9D!2;_C8h)3g!qM6heRrp8$OW{si76OjQ3BRw5uu
z;i1nl_hAC@yl^$$?9%qdnUl|zFfrLtFN__Umr$VqWhH=PeZHOfScD>QUynIp&QWQt
z*?s0Twv8v39Ow$tR|bWDjEo*43z@Ontlp~k_!(_ual%=_yf
z-`z!h;M3q~#-1mbPdY5hW`&S(0FwM8_WhTLck%%Z58~ai7RIoL+G?-QQ?Z3mIRX5M
z%yLl&op3{ysnJBf3yw}~}@
zO9&&d+(#VdRA^;KIaOHNRe0a%gF9^KuYjEGy*w=jFWZsZ2;pn{Z|G1Y1IpOb&mR(r
z6KW*cYie;G)Sh~B_PJM^b<+I%1EM-Uj!p^$SFrx!i67iuXu1Uu9G`98noSpu<;EsP
z3!AWev8lQ2I&Np+*VZNfh~6;9DyQ)F?ZC0ubldW7&h$52sa{;{9N(i#N!{dK??cjy
z74p=y
zk+5Ob(6FI1jjP9n&|L`&EAlo6icDiu6D^huJ-3nIR7gYu0k&t8K%j`^+7VvdKyX5U
z%9*xykx8GfI9tS-j-7meqZ8g;*tC&``zGI;4n92RCv*mPM_QOo4nI@h4Ugu^htiim
z{9f%xFU~%FV)jgUKc@8*^e1&yg=eOsF8FTLwdj?;g+Rm)CC|ZMp8MOM?K-gg{vk~1
zeByna#VKioAkH;8OZ7E{jMv9uH%l1sq<=Qr!0GHBbFFV(wJuoN>$Gf4ax!E1iEvqm
zh;MN4_`9NEDyD{{Xr9PIL{c~eNMVqPPyP>7ig1!YM0;K)W;aelrS}u;BWMu;v)_5Q
zcKStM1i(?=S~O~`d4}XVPm}fGACO_iO#yoc?X=fs51NWF*|MHQ|?s+y}>Dg?xomUt7=*o`D$u$|$vz$;$0HcFz)2SP!
zy?_+WLa+ZeDFdr&Ck3w
zd*WnNzQH{8zd$<;<{5(BGat_gy9}}DO9ZV3p|@BUhIOHQDBunM{Vx)ilvIZ8GY1wP
zt6K0mU#&UD4_QRm;{+nmy*6|&jXdgI9D@^$Yf{Vl8W&HjK+}?WZ0J=jOht1E-lUN@
zbFi0Be^!t~|5r68nl=VaJKC&iVG6}51&^!@G;TMB72&ao;b87vlgrw#kN7Mk`1k}`
z7H|1DcT@bvWy#QTdxbk@C*F&?9}3mS1PB=lX?24Zx0^%
znZvo_Xw--(=U{*H-2Jkf00r&n-^P6D-yxVs8S70e7rg^lro4`!1jVF2M@~Z3Gvx
zuF$)0?Y&BJTY|EN11)Ryh?ZR`8a4sBJU=Tp)o(HSC<3B|;s_F<(H9b)Y=7eJpOD={
zI1*8Zhcs1ouw=8ZT_17rP4S@zUsV
z5$kiP%)i1E$?}@qce?c9+1ZnCUi#qK+Us9S2OF!W-@g2#r=wPzh5N6c`~s-Zb;?ka8BICq|O?86AU$VRk$bh1mo5CQkePu
z2J+~p^z$TFc3PaOHDgvCx*u5f)7(c%Oec@@bG^nCVmK4543_3qlqxTIV;wApk}TG?
zqIbU3r!K|a*Vrg7Y$S3MxTB$DQws|?v4`mZa1-J&D0_)RpvHps>^fAZ=#y
zoMwhHt96>=T19XZ!RH7bCU}J4SpwP%O&8+i!0vA{@5=-^0-DvDY_G=%#9%zam}o$u
z!N-~8{-u6`;1Gc*$=_p4WZ#R7y-e_Zf*FEW30@;OO3*-!#1=7cHNXS-l?MTM8
z|6tqw)=F#PFYScWy<~NNPv3P;Ur)c2a=M)kd{g+{=qz_qeYVr(B=EZqPr8x6#91sY
zwurYFS|&S*UM1qIAV&~lPt}|@N?k}$%?z`3xq3Gqtm?$N>Ov%-Mh(|4BpvQn%77TAdB*
zAHd-x&Xu+S&RJd@Yvaj+=kHUupDz)|&)>5PENxgROLDIM`Yk9$gyBk^rPr|=Hu?ra
zT?v|hDUFnJ{DYi5N$K)x|PB~+$HA42prqx8D4Q4(5mA`2zEu@MHBF(ri$fa2}W)yU+~>l
z#DJHQ7Uht(U_)O-Zu=yB8+pHsCc(PN>av`eW^M}>*x_grD7u=)zL;Y#50>bO!?**C
ztW|cD3-dV0-J%k}(?fzMo33i*VDdVq9uq?#f2fc@oGuQ_Sg~1i0HepHzxf9*H3GB2
zFYB)dLmN__`}_78bXY%0uoa-bn&4)DeLDml{7VtPg?8+mL?H(R7#w9Agz(eMYhf)S
zn<8Qxexfye0`->zr?6osWNM!RRqzsd#vb}bZ1ctkrmW37eT{bk70=;sTzGNuPCwY-
z2Y30wM*TjDw0G`n$XmqD`NZ32Y3BSRyQcp=+CbNa8(q`?VV;+tLf-P0mouk@X7IFO
zeWa1_|AM7z3_{?5c|k3(8n9_0P8}aJ^ER*-b;TY!2-f>j7mj`4A6;Itzil0%%fWPG
z0~`FcG;W+&>joe_fyg#&d8y5##d0M-J~naRP3W1{XmsiZxe%H?R5-S7Gya|di;epb
z3CsP$OydI0XHZaA01y}3BZcC)iw|xsx4)z#YHwA98yE`uKcRYyWk%dk6A8_b6sJ<4
z7vSYF;K7EBIgSe{{Qjmvg*PBj)E*oRe}?iHYMh46F%M_tCdaaQc)8|uX91SsS$TYb
zo;sKf14DO_Ifba5XP*VVf*J3U@^c!f^9APpN&^94<(3tIPuWp|`b16Dk&b@~8?
zaC{)moK*xj0pNPwu8nuyIpC)JFuU>I{jTI~4DvP(gn9Q24BW$K_cp@MxVYbkL)>=`
zJmMyU7$XnebvN2%*z1hjXQ`{+9b4&=KE@vx;mEs3{U_6
zv5Y?i=)s=>^x}^I`tT5ib5O#-;!3+@)#>F8MFT1^<3r?q7zB{mXHw
z|9V{LUxCZ~H{c@wN;j=;#0CB|F7K~$SF4-co7HOf7Im|`M&07xs@Ax-saxHQy3Jjy
zGVZ6;TK9JKDfbR_ySq-^;jUNfFw^V3&ZhsU_(b!p{|gY{-=+U6#_l5!dHob)+X=Q3
zd?Zg;z))tOjTl|$3;VO8b6w}*ePQg;Mr>P<7u&jRn;RS4zRgX-
zD$VZQJ?JKO?cJB%iE#VgZ48t9cJA8#=oj|69ozQpc_h2*(ZTJmJ%~LD7ypA*(3N7&
zO&oe0XZ*FBDEpzccgT$m?cVKLd&;i$$X?gpF*>Icj5<)HYUlKDrz@2ksWjGW8;&Rd70{G=;mfJm5a#Lm})*o
z`($~V@3}f#uE5i)vn5DAp?gq7jQTcdMpu8D&mvK!dy#aLyC2<~#jPecMv&Skx3aUi
z`Y|WSLCtwhi@)4X8h)=<_hzRtoLxiKU9ueM^sZs?KE&z4_r9@In#0HcqBV`IV;4v+qP@HzhW5@XQ>Iy;0{y%Sm>z$@N}dC4@75a7*c<&R|8U%%%-UPaxkvMR_p^n$&HbH^
z?5&mD<5g#FDZhH>$;hZqYm?8wVNXps>!0MdvxQp^_jhI>!`Yb4uVwbu=G+>W0@Ng*
zh4e_m=ud>TzP6C$_L1H_2NQ(egW9Kh{2x}IZ5(Hn3_^z<$-X%w^-A`_WP=N>zbh@;>Bd+ecgman&VT{E4r#0
zmQN2WS~zaBp0V(dg@-LJ7J}Uly90J7>@JzR6p^{h5!oM44yt9R_$3SXSh!dAGDc=G(%B}9?#tZ=Pot=}Q{D2qe%Y1n_mSQR5C2d4
zDh8v8K{Xg13ObpY!ouyKl1yCHf?Z0YKNtzeBFR|TRD;^UbJvC~L5p8kbwhpC19ip)
zI8d1fdw2<#V}Yx}0uJykxP=qI5-_zzUchcNK%?OG;~EZhjst%S_F_LTVm~Khk<*(2
iua(p3UlY?A$74qm1T5kC!Gw~GsV~rP$)I1P!2b=Q6nZ}Z
literal 0
HcmV?d00001
diff --git a/calculate/__pycache__/trajectory.cpython-39.pyc b/calculate/__pycache__/trajectory.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2d3166a8e88b69df23623b36f15bef5a2be70e12
GIT binary patch
literal 2209
zcmaJ?-D@0G6u?*0AFIa3@T
z&oEqfjl+Q!dF#
zIR(v<=VVsSK(pnnoV&(WQ;R)yC#pz2;VZFSLu*a>c>iT&uXAAKEQF?=zLftN(chqOe}ubPDUu!tw4_^zB|j
zHfkGg;aOR`TqqSQrADRbI|W}AH=L4RQ>~}#t#GR3s#3*S8(0Y)FAc#&md|3SS?K#h
z5pnuJfVtVzBl`G*5xxX(58)Ho8UauTni}bO)<;;tMn0Mu$;3Cn*@$tz|Mf5Tf4w)1
zvqHA9yig;wH_AZ(YI%N9`Jv%drJ95(Auduhaaac3pORQ}LmgMg;wJe1+0xQd;soA!r)lOC4gx7}mZJBDrLJ^V^=Sftm
z`T{IzM7$?c+H3c`^9)ZIc*ndHq-k$@$eRkp4iD^I&XH5lQlwp(4n$yRU`HB+PVV^j
zcgJ5I4vMxMpYx9uMU6wzNuz%5C(b`tqAl2yRd*#XLQA1Q9h7Okj%>@PL(_Am6SL4K
zAmpw(j|W5E40-Ex0#12X8bvkC#v)?9MVqp(u4=&pAFC1DIHSw>3k@wNr#gl6uGw(SG8KrEsDvg|5vr)VX`BAZ1F&0sZ+
z^h_EO2X7GTx$Z}|IzMh79bS*y!{5Jcb$|c7^TGE=2RDumZgoEX
zs=Ir$^U2ShPk+kk?d8R4y43Le+U8obul+Y2laRr`fqthR?Su*SJY?R`u*bg!Dij#z
zFsXSGl)MQs>R~eENa!%3r&u@CQ)KTK8%oE*f3Mz(J30M*bmLVQNTc&ALP&352y6|z
b?&Im@&6;dfoEPbp-WWt$WKw#}WXAsmgOb}{
literal 0
HcmV?d00001
diff --git a/calculate/angle_A.txt b/calculate/angle_A.txt
new file mode 100644
index 0000000..6c53205
--- /dev/null
+++ b/calculate/angle_A.txt
@@ -0,0 +1 @@
+2.25,2.24,2.23,2.22,2.21,2.20,2.19,2.19,2.18,2.17,2.16,2.15,2.14,2.12,2.11,2.10,2.09,2.08,2.07,2.05
\ No newline at end of file
diff --git a/calculate/angle_B.txt b/calculate/angle_B.txt
new file mode 100644
index 0000000..84d3363
--- /dev/null
+++ b/calculate/angle_B.txt
@@ -0,0 +1 @@
+0.90,0.90,0.91,0.92,0.93,0.94,0.95,0.96,0.97,0.98,0.99,1.00,1.01,1.02,1.03,1.04,1.05,1.06,1.08,1.09
\ No newline at end of file
diff --git a/calculate/calculate_angle.py b/calculate/calculate_angle.py
new file mode 100644
index 0000000..48acfb2
--- /dev/null
+++ b/calculate/calculate_angle.py
@@ -0,0 +1,179 @@
+# qt_main.py
+
+import numpy as np
+import matplotlib.pyplot as plt
+from ik import inverseF # 假设这是你自己的逆运动学函数
+from matplotlib.animation import FuncAnimation
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 杆长参数
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+
+# 1. 轨迹生成函数:每个轨迹类型独立封装,支持外部传参
+# --------------------------------------------------
+
+def generate_circle(center=(100, 300), radius=40):
+ from trajectory import circle_trajectory
+ return circle_trajectory(center=center, radius=radius)
+
+def generate_line(start=(125, 300), end=(125, 400)):
+ from trajectory import line_trajectory
+ return line_trajectory(start=start, end=end)
+
+def generate_ellipse(center=(100, 200), rx=50, ry=25):
+ from trajectory import ellipse_trajectory
+ return ellipse_trajectory(center=center, rx=rx, ry=ry)
+
+def generate_square(side=60, start_point=(100, 200)):
+ from trajectory import square_trajectory
+ return square_trajectory(side=side, start_point=start_point)
+
+def generate_triangle(base_length=100, height=80, base_center=(100, 200)):
+ from trajectory import triangle_trajectory
+ return triangle_trajectory(base_length=base_length, height=height, base_center=base_center)
+
+
+# 4. 主函数:根据轨迹类型调用对应函数并执行
+# --------------------------------------------------
+
+
+def main_of_5dof(trajectory_type='line', show_animation=True, save_angle_a='angle_A.txt',
+ save_angle_b='angle_B.txt', **kwargs):
+ """
+ 主函数:生成轨迹、计算逆解,并将 theta1 和 theta4 分别保存为两个 txt 文件,逗号分隔
+ 参数:
+ - trajectory_type: 轨迹类型 ('circle', 'line', 'ellipse', 'square', 'triangle')
+ - show_animation: 是否显示动画
+ - save_angle_a: 保存 theta1(A角)的文件名,设为 None 不保存
+ - save_angle_b: 保存 theta4(B角)的文件名,设为 None 不保存
+ - **kwargs: 传递给轨迹生成函数的参数
+ """
+ # 生成轨迹
+ if trajectory_type == 'circle':
+ x_list, y_list = generate_circle(
+ center=kwargs.get('center', (100, 300)),
+ radius=kwargs.get('radius', 40)
+ )
+ elif trajectory_type == 'line':
+ x_list, y_list = generate_line(
+ start=kwargs.get('start', (125, 300)),
+ end=kwargs.get('end', (125, 400))
+ )
+ elif trajectory_type == 'ellipse':
+ x_list, y_list = generate_ellipse(
+ center=kwargs.get('center', (100, 200)),
+ rx=kwargs.get('rx', 50),
+ ry=kwargs.get('ry', 25)
+ )
+ elif trajectory_type == 'square':
+ x_list, y_list = generate_square(
+ side=kwargs.get('side', 60),
+ start_point=kwargs.get('start_point', (100, 200))
+ )
+ elif trajectory_type == 'triangle':
+ x_list, y_list = generate_triangle(
+ base_length=kwargs.get('base_length', 100),
+ height=kwargs.get('height', 80),
+ base_center=kwargs.get('base_center', (100, 200))
+ )
+ else:
+ raise ValueError(f"不支持的轨迹类型: {trajectory_type}")
+
+ # 存储角度值的列表
+ angle_A_list = [] # theta1 (弧度或角度)
+ angle_B_list = [] # theta4 (弧度或角度)
+
+ # 计算每个点的逆运动学并存储角度(以角度制保存)
+ for i in range(len(x_list)):
+ x = x_list[i]
+ y = y_list[i]
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ angle_A_list.append(theta1)
+ angle_B_list.append(theta4)
+ print(f"第 {i} 个点: A角 = {theta1:.2f}°, B角 = {theta4:.2f}°")
+ except Exception as e:
+ print(f"第 {i} 点逆解失败: {e}")
+ # 可选择插入 NaN 或上一个有效值
+ angle_A_list.append(np.nan)
+ angle_B_list.append(np.nan)
+
+ # ==================== 保存为两个独立的 txt 文件 ====================
+ if save_angle_a:
+ with open(save_angle_a, 'w') as f:
+ # 将所有 A 角度转为字符串,保留2位小数,用逗号连接
+ formatted = ",".join([f"{angle:.2f}" for angle in angle_A_list])
+ f.write(formatted)
+ print(f"\n✅ A角度(theta1)已保存至: {save_angle_a}")
+
+ if save_angle_b:
+ with open(save_angle_b, 'w') as f:
+ formatted = ",".join([f"{angle:.2f}" for angle in angle_B_list])
+ f.write(formatted)
+ print(f"✅ B角度(theta4)已保存至: {save_angle_b}")
+
+ # ==================== 可选:显示动画 ====================
+ if show_animation:
+ fig, ax = plt.subplots()
+ ax.set_xlim(-300, 500)
+ ax.set_ylim(0, 500)
+ ax.set_aspect('equal')
+ ax.grid(True)
+ ax.set_title("五连杆末端沿轨迹运动")
+ ax.plot(x_list, y_list, 'b--', label='理想轨迹')
+ line, = ax.plot([], [], 'r-o', linewidth=2, markersize=6, label='五连杆结构')
+
+ def draw_frame(i):
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ except Exception as e:
+ print(f"第 {i} 帧: 计算失败 -> {e}")
+ theta1 = theta4 = None
+
+ if theta1 is None or theta4 is None:
+ line.set_data([], [])
+ return line,
+
+ # 计算连杆坐标
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ x4 = L4 * np.cos(theta4) + L0
+ y4 = L4 * np.sin(theta4)
+
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+
+ line.set_data(x_coords, y_coords)
+ return line,
+
+ ani = FuncAnimation(fig, draw_frame, frames=len(x_list), interval=50, blit=True)
+ plt.legend()
+ plt.show()
+
+# 📌 运行主函数
+if __name__ == "__main__":
+ main_of_5dof(trajectory_type='line',start=(125, 300), end=(125, 400), show_animation=False)
+ #main_of_5dof(
+ # trajectory_type='circle',
+ # center=(150, 250),
+ # radius=60,
+ # show_animation=False # 设置为 False 则不显示动画
+ #)
+
+ # 示例:其他轨迹使用方式
+ # main_of_5dof(trajectory_type='line', start=(0, 0), end=(200, 300), show_animation=False)
+ # main_of_5dof(trajectory_type='ellipse', center=(100, 200), rx=80, ry=40)
+ # main_of_5dof(trajectory_type='square', side=100, start_point=(100, 200))
+ # main_of_5dof(trajectory_type='triangle', base_length=120, height=100, base_center=(100, 200))
\ No newline at end of file
diff --git a/calculate/fk.py b/calculate/fk.py
new file mode 100644
index 0000000..07200bf
--- /dev/null
+++ b/calculate/fk.py
@@ -0,0 +1,40 @@
+import numpy as np
+
+
+def forwardF(u1, u4, omega1, omega4, l1, l2, l3, l4, l5, alpha1, alpha4):
+ # 位置分析
+ xb = l1 * np.cos(u1)
+ yb = l1 * np.sin(u1)
+ xd = l5 + l4 * np.cos(u4)
+ yd = l4 * np.sin(u4)
+ lbd = np.sqrt((xd - xb) ** 2 + (yd - yb) ** 2)
+ A0 = 2 * l2 * (xd - xb)
+ B0 = 2 * l2 * (yd - yb)
+ C0 = l2 ** 2 + lbd ** 2 - l3 ** 2
+ u2 = 2 * np.arctan((B0 + np.sqrt(A0 ** 2 + B0 ** 2 - C0 ** 2)) / (A0 + C0))
+ xc = xb + l2 * np.cos(u2)
+ yc = yb + l2 * np.sin(u2)
+ u3 = np.arctan2((yc - yd), (xc - xd)) + np.pi
+
+ # 速度分析
+ A = np.array([[l2 * np.sin(u2), -l3 * np.sin(u3)],
+ [l2 * np.cos(u2), -l3 * np.cos(u3)]])
+ B = np.array([-l1 * omega1 * np.sin(u1) + l4 * omega4 * np.sin(u4),
+ -l1 * omega1 * np.cos(u1) + l4 * omega4 * np.cos(u4)])
+ omega = np.linalg.solve(A, B)
+ omega2, omega3 = omega[0], omega[1]
+
+ # 加速度分析
+ Aa = np.array([[l2 * np.sin(u2), -l3 * np.sin(u3)],
+ [l2 * np.cos(u2), -l3 * np.cos(u3)]])
+ Ba = np.array([l3 * np.cos(u3) * omega3 ** 2 - l2 * np.cos(u2) * omega2 ** 2 + l4 * np.cos(
+ u4) * omega4 ** 2 + l4 * np.sin(u4) * alpha4 - l1 * np.cos(u1) * omega1 ** 2 - l1 * np.sin(u1) * alpha1,
+ -l3 * np.sin(u3) * omega3 ** 2 + l2 * np.sin(u2) * omega2 ** 2 - l4 * np.sin(
+ u4) * omega4 ** 2 + l4 * np.cos(u4) * alpha4 + l1 * np.sin(u1) * omega1 ** 2 - l1 * np.cos(
+ u1) * alpha1])
+ alpha = np.linalg.solve(Aa, Ba)
+
+ return xc, yc, u2, u3, omega2, omega3, alpha[0], alpha[1]
+
+# 示例调用
+# xc, yc, u2, u3, omega2, omega3, alpha2, alpha3 = five(np.pi/6, np.pi/4, 1, 2, 1, 2, 3, 4, 5, 0.1, 0.2)
\ No newline at end of file
diff --git a/calculate/ik.py b/calculate/ik.py
new file mode 100644
index 0000000..6046976
--- /dev/null
+++ b/calculate/ik.py
@@ -0,0 +1,69 @@
+import math
+
+
+def inverseF(x, y, l1, l2, l3, l4, l5):
+ """
+ 五连杆机构逆向运动学函数(Python 实现)
+
+ 输入:
+ x, y: 末端执行器坐标
+ l1~l5: 各杆长度
+
+ 输出:
+ theta1, theta2: 两个主动关节角度(弧度)
+ """
+ Xc = x
+ Yc = y
+
+ # ===== 左侧链路(l1, l2)=====
+ numerator_left = Xc ** 2 + Yc ** 2 - l1 ** 2 - l2 ** 2
+ denominator_left = 2 * l1 * l2
+ cosfoai_12 = numerator_left / denominator_left
+
+ if cosfoai_12 > 1 or cosfoai_12 < -1:
+ raise ValueError("目标点超出工作空间!左侧无解。")
+ foai_12 = 2 * math.pi - math.acos(cosfoai_12)
+
+ # 求第一个电机角度 foai_01
+ numerator_foai01 = l2 * Yc * math.sin(foai_12) + Xc * (l2 * math.cos(foai_12) + l1)
+ denominator_foai01 = (l2 * math.cos(foai_12) + l1) ** 2 + (l2 * math.sin(foai_12)) ** 2
+
+ if denominator_foai01 == 0:
+ raise ZeroDivisionError("分母为零,无法计算左侧角度。")
+
+ cosfoai_01 = numerator_foai01 / denominator_foai01
+ if cosfoai_01 > 1 or cosfoai_01 < -1:
+ raise ValueError("cosfoai_01 超出 [-1, 1],左侧无解。")
+ foai_01 = math.acos(cosfoai_01)
+
+ # ===== 右侧链路(l3, l4)=====
+ Xc_shifted = Xc - l5
+ numerator_right = Xc_shifted ** 2 + Yc ** 2 - l3 ** 2 - l4 ** 2
+ denominator_right = 2 * l3 * l4
+ cosfoai_34 = numerator_right / denominator_right
+
+ if cosfoai_34 > 1 or cosfoai_34 < -1:
+ raise ValueError("目标点超出工作空间!右侧无解。")
+ foai_34 = 2 * math.pi - math.acos(cosfoai_34)
+
+ A = l5 - Xc
+ B = l3 * math.sin(foai_34)
+ C = l4 + l3 * math.cos(foai_34)
+
+ if B == 0 and C == 0:
+ raise ZeroDivisionError("B 和 C 均为零,无法计算右侧角度。")
+
+ try:
+ foai_t = math.acos(B / math.sqrt(B ** 2 + C ** 2))
+ foai_40 = foai_t - math.asin(A / math.sqrt(B ** 2 + C ** 2))
+ except:
+ raise ValueError("右侧三角函数计算失败,请检查输入是否合法。")
+
+ # 转换为角度再转回弧度
+ theta1_deg = math.degrees(foai_01)
+ theta2_deg = 180 - math.degrees(foai_40)
+
+ theta1 = math.radians(theta1_deg)
+ theta2 = math.radians(theta2_deg)
+
+ return theta1, theta2
\ No newline at end of file
diff --git a/calculate/test_ik.py b/calculate/test_ik.py
new file mode 100644
index 0000000..5e3ee00
--- /dev/null
+++ b/calculate/test_ik.py
@@ -0,0 +1,64 @@
+import math
+from ik import inverseF
+from fk import forwardF
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 输入数据
+l1 = 250
+l2 = 300
+l3 = 300
+l4 = 250
+l5 = 250
+x = 125
+#y = 382.338
+y = 300
+omega1 = omega4 = 500
+alpha1 = alpha4 = 0
+
+# 逆解
+u1, u4 = inverseF(x, y, l1, l2, l3, l4, l5)
+
+# 正解验证
+xc, yc, u2, u3, omega, alpha, _, _ = forwardF(u1, u4, omega1, omega4, l1, l2, l3, l4, l5, alpha1, alpha4)
+
+# 左侧链路
+x1, y1 = 0, 0
+x2 = l1 * math.cos(u1)
+y2 = l1 * math.sin(u1)
+
+# 右侧链路
+x5, y5 = l5, 0
+x4 = l4 * math.cos(u4)+l5 # 注意方向
+y4 = l4 * math.sin(u4)
+
+# 绘图
+plt.figure(figsize=(8, 8))
+
+# 左侧链路
+plt.plot([x1, x2, xc], [y1, y2, yc], 'b-o', label='左侧链路')
+
+# 右侧链路
+plt.plot([x5, x4, xc], [y5, y4, yc], 'r-o', label='右侧链路')
+
+# 标记关键点
+plt.plot(x1, y1, 'ro') # O1
+plt.plot(x5, y5, 'go') # O2
+plt.plot(x2, y2, 'yo') # B
+plt.plot(x4, y4, 'mo') # D
+plt.plot(xc, yc, 'ko', markersize=10) # C(末端)
+
+# 设置图形
+plt.grid(True)
+plt.axis('equal')
+plt.xlim([-200, l5 + 200])
+plt.ylim([-200, 600])
+plt.title('SCARA 五连杆逆解结构图')
+plt.xlabel('X (mm)')
+plt.ylabel('Y (mm)')
+plt.legend()
+plt.show()
\ No newline at end of file
diff --git a/calculate/traj_fk.py b/calculate/traj_fk.py
new file mode 100644
index 0000000..d12693f
--- /dev/null
+++ b/calculate/traj_fk.py
@@ -0,0 +1,62 @@
+import numpy as np
+
+from fk import forwardF
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 杆长参数
+l1 = 50
+l2 = 50
+l3 = 50
+l4 = 50
+l5 = 50
+
+# 初始角度(弧度)
+u1_base = np.deg2rad(120) # 左侧电机初始角
+u4_base = np.deg2rad(120) # 右侧电机初始角
+
+# 设置绘图区域
+plt.figure(figsize=(8, 8))
+ax = plt.gca()
+ax.set_xlim(-100, l5 + 100)
+ax.set_ylim(-100, 100)
+ax.set_aspect('equal')
+ax.grid(True)
+ax.set_title("五连杆机构运动仿真(调用FK函数)")
+
+for i in range(1, 61):
+ # 更新两个驱动臂的角度
+ angle1 = u1_base - np.deg2rad(1.75 * i) # 左侧角度变化
+ angle4 = u4_base + np.deg2rad(1.75 * i) # 右侧角度变化
+
+ #正向运动学函数获取末端位置和中间角度
+ result = forwardF(angle1, angle4, 0, 0, l1, l2, l3, l4, l5, 0, 0)
+ xc, yc, u2, u3 = result[:4]
+
+ # 构建各点坐标
+ x = [0, l1*np.cos(angle1), xc, l4*np.cos(angle4)+l5, l5]
+ y = [0, l1*np.sin(angle1), yc, l4*np.sin(angle4), 0]
+
+ # 清除上一帧并绘制新图形
+ ax.cla()
+ ax.set_xlim(-100, l5 + 100)
+ ax.set_ylim(-100, 100)
+ ax.set_aspect('equal')
+ ax.grid(True)
+ ax.set_title("五连杆机构运动仿真(调用FK函数)")
+
+ # 绘制结构线和关键点
+ ax.plot(x, y, 'r-o', linewidth=2, markersize=6, markerfacecolor='red')
+ ax.plot(x[0], y[0], 'go') # 原点
+ ax.plot(x[1], y[1], 'bo') # 第二个点
+ ax.plot(x[2], y[2], 'mo') # 中间点
+ ax.plot(x[3], y[3], 'co') # 第四个点
+ ax.plot(x[4], y[4], 'yo') # 最后一个点
+
+ plt.pause(0.1)
+
+plt.close()
\ No newline at end of file
diff --git a/calculate/traj_main.py b/calculate/traj_main.py
new file mode 100644
index 0000000..5df2cfb
--- /dev/null
+++ b/calculate/traj_main.py
@@ -0,0 +1,71 @@
+# main_animation.py
+import numpy as np
+import matplotlib.pyplot as plt
+from ik import inverseF
+from trajectory import circle_trajectory, line_trajectory, ellipse_trajectory, square_trajectory, triangle_trajectory
+from matplotlib.animation import FuncAnimation
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# 杆长参数
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+# 设置绘图区域
+fig, ax = plt.subplots()
+ax.set_xlim(-300, 500)
+ax.set_ylim(0, 500)
+ax.set_aspect('equal')
+ax.grid(True)
+ax.set_title("五连杆末端沿轨迹运动")
+
+line, = ax.plot([], [], 'r-o', linewidth=2, markersize=6)
+# 选择轨迹类型:
+TRAJECTORY_TYPE = 'ellipse' # 可选: circle, line, ellipse, square, triangle
+if TRAJECTORY_TYPE == 'line':
+ x_list, y_list = circle_trajectory(center=(100, 300), radius=40)
+elif TRAJECTORY_TYPE == 'line':
+ x_list, y_list = line_trajectory(start=(125, 300), end=(125, 400))
+elif TRAJECTORY_TYPE == 'ellipse':
+ x_list, y_list = ellipse_trajectory(center=(100, 200), rx=50, ry=25)
+elif TRAJECTORY_TYPE == 'square':
+ x_list, y_list = square_trajectory(side=60)
+elif TRAJECTORY_TYPE == 'triangle':
+ x_list, y_list = triangle_trajectory(base_length=100, height=80)
+else:
+ raise ValueError("未知的轨迹类型,请选择 circle / line / ellipse / square / triangle")
+
+# 动画函数
+def draw_frame(i):
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ print(theta1)
+ print(theta4)
+ # 左侧电机臂末端
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ # 右侧电机臂末端
+ x4 = L4 * np.cos(theta4)+L0
+ y4 = L4 * np.sin(theta4)
+ # 构建点序列
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+ line.set_data(x_coords, y_coords)
+ except Exception as e:
+ print(f"第 {i} 帧跳过,错误: {e}")
+ line.set_data([], [])
+
+ return line,
+
+# 创建动画
+ani = FuncAnimation(fig, draw_frame, frames=len(x_list), interval=100, repeat=True)
+
+plt.show()
\ No newline at end of file
diff --git a/calculate/trajectory.py b/calculate/trajectory.py
new file mode 100644
index 0000000..160c21c
--- /dev/null
+++ b/calculate/trajectory.py
@@ -0,0 +1,62 @@
+# trajectory.py
+
+import numpy as np
+
+def circle_trajectory(center=(80, 0), radius=40, num_points=60):
+ """ 圆形轨迹 """
+ angles = np.linspace(0, 2 * np.pi, num_points)
+ x_list = center[0] + radius * np.cos(angles)
+ y_list = center[1] + radius * np.sin(angles)
+ return x_list, y_list
+
+def line_trajectory(start=(40, 0), end=(120, 0), num_points=20):
+ """ 直线轨迹 """
+ t = np.linspace(0, 1, num_points)
+ x_list = start[0] + t * (end[0] - start[0])
+ y_list = start[1] + t * (end[1] - start[1])
+ return x_list, y_list
+
+def ellipse_trajectory(center=(80, 0), rx=50, ry=25, num_points=60):
+ """ 椭圆轨迹 """
+ angles = np.linspace(0, 2 * np.pi, num_points)
+ x_list = center[0] + rx * np.cos(angles)
+ y_list = center[1] + ry * np.sin(angles)
+ return x_list, y_list
+
+def square_trajectory(side=60, num_points=60):
+ """ 正方形轨迹 """
+ x_list, y_list = [], []
+ for i in range(num_points):
+ t = i / num_points
+ if t < 0.25:
+ x = 80 + 60 * t * 4
+ y = 0
+ elif t < 0.5:
+ x = 140
+ y = 0 + 60 * (t - 0.25) * 4
+ elif t < 0.75:
+ x = 140 - 60 * (t - 0.5) * 4
+ y = 60
+ else:
+ x = 80
+ y = 60 - 60 * (t - 0.75) * 4
+ x_list.append(x)
+ y_list.append(y)
+ return x_list, y_list
+
+def triangle_trajectory(base_length=100, height=80, num_points=60):
+ """ 三角形轨迹 """
+ x_list, y_list = [], []
+ points = [(80, 0), (130, 80), (30, 80), (80, 0)]
+ for i in range(num_points):
+ idx = int(i / num_points * 3)
+ t = (i % (num_points // 3)) / (num_points // 3)
+ x = points[idx][0] + t * (points[idx+1][0] - points[idx][0])
+ y = points[idx][1] + t * (points[idx+1][1] - points[idx][1])
+ x_list.append(x)
+ y_list.append(y)
+ return x_list, y_list
+
+def custom_trajectory(custom_x, custom_y):
+ """ 自定义轨迹,输入两个列表即可 """
+ return custom_x, custom_y
\ No newline at end of file
diff --git a/can_test_motor/__pycache__/can_main.cpython-38.pyc b/can_test_motor/__pycache__/can_main.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d83a18456b88b0f4e3824996d2d7fb5e01c60833
GIT binary patch
literal 2930
zcmcJRTW=Fb6o6;;=C#QM!kylhLMd*mB%mUs2qP#oQXiU^(n1{t%Xl`#!QM@0cc~j~
zP`HN@b8A{ELR!)S0aPeR5Va6msXt?1%i8f1PrUHdb9NmkQ2K(@P4?_u&di)SbG|wL
zw7xz@;Hirp=yB@^`5P;99~CNF;I(#xU?fQxWkQZ7Y48=20(`}sn3s|gC2T2+F!cyc
z%4`{nv3XEZ*mBm$=0ho>t8BqOfhY9{lUNg5$QCh~EoMuK3XBv!A@LRcTVt!VEjzM#
zU4y4hTi?2VL(9gN_3N;Ct7G?=9GiOzSZ~kfs%n|Ek;`Ip;S01O7*fNt`%H&arDJ*hHVJeFrAuicY4%2Zu
zN?2@6J`8#=A~H@W*-4C6iP-AC7(YeH0Jx*S3FesRDx+jfK+TK!yK2ENk#(`PG#2e)nQ@PC^K8ru5OJe}8A+D^A=8~vN-r@uZ_zHr1JIzK&r
zZLS7-FU#6A7^{7>{h6Mgv|;GEu&Xk7p>pt>x!qaA)_He2qqk|L!3X90f0pk&@P9ZN
zs9YDkNG2TaMQe)fanWl?^F3J`4hByo%RC7!Og3b#lwPoPgXs)fohH~<=iEJYfo%y-
zY+b+WNfi3D(zmxu#aoHar^0($1`l-R5uku4Y2mv9giE@b1}GzNB6L(56Fvive<5JN
zT^Ex=Yk|)LrH<ulY!Q+d-ic~Gs!p%>kR@mJ{Mo8N#rEVM`>0_k6A!5&jMup5p?Z{CzMGwGbQxw)qQ
zly*62L3R87k7>{%DsD}T`
z&{JjChKta2>^7}@^MwD$(QqMPczW_^>HH-8SI*ykJT+9FIQMw!M6fhRPdl%I=nM@F
z$8=R^D2-j2K7K=U>On!n%OEfvrU(KK<_Twp*_xZ1;SP;dEeLTejZFGOw`XPq90PJV
zWDUY`#+SvAH{c8bbQo99#UEb;ZSTW7%g~x!)`F`dq)6A_{!pqK!=Cc`YLZI|{a@yFN)176YUmp3fG;{`HAIJ@vg@oCrb^hNG
z+zxBcM(}F9frF$0KS~V9hPnTW-?+eEgP}o?#cA+yG-yCamq+fEF5a(t_TItrwUhp>
zVb~q|H!cU!%P}2>af{b8$6$gq@HUq5yg0_*>bcNVJHL*dIXiVVmLXW4Wf*)zAROK#ID|r4Dv~C9=^k4SycR(R7a!e*1b10X?
zhFY1wi46+Jp8)(V6l+k_2DC%kjHFV`%%oBQ-kyY)T5|Cm@0+>cDoN*Zp}T6Hsu^xT
ss)q5iEC;0^J*u1XCUbK7W?a%j_$5W9s#vGQ7RQ#w7O8?j=lnhW2fR%ztN;K2
literal 0
HcmV?d00001
diff --git a/can_test_motor/__pycache__/motor_control_can_all.cpython-38.pyc b/can_test_motor/__pycache__/motor_control_can_all.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..330d00e868c917d8d86c3a7a8f6cbb5279444c60
GIT binary patch
literal 4798
zcmc&&>u(g-6`$wM?8C;`4hGVcNn17E)@F^vBi&+R2S}AlxG1548qw9VcUZ5zFYe3~
zV~vq{gg6NXLQPR)T3(Jmm=8u
zaEM+Og|UDrj5fm@VK*=>%t+Ytu&_fQk+Hy;z!BkRLS8xoH4tZma}DMInKMYzPR5O0rQhFJdjDMIqYI^p8%uL{>wBOyGbC=nSmTX-t2l$n
zeBR7@tm^nj)w92>XCHSg`*hKa8|CqZ%KTp{pDdJq^8xP=m6(?D1~aYJ?e(b48k6*R
z#->R-Q@}Nug1cjl87pCq*k+!X1X`VDIMdPk^BjYH^)R`s`-l?+o>9L3hw|k8o}*t%
z9YzXOzx*HqP>UA6n}N8(vF0(62%rsdTA7jF25Fy^5Q_I?CUuR_b~}_7p)9Mqjkxz#PgaKu_k@-PuAanYH$8_4|J*9&>PiHCdEC{$%NozdL61
zM}tg;)mbo;w@rE?nKGIDnqx8TfN5FDw8<2lnyEz!9hfpy$mE%B4LUX{oPtsNM^a`H
zH4xQk15D2X9s(%horbYR2AwfEXQi2Fu1JKoA+K@uzMqFS3++wNC9w&9A+cSIz?b52
zQ)qq5585Bv6}PE=D1hyV;2ruNq+$O%n{v+Yawtn!Nt#i^;e_lO$$&eJO&MBAU5-s&Z|h
zxT;?P0k}3Ccw_IIKY8oW{)34FeFvGGHf<(@hNC(Q`e$K5e|t>j>#|0lnK*vhHd&+J
z!>b2r@>Ig>8seYCU?x2XjuCgA$QDjLiGU3VXfR^a8wGATmrLeJ7i`mt01>n%F(O3(
z1-eLgLjC`U0u51s2mmDL90ft=AuL&k0uXfNi~G+W1<8Cm3(j)s&NTq#DilC{O$^X>
zI0;1z&?pi#rYgbG_kcuox)sYYAWY@6({0$$jSXC@ypid}g5~K{!_pcW-Q;T&I%K$a
zt|^t<>Jv(p=x$iy>y`Rr;QlAmu9>w0X06uD8owF|)G5?6Yu))1OjxJT()-huTlb#5
zLQ@Wb0CaHT5BGN)Cydw1L5V`
z8j5Z66$|mxNyl8a)x)rEovrFs3E_y>xS;vM^J6@VRG5
z&mIOfyRJ;%FVDW~ZOUk0cJRJw*TOOGRR7DX;7K>2fJ{8lL{>ZieFa<52d|)p{s60S
zBtJy*GLr2`b|CRX9q(e_;$tOw+$v-p@bCO+%a|F4F&4&Vg6{iw&&$iS7g|%uT7Gbk
zc!P1|jKZyk(KDtNlj0blXb56v+fOjk3P#A23Pi-n2>~Zelr-G`BtjY{)mnQaw5yPg
zK-xjpr&UZl>bNboLRvBP@aJiRmW^c7q>STd)j45I99?w9p#V`J>bPxdP}boM!W%j-
zk4bsTxhin~YKNcV9^%{paQ|jUp5pH0Ty0poD$hAXei
z5`c3?_Ok>5;-VM(9lS^&zDGxObKWzV%H0o2^H&}(UMf#LC|^3ey>e%=e0Q$2cqJ;b
zFfpwZ&G0C3G|<_ZD-hF*Pq^3Wf!TOU2eS6W%#%
zF
zE5Q#JVXa3C?^LGlKAOK=FFsFQIJ4xP9M9_X0(rVu_2&4}?X%@8S0DX(x;F8ZbC5?F
z?Q1L_jm8#Bm)BQ
zW|6)Lb1*%Tz*!Rs03gpXP;?X%>1c#$i3BO65($cC0M-b9%+Mbp|NBVz3Aj)Y{9Fd!
zA(to4L*@#DS5Dz8lr0bn)Ld
C>Vr%G
literal 0
HcmV?d00001
diff --git a/can_test_motor/can_main.py b/can_test_motor/can_main.py
new file mode 100644
index 0000000..97c37ef
--- /dev/null
+++ b/can_test_motor/can_main.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python 主程序
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/21
+# @Author : hx
+# @File : can_main.py
+'''
+
+import can
+import time
+
+# 导入你写的 generate_position_command 函数
+from motor_control_can_all import generate_position_command
+
+
+# ========================================
+# 发送CAN帧函数
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='vcan0', interface='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 监听CAN反馈函数
+# ========================================
+def listen_can_feedback(channel='vcan0', interface='socketcan', timeout=2.0):
+ """
+ 监听是否有CAN反馈数据
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ :param timeout: 等待反馈的超时时间(秒)
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ print(f"[监听反馈] 等待来自 {channel} 的反馈({timeout}秒超时)...")
+ msg = bus.recv(timeout=timeout)
+ if msg:
+ print(f"[收到反馈] ID={msg.arbitration_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in msg.data)}]")
+ else:
+ print("[未收到反馈]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[监听失败] {e}")
+
+
+# ========================================
+# 主函数:发送位置控制指令并监听反馈
+# ========================================
+
+def can_motor_contral(direction=0, angle=100, microstep=32, can_id=0x01, channel='vcan0', listen_feedback=False):
+ print("=== 开始发送CAN位置控制指令 ===")
+ print(f"参数:方向={direction}(0=逆时针,1=顺时针),角度={angle}°,细分={microstep}")
+
+ # 生成CAN数据帧
+ can_data = generate_position_command(direction=direction, microstep=microstep, angle=angle)
+ print(f"生成的CAN数据帧: [{', '.join(f'0x{x:02X}' for x in can_data)}]")
+
+ # 发送CAN帧
+ send_can_frame(can_data, can_id=can_id, channel=channel)
+
+ # 如果启用监听,等待反馈
+ if listen_feedback:
+ listen_can_feedback(channel=channel)
+
+ print("=== 电机控制流程完成 ===")
+
+# ========================================
+# 程序入口(直接运行时使用)
+# ========================================
+if __name__ == "__main__":
+ # 这里写死参数,方便调试
+ can_motor_contral(
+ direction=1, # 顺时针
+ angle=180, # 180度
+ microstep=16, # 细分值16
+ can_id=0x02, # CAN ID 0x02
+ channel='vcan0', # 使用虚拟CAN
+ listen_feedback=True # 是否监听反馈
+ )
\ No newline at end of file
diff --git a/can_test_motor/can_run_demo.py b/can_test_motor/can_run_demo.py
new file mode 100644
index 0000000..0df8e0f
--- /dev/null
+++ b/can_test_motor/can_run_demo.py
@@ -0,0 +1,15 @@
+# can_run_demo.py 调用程序demo
+
+from can_main import can_motor_contral
+
+
+
+if __name__ == "__main__":
+ can_motor_contral(
+ direction=1,
+ angle=90,
+ microstep=32,
+ can_id=0x03,
+ channel='vcan0',
+ listen_feedback=True
+ )
\ No newline at end of file
diff --git a/can_test_motor/motor_control_can_all.py b/can_test_motor/motor_control_can_all.py
new file mode 100644
index 0000000..acd7914
--- /dev/null
+++ b/can_test_motor/motor_control_can_all.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python
+# 不同模式can发送指令集成
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/21
+# @Author : hx
+# @File : motor_control_can_all.py
+'''
+
+import can
+import time
+import argparse
+
+
+# 细分值映射表
+MICROSTEP_MAP = {
+ 2: 0x01,
+ 4: 0x02,
+ 8: 0x04,
+ 16: 0x10,
+ 32: 0x20
+}
+
+# ========================================
+# CAN 发送函数(使用 socketcan)
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='can0', bustype='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param bustype: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, bustype=bustype)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 模式1:速度控制模式 (0x01)
+# ========================================
+def generate_speed_command(direction, microstep, speed):
+ """
+ 生成速度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+ return [0x01, direction_byte, microstep_byte, 0x00, 0x00, speed_high, speed_low]
+
+
+# ========================================
+# 模式2:位置控制模式 (0x02)
+# ========================================
+def generate_position_command(direction, microstep, angle):
+ """
+ 生成位置控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 角度值 (单位: 度)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ return [0x02, direction_byte, microstep_byte, pos_high, pos_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式3:力矩控制模式 (0x03)
+# ========================================
+def generate_torque_command(direction, microstep, current):
+ """
+ 生成力矩控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param current: 电流值 (单位: mA)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_current = int(current)
+ current_high = (raw_current >> 8) & 0xFF
+ current_low = raw_current & 0xFF
+
+ return [0x03, direction_byte, microstep_byte, current_high, current_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式4:单圈绝对角度控制模式 (0x04)
+# ========================================
+def generate_absolute_angle_command(direction, microstep, angle, speed):
+ """
+ 生成单圈绝对角度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 目标角度 (单位: 度)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+
+ return [0x04, direction_byte, microstep_byte, pos_high, pos_low, speed_high, speed_low]
+
+
+# ========================================
+# 主函数(命令行调用)
+# ========================================
+def main():
+ parser = argparse.ArgumentParser(description="CAN电机控制程序,支持4种模式")
+ parser.add_argument("--mode", type=int, choices=[1, 2, 3, 4],
+ help="控制模式: 1=速度, 2=位置, 3=力矩, 4=绝对角度")
+ parser.add_argument("--direction", type=int, choices=[0, 1], default=1,
+ help="方向: 0=逆时针, 1=顺时针")
+ parser.add_argument("--microstep", type=int, choices=[2, 4, 8, 16, 32], default=32,
+ help="细分值: 2, 4, 8, 16, 32")
+ parser.add_argument("--value", type=float, required=True,
+ help="控制值(速度: rad/s, 位置/角度: 度, 力矩: mA)")
+ parser.add_argument("--speed_rad_per_sec", type=float,
+ help="速度值(仅用于绝对角度模式)")
+ args = parser.parse_args()
+
+ try:
+ if args.mode == 1:
+ cmd = generate_speed_command(args.direction, args.microstep, args.value)
+ elif args.mode == 2:
+ cmd = generate_position_command(args.direction, args.microstep, args.value)
+ elif args.mode == 3:
+ cmd = generate_torque_command(args.direction, args.microstep, args.value)
+ elif args.mode == 4:
+ if args.speed_rad_per_sec is None:
+ raise ValueError("绝对角度模式需要提供速度参数 (--speed_rad_per_sec)")
+ cmd = generate_absolute_angle_command(args.direction, args.microstep, args.value, args.speed_rad_per_sec)
+
+ print(f"生成CAN指令: [{', '.join(f'0x{x:02X}' for x in cmd)}]")
+ send_can_frame(cmd)
+
+ except Exception as e:
+ print(f"错误: {e}")
+
+
+# ========================================
+# 程序入口
+# ========================================
+if __name__ == "__main__":
+ main()
+ #python motor_control_can_all.py --mode 1 --direction 1 --microstep 32 --value 10
+ #python motor_control_can_all.py --mode 2 --direction 1 --microstep 32 --value 1000
+ #python motor_control_can_all.py --mode 3 --direction 1 --microstep 32 --value 1000
+
diff --git a/main_limit.py b/main_limit.py
new file mode 100644
index 0000000..2cedced
--- /dev/null
+++ b/main_limit.py
@@ -0,0 +1,187 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.widgets import Slider
+import matplotlib.pyplot as plt
+import matplotlib
+
+# 显式设置中文字体和正常显示负号
+matplotlib.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei']
+matplotlib.rcParams['axes.unicode_minus'] = False # 解决负号 '-' 显示为方块的问题
+
+# 测试绘图
+plt.figure(figsize=(6, 4))
+plt.text(0.5, 0.5, '你好,世界!\nThis is 文泉驿微米黑', fontsize=16, ha='center')
+plt.axis('off')
+plt.title("中文测试")
+plt.show()
+
+# ======================= 对称五自由度机器人模型 =======================
+class Symmetric5DoFRobot:
+ def __init__(self):
+ self.L1 = 0.5 # 臂段长度
+ self.L2 = 0.4
+ self.Y0 = 0.6 # 左右臂起始点在 Y 轴上的偏移
+ self.tcp_x = 0.8 # 默认 TCP 目标位置
+ self.tcp_y = 0.0
+ self.tcp_theta = 0 # 夹具方向角度(弧度)
+
+ # 关节限位 [min, max](单位:弧度)
+ self.joint_limits = {
+ 'theta1': [-np.pi, np.pi], # 左肩
+ 'theta2': [-np.pi, np.pi], # 左肘
+ 'theta3': [-np.pi, np.pi], # 右肘
+ 'theta4': [-np.pi, np.pi], # 右肩
+ 'theta5': [-np.pi, np.pi] # 夹具方向
+ }
+
+ # 初始角度
+ self.joint_angles = [0.0, 0.0, 0.0, 0.0, 0.0]
+
+ def inverse_kinematics(self, x, y, theta):
+ """给定末端位置和方向,返回左右臂各关节角度"""
+ try:
+ cos_q2_left = (x**2 + y**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)
+ sin_q2_left = np.sqrt(1 - cos_q2_left**2)
+ q2_left = np.arctan2(sin_q2_left, cos_q2_left)
+
+ k1 = self.L1 + self.L2 * np.cos(q2_left)
+ k2 = self.L2 * np.sin(q2_left)
+ q1_left = np.arctan2(y, x) - np.arctan2(k2, k1)
+
+ # 限制在合理范围内
+ q1_left = np.clip(q1_left, *self.joint_limits['theta1'])
+ q2_left = np.clip(q2_left, *self.joint_limits['theta2'])
+
+ except:
+ q1_left = 0
+ q2_left = 0
+
+ # 右臂镜像求解
+ try:
+ cos_q2_right = (x**2 + y**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)
+ sin_q2_right = -np.sqrt(1 - cos_q2_right**2)
+ q2_right = np.arctan2(sin_q2_right, cos_q2_right)
+
+ k1 = self.L1 + self.L2 * np.cos(q2_right)
+ k2 = self.L2 * np.sin(q2_right)
+ q1_right = np.arctan2(y, x) - np.arctan2(k2, k1)
+
+ q1_right = np.clip(q1_right, *self.joint_limits['theta4'])
+ q2_right = np.clip(q2_right, *self.joint_limits['theta3'])
+
+ except:
+ q1_right = 0
+ q2_right = 0
+
+ self.joint_angles = [q1_left, q2_left, q2_right, q1_right, theta]
+ return {
+ 'left': [q1_left, q2_left],
+ 'right': [q1_right, q2_right],
+ 'theta': theta
+ }
+
+ def forward_kinematics(self):
+ ik = self.inverse_kinematics(self.tcp_x, self.tcp_y, self.tcp_theta)
+ θ1, θ2 = ik['left']
+ θ4, θ3 = ik['right']
+
+ # 左臂坐标计算
+ left_base = np.array([0, -self.Y0])
+ j1_left = left_base + np.array([self.L1 * np.cos(θ1), self.L1 * np.sin(θ1)])
+ tcp_left = j1_left + np.array([
+ self.L2 * np.cos(θ1 + θ2),
+ self.L2 * np.sin(θ1 + θ2)
+ ])
+
+ # 右臂坐标计算
+ right_base = np.array([0, self.Y0])
+ j1_right = right_base + np.array([self.L1 * np.cos(θ4), self.L1 * np.sin(θ4)])
+ tcp_right = j1_right + np.array([
+ self.L2 * np.cos(θ4 + θ3),
+ self.L2 * np.sin(θ4 + θ3)
+ ])
+
+ # 确保末端相连
+ tcp_point = (tcp_left + tcp_right) / 2
+ gripper_dir = np.array([np.cos(self.tcp_theta), np.sin(self.tcp_theta)]) * 0.3
+
+ return {
+ 'left_arm': np.array([left_base, j1_left, tcp_point]),
+ 'right_arm': np.array([right_base, j1_right, tcp_point]),
+ 'gripper': [tcp_point, tcp_point + gripper_dir]
+ }
+
+# ======================= GUI界面 =======================
+class Symmetric5DoF_GUI:
+ def __init__(self):
+ self.robot = Symmetric5DoFRobot()
+
+ self.fig, self.ax = plt.subplots(figsize=(8, 6))
+ plt.subplots_adjust(left=0.1, right=0.9, top=0.85, bottom=0.35)
+ self.ax.set_title("非共基座对称五自由度机器人(带关节限位 + 自由度显示)")
+ self.ax.set_xlim(-1.5, 1.5)
+ self.ax.set_ylim(-1.5, 1.5)
+ self.ax.set_aspect('equal')
+
+ # 初始化线条
+ self.left_line, = self.ax.plot([], [], 'b-o', lw=2, markersize=6, label="左臂")
+ self.right_line, = self.ax.plot([], [], 'r-o', lw=2, markersize=6, label="右臂")
+ self.gripper_line, = self.ax.plot([], [], 'g-->', lw=2, markersize=4, label="夹具方向")
+
+ plt.legend()
+
+ # 创建滑动条
+ self.sliders = []
+ self.slider_labels = ['X', 'Y', 'Theta']
+ self.slider_values = [self.robot.tcp_x, self.robot.tcp_y, self.robot.tcp_theta]
+
+ y_pos = 0.25
+ for i in range(3):
+ ax_slider = plt.axes([0.2, y_pos, 0.6, 0.03])
+ slider = Slider(ax_slider, self.slider_labels[i], -2, 2, valinit=self.slider_values[i])
+ slider.on_changed(lambda val, idx=i: self.update_tcp(idx, val))
+ self.sliders.append(slider)
+ y_pos -= 0.04
+
+ # 添加自由度标签
+ joint_names = ['θ1 (Left Shoulder)', 'θ2 (Left Elbow)',
+ 'θ3 (Right Elbow)', 'θ4 (Right Shoulder)', 'θ5 (Gripper)']
+ limits = self.robot.joint_limits
+ ranges = [
+ f"[{-round(np.degrees(v[0]), 1)}, {round(np.degrees(v[1]), 1)}]°"
+ for v in limits.values()
+ ]
+
+ y_pos = 0.15
+ for name, rng in zip(joint_names, ranges):
+ plt.figtext(0.15, y_pos, f"{name}: {rng}", fontsize=10)
+ y_pos -= 0.02
+
+ def update_tcp(self, idx, value):
+ if idx == 0:
+ self.robot.tcp_x = value
+ elif idx == 1:
+ self.robot.tcp_y = value
+ elif idx == 2:
+ self.robot.tcp_theta = value
+ self.update_plot()
+
+ def update_plot(self):
+ kinematics = self.robot.forward_kinematics()
+ self.left_line.set_data(kinematics['left_arm'][:, 0], kinematics['left_arm'][:, 1])
+ self.right_line.set_data(kinematics['right_arm'][:, 0], kinematics['right_arm'][:, 1])
+
+ gb = kinematics['gripper'][0]
+ gt = kinematics['gripper'][1]
+ self.gripper_line.set_data([gb[0], gt[0]], [gb[1], gt[1]])
+
+ self.fig.canvas.draw_idle()
+
+ def run(self):
+ self.update_plot()
+ plt.show()
+
+# ======================= 主程序入口 =======================
+if __name__ == "__main__":
+ gui = Symmetric5DoF_GUI()
+ gui.run()
\ No newline at end of file
diff --git a/motor_control/__pycache__/can_main.cpython-38.pyc b/motor_control/__pycache__/can_main.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6e8ee4bf65d7872ab6ea02cfada30a4bade82d14
GIT binary patch
literal 3368
zcmd5;-ES0C6u)=Ac6R9pl&E;5K!$?48mryEFC9tgg+J
zluwIvp&*K(RHy=_Mo<$Z3RM&TjCs}B?LP6;7oI$4X1i?xeJ~+$+C6hW&OP_sbAP{c
z=AHWb2!ZFKa-e%%BO!lcXYQjyXDj@yZV-$l2%}8M(ggM2LPCJIm=$wULZXDNWFe*=
zp$VC-ViC3gS_*rNHL``!3bEC!i7kSbs)yO){Q_rth)HY-TgsL(nJs54iVD~e-X_hD
z_ic-;(YEf&Jaijtc
zllErnuH2^^I#1bpve&dScE&W4X)~8g8LUD<#g#vVWi4*lGSmBY8`^0JmsXz;yfH-E
z;b)x%(f7VhdT5W}2#&~TQRtVTFFMo#*O^!p`(;PEBsuaJrNow*x4sw`|(GkKTqw-
z>sJmad&qml5cWV10#=_#*CG%r%{z6vGH`JFws!C5aett#H^oyqjiu~Vo3_cjes1d1
zL*?^Fyuou*W7p{~Lc~815O5P>LTD-Q1)$VELBM4p+Tzjz2SyNIOur1RShtzeW8H;VX>6!6`gM%u
z&7MqbBQv{VIny>dz{-I0WMde}Dh%E5W`tj%mv4CzOjziUL8^xEUAdP-
zpk1lQ%owg}b?0qneq^}e*9&RA7o*)A;s|M1!R<4QBgU)d_U0{D%vt+fs?VODFGlT#
z`5xn#wGaeR1-cOaB6JzMW&(^dUG<>OmtdZ#kbovTaGWnMRWD;$W2@U~j
zh>ieKsuD!(ksN`^McOYp67K>n#gTes76NJkseoFTZ-7WWp&GgBg0+SU+Vwuo#7pWN
znuUR8*MMfSPcxAD(>&ubjJicXF^ie&+7v
zaer%$k;#sqfZ=O2gW#B{I4wJ4QRpV9OCLe{P86)hBTbz(6iZ$1(_mXk*>47sr3Gb
z5Q#5^Rrw|m9iWaZ0KD`(LiZrxOH^ct%cwF-)2fXnUwcP#Juf_7OH%ceRy}?_h
zp-=w0b6EqeX}oIh)FI%_-O2N%FMjccPse=|0LPYiW#Aa%`^@0o$)OGL%Gm9h-f
zV^>12Uxn2{{--KkB{on^Y!D?H5%?=G^S?zWe-2NLOyh`C{yd5oP{dF?;{5e4>@j=$
zYcas{J{|rkd+`=@ZUc%|6fdE`WO)D8G(BMQEzo-SJ#B&+S55k#D#>K!CUh&v#tH&w
z7bLWQf(qXV3e6(N($4XVhReY~t<9gt0fpoD7=H%EvnXl{cK+*J#0@2r%uFYfej;`y
zEI)~+Igv86nDhgL*TC%qkI$USfUhbrFq?M$mj5NAZqAu3pVhaZKb8hUQdFvnbxLGu
QWO<}1vP2aGI_K}+-+|C|=Kufz
literal 0
HcmV?d00001
diff --git a/motor_control/can_main.py b/motor_control/can_main.py
new file mode 100644
index 0000000..d7cc91e
--- /dev/null
+++ b/motor_control/can_main.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/21
+# @Author : hx
+# @File : can_main.py
+'''
+
+import can
+import time
+
+# 导入你写的 generate_position_command 函数
+from motor_control_can_all import generate_position_command
+
+
+# ========================================
+# 发送CAN帧函数
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='vcan0', interface='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 监听CAN反馈函数
+# ========================================
+def listen_can_feedback(channel='vcan0', interface='socketcan', timeout=2.0):
+ """
+ 监听是否有CAN反馈数据
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ :param timeout: 等待反馈的超时时间(秒)
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ print(f"[监听反馈] 等待来自 {channel} 的反馈({timeout}秒超时)...")
+ msg = bus.recv(timeout=timeout)
+ if msg:
+ print(f"[收到反馈] ID={msg.arbitration_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in msg.data)}]")
+ else:
+ print("[未收到反馈]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[监听失败] {e}")
+
+
+# ========================================
+# 主函数:发送位置控制指令并监听反馈
+# ========================================
+
+def can_motor_contral(direction=0, angle=100, microstep=32, can_id=0x01, channel='vcan0', listen_feedback=False):
+ print("=== 开始发送CAN位置控制指令 ===")
+ print(f"参数:方向={direction}(0=逆时针,1=顺时针),角度={angle}°,细分={microstep}")
+
+ # 生成CAN数据帧
+ can_data = generate_position_command(direction=direction, microstep=microstep, angle=angle)
+ print(f"生成的CAN数据帧: [{', '.join(f'0x{x:02X}' for x in can_data)}]")
+
+ # 发送CAN帧
+ send_can_frame(can_data, can_id=can_id, channel=channel)
+
+ # 如果启用监听,等待反馈
+ if listen_feedback:
+ listen_can_feedback(channel=channel)
+
+ print("=== 电机控制流程完成 ===")
+
+# ========================================
+# 程序入口(直接运行时使用)
+# ========================================
+if __name__ == "__main__":
+ # 这里写死参数,方便调试
+ can_motor_contral(
+ direction=1, # 顺时针
+ angle=180, # 180度
+ microstep=16, # 细分值16
+ can_id=0x02, # CAN ID 0x02
+ channel='vcan0', # 使用虚拟CAN
+ listen_feedback=True # 是否监听反馈
+ )
\ No newline at end of file
diff --git a/motor_control/can_run_demo.py b/motor_control/can_run_demo.py
new file mode 100644
index 0000000..58ec7cf
--- /dev/null
+++ b/motor_control/can_run_demo.py
@@ -0,0 +1,13 @@
+# can_run_demo.py
+
+from can_main import can_motor_contral
+
+if __name__ == "__main__":
+ can_motor_contral(
+ direction=1,
+ angle=90,
+ microstep=32,
+ can_id=0x03,
+ channel='vcan0',
+ listen_feedback=True
+ )
\ No newline at end of file
diff --git a/motor_control/can_test.py b/motor_control/can_test.py
new file mode 100644
index 0000000..ebbed05
--- /dev/null
+++ b/motor_control/can_test.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/15 17:52
+# @Author : hx
+# @File : motor.py
+'''
+
+import time
+
+start_speed = 8
+stop_speed = 0
+
+# 细分值映射表
+MICROSTEP_MAP = {
+ 2: 0x01,
+ 4: 0x02,
+ 8: 0x04,
+ 16: 0x10,
+ 32: 0x20
+}
+
+def generate_speed_command(direction=1, microstep=32, speed=10):
+ """
+ 生成速度控制模式的 CAN 数据帧(7字节)
+ :param direction: 0=逆时针,1=顺时针
+ :param microstep: 细分值(2, 4, 8, 16, 32)
+ :param speed: 速度值(Rad/s)
+ :return: 7字节 CAN 数据帧
+ """
+ control_mode = 0x01 # 速度控制模式
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+ return [control_mode, direction_byte, microstep_byte, 0x00, 0x00, speed_high, speed_low]
+
+
+def generate_position_command(angle, speed=10, direction=1, microstep=32):
+ """
+ 生成位置控制模式的 CAN 数据帧(7字节)
+ :param angle: 目标角度(度)
+ :param speed: 速度值(Rad/s)
+ :param direction: 0=逆时针,1=顺时针
+ :param microstep: 细分值(2, 4, 8, 16, 32)
+ :return: 7字节 CAN 数据帧
+ """
+ control_mode = 0x02 # 位置控制模式
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+ return [control_mode, direction_byte, microstep_byte, pos_high, pos_low, speed_high, speed_low]
+
+
+def send_can_frame(data, can_id=0x01):
+ """
+ 模拟发送CAN帧(只打印,不实际发送)
+ """
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+
+
+def listen_and_respond():
+ """
+ 模拟监听并响应信号的逻辑
+ """
+ try:
+ print("[监听中] 等待信号...")
+
+ # 模拟收到信号后执行动作
+ time.sleep(2)
+
+ # 示例:位置控制模式 - 顺时针转动 1000 度,速度 10Rad/s,32细分
+ pos_data = generate_position_command(angle=1000, speed=10, direction=1, microstep=32)
+ send_can_frame(pos_data, can_id=0x01)
+
+ time.sleep(2)
+
+ # 示例:速度控制模式 - 顺时针,速度 8Rad/s,32细分
+ speed_data = generate_speed_command(direction=1, microstep=32, speed=8)
+ send_can_frame(speed_data, can_id=0x01)
+
+ print("[已完成一次响应]")
+
+ except Exception as e:
+ print(f"监听时发生错误: {e}")
+
+
+if __name__ == "__main__":
+ try:
+ # 示例:执行一次电机控制流程
+ listen_and_respond()
+
+ except Exception as e:
+ print(f"运行错误: {e}")
\ No newline at end of file
diff --git a/motor_control/contral.py b/motor_control/contral.py
new file mode 100644
index 0000000..5513d03
--- /dev/null
+++ b/motor_control/contral.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/15 17:52
+# @Author : hx
+# @File : motor.py
+'''
+import serial
+import time
+import math
+
+start_speed = 8
+stop_speed = 0
+
+def send_hex_command(port, baudrate, hex_data):
+ """
+ 通过串口发送十六进制指令
+ :param port: 串口号 (如 'COM3' 或 '/dev/ttyUSB0')
+ :param baudrate: 波特率 (如 9600)
+ :param hex_data: 十六进制字符串 (如 "7B 01 02 01 20 0E 10 00 64 23 7D")
+ """
+ try:
+ # 转换十六进制字符串为字节数据
+ byte_data = bytes.fromhex(hex_data.replace(" ", ""))
+
+ # 打开串口
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ print(f"已连接串口 {port}, 波特率 {baudrate}")
+
+ # 发送指令
+ ser.write(byte_data)
+ print(f"已发送指令: {hex_data}")
+
+ # 可选:等待并读取返回数据
+ time.sleep(0.1)
+ if ser.in_waiting > 0:
+ response = ser.read(ser.in_waiting)
+ print(f"收到响应: {response.hex().upper()}")
+
+ except Exception as e:
+ print(f"发生错误: {e}")
+
+def generate_fixed_command(speed):
+ """
+ 生成固定的速度控制指令: 7B 01 01 00 20 00 00 00 7D
+ 参数:
+ speed (int): 十进制速度值 (0~255)
+ 返回:
+ str: 十六进制命令字符串
+ """
+
+ actual_speed = int(speed * 10)
+
+ if not (0 <= actual_speed <= 255):
+ raise ValueError("速度必须在 0~255 范围内")
+
+ command_bytes = [
+ 0x7B, 0x01, 0x01, # 帧头、地址、控制模式
+ 0x00, 0x20, # 方向、细分
+ 0x00, 0x00, # 位置高/低字节(持续旋转)
+ 0x00, actual_speed # 保持整数速度值
+ ]
+
+ # 计算校验和(前9个字节异或)
+ checksum = 0
+ for byte in command_bytes:
+ checksum ^= byte
+
+ full_command = command_bytes + [checksum, 0x7D]
+ return ' '.join(f'{byte:02X}' for byte in full_command)
+
+def listen_and_respond(port, baudrate):
+ """
+ 持续监听串口,一旦有信号就执行开始和停止的指令
+ """
+ try:
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ ser.reset_input_buffer()
+ print(f"[监听中] 串口 {port} 已打开,等待信号...")
+
+ while True:
+ if ser.in_waiting > 0:
+ incoming = ser.read(ser.in_waiting)
+ print(f"[收到信号] 内容: {incoming.hex().upper()}")
+
+ # 执行开始旋转
+ start_cmd = generate_fixed_command(8)
+ print("发送开始指令:", start_cmd)
+ ser.write(bytes.fromhex(start_cmd.replace(" ", "")))
+
+ # 延迟一段时间
+ time.sleep(4)
+
+ # 执行停止
+ stop_cmd = generate_fixed_command(0)
+ print("发送停止指令:", stop_cmd)
+ ser.write(bytes.fromhex(stop_cmd.replace(" ", "")))
+
+ print("[已完成一次响应]\n等待下一次信号...\n")
+
+ time.sleep(0.1) # 避免CPU占用过高
+
+ except Exception as e:
+ print(f"监听时发生错误: {e}")
+
+if __name__ == "__main__":
+ # 配置参数
+ PORT = "/dev/ttyACM0" # 修改为你的串口号
+ BAUD_RATE = 115200 # 修改为你的设备波特率
+ HEX_COMMAND = {
+ "Clockwise_rotation":"7B 01 02 01 20 0E 10 00 64 23 7D", # 顺时针旋转360
+ "Counterclockwise_rotation":"7B 01 02 00 20 0E 10 00 64 22 7D",
+ "One_revolution_per_second":"7B 01 01 00 20 00 00 00 3F 64 7D", # 有问题
+ "Ten_radin_per_second":"7B 01 01 00 20 00 00 00 64 3F 7D",
+ "Eight_radin_per_second": "7B 01 01 00 20 00 00 00 50 0B 7D",
+ "stop":"7B 01 01 00 20 00 00 00 00 5B 7D"
+ }# 要发送的指令
+
+ try:
+ # 生成并发送开始旋转指令(速度 = 8)
+ start_cmd = generate_fixed_command(start_speed)
+ send_hex_command(PORT, BAUD_RATE, start_cmd)
+
+ # 延迟一段时间(比如 4 秒)
+ time.sleep(4)
+
+ # 生成并发送停止指令(速度 = 0)
+ stop_cmd = generate_fixed_command(stop_speed)
+ send_hex_command(PORT, BAUD_RATE, stop_cmd)
+
+ print("电机控制流程执行完成。")
+
+ except Exception as e:
+ print(f"运行错误: {e}")
\ No newline at end of file
diff --git a/motor_control/motor_control_can_all.py b/motor_control/motor_control_can_all.py
new file mode 100644
index 0000000..b8104e5
--- /dev/null
+++ b/motor_control/motor_control_can_all.py
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/18
+# @Author : hx
+# @File : motor_control_can_all.py
+'''
+
+import can
+import time
+import argparse
+
+
+# 细分值映射表
+MICROSTEP_MAP = {
+ 2: 0x01,
+ 4: 0x02,
+ 8: 0x04,
+ 16: 0x0,
+ 32: 0x20
+}
+
+# ========================================
+# CAN 发送函数(使用 socketcan)
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='can0', bustype='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param bustype: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, bustype=bustype)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 模式1:速度控制模式 (0x01)
+# ========================================
+def generate_speed_command(direction, microstep, speed):
+ """
+ 生成速度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+ return [0x01, direction_byte, microstep_byte, 0x00, 0x00, speed_high, speed_low]
+
+
+# ========================================
+# 模式2:位置控制模式 (0x02)
+# ========================================
+def generate_position_command(direction, microstep, angle):
+ """
+ 生成位置控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 角度值 (单位: 度)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ return [0x02, direction_byte, microstep_byte, pos_high, pos_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式3:力矩控制模式 (0x03)
+# ========================================
+def generate_torque_command(direction, microstep, current):
+ """
+ 生成力矩控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param current: 电流值 (单位: mA)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_current = int(current)
+ current_high = (raw_current >> 8) & 0xFF
+ current_low = raw_current & 0xFF
+
+ return [0x03, direction_byte, microstep_byte, current_high, current_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式4:单圈绝对角度控制模式 (0x04)
+# ========================================
+def generate_absolute_angle_command(direction, microstep, angle, speed):
+ """
+ 生成单圈绝对角度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 目标角度 (单位: 度)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+
+ return [0x04, direction_byte, microstep_byte, pos_high, pos_low, speed_high, speed_low]
+
+
+# ========================================
+# 主函数(命令行调用)
+# ========================================
+def main():
+ parser = argparse.ArgumentParser(description="CAN电机控制程序,支持4种模式")
+ parser.add_argument("--mode", type=int, choices=[1, 2, 3, 4],
+ help="控制模式: 1=速度, 2=位置, 3=力矩, 4=绝对角度")
+ parser.add_argument("--direction", type=int, choices=[0, 1], default=1,
+ help="方向: 0=逆时针, 1=顺时针")
+ parser.add_argument("--microstep", type=int, choices=[2, 4, 8, 16, 32], default=32,
+ help="细分值: 2, 4, 8, 16, 32")
+ parser.add_argument("--value", type=float, required=True,
+ help="控制值(速度: rad/s, 位置/角度: 度, 力矩: mA)")
+ parser.add_argument("--speed_rad_per_sec", type=float,
+ help="速度值(仅用于绝对角度模式)")
+ args = parser.parse_args()
+
+ try:
+ if args.mode == 1:
+ cmd = generate_speed_command(args.direction, args.microstep, args.value)
+ elif args.mode == 2:
+ cmd = generate_position_command(args.direction, args.microstep, args.value)
+ elif args.mode == 3:
+ cmd = generate_torque_command(args.direction, args.microstep, args.value)
+ elif args.mode == 4:
+ if args.speed_rad_per_sec is None:
+ raise ValueError("绝对角度模式需要提供速度参数 (--speed_rad_per_sec)")
+ cmd = generate_absolute_angle_command(args.direction, args.microstep, args.value, args.speed_rad_per_sec)
+
+ print(f"生成CAN指令: [{', '.join(f'0x{x:02X}' for x in cmd)}]")
+ send_can_frame(cmd)
+
+ except Exception as e:
+ print(f"错误: {e}")
+
+
+# ========================================
+# 程序入口
+# ========================================
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/motor_control/motor_control_usb_all.py b/motor_control/motor_control_usb_all.py
new file mode 100644
index 0000000..3ea4961
--- /dev/null
+++ b/motor_control/motor_control_usb_all.py
@@ -0,0 +1,212 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/18
+# @Author : hx
+# @File : motor_control_modes.py
+'''
+
+import serial
+import time
+import argparse
+
+
+# ========================================
+# 通用发送函数
+# ========================================
+def send_hex_command(port, baudrate, hex_data):
+ """
+ 发送十六进制指令
+ """
+ try:
+ byte_data = bytes.fromhex(hex_data.replace(" ", ""))
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ print(f"已连接串口 {port}, 波特率 {baudrate}")
+ ser.write(byte_data)
+ print(f"已发送指令: {hex_data}")
+ time.sleep(0.1)
+ if ser.in_waiting > 0:
+ response = ser.read(ser.in_waiting)
+ print(f"收到响应: {response.hex().upper()}")
+ except Exception as e:
+ print(f"发送失败: {e}")
+
+
+# ========================================
+# 模式1:速度控制模式 (0x01)
+# 控制字节3 = 0x01
+# 第6~7字节为0
+# 第8~9字节:速度值(单位 rad/s,放大10倍)
+# ========================================
+def generate_speed_command(direction, microstep, speed):
+ """
+ 生成速度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ command = [0x7B, 0x01, 0x01, direction, microstep, 0x00, 0x00]
+
+ # 转速数据,单位为 rad/s,放大10倍传输
+ raw_speed = int(speed * 10)
+ high_byte = (raw_speed >> 8) & 0xFF
+ low_byte = raw_speed & 0xFF
+ command += [high_byte, low_byte]
+
+ # 计算 BCC 校验和(前9个字节异或)
+ checksum = 0
+ for b in command:
+ checksum ^= b
+
+ full_command = command + [checksum, 0x7D]
+ return ' '.join(f'{b:02X}' for b in full_command)
+
+
+# ========================================
+# 模式2:位置控制模式 (0x02)
+# 控制字节3 = 0x02
+# 第6~7字节:角度值(单位 度,放大10倍)
+# 第8~9字节为0
+# ========================================
+def generate_position_command(direction, microstep, angle):
+ """
+ 生成位置控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 角度值 (单位: 度)
+ """
+ command = [0x7B, 0x01, 0x02, direction, microstep]
+
+ # 角度数据,单位为度,放大10倍传输
+ raw_angle = int(angle * 10)
+ high_byte = (raw_angle >> 8) & 0xFF
+ low_byte = raw_angle & 0xFF
+ command += [low_byte, high_byte, 0x00, 0x64]
+
+ # 计算 BCC 校验和(前9个字节异或)
+ checksum = 0
+ for b in command:
+ checksum ^= b
+
+ full_command = command + [checksum, 0x7D]
+ return ' '.join(f'{b:02X}' for b in full_command)
+
+
+# ========================================
+# 模式3:力矩控制模式 (0x03)
+# 控制字节3 = 0x03
+# 第6~7字节:电流值(单位 mA)
+# 第8~9字节为0
+# ========================================
+def generate_torque_command(direction, microstep, current):
+ """
+ 生成力矩控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param current: 电流值 (单位: mA)
+ """
+ command = [0x7B, 0x01, 0x03, direction, microstep]
+
+ # 电流数据,单位为 mA
+ raw_current = int(current)
+ high_byte = (raw_current >> 8) & 0xFF
+ low_byte = raw_current & 0xFF
+ command += [low_byte, high_byte, 0x00, 0x64]
+
+ # 计算 BCC 校验和(前9个字节异或)
+ checksum = 0
+ for b in command:
+ checksum ^= b
+
+ full_command = command + [checksum, 0x7D]
+ return ' '.join(f'{b:02X}' for b in full_command)
+
+
+# ========================================
+# 模式4:单圈绝对角度控制模式 (0x04)
+# 控制字节3 = 0x04
+# 第6~7字节:目标角度(单位 度,放大10倍)
+# 第8~9字节为0
+# ========================================
+def generate_absolute_angle_command(direction, microstep, target_angle, speed_rad_per_sec):
+ """
+ 生成单圈绝对角度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param target_angle: 目标角度 (单位: 度)
+ :param speed_rad_per_sec: 速度 (单位: rad/s)
+ :return: 十六进制指令字符串
+ """
+ command = [0x7B, 0x01, 0x04, direction, microstep]
+
+ # 目标角度(放大10倍)
+ raw_angle = int(target_angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+ command += [pos_high, pos_low]
+
+ # 速度(放大10倍)
+ raw_speed = int(speed_rad_per_sec * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+ command += [speed_high, speed_low]
+
+ # 计算 BCC 校验和(前9个字节异或)
+ checksum = 0
+ for b in command:
+ checksum ^= b
+
+ full_command = command + [checksum, 0x7D]
+ return ' '.join(f'{b:02X}' for b in full_command)
+
+
+# ======================================1
+# 主函数(命令行调用)
+# ========================================
+def main():
+ parser = argparse.ArgumentParser(description="电机控制程序,支持4种模式")
+ parser.add_argument("--port", default="/dev/ttyACM0", help="串口号")
+ parser.add_argument("--baud", type=int, default=115200, help="波特率")
+ parser.add_argument("--mode", type=int, choices=[1, 2, 3, 4],
+ help="控制模式: 1=速度, 2=位置, 3=力矩, 4=绝对角度")
+ parser.add_argument("--direction", type=int, choices=[0, 1], default=1,
+ help="方向: 0=逆时针, 1=顺时针")
+ parser.add_argument("--microstep", type=int, choices=[2, 4, 8, 16, 32], default=32,
+ help="细分值: 2, 4, 8, 16, 32")
+ parser.add_argument("--value", type=float, required=True,
+ help="控制值(速度: rad/s, 位置/角度: 度, 力矩: mA)")
+ parser.add_argument("--speed_rad_per_sec", type=float, required=True,
+ help="控制值(速度)")
+ args = parser.parse_args()
+
+ try:
+ if args.mode == 1:
+ cmd = generate_speed_command(args.direction, args.microstep, args.value)
+ elif args.mode == 2:
+ cmd = generate_position_command(args.direction, args.microstep, args.value)
+ elif args.mode == 3:
+ cmd = generate_torque_command(args.direction, args.microstep, args.value)
+ elif args.mode == 4:
+ cmd = generate_absolute_angle_command(args.direction, args.microstep, args.value,args.speed_rad_per_sec)
+
+ print(f"生成指令: {cmd}")
+ send_hex_command(args.port, args.baud, cmd)
+ print("指令发送完成。")
+
+ except Exception as e:
+ print(f"错误: {e}")
+
+
+# ========================================
+# 程序入口
+# ========================================
+if __name__ == "__main__":
+ main()
+
+ #python motor_control_usb_all.py --mode 1 --direction 1 --microstep 32 --value 10 --speed_rad_per_sec=10
+
+ #python motor_control_usb_all.py --mode 2 --direction 1 --microstep 32 --value 360.0 --speed_rad_per_sec=10
+
+ #python motor_control_usb_all.py --mode 3 --direction 1 --microstep 32 --value 1000 --speed_rad_per_sec=10
+
+ #python motor_control_usb_all.py --mode 4 --direction 1 --microstep 32 --value 100.0 --speed_rad_per_sec=10
diff --git a/motor_control/motor_test_mostimprotant.py b/motor_control/motor_test_mostimprotant.py
new file mode 100644
index 0000000..8023253
--- /dev/null
+++ b/motor_control/motor_test_mostimprotant.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/15 17:52
+# @Author : hx
+# @File : motor.py
+'''
+import serial
+import time
+import math
+
+start_speed = 8
+stop_speed = 0
+
+def send_hex_command(port, baudrate, hex_data):
+ """
+ 通过串口发送十六进制指令
+ :param port: 串口号 (如 'COM3' 或 '/dev/ttyUSB0')
+ :param baudrate: 波特率 (如 9600)
+ :param hex_data: 十六进制字符串 (如 "7B 01 02 01 20 0E 10 00 64 23 7D")
+ """
+ try:
+ # 转换十六进制字符串为字节数据
+ byte_data = bytes.fromhex(hex_data.replace(" ", ""))
+
+ # 打开串口
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ print(f"已连接串口 {port}, 波特率 {baudrate}")
+
+ # 发送指令
+ ser.write(byte_data)
+ print(f"已发送指令: {hex_data}")
+
+ # 可选:等待并读取返回数据
+ time.sleep(0.1)
+ if ser.in_waiting > 0:
+ response = ser.read(ser.in_waiting)
+ print(f"收到响应: {response.hex().upper()}")
+
+ except Exception as e:
+ print(f"发生错误: {e}")
+
+def generate_fixed_command(speed):
+ """
+ 生成固定的速度控制指令: 7B 01 01 00 20 00 00 00 7D
+ 参数:
+ speed (int): 十进制速度值 (0~255)
+ 返回:
+ str: 十六进制命令字符串
+ """
+
+ actual_speed = int(speed * 10)
+
+ if not (0 <= actual_speed <= 255):
+ raise ValueError("速度必须在 0~255 范围内")
+
+ command_bytes = [
+ 0x7B, 0x01, 0x01, # 帧头、地址、控制模式
+ 0x00, 0x20, # 方向、细分
+ 0x00, 0x00, # 位置高/低字节(持续旋转)
+ 0x00, actual_speed # 保持整数速度值
+ ]
+
+ # 计算校验和(前9个字节异或)
+ checksum = 0
+ for byte in command_bytes:
+ checksum ^= byte
+
+ full_command = command_bytes + [checksum, 0x7D]
+ return ' '.join(f'{byte:02X}' for byte in full_command)
+
+def listen_and_respond(port, baudrate):
+ """
+ 持续监听串口,一旦有信号就执行开始和停止的指令
+ """
+ try:
+ with serial.Serial(port, baudrate, timeout=1) as ser:
+ ser.reset_input_buffer()
+ print(f"[监听中] 串口 {port} 已打开,等待信号...")
+
+ while True:
+ if ser.in_waiting > 0:
+ incoming = ser.read(ser.in_waiting)
+ print(f"[收到信号] 内容: {incoming.hex().upper()}")
+
+ # 执行开始旋转
+ start_cmd = generate_fixed_command(8)
+ print("发送开始指令:", start_cmd)
+ ser.write(bytes.fromhex(start_cmd.replace(" ", "")))
+
+ # 延迟一段时间
+ time.sleep(4)
+
+ # 执行停止
+ stop_cmd = generate_fixed_command(0)
+ print("发送停止指令:", stop_cmd)
+ ser.write(bytes.fromhex(stop_cmd.replace(" ", "")))
+
+ print("[已完成一次响应]\n等待下一次信号...\n")
+
+ time.sleep(0.1) # 避免CPU占用过高
+
+ except Exception as e:
+ print(f"监听时发生错误: {e}")
+
+if __name__ == "__main__":
+ # 配置参数
+ PORT = "/dev/ttyACM0" # 修改为你的串口号
+ BAUD_RATE = 115200 # 修改为你的设备波特率
+ HEX_COMMAND = {
+ "Clockwise_rotation":"7B 01 02 01 20 0E 10 00 64 23 7D", # 顺时针旋转360
+ "Counterclockwise_rotation":"7B 01 02 00 20 0E 10 00 64 22 7D",
+ "One_revolution_per_second":"7B 01 01 00 20 00 00 00 3F 64 7D", # 有问题
+ "Ten_radin_per_second":"7B 01 01 00 20 00 00 00 64 3F 7D",
+ "Eight_radin_per_second": "7B 01 01 00 20 00 00 00 50 0B 7D",
+ "stop":"7B 01 01 00 20 00 00 00 00 5B 7D"
+ }# 要发送的指令
+
+ try:
+ # 生成并发送开始旋转指令(速度 = 8)
+ start_cmd = generate_fixed_command(start_speed)
+ send_hex_command(PORT, BAUD_RATE, start_cmd)
+
+ # 延迟一段时间(比如 4 秒)
+ time.sleep(4)
+
+ # 生成并发送停止指令(速度 = 0)
+ stop_cmd = generate_fixed_command(stop_speed)
+ send_hex_command(PORT, BAUD_RATE, stop_cmd)
+
+ print("电机控制流程执行完成。")
+
+ except Exception as e:
+ print(f"运行错误: {e}")
diff --git a/motor_control/test.py b/motor_control/test.py
new file mode 100644
index 0000000..340d1d4
--- /dev/null
+++ b/motor_control/test.py
@@ -0,0 +1,9 @@
+import time
+
+print("守护进程已启动,将持续运行...")
+
+try:
+ while True:
+ time.sleep(1)
+except KeyboardInterrupt:
+ print("守护进程已停止")
\ No newline at end of file
diff --git a/qiege_motor.py b/qiege_motor.py
new file mode 100644
index 0000000..3799dc3
--- /dev/null
+++ b/qiege_motor.py
@@ -0,0 +1,183 @@
+# qiege_motor.py
+# 切割电机控制模块(支持 CAN 和 USB 两种通信方式)
+# 使用 control(enable) 统一接口
+
+import time
+import serial
+
+# ------------------------ 配置区 ------------------------
+
+# <<< 切换通信模式 >>>
+MODE = 'usb' # 'can' 或 'usb'
+
+# CAN 模式配置
+CAN_SERIAL_PORT = '/dev/ttyACM0' # CAN 转串口设备
+CAN_BAUDRATE = 921600
+CAN_MOTOR_ID = 0x05 # 轮趣电机对应的 CAN ID
+
+# USB 串口模式配置(轮趣 HEX 指令)
+USB_SERIAL_PORT = '/dev/ttyACM0' # 或 '/dev/ttyUSB0'
+USB_BAUDRATE = 115200
+
+# 目标转速(rad/s)
+CUTTING_SPEED_RAD_PER_SEC = 8.0 # 对应 HEX 指令中的 speed=8 → 80 (0x50)
+
+# 是否启用调试输出
+DEBUG = True
+
+# ------------------------ 全局变量 ------------------------
+usb_serial = None
+can_serial = None
+
+
+# ------------------------ USB 模式:HEX 指令生成与发送 ------------------------
+def generate_usb_command(speed):
+ """
+ 生成轮趣电机 USB 串口 HEX 指令
+ 格式: 7B 01 01 00 20 00 00 00 7D
+ speed: float (rad/s), 映射为 0~255
+ """
+ # 将 rad/s 映射为速度字节(示例:8 rad/s → 80)
+ # 你可以根据实际响应调整映射关系
+ speed_val = int(speed * 10) # 示例:8 → 80
+ speed_byte = max(0, min(255, speed_val))
+
+ # 构造前9字节
+ cmd = [
+ 0x7B, 0x01, 0x02, # 帧头、地址、模式
+ 0x01, 0x20, # 方向、细分
+ 0x0E, 0x10, # 位置高、低(持续旋转)
+ 0x00, 0x64, # 速度值
+ ]
+
+ # 计算校验和(前9字节异或)
+ checksum = 0
+ for b in cmd:
+ checksum ^= b
+ cmd.append(checksum)
+ cmd.append(0x7D) # 帧尾
+
+ return bytes(cmd)
+
+
+def send_usb_command(port, baudrate, command):
+ """发送 USB 串口指令"""
+ global usb_serial
+ try:
+ if usb_serial is None or not usb_serial.is_open:
+ usb_serial = serial.Serial(port, baudrate, timeout=0.5)
+ time.sleep(0.5) # 等待串口稳定
+
+ usb_serial.write(command)
+ if DEBUG:
+ print(f"[qiege_motor] USB 发送: {command.hex().upper()}")
+ return True
+ except Exception as e:
+ if DEBUG:
+ print(f"[qiege_motor] USB 发送失败: {e}")
+ usb_serial = None
+ return False
+
+
+# ------------------------ CAN 模式:CAN 指令发送 ------------------------
+def send_can_command(data, can_id=CAN_MOTOR_ID):
+ """通过 CAN 发送指令(假设使用串口转 CAN 模块)"""
+ global can_serial
+ try:
+ if can_serial is None or not can_serial.is_open:
+ can_serial = serial.Serial(CAN_SERIAL_PORT, CAN_BAUDRATE, timeout=0.5)
+ time.sleep(0.5)
+
+ # 示例:简单封装 CAN 帧(ID + DLC + Data)
+ # 格式根据你使用的 CAN 模块调整(如 CANable、TJA1050 等)
+ can_frame = [
+ (can_id >> 24) & 0xFF,
+ (can_id >> 16) & 0xFF,
+ (can_id >> 8) & 0xFF,
+ can_id & 0xFF,
+ len(data) # DLC
+ ] + list(data)
+ can_serial.write(bytes(can_frame))
+
+ if DEBUG:
+ print(f"[qiege_motor] CAN 发送 → ID:{can_id:05X}, Data:{[f'{b:02X}' for b in data]}")
+ return True
+ except Exception as e:
+ if DEBUG:
+ print(f"[qiege_motor] CAN 发送失败: {e}")
+ can_serial = None
+ return False
+
+
+def generate_can_command(speed_rpm=300, direction=1, microstep=32):
+ """生成轮趣 CAN 速度指令(示例)"""
+ speed_01rpm = int(abs(speed_rpm) * 10)
+ high_byte = (speed_01rpm >> 8) & 0xFF
+ low_byte = speed_01rpm & 0xFF
+ direction_bit = 1 if direction else 0
+
+ data = [
+ 0x88, # 控制字:速度模式
+ ((direction_bit & 0x01) << 7) | (microstep & 0x7F),
+ 0x00, 0x00,
+ high_byte, low_byte,
+ 0x00, 0x00
+ ]
+ return data
+
+
+# ------------------------ 统一控制接口 ------------------------
+def start_motor():
+ """启动切割电机"""
+ if MODE == 'usb':
+ cmd = generate_usb_command(CUTTING_SPEED_RAD_PER_SEC)
+ return send_usb_command(USB_SERIAL_PORT, USB_BAUDRATE, cmd)
+ elif MODE == 'can':
+ # 示例:8 rad/s ≈ 76.4 RPM
+ rpm = CUTTING_SPEED_RAD_PER_SEC * 60 / (2 * 3.1416)
+ cmd = generate_can_command(speed_rpm=rpm, direction=1)
+ return send_can_command(cmd, can_id=CAN_MOTOR_ID)
+ else:
+ print(f"[qiege_motor] 错误:不支持的模式 '{MODE}'")
+ return False
+
+
+def stop_motor():
+ """停止切割电机"""
+ if MODE == 'usb':
+ cmd = generate_usb_command(0.0)
+ return send_usb_command(USB_SERIAL_PORT, USB_BAUDRATE, cmd)
+ elif MODE == 'can':
+ cmd = generate_can_command(speed_rpm=0, direction=1)
+ return send_can_command(cmd, can_id=CAN_MOTOR_ID)
+ else:
+ return False
+
+
+def control(enable: bool):
+ """
+ 统一控制接口
+ :param enable: True 启动, False 停止
+ :return: bool 成功与否
+ """
+ try:
+ if enable:
+ return start_motor()
+ else:
+ return stop_motor()
+ except Exception as e:
+ if DEBUG:
+ print(f"[qiege_motor] 控制异常: {e}")
+ return False
+
+
+# ------------------------ 测试 ------------------------
+if __name__ == "__main__":
+ print(f"【qiege_motor】测试开始 (模式: {MODE})")
+ print("启动 → 1秒后停止")
+
+ control(True)
+ time.sleep(1)
+ control(False)
+
+ print("测试结束。")
\ No newline at end of file
diff --git a/qt/DM_CAN/DM_CAN.py b/qt/DM_CAN/DM_CAN.py
new file mode 100644
index 0000000..d548f7f
--- /dev/null
+++ b/qt/DM_CAN/DM_CAN.py
@@ -0,0 +1,624 @@
+from time import sleep
+import numpy as np
+from enum import IntEnum
+from struct import unpack
+from struct import pack
+
+
+class Motor:
+ def __init__(self, MotorType, SlaveID, MasterID):
+ """
+ define Motor object 定义电机对象
+ :param MotorType: Motor type 电机类型
+ :param SlaveID: CANID 电机ID
+ :param MasterID: MasterID 主机ID 建议不要设为0
+ """
+ self.Pd = float(0)
+ self.Vd = float(0)
+ self.state_q = float(0)
+ self.state_dq = float(0)
+ self.state_tau = float(0)
+ self.SlaveID = SlaveID
+ self.MasterID = MasterID
+ self.MotorType = MotorType
+ self.isEnable = False
+ self.NowControlMode = Control_Type.MIT
+ self.temp_param_dict = {}
+
+ def recv_data(self, q: float, dq: float, tau: float):
+ self.state_q = q
+ self.state_dq = dq
+ self.state_tau = tau
+
+ def getPosition(self):
+ """
+ get the position of the motor 获取电机位置
+ :return: the position of the motor 电机位置
+ """
+ return self.state_q
+
+ def getVelocity(self):
+ """
+ get the velocity of the motor 获取电机速度
+ :return: the velocity of the motor 电机速度
+ """
+ return self.state_dq
+
+ def getTorque(self):
+ """
+ get the torque of the motor 获取电机力矩
+ :return: the torque of the motor 电机力矩
+ """
+ return self.state_tau
+
+ def getParam(self, RID):
+ """
+ get the parameter of the motor 获取电机内部的参数,需要提前读取
+ :param RID: DM_variable 电机参数
+ :return: the parameter of the motor 电机参数
+ """
+ if RID in self.temp_param_dict:
+ return self.temp_param_dict[RID]
+ else:
+ return None
+
+
+class MotorControl:
+ send_data_frame = np.array(
+ [0x55, 0xAA, 0x1e, 0x03, 0x01, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0, 0, 0, 0, 0x00, 0x08, 0x00,
+ 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0x00], np.uint8)
+ # 4310 4310_48 4340 4340_48
+ Limit_Param = [[12.5, 30, 10], [12.5, 50, 10], [12.5, 8, 28], [12.5, 10, 28],
+ # 6006 8006 8009 10010L 10010
+ [12.5, 45, 20], [12.5, 45, 40], [12.5, 45, 54], [12.5, 25, 200], [12.5, 20, 200],
+ # H3510 DMG62150 DMH6220
+ [12.5 , 280 , 1],[12.5 , 45 , 10],[12.5 , 45 , 10]]
+
+ def __init__(self, serial_device):
+ """
+ define MotorControl object 定义电机控制对象
+ :param serial_device: serial object 串口对象
+ """
+ self.serial_ = serial_device
+ self.motors_map = dict()
+ self.data_save = bytes() # save data
+ if self.serial_.is_open: # open the serial port
+ print("Serial port is open")
+ serial_device.close()
+ self.serial_.open()
+
+ def controlMIT(self, DM_Motor, kp: float, kd: float, q: float, dq: float, tau: float):
+ """
+ MIT Control Mode Function 达妙电机MIT控制模式函数
+ :param DM_Motor: Motor object 电机对象
+ :param kp: kp
+ :param kd: kd
+ :param q: position 期望位置
+ :param dq: velocity 期望速度
+ :param tau: torque 期望力矩
+ :return: None
+ """
+ if DM_Motor.SlaveID not in self.motors_map:
+ print("controlMIT ERROR : Motor ID not found")
+ return
+ kp_uint = float_to_uint(kp, 0, 500, 12)
+ kd_uint = float_to_uint(kd, 0, 5, 12)
+ MotorType = DM_Motor.MotorType
+ Q_MAX = self.Limit_Param[MotorType][0]
+ DQ_MAX = self.Limit_Param[MotorType][1]
+ TAU_MAX = self.Limit_Param[MotorType][2]
+ q_uint = float_to_uint(q, -Q_MAX, Q_MAX, 16)
+ dq_uint = float_to_uint(dq, -DQ_MAX, DQ_MAX, 12)
+ tau_uint = float_to_uint(tau, -TAU_MAX, TAU_MAX, 12)
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ data_buf[0] = (q_uint >> 8) & 0xff
+ data_buf[1] = q_uint & 0xff
+ data_buf[2] = dq_uint >> 4
+ data_buf[3] = ((dq_uint & 0xf) << 4) | ((kp_uint >> 8) & 0xf)
+ data_buf[4] = kp_uint & 0xff
+ data_buf[5] = kd_uint >> 4
+ data_buf[6] = ((kd_uint & 0xf) << 4) | ((tau_uint >> 8) & 0xf)
+ data_buf[7] = tau_uint & 0xff
+ self.__send_data(DM_Motor.SlaveID, data_buf)
+ self.recv() # receive the data from serial port
+
+ def control_delay(self, DM_Motor, kp: float, kd: float, q: float, dq: float, tau: float, delay: float):
+ """
+ MIT Control Mode Function with delay 达妙电机MIT控制模式函数带延迟
+ :param DM_Motor: Motor object 电机对象
+ :param kp: kp
+ :param kd: kd
+ :param q: position 期望位置
+ :param dq: velocity 期望速度
+ :param tau: torque 期望力矩
+ :param delay: delay time 延迟时间 单位秒
+ """
+ self.controlMIT(DM_Motor, kp, kd, q, dq, tau)
+ sleep(delay)
+
+ def control_Pos_Vel(self, Motor, P_desired: float, V_desired: float):
+ """
+ control the motor in position and velocity control mode 电机位置速度控制模式
+ :param Motor: Motor object 电机对象
+ :param P_desired: desired position 期望位置
+ :param V_desired: desired velocity 期望速度
+ :return: None
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("Control Pos_Vel Error : Motor ID not found")
+ return
+ motorid = 0x100 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ P_desired_uint8s = float_to_uint8s(P_desired)
+ V_desired_uint8s = float_to_uint8s(V_desired)
+ data_buf[0:4] = P_desired_uint8s
+ data_buf[4:8] = V_desired_uint8s
+ self.__send_data(motorid, data_buf)
+ # time.sleep(0.001)
+ self.recv() # receive the data from serial port
+
+ def control_Vel(self, Motor, Vel_desired):
+ """
+ control the motor in velocity control mode 电机速度控制模式
+ :param Motor: Motor object 电机对象
+ :param Vel_desired: desired velocity 期望速度
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("control_VEL ERROR : Motor ID not found")
+ return
+ motorid = 0x200 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ Vel_desired_uint8s = float_to_uint8s(Vel_desired)
+ data_buf[0:4] = Vel_desired_uint8s
+ self.__send_data(motorid, data_buf)
+ self.recv() # receive the data from serial port
+
+ def control_pos_force(self, Motor, Pos_des: float, Vel_des, i_des):
+ """
+ control the motor in EMIT control mode 电机力位混合模式
+ :param Pos_des: desired position rad 期望位置 单位为rad
+ :param Vel_des: desired velocity rad/s 期望速度 为放大100倍
+ :param i_des: desired current rang 0-10000 期望电流标幺值放大10000倍
+ 电流标幺值:实际电流值除以最大电流值,最大电流见上电打印
+ """
+ if Motor.SlaveID not in self.motors_map:
+ print("control_pos_vel ERROR : Motor ID not found")
+ return
+ motorid = 0x300 + Motor.SlaveID
+ data_buf = np.array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ Pos_desired_uint8s = float_to_uint8s(Pos_des)
+ data_buf[0:4] = Pos_desired_uint8s
+ Vel_uint = np.uint16(Vel_des)
+ ides_uint = np.uint16(i_des)
+ data_buf[4] = Vel_uint & 0xff
+ data_buf[5] = Vel_uint >> 8
+ data_buf[6] = ides_uint & 0xff
+ data_buf[7] = ides_uint >> 8
+ self.__send_data(motorid, data_buf)
+ self.recv() # receive the data from serial port
+
+ def enable(self, Motor):
+ """
+ enable motor 使能电机
+ 最好在上电后几秒后再使能电机
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFC))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def enable_old(self, Motor ,ControlMode):
+ """
+ enable motor old firmware 使能电机旧版本固件,这个是为了旧版本电机固件的兼容性
+ 可恶的旧版本固件使能需要加上偏移量
+ 最好在上电后几秒后再使能电机
+ :param Motor: Motor object 电机对象
+ """
+ data_buf = np.array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfc], np.uint8)
+ enable_id = ((int(ControlMode)-1) << 2) + Motor.SlaveID
+ self.__send_data(enable_id, data_buf)
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def disable(self, Motor):
+ """
+ disable motor 失能电机
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFD))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def set_zero_position(self, Motor):
+ """
+ set the zero position of the motor 设置电机0位
+ :param Motor: Motor object 电机对象
+ """
+ self.__control_cmd(Motor, np.uint8(0xFE))
+ sleep(0.1)
+ self.recv() # receive the data from serial port
+
+ def recv(self):
+ # 把上次没有解析完的剩下的也放进来
+ data_recv = b''.join([self.data_save, self.serial_.read_all()])
+ packets = self.__extract_packets(data_recv)
+ for packet in packets:
+ data = packet[7:15]
+ CANID = (packet[6] << 24) | (packet[5] << 16) | (packet[4] << 8) | packet[3]
+ CMD = packet[1]
+ self.__process_packet(data, CANID, CMD)
+
+ def recv_set_param_data(self):
+ data_recv = self.serial_.read_all()
+ packets = self.__extract_packets(data_recv)
+ for packet in packets:
+ data = packet[7:15]
+ CANID = (packet[6] << 24) | (packet[5] << 16) | (packet[4] << 8) | packet[3]
+ CMD = packet[1]
+ self.__process_set_param_packet(data, CANID, CMD)
+
+ def __process_packet(self, data, CANID, CMD):
+ if CMD == 0x11:
+ if CANID != 0x00:
+ if CANID in self.motors_map:
+ q_uint = np.uint16((np.uint16(data[1]) << 8) | data[2])
+ dq_uint = np.uint16((np.uint16(data[3]) << 4) | (data[4] >> 4))
+ tau_uint = np.uint16(((data[4] & 0xf) << 8) | data[5])
+ MotorType_recv = self.motors_map[CANID].MotorType
+ Q_MAX = self.Limit_Param[MotorType_recv][0]
+ DQ_MAX = self.Limit_Param[MotorType_recv][1]
+ TAU_MAX = self.Limit_Param[MotorType_recv][2]
+ recv_q = uint_to_float(q_uint, -Q_MAX, Q_MAX, 16)
+ recv_dq = uint_to_float(dq_uint, -DQ_MAX, DQ_MAX, 12)
+ recv_tau = uint_to_float(tau_uint, -TAU_MAX, TAU_MAX, 12)
+ self.motors_map[CANID].recv_data(recv_q, recv_dq, recv_tau)
+ else:
+ MasterID=data[0] & 0x0f
+ if MasterID in self.motors_map:
+ q_uint = np.uint16((np.uint16(data[1]) << 8) | data[2])
+ dq_uint = np.uint16((np.uint16(data[3]) << 4) | (data[4] >> 4))
+ tau_uint = np.uint16(((data[4] & 0xf) << 8) | data[5])
+ MotorType_recv = self.motors_map[MasterID].MotorType
+ Q_MAX = self.Limit_Param[MotorType_recv][0]
+ DQ_MAX = self.Limit_Param[MotorType_recv][1]
+ TAU_MAX = self.Limit_Param[MotorType_recv][2]
+ recv_q = uint_to_float(q_uint, -Q_MAX, Q_MAX, 16)
+ recv_dq = uint_to_float(dq_uint, -DQ_MAX, DQ_MAX, 12)
+ recv_tau = uint_to_float(tau_uint, -TAU_MAX, TAU_MAX, 12)
+ self.motors_map[MasterID].recv_data(recv_q, recv_dq, recv_tau)
+
+
+ def __process_set_param_packet(self, data, CANID, CMD):
+ if CMD == 0x11 and (data[2] == 0x33 or data[2] == 0x55):
+ masterid=CANID
+ slaveId = ((data[1] << 8) | data[0])
+ if CANID==0x00: #防止有人把MasterID设为0稳一手
+ masterid=slaveId
+
+ if masterid not in self.motors_map:
+ if slaveId not in self.motors_map:
+ return
+ else:
+ masterid=slaveId
+
+ RID = data[3]
+ # 读取参数得到的数据
+ if is_in_ranges(RID):
+ #uint32类型
+ num = uint8s_to_uint32(data[4], data[5], data[6], data[7])
+ self.motors_map[masterid].temp_param_dict[RID] = num
+
+ else:
+ #float类型
+ num = uint8s_to_float(data[4], data[5], data[6], data[7])
+ self.motors_map[masterid].temp_param_dict[RID] = num
+
+
+ def addMotor(self, Motor):
+ """
+ add motor to the motor control object 添加电机到电机控制对象
+ :param Motor: Motor object 电机对象
+ """
+ self.motors_map[Motor.SlaveID] = Motor
+ if Motor.MasterID != 0:
+ self.motors_map[Motor.MasterID] = Motor
+ return True
+
+ def __control_cmd(self, Motor, cmd: np.uint8):
+ data_buf = np.array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, cmd], np.uint8)
+ self.__send_data(Motor.SlaveID, data_buf)
+
+ def __send_data(self, motor_id, data):
+ """
+ send data to the motor 发送数据到电机
+ :param motor_id:
+ :param data:
+ :return:
+ """
+ self.send_data_frame[13] = motor_id & 0xff
+ self.send_data_frame[14] = (motor_id >> 8)& 0xff #id high 8 bits
+ self.send_data_frame[21:29] = data
+ self.serial_.write(bytes(self.send_data_frame.T))
+
+ def __read_RID_param(self, Motor, RID):
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0x33, np.uint8(RID), 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.__send_data(0x7FF, data_buf)
+
+ def __write_motor_param(self, Motor, RID, data):
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0x55, np.uint8(RID), 0x00, 0x00, 0x00, 0x00], np.uint8)
+ if not is_in_ranges(RID):
+ # data is float
+ data_buf[4:8] = float_to_uint8s(data)
+ else:
+ # data is int
+ data_buf[4:8] = data_to_uint8s(int(data))
+ self.__send_data(0x7FF, data_buf)
+
+ def switchControlMode(self, Motor, ControlMode):
+ """
+ switch the control mode of the motor 切换电机控制模式
+ :param Motor: Motor object 电机对象
+ :param ControlMode: Control_Type 电机控制模式 example:MIT:Control_Type.MIT MIT模式
+ """
+ max_retries = 20
+ retry_interval = 0.1 #retry times
+ RID = 10
+ self.__write_motor_param(Motor, RID, np.uint8(ControlMode))
+ for _ in range(max_retries):
+ sleep(retry_interval)
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map:
+ if RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ if abs(self.motors_map[Motor.SlaveID].temp_param_dict[RID] - ControlMode) < 0.1:
+ return True
+ else:
+ return False
+ return False
+
+ def save_motor_param(self, Motor):
+ """
+ save the all parameter to flash 保存所有电机参数
+ :param Motor: Motor object 电机对象
+ :return:
+ """
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8)& 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.disable(Motor) # before save disable the motor
+ self.__send_data(0x7FF, data_buf)
+ sleep(0.001)
+
+ def change_limit_param(self, Motor_Type, PMAX, VMAX, TMAX):
+ """
+ change the PMAX VMAX TMAX of the motor 改变电机的PMAX VMAX TMAX
+ :param Motor_Type:
+ :param PMAX: 电机的PMAX
+ :param VMAX: 电机的VMAX
+ :param TMAX: 电机的TMAX
+ :return:
+ """
+ self.Limit_Param[Motor_Type][0] = PMAX
+ self.Limit_Param[Motor_Type][1] = VMAX
+ self.Limit_Param[Motor_Type][2] = TMAX
+
+ def refresh_motor_status(self,Motor):
+ """
+ get the motor status 获得电机状态
+ """
+ can_id_l = Motor.SlaveID & 0xff #id low 8 bits
+ can_id_h = (Motor.SlaveID >> 8) & 0xff #id high 8 bits
+ data_buf = np.array([np.uint8(can_id_l), np.uint8(can_id_h), 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00], np.uint8)
+ self.__send_data(0x7FF, data_buf)
+ self.recv() # receive the data from serial port
+
+ def change_motor_param(self, Motor, RID, data):
+ """
+ change the RID of the motor 改变电机的参数
+ :param Motor: Motor object 电机对象
+ :param RID: DM_variable 电机参数
+ :param data: 电机参数的值
+ :return: True or False ,True means success, False means fail
+ """
+ max_retries = 20
+ retry_interval = 0.05 #retry times
+
+ self.__write_motor_param(Motor, RID, data)
+ for _ in range(max_retries):
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map and RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ if abs(self.motors_map[Motor.SlaveID].temp_param_dict[RID] - data) < 0.1:
+ return True
+ else:
+ return False
+ sleep(retry_interval)
+ return False
+
+ def read_motor_param(self, Motor, RID):
+ """
+ read only the RID of the motor 读取电机的内部信息例如 版本号等
+ :param Motor: Motor object 电机对象
+ :param RID: DM_variable 电机参数
+ :return: 电机参数的值
+ """
+ max_retries = 20
+ retry_interval = 0.05 #retry times
+ self.__read_RID_param(Motor, RID)
+ for _ in range(max_retries):
+ sleep(retry_interval)
+ self.recv_set_param_data()
+ if Motor.SlaveID in self.motors_map:
+ if RID in self.motors_map[Motor.SlaveID].temp_param_dict:
+ return self.motors_map[Motor.SlaveID].temp_param_dict[RID]
+ return None
+
+ # -------------------------------------------------
+ # Extract packets from the serial data
+ def __extract_packets(self, data):
+ frames = []
+ header = 0xAA
+ tail = 0x55
+ frame_length = 16
+ i = 0
+ remainder_pos = 0
+
+ while i <= len(data) - frame_length:
+ if data[i] == header and data[i + frame_length - 1] == tail:
+ frame = data[i:i + frame_length]
+ frames.append(frame)
+ i += frame_length
+ remainder_pos = i
+ else:
+ i += 1
+ self.data_save = data[remainder_pos:]
+ return frames
+
+
+def LIMIT_MIN_MAX(x, min, max):
+ if x <= min:
+ x = min
+ elif x > max:
+ x = max
+
+
+def float_to_uint(x: float, x_min: float, x_max: float, bits):
+ LIMIT_MIN_MAX(x, x_min, x_max)
+ span = x_max - x_min
+ data_norm = (x - x_min) / span
+ return np.uint16(data_norm * ((1 << bits) - 1))
+
+
+def uint_to_float(x: np.uint16, min: float, max: float, bits):
+ span = max - min
+ data_norm = float(x) / ((1 << bits) - 1)
+ temp = data_norm * span + min
+ return np.float32(temp)
+
+
+def float_to_uint8s(value):
+ # Pack the float into 4 bytes
+ packed = pack('f', value)
+ # Unpack the bytes into four uint8 values
+ return unpack('4B', packed)
+
+
+def data_to_uint8s(value):
+ # Check if the value is within the range of uint32
+ if isinstance(value, int) and (0 <= value <= 0xFFFFFFFF):
+ # Pack the uint32 into 4 bytes
+ packed = pack('I', value)
+ else:
+ raise ValueError("Value must be an integer within the range of uint32")
+
+ # Unpack the bytes into four uint8 values
+ return unpack('4B', packed)
+
+
+def is_in_ranges(number):
+ """
+ check if the number is in the range of uint32
+ :param number:
+ :return:
+ """
+ if (7 <= number <= 10) or (13 <= number <= 16) or (35 <= number <= 36):
+ return True
+ return False
+
+
+def uint8s_to_uint32(byte1, byte2, byte3, byte4):
+ # Pack the four uint8 values into a single uint32 value in little-endian order
+ packed = pack('<4B', byte1, byte2, byte3, byte4)
+ # Unpack the packed bytes into a uint32 value
+ return unpack('x%WLuk)5N9WN&U;UO
zy>1h@o{jc4pH&F?8y`+z8a!Nuk-QIv5k^DOr!MU)uF_Xsm6BD$6sF!MOy%0H>gr5e
zCoO#jG-;{QaKuU!bE!LGH4UWYh?@bs!$s0X;Pj=y!&Ml`TVObGDI>1JC{ykeS7j>G
z;I1(poEy^G2)s`NKaB2laoMX2=O&C57|9wXWJJdh@jj(E16NGlS11w6SRH{nBAzxP
zJ#DNDV_ae2oEfaLuVp;ygqb)8-Yj?vc&p?sX0vL^Tfo-}{*`|lc)Q^1Z}_)?pDXxk
z;lBoat@Phu^K7B?Ujx4R244rhUh-#vUo81L@MjBtuJGRgzESe$ApY|u-vE9I`4Jh<
zBM<-F=q-$!`J53r`sI!^@`Df7RYHOfz|63KW{YSdG|OP5Jxr6$YpD>?AkZKa!9~R1
zh73t#E31In9&)u4&bvJO4A<|XS-_Wi=-=Yu>;(#EQK)1BfG$ekpk#(YBEAavR_lzc
zCtQ4StH-;d~Z1D=SW3qj3$y
zcn((^&UFkWZlZ#rxR!Ih*f?H7q8@`Ee~^T};2VKkbw209&x3KB-DofX&+B0%l55)-
zTuYH!!o%CK0gHbj@ddcOY=LQSLPYJ&d+q1@d$XS&w^&U<8n7z)5DcLNEjGFdbFo}tw1BG
zBkW7MtK`Lk4U{QLsThT#mP(~iv{Eq(MbDM2yo^qE9w{F4@sGbBA3mKPJ()iJ^Z4Me
z$-^g^JbCnBa`gCL^6~e7Jb!dJJ^B^6>A`oC!=F<;mzedWaV&D1}!yy7q-cwc5Sb8lI1fu
z7xDc&VDJN)U`Y{b(s|mZFvE;Y+p>Sn
KLJ4-wqWTY8&YDC3
literal 0
HcmV?d00001
diff --git a/qt/calculate/__pycache__/ik.cpython-39.pyc b/qt/calculate/__pycache__/ik.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6235dee05753a8c900043b85ccf34b8b9848f54c
GIT binary patch
literal 1819
zcma)7TTC2P7(Qoa*&PO0Zregjq4NmZx-6F$Vzh!9V;Wo1LLMWiRc_Yy;-3
zLFC$?u2q8Cf^1u$wGlLsf-5brKA8AE@xi1!!|oGLKI?zZESFSGJeza=`}cqUoSAI6
zs!Bw#&ixjT{wg5!gc?T|2gW(rMgR^4DL95?q;Lv9hGeYZw@{m>2odx~#79MGod#JB
zyAQU}1sDXSeFUck;!|)vA_6@d5aFn7M1*%`x?I783xFL!X_1r@u?eLsNQJ@;dW`cR
z`9Y+=Z6d|5PCcX^WCSK%8MHV-rbS2@5z&svOnZI^|Y6<
zqM4;SmtWdX{7Ts%r~hJl6)){cs)!#rRg-GQ=?{>UQvo>nXq<7W&f;_pboK(HT2jlf
z_b2walaex5rt4s05AZC5iR(!nsR#4`2~hM5HhMt={0!D#hYrF&rVg;Fj}b5NL9gS)
z!zMUJCr>w!GG><%X2aBtq=8NYXtzn@z6O((Q``?;W853y^g$lIrAB%-pe$~JojyTM
zKujRV|0@F+oO~B00vORB@D(ZUMU>#$=^d4m8bE1MDwuwfF+T;mNSeUk%(PRWwUB1g
zGQh!`Rhjx7-XBqdmCt=y*jaGqC!AY3XJM>3GGS*vFYILO>-X&Mm+i?%&g>6++f#22
zo6%%KvR4;&KLvYQWW|NYU)bYId)wbIi^2_^k%oID=hlPW2W!ssN+COAe|^uM&p3;d
z2RX6Ohq3M+$r
zD>tAX7~0z_OqphViuCg;VtV`qD|HsM-J7dGV|VnY!ozWUGG}it|o?%`UYx!QCGV8*qa&g|$uR&WQ8vl6$AlS8H}=+0IPCtT0(|12EbuEb*Oi
zETz7p>q*`6#KUIP;;v|xCk#VdyrCs5o@%wSswZF8u4#srOq7z1tGWq^G{>i?mvmJ%
zET0}$v~a>`J8j`13lCddECjm)b|>sE*xfRBAtG}ZBeFk{imQ6qOzQnHb-1vln_vP+~r%}>7sBU>(e@S-SM|v|n{6Fa%
z9E>L8YA`w!bh0zW`P)GymAtG4yOrcX@TwV%gkzCZENrSlZSc7(!x$cj)dxk|#dZGxUwpn#f=xGh^2v99XYS2ME9)KZnKS2{+|2xb=bXup
zjini$cYfb0MGj+s(PH>9v3LQm|1yMRl9$;!mvhpR_BFmPyG5FvFvge-(r{cMfX+AibHhIlURi)7!bY3hGOA;5wHIW
zM8I~qww5NS55|vt%Dti^bmM^%GsCki5sQQuNdzGvm*#)-}xGJ($
zUV3#uN>}Pz8?~xe3H->;SBhm9b7Q0I`9Wki^}Nqi2A}@tB1EoK-EwoKM(%ibyZg;v
zPS&fNUhbl-Ud~+!a)o@kP%q~LHy5bERVcd~11kaKry!Wf@L3Es
z3w>WG;!d9lNH===!|}a@kRHA#kTpVJ4lFg!^Q@1rfQ@`KGm?pKfU*&z{NSsfAKbk^
zjIu(mv8+%dv@Oa&z-jqGUImfiR-~GMDdK^eBL6fAf-M#^h2GWWWXpmNBl2%rdR!G1dGwfOI&sn4q>DdeFio^s1&ujUUQnt(aU{5e<(l)=4+{;Yr0U$7`*2-XrU
zBS6huxw8uB`!*x{kQ%QRyk!SKEfho^u#5}O87A{^12nb6Xjvqkg-Q*&m;oE;rp
z?|lF1fU~TrC_}TN`yM6@0Q5(Dx^{Looz#WnPGP9Wp+^RKt9k-jc65F8tvrgM>{g0F
zDY8qhS1bjm8MNbWtj@$IfAZss-d_%&X(}G;TZMMU5r(2O42lhKtj5<%$r}8yEsx!>;>y
YYIUnB>t*+OBHX7go)T$CkD2t?zZKxxGynhq
literal 0
HcmV?d00001
diff --git a/qt/calculate/calculate_angle.py b/qt/calculate/calculate_angle.py
new file mode 100644
index 0000000..6e8c764
--- /dev/null
+++ b/qt/calculate/calculate_angle.py
@@ -0,0 +1,138 @@
+# qt_main.py
+
+import numpy as np
+import matplotlib.pyplot as plt
+from ik import inverseF # 假设这是你自己的逆运动学函数
+from matplotlib.animation import FuncAnimation
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 杆长参数
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+
+# 1. 轨迹生成函数:每个轨迹类型独立封装,支持外部传参
+# --------------------------------------------------
+
+def generate_circle(center=(100, 300), radius=40):
+ from trajectory import circle_trajectory
+ return circle_trajectory(center=center, radius=radius)
+
+def generate_line(start=(125, 300), end=(125, 400)):
+ from trajectory import line_trajectory
+ return line_trajectory(start=start, end=end)
+
+def generate_ellipse(center=(100, 200), rx=50, ry=25):
+ from trajectory import ellipse_trajectory
+ return ellipse_trajectory(center=center, rx=rx, ry=ry)
+
+def generate_square(side=60, start_point=(100, 200)):
+ from trajectory import square_trajectory
+ return square_trajectory(side=side, start_point=start_point)
+
+def generate_triangle(base_length=100, height=80, base_center=(100, 200)):
+ from trajectory import triangle_trajectory
+ return triangle_trajectory(base_length=base_length, height=height, base_center=base_center)
+
+
+# 4. 主函数:根据轨迹类型调用对应函数并执行
+# --------------------------------------------------
+
+
+def main_of_5dof(trajectory_type='circle', show_animation=True, **kwargs):
+ """
+ 主函数:根据轨迹类型和参数生成轨迹、计算角度、并可视化
+ 支持传参:
+ - circle: center, radius
+ - line: start, end
+ - ellipse: center, rx, ry
+ - square: side, start_point
+ - triangle: base_length, height, base_center
+ - show_animation: 是否显示动画 (True/False)
+ """
+
+ # 根据轨迹类型生成轨迹
+ if trajectory_type == 'circle':
+ x_list, y_list = generate_circle(
+ center=kwargs.get('center', (100, 300)),
+ radius=kwargs.get('radius', 40)
+ )
+ elif trajectory_type == 'line':
+ x_list, y_list = generate_line(
+ start=kwargs.get('start', (125, 300)),
+ end=kwargs.get('end', (125, 400))
+ )
+ # 其他轨迹类型的处理...
+
+ # 实时计算并输出角度(无论是否显示动画)
+ for i in range(len(x_list)):
+ x = x_list[i]
+ y = y_list[i]
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ print(f"第 {i} 个点: theta1 = {np.degrees(theta1):.2f}°, theta4 = {np.degrees(theta4):.2f}°")
+ except Exception as e:
+ print(f"第 {i} 点计算失败: {e}")
+
+ # 如果需要显示动画
+ if show_animation:
+ fig, ax = plt.subplots()
+ ax.set_xlim(-300, 500)
+ ax.set_ylim(0, 500)
+ ax.set_aspect('equal')
+ ax.grid(True)
+ ax.set_title("五连杆末端沿轨迹运动")
+ ax.plot(x_list, y_list, 'b--', label='理想轨迹')
+ line, = ax.plot([], [], 'r-o', linewidth=2, markersize=6, label='五连杆结构')
+
+ def draw_frame(i):
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ except Exception as e:
+ print(f"第 {i} 帧: 计算失败 -> {e}")
+ theta1 = theta4 = None
+
+ if theta1 is None or theta4 is None:
+ line.set_data([], [])
+ return line,
+
+ # 左右臂坐标计算
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ x4 = L4 * np.cos(theta4) + L0
+ y4 = L4 * np.sin(theta4)
+
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+
+ line.set_data(x_coords, y_coords)
+ return line,
+
+ ani = FuncAnimation(fig, draw_frame, frames=len(x_list), interval=50, blit=True)
+ plt.legend()
+ plt.show()
+
+# 📌 运行主函数
+if __name__ == "__main__":
+ main_of_5dof(
+ trajectory_type='circle',
+ center=(150, 250),
+ radius=60,
+ show_animation=False # 设置为 False 则不显示动画
+ )
+
+ # 示例:其他轨迹使用方式
+ # main_of_5dof(trajectory_type='line', start=(0, 0), end=(200, 300), show_animation=False)
+ # main_of_5dof(trajectory_type='ellipse', center=(100, 200), rx=80, ry=40)
+ # main_of_5dof(trajectory_type='square', side=100, start_point=(100, 200))
+ # main_of_5dof(trajectory_type='triangle', base_length=120, height=100, base_center=(100, 200))
\ No newline at end of file
diff --git a/qt/calculate/fk.py b/qt/calculate/fk.py
new file mode 100644
index 0000000..07200bf
--- /dev/null
+++ b/qt/calculate/fk.py
@@ -0,0 +1,40 @@
+import numpy as np
+
+
+def forwardF(u1, u4, omega1, omega4, l1, l2, l3, l4, l5, alpha1, alpha4):
+ # 位置分析
+ xb = l1 * np.cos(u1)
+ yb = l1 * np.sin(u1)
+ xd = l5 + l4 * np.cos(u4)
+ yd = l4 * np.sin(u4)
+ lbd = np.sqrt((xd - xb) ** 2 + (yd - yb) ** 2)
+ A0 = 2 * l2 * (xd - xb)
+ B0 = 2 * l2 * (yd - yb)
+ C0 = l2 ** 2 + lbd ** 2 - l3 ** 2
+ u2 = 2 * np.arctan((B0 + np.sqrt(A0 ** 2 + B0 ** 2 - C0 ** 2)) / (A0 + C0))
+ xc = xb + l2 * np.cos(u2)
+ yc = yb + l2 * np.sin(u2)
+ u3 = np.arctan2((yc - yd), (xc - xd)) + np.pi
+
+ # 速度分析
+ A = np.array([[l2 * np.sin(u2), -l3 * np.sin(u3)],
+ [l2 * np.cos(u2), -l3 * np.cos(u3)]])
+ B = np.array([-l1 * omega1 * np.sin(u1) + l4 * omega4 * np.sin(u4),
+ -l1 * omega1 * np.cos(u1) + l4 * omega4 * np.cos(u4)])
+ omega = np.linalg.solve(A, B)
+ omega2, omega3 = omega[0], omega[1]
+
+ # 加速度分析
+ Aa = np.array([[l2 * np.sin(u2), -l3 * np.sin(u3)],
+ [l2 * np.cos(u2), -l3 * np.cos(u3)]])
+ Ba = np.array([l3 * np.cos(u3) * omega3 ** 2 - l2 * np.cos(u2) * omega2 ** 2 + l4 * np.cos(
+ u4) * omega4 ** 2 + l4 * np.sin(u4) * alpha4 - l1 * np.cos(u1) * omega1 ** 2 - l1 * np.sin(u1) * alpha1,
+ -l3 * np.sin(u3) * omega3 ** 2 + l2 * np.sin(u2) * omega2 ** 2 - l4 * np.sin(
+ u4) * omega4 ** 2 + l4 * np.cos(u4) * alpha4 + l1 * np.sin(u1) * omega1 ** 2 - l1 * np.cos(
+ u1) * alpha1])
+ alpha = np.linalg.solve(Aa, Ba)
+
+ return xc, yc, u2, u3, omega2, omega3, alpha[0], alpha[1]
+
+# 示例调用
+# xc, yc, u2, u3, omega2, omega3, alpha2, alpha3 = five(np.pi/6, np.pi/4, 1, 2, 1, 2, 3, 4, 5, 0.1, 0.2)
\ No newline at end of file
diff --git a/qt/calculate/ik.py b/qt/calculate/ik.py
new file mode 100644
index 0000000..6046976
--- /dev/null
+++ b/qt/calculate/ik.py
@@ -0,0 +1,69 @@
+import math
+
+
+def inverseF(x, y, l1, l2, l3, l4, l5):
+ """
+ 五连杆机构逆向运动学函数(Python 实现)
+
+ 输入:
+ x, y: 末端执行器坐标
+ l1~l5: 各杆长度
+
+ 输出:
+ theta1, theta2: 两个主动关节角度(弧度)
+ """
+ Xc = x
+ Yc = y
+
+ # ===== 左侧链路(l1, l2)=====
+ numerator_left = Xc ** 2 + Yc ** 2 - l1 ** 2 - l2 ** 2
+ denominator_left = 2 * l1 * l2
+ cosfoai_12 = numerator_left / denominator_left
+
+ if cosfoai_12 > 1 or cosfoai_12 < -1:
+ raise ValueError("目标点超出工作空间!左侧无解。")
+ foai_12 = 2 * math.pi - math.acos(cosfoai_12)
+
+ # 求第一个电机角度 foai_01
+ numerator_foai01 = l2 * Yc * math.sin(foai_12) + Xc * (l2 * math.cos(foai_12) + l1)
+ denominator_foai01 = (l2 * math.cos(foai_12) + l1) ** 2 + (l2 * math.sin(foai_12)) ** 2
+
+ if denominator_foai01 == 0:
+ raise ZeroDivisionError("分母为零,无法计算左侧角度。")
+
+ cosfoai_01 = numerator_foai01 / denominator_foai01
+ if cosfoai_01 > 1 or cosfoai_01 < -1:
+ raise ValueError("cosfoai_01 超出 [-1, 1],左侧无解。")
+ foai_01 = math.acos(cosfoai_01)
+
+ # ===== 右侧链路(l3, l4)=====
+ Xc_shifted = Xc - l5
+ numerator_right = Xc_shifted ** 2 + Yc ** 2 - l3 ** 2 - l4 ** 2
+ denominator_right = 2 * l3 * l4
+ cosfoai_34 = numerator_right / denominator_right
+
+ if cosfoai_34 > 1 or cosfoai_34 < -1:
+ raise ValueError("目标点超出工作空间!右侧无解。")
+ foai_34 = 2 * math.pi - math.acos(cosfoai_34)
+
+ A = l5 - Xc
+ B = l3 * math.sin(foai_34)
+ C = l4 + l3 * math.cos(foai_34)
+
+ if B == 0 and C == 0:
+ raise ZeroDivisionError("B 和 C 均为零,无法计算右侧角度。")
+
+ try:
+ foai_t = math.acos(B / math.sqrt(B ** 2 + C ** 2))
+ foai_40 = foai_t - math.asin(A / math.sqrt(B ** 2 + C ** 2))
+ except:
+ raise ValueError("右侧三角函数计算失败,请检查输入是否合法。")
+
+ # 转换为角度再转回弧度
+ theta1_deg = math.degrees(foai_01)
+ theta2_deg = 180 - math.degrees(foai_40)
+
+ theta1 = math.radians(theta1_deg)
+ theta2 = math.radians(theta2_deg)
+
+ return theta1, theta2
\ No newline at end of file
diff --git a/qt/calculate/test_ik.py b/qt/calculate/test_ik.py
new file mode 100644
index 0000000..5e3ee00
--- /dev/null
+++ b/qt/calculate/test_ik.py
@@ -0,0 +1,64 @@
+import math
+from ik import inverseF
+from fk import forwardF
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 输入数据
+l1 = 250
+l2 = 300
+l3 = 300
+l4 = 250
+l5 = 250
+x = 125
+#y = 382.338
+y = 300
+omega1 = omega4 = 500
+alpha1 = alpha4 = 0
+
+# 逆解
+u1, u4 = inverseF(x, y, l1, l2, l3, l4, l5)
+
+# 正解验证
+xc, yc, u2, u3, omega, alpha, _, _ = forwardF(u1, u4, omega1, omega4, l1, l2, l3, l4, l5, alpha1, alpha4)
+
+# 左侧链路
+x1, y1 = 0, 0
+x2 = l1 * math.cos(u1)
+y2 = l1 * math.sin(u1)
+
+# 右侧链路
+x5, y5 = l5, 0
+x4 = l4 * math.cos(u4)+l5 # 注意方向
+y4 = l4 * math.sin(u4)
+
+# 绘图
+plt.figure(figsize=(8, 8))
+
+# 左侧链路
+plt.plot([x1, x2, xc], [y1, y2, yc], 'b-o', label='左侧链路')
+
+# 右侧链路
+plt.plot([x5, x4, xc], [y5, y4, yc], 'r-o', label='右侧链路')
+
+# 标记关键点
+plt.plot(x1, y1, 'ro') # O1
+plt.plot(x5, y5, 'go') # O2
+plt.plot(x2, y2, 'yo') # B
+plt.plot(x4, y4, 'mo') # D
+plt.plot(xc, yc, 'ko', markersize=10) # C(末端)
+
+# 设置图形
+plt.grid(True)
+plt.axis('equal')
+plt.xlim([-200, l5 + 200])
+plt.ylim([-200, 600])
+plt.title('SCARA 五连杆逆解结构图')
+plt.xlabel('X (mm)')
+plt.ylabel('Y (mm)')
+plt.legend()
+plt.show()
\ No newline at end of file
diff --git a/qt/calculate/traj_fk.py b/qt/calculate/traj_fk.py
new file mode 100644
index 0000000..d12693f
--- /dev/null
+++ b/qt/calculate/traj_fk.py
@@ -0,0 +1,62 @@
+import numpy as np
+
+from fk import forwardF
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+
+# 杆长参数
+l1 = 50
+l2 = 50
+l3 = 50
+l4 = 50
+l5 = 50
+
+# 初始角度(弧度)
+u1_base = np.deg2rad(120) # 左侧电机初始角
+u4_base = np.deg2rad(120) # 右侧电机初始角
+
+# 设置绘图区域
+plt.figure(figsize=(8, 8))
+ax = plt.gca()
+ax.set_xlim(-100, l5 + 100)
+ax.set_ylim(-100, 100)
+ax.set_aspect('equal')
+ax.grid(True)
+ax.set_title("五连杆机构运动仿真(调用FK函数)")
+
+for i in range(1, 61):
+ # 更新两个驱动臂的角度
+ angle1 = u1_base - np.deg2rad(1.75 * i) # 左侧角度变化
+ angle4 = u4_base + np.deg2rad(1.75 * i) # 右侧角度变化
+
+ #正向运动学函数获取末端位置和中间角度
+ result = forwardF(angle1, angle4, 0, 0, l1, l2, l3, l4, l5, 0, 0)
+ xc, yc, u2, u3 = result[:4]
+
+ # 构建各点坐标
+ x = [0, l1*np.cos(angle1), xc, l4*np.cos(angle4)+l5, l5]
+ y = [0, l1*np.sin(angle1), yc, l4*np.sin(angle4), 0]
+
+ # 清除上一帧并绘制新图形
+ ax.cla()
+ ax.set_xlim(-100, l5 + 100)
+ ax.set_ylim(-100, 100)
+ ax.set_aspect('equal')
+ ax.grid(True)
+ ax.set_title("五连杆机构运动仿真(调用FK函数)")
+
+ # 绘制结构线和关键点
+ ax.plot(x, y, 'r-o', linewidth=2, markersize=6, markerfacecolor='red')
+ ax.plot(x[0], y[0], 'go') # 原点
+ ax.plot(x[1], y[1], 'bo') # 第二个点
+ ax.plot(x[2], y[2], 'mo') # 中间点
+ ax.plot(x[3], y[3], 'co') # 第四个点
+ ax.plot(x[4], y[4], 'yo') # 最后一个点
+
+ plt.pause(0.1)
+
+plt.close()
\ No newline at end of file
diff --git a/qt/calculate/traj_main.py b/qt/calculate/traj_main.py
new file mode 100644
index 0000000..5df2cfb
--- /dev/null
+++ b/qt/calculate/traj_main.py
@@ -0,0 +1,71 @@
+# main_animation.py
+import numpy as np
+import matplotlib.pyplot as plt
+from ik import inverseF
+from trajectory import circle_trajectory, line_trajectory, ellipse_trajectory, square_trajectory, triangle_trajectory
+from matplotlib.animation import FuncAnimation
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# 杆长参数
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+# 设置绘图区域
+fig, ax = plt.subplots()
+ax.set_xlim(-300, 500)
+ax.set_ylim(0, 500)
+ax.set_aspect('equal')
+ax.grid(True)
+ax.set_title("五连杆末端沿轨迹运动")
+
+line, = ax.plot([], [], 'r-o', linewidth=2, markersize=6)
+# 选择轨迹类型:
+TRAJECTORY_TYPE = 'ellipse' # 可选: circle, line, ellipse, square, triangle
+if TRAJECTORY_TYPE == 'line':
+ x_list, y_list = circle_trajectory(center=(100, 300), radius=40)
+elif TRAJECTORY_TYPE == 'line':
+ x_list, y_list = line_trajectory(start=(125, 300), end=(125, 400))
+elif TRAJECTORY_TYPE == 'ellipse':
+ x_list, y_list = ellipse_trajectory(center=(100, 200), rx=50, ry=25)
+elif TRAJECTORY_TYPE == 'square':
+ x_list, y_list = square_trajectory(side=60)
+elif TRAJECTORY_TYPE == 'triangle':
+ x_list, y_list = triangle_trajectory(base_length=100, height=80)
+else:
+ raise ValueError("未知的轨迹类型,请选择 circle / line / ellipse / square / triangle")
+
+# 动画函数
+def draw_frame(i):
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ print(theta1)
+ print(theta4)
+ # 左侧电机臂末端
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ # 右侧电机臂末端
+ x4 = L4 * np.cos(theta4)+L0
+ y4 = L4 * np.sin(theta4)
+ # 构建点序列
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+ line.set_data(x_coords, y_coords)
+ except Exception as e:
+ print(f"第 {i} 帧跳过,错误: {e}")
+ line.set_data([], [])
+
+ return line,
+
+# 创建动画
+ani = FuncAnimation(fig, draw_frame, frames=len(x_list), interval=100, repeat=True)
+
+plt.show()
\ No newline at end of file
diff --git a/qt/calculate/trajectory.py b/qt/calculate/trajectory.py
new file mode 100644
index 0000000..031033d
--- /dev/null
+++ b/qt/calculate/trajectory.py
@@ -0,0 +1,62 @@
+# trajectory.py
+
+import numpy as np
+
+def circle_trajectory(center=(80, 0), radius=40, num_points=60):
+ """ 圆形轨迹 """
+ angles = np.linspace(0, 2 * np.pi, num_points)
+ x_list = center[0] + radius * np.cos(angles)
+ y_list = center[1] + radius * np.sin(angles)
+ return x_list, y_list
+
+def line_trajectory(start=(40, 0), end=(120, 0), num_points=60):
+ """ 直线轨迹 """
+ t = np.linspace(0, 1, num_points)
+ x_list = start[0] + t * (end[0] - start[0])
+ y_list = start[1] + t * (end[1] - start[1])
+ return x_list, y_list
+
+def ellipse_trajectory(center=(80, 0), rx=50, ry=25, num_points=60):
+ """ 椭圆轨迹 """
+ angles = np.linspace(0, 2 * np.pi, num_points)
+ x_list = center[0] + rx * np.cos(angles)
+ y_list = center[1] + ry * np.sin(angles)
+ return x_list, y_list
+
+def square_trajectory(side=60, num_points=60):
+ """ 正方形轨迹 """
+ x_list, y_list = [], []
+ for i in range(num_points):
+ t = i / num_points
+ if t < 0.25:
+ x = 80 + 60 * t * 4
+ y = 0
+ elif t < 0.5:
+ x = 140
+ y = 0 + 60 * (t - 0.25) * 4
+ elif t < 0.75:
+ x = 140 - 60 * (t - 0.5) * 4
+ y = 60
+ else:
+ x = 80
+ y = 60 - 60 * (t - 0.75) * 4
+ x_list.append(x)
+ y_list.append(y)
+ return x_list, y_list
+
+def triangle_trajectory(base_length=100, height=80, num_points=60):
+ """ 三角形轨迹 """
+ x_list, y_list = [], []
+ points = [(80, 0), (130, 80), (30, 80), (80, 0)]
+ for i in range(num_points):
+ idx = int(i / num_points * 3)
+ t = (i % (num_points // 3)) / (num_points // 3)
+ x = points[idx][0] + t * (points[idx+1][0] - points[idx][0])
+ y = points[idx][1] + t * (points[idx+1][1] - points[idx][1])
+ x_list.append(x)
+ y_list.append(y)
+ return x_list, y_list
+
+def custom_trajectory(custom_x, custom_y):
+ """ 自定义轨迹,输入两个列表即可 """
+ return custom_x, custom_y
\ No newline at end of file
diff --git a/qt/can_test_motor/can_main.py b/qt/can_test_motor/can_main.py
new file mode 100644
index 0000000..97c37ef
--- /dev/null
+++ b/qt/can_test_motor/can_main.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python 主程序
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/21
+# @Author : hx
+# @File : can_main.py
+'''
+
+import can
+import time
+
+# 导入你写的 generate_position_command 函数
+from motor_control_can_all import generate_position_command
+
+
+# ========================================
+# 发送CAN帧函数
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='vcan0', interface='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 监听CAN反馈函数
+# ========================================
+def listen_can_feedback(channel='vcan0', interface='socketcan', timeout=2.0):
+ """
+ 监听是否有CAN反馈数据
+ :param channel: CAN 通道
+ :param interface: 总线类型
+ :param timeout: 等待反馈的超时时间(秒)
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, interface=interface)
+ print(f"[监听反馈] 等待来自 {channel} 的反馈({timeout}秒超时)...")
+ msg = bus.recv(timeout=timeout)
+ if msg:
+ print(f"[收到反馈] ID={msg.arbitration_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in msg.data)}]")
+ else:
+ print("[未收到反馈]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[监听失败] {e}")
+
+
+# ========================================
+# 主函数:发送位置控制指令并监听反馈
+# ========================================
+
+def can_motor_contral(direction=0, angle=100, microstep=32, can_id=0x01, channel='vcan0', listen_feedback=False):
+ print("=== 开始发送CAN位置控制指令 ===")
+ print(f"参数:方向={direction}(0=逆时针,1=顺时针),角度={angle}°,细分={microstep}")
+
+ # 生成CAN数据帧
+ can_data = generate_position_command(direction=direction, microstep=microstep, angle=angle)
+ print(f"生成的CAN数据帧: [{', '.join(f'0x{x:02X}' for x in can_data)}]")
+
+ # 发送CAN帧
+ send_can_frame(can_data, can_id=can_id, channel=channel)
+
+ # 如果启用监听,等待反馈
+ if listen_feedback:
+ listen_can_feedback(channel=channel)
+
+ print("=== 电机控制流程完成 ===")
+
+# ========================================
+# 程序入口(直接运行时使用)
+# ========================================
+if __name__ == "__main__":
+ # 这里写死参数,方便调试
+ can_motor_contral(
+ direction=1, # 顺时针
+ angle=180, # 180度
+ microstep=16, # 细分值16
+ can_id=0x02, # CAN ID 0x02
+ channel='vcan0', # 使用虚拟CAN
+ listen_feedback=True # 是否监听反馈
+ )
\ No newline at end of file
diff --git a/qt/can_test_motor/can_run_demo.py b/qt/can_test_motor/can_run_demo.py
new file mode 100644
index 0000000..0df8e0f
--- /dev/null
+++ b/qt/can_test_motor/can_run_demo.py
@@ -0,0 +1,15 @@
+# can_run_demo.py 调用程序demo
+
+from can_main import can_motor_contral
+
+
+
+if __name__ == "__main__":
+ can_motor_contral(
+ direction=1,
+ angle=90,
+ microstep=32,
+ can_id=0x03,
+ channel='vcan0',
+ listen_feedback=True
+ )
\ No newline at end of file
diff --git a/qt/can_test_motor/motor_control_can_all.py b/qt/can_test_motor/motor_control_can_all.py
new file mode 100644
index 0000000..acd7914
--- /dev/null
+++ b/qt/can_test_motor/motor_control_can_all.py
@@ -0,0 +1,174 @@
+#!/usr/bin/env python
+# 不同模式can发送指令集成
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/7/21
+# @Author : hx
+# @File : motor_control_can_all.py
+'''
+
+import can
+import time
+import argparse
+
+
+# 细分值映射表
+MICROSTEP_MAP = {
+ 2: 0x01,
+ 4: 0x02,
+ 8: 0x04,
+ 16: 0x10,
+ 32: 0x20
+}
+
+# ========================================
+# CAN 发送函数(使用 socketcan)
+# ========================================
+def send_can_frame(data, can_id=0x01, channel='can0', bustype='socketcan'):
+ """
+ 发送CAN帧
+ :param data: 7字节的列表
+ :param can_id: CAN ID
+ :param channel: CAN 通道
+ :param bustype: 总线类型
+ """
+ try:
+ bus = can.interface.Bus(channel=channel, bustype=bustype)
+ msg = can.Message(arbitration_id=can_id, data=data, is_extended_id=False)
+ bus.send(msg)
+ print(f"[发送CAN帧] ID={can_id:02X} 数据=[{', '.join(f'0x{x:02X}' for x in data)}]")
+ bus.shutdown()
+ except Exception as e:
+ print(f"[发送失败] {e}")
+
+
+# ========================================
+# 模式1:速度控制模式 (0x01)
+# ========================================
+def generate_speed_command(direction, microstep, speed):
+ """
+ 生成速度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+ return [0x01, direction_byte, microstep_byte, 0x00, 0x00, speed_high, speed_low]
+
+
+# ========================================
+# 模式2:位置控制模式 (0x02)
+# ========================================
+def generate_position_command(direction, microstep, angle):
+ """
+ 生成位置控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 角度值 (单位: 度)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ return [0x02, direction_byte, microstep_byte, pos_high, pos_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式3:力矩控制模式 (0x03)
+# ========================================
+def generate_torque_command(direction, microstep, current):
+ """
+ 生成力矩控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param current: 电流值 (单位: mA)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_current = int(current)
+ current_high = (raw_current >> 8) & 0xFF
+ current_low = raw_current & 0xFF
+
+ return [0x03, direction_byte, microstep_byte, current_high, current_low, 0x00, 0x64]
+
+
+# ========================================
+# 模式4:单圈绝对角度控制模式 (0x04)
+# ========================================
+def generate_absolute_angle_command(direction, microstep, angle, speed):
+ """
+ 生成单圈绝对角度控制指令
+ :param direction: 方向 (0:逆时针, 1:顺时针)
+ :param microstep: 细分值 (2, 4, 8, 16, 32)
+ :param angle: 目标角度 (单位: 度)
+ :param speed: 速度值 (单位: rad/s)
+ """
+ direction_byte = 0x01 if direction == 1 else 0x00
+ microstep_byte = MICROSTEP_MAP.get(microstep, 0x20)
+
+ raw_angle = int(angle * 10)
+ pos_high = (raw_angle >> 8) & 0xFF
+ pos_low = raw_angle & 0xFF
+
+ raw_speed = int(speed * 10)
+ speed_high = (raw_speed >> 8) & 0xFF
+ speed_low = raw_speed & 0xFF
+
+
+ return [0x04, direction_byte, microstep_byte, pos_high, pos_low, speed_high, speed_low]
+
+
+# ========================================
+# 主函数(命令行调用)
+# ========================================
+def main():
+ parser = argparse.ArgumentParser(description="CAN电机控制程序,支持4种模式")
+ parser.add_argument("--mode", type=int, choices=[1, 2, 3, 4],
+ help="控制模式: 1=速度, 2=位置, 3=力矩, 4=绝对角度")
+ parser.add_argument("--direction", type=int, choices=[0, 1], default=1,
+ help="方向: 0=逆时针, 1=顺时针")
+ parser.add_argument("--microstep", type=int, choices=[2, 4, 8, 16, 32], default=32,
+ help="细分值: 2, 4, 8, 16, 32")
+ parser.add_argument("--value", type=float, required=True,
+ help="控制值(速度: rad/s, 位置/角度: 度, 力矩: mA)")
+ parser.add_argument("--speed_rad_per_sec", type=float,
+ help="速度值(仅用于绝对角度模式)")
+ args = parser.parse_args()
+
+ try:
+ if args.mode == 1:
+ cmd = generate_speed_command(args.direction, args.microstep, args.value)
+ elif args.mode == 2:
+ cmd = generate_position_command(args.direction, args.microstep, args.value)
+ elif args.mode == 3:
+ cmd = generate_torque_command(args.direction, args.microstep, args.value)
+ elif args.mode == 4:
+ if args.speed_rad_per_sec is None:
+ raise ValueError("绝对角度模式需要提供速度参数 (--speed_rad_per_sec)")
+ cmd = generate_absolute_angle_command(args.direction, args.microstep, args.value, args.speed_rad_per_sec)
+
+ print(f"生成CAN指令: [{', '.join(f'0x{x:02X}' for x in cmd)}]")
+ send_can_frame(cmd)
+
+ except Exception as e:
+ print(f"错误: {e}")
+
+
+# ========================================
+# 程序入口
+# ========================================
+if __name__ == "__main__":
+ main()
+ #python motor_control_can_all.py --mode 1 --direction 1 --microstep 32 --value 10
+ #python motor_control_can_all.py --mode 2 --direction 1 --microstep 32 --value 1000
+ #python motor_control_can_all.py --mode 3 --direction 1 --microstep 32 --value 1000
+
diff --git a/qt/qt_main.py b/qt/qt_main.py
new file mode 100644
index 0000000..8687957
--- /dev/null
+++ b/qt/qt_main.py
@@ -0,0 +1,146 @@
+import sys
+import numpy as np
+import matplotlib
+
+matplotlib.use('Qt5Agg') # 必须在导入 pyplot 前设置
+
+from PyQt5.QtWidgets import QApplication, QDialog, QVBoxLayout
+from PyQt5.QtGui import QStandardItemModel, QStandardItem # ✅ 正确的模块
+from calculate.trajectory import circle_trajectory, line_trajectory
+from calculate.ik import inverseF
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.animation import FuncAnimation
+
+# 导入你生成的界面
+from untitled import Ui_Dialog
+
+import matplotlib.pyplot as plt
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# 杆长参数
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+
+class MyDialog(QDialog, Ui_Dialog):
+ def __init__(self):
+ super().__init__()
+ self.setupUi(self)
+
+ # ****** 新增:为 listView 初始化模型 ******
+ self.angle_model = QStandardItemModel(self.listView)
+ self.listView.setModel(self.angle_model)
+
+ # 创建 matplotlib 图形
+ self.figure = Figure(figsize=(8, 6), dpi=100)
+ self.ax = self.figure.add_subplot(111)
+ self.ax.set_xlim(-300, 500)
+ self.ax.set_ylim(0, 500)
+ self.ax.set_aspect('equal')
+ self.ax.grid(True)
+ self.ax.set_title("五连杆沿轨迹运动")
+
+ # 创建画布
+ self.canvas = FigureCanvas(self.figure)
+ self.widget.setLayout(QVBoxLayout())
+ self.widget.layout().addWidget(self.canvas)
+
+ # 添加 line 对象
+ self.line, = self.ax.plot([], [], 'r-o', linewidth=2, markersize=6)
+ # 初始化轨迹数据
+ self.x_list = []
+ self.y_list = []
+ self.ani = None # 动画对象
+
+ # 连接按钮
+ self.pushButton.clicked.connect(self.start_line_trajectory)
+ self.pushButton_2.clicked.connect(self.start_circle_trajectory)
+
+ def start_line_trajectory(self):
+ # 清除旧的角度数据显示
+ self.angle_model.clear()
+
+ self.ax.set_title("五连杆 - 直线轨迹运动")
+ x, y = line_trajectory(start=(125, 300), end=(125, 400))
+ self.start_animation(x, y)
+
+ def start_circle_trajectory(self):
+ # 清除旧的角度数据显示
+ self.angle_model.clear()
+
+ self.ax.set_title("五连杆 - 圆形轨迹运动")
+ x, y = circle_trajectory(center=(100, 200), radius=40)
+ self.start_animation(x, y)
+
+ def start_animation(self, x_list, y_list):
+ self.x_list = x_list
+ self.y_list = y_list
+
+ # 安全停止旧动画
+ try:
+ if self.ani is not None and self.ani.event_source is not None:
+ self.ani.event_source.stop()
+ except:
+ pass # 忽略停止过程中的任何异常
+
+
+ # 动画函数
+ def draw_frame(i):
+ x = x_list[i]
+ y = y_list[i]
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+
+ # ****** 新增:将弧度转换为角度并显示 ******
+ theta1_deg = np.degrees(theta1)
+ theta4_deg = np.degrees(theta4)
+ angle_text = f"点 {i}: θ1={theta1_deg:.1f}°, θ4={theta4_deg:.1f}°"
+
+ # 限制 listView 只显示最近 10 条
+ if self.angle_model.rowCount() >= 10:
+ self.angle_model.removeRow(0)
+ # 创建新项并添加
+ item = QStandardItem(angle_text)
+ self.angle_model.appendRow(item)
+
+ # 左侧电机臂末端
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+
+ # 右侧电机臂末端
+ x4 = L4 * np.cos(theta4) + L0
+ y4 = L4 * np.sin(theta4)
+
+ # 构建点序列
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+
+ self.line.set_data(x_coords, y_coords)
+ except Exception as e:
+ print(f"第 {i} 帧错误: {e}")
+ return self.line,
+
+ # 创建新动画
+ self.ani = FuncAnimation(
+ self.figure,
+ draw_frame,
+ frames=len(x_list),
+ interval=100,
+ repeat=False ,
+ blit=False # PyQt 中 blit=True 有时有问题,先设 False
+ )
+ self.canvas.draw()
+
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ dialog = MyDialog()
+ dialog.show()
+ sys.exit(app.exec_())
\ No newline at end of file
diff --git a/qt/qt_motor_main.py b/qt/qt_motor_main.py
new file mode 100644
index 0000000..eea04f8
--- /dev/null
+++ b/qt/qt_motor_main.py
@@ -0,0 +1,305 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+'''
+# @Time : 2025/08/04
+# @Author : hx
+# @File : qt_main.py
+# 功能:PyQt5 GUI + 五连杆轨迹动画 + 真实电机控制(DM4310 + 轮趣切割)
+# 修改:使用 qiege_motor.control() 统一控制切割电机
+'''
+
+import sys
+import time
+import numpy as np
+import matplotlib
+matplotlib.use('Qt5Agg')
+from PyQt5.QtWidgets import (
+ QApplication, QDialog, QVBoxLayout, QMessageBox
+)
+from PyQt5.QtGui import QStandardItemModel, QStandardItem
+from PyQt5.QtCore import Qt
+
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
+from matplotlib.animation import FuncAnimation
+
+# 导入你的 UI 和算法模块
+from untitled import Ui_Dialog
+from calculate.ik import inverseF
+from calculate.trajectory import circle_trajectory, line_trajectory
+
+import matplotlib.pyplot as plt
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# ------------------------ 调试与模式开关 ------------------------
+DEBUG_MODE = True # <<< 设为 False 启用真实电机控制
+USE_QIEGE_MOTOR = False # <<< 设为 True 才启用 qiege_motor.py 控制切割电机
+
+# ------------------------ 机械臂参数 ------------------------
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250 # 右侧基座偏移
+
+# ------------------------ 电机配置 ------------------------
+CAN_PORT = '/dev/ttyACM0'
+CAN_BAUD = 921600
+
+DM_MOTOR1_ID = 0x01 # 左臂 DM4310
+DM_MOTOR2_ID = 0x04 # 右臂 DM4310
+
+KP = 5.0
+KD = 1.0
+
+# ------------------------ 切割参数 ------------------------
+CUTTING_MIN_Y = 280 # Y > 此值才启动切割
+CUT_SPEED_RAD = 10.0 # 切割转速 (rad/s),用于 qiege_motor 内部映射
+
+# ------------------------ 全局变量 ------------------------
+motor1 = motor2 = motor_control = None
+current_cutting_state = False # 避免重复启停切割电机
+
+
+# -------------------- 导入硬件模块(仅非 DEBUG 模式) --------------------
+if not DEBUG_MODE:
+ try:
+ from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type
+ import serial
+ print("【硬件模块导入成功】: DM4310 电机")
+ except Exception as e:
+ print(f"【硬件模块导入失败】: {e}")
+ sys.exit(1)
+else:
+ print("【DEBUG MODE】: 不连接真实电机")
+
+
+# -------------------- 导入切割电机模块(仅当启用时) --------------------
+if USE_QIEGE_MOTOR and not DEBUG_MODE:
+ try:
+ from qiege_motor import control as cutting_control
+ print("【切割电机模块导入成功】: qiege_motor.control")
+ except Exception as e:
+ print(f"【qiege_motor 导入失败】: {e}")
+ print("【警告】将使用模拟模式")
+ USE_QIEGE_MOTOR = False
+else:
+ def cutting_control(enable: bool):
+ """模拟切割电机控制"""
+ state = "启动" if enable else "停止"
+ if DEBUG_MODE:
+ print(f"[DEBUG] 切割电机: {state}")
+ else:
+ if USE_QIEGE_MOTOR:
+ print(f"[WARN] qiege_motor 未启用或导入失败,control({enable}) 被忽略")
+
+
+class MyDialog(QDialog, Ui_Dialog):
+ def __init__(self):
+ super().__init__()
+ self.setupUi(self)
+
+ # 初始化 listView 模型
+ self.angle_model = QStandardItemModel(self.listView)
+ self.listView.setModel(self.angle_model)
+
+ # Matplotlib 图形初始化
+ self.figure = Figure(figsize=(8, 6), dpi=100)
+ self.ax = self.figure.add_subplot(111)
+ self.ax.set_xlim(-300, 500)
+ self.ax.set_ylim(0, 500)
+ self.ax.set_aspect('equal')
+ self.ax.grid(True, linestyle='--', alpha=0.6)
+ self.ax.set_title("五连杆机械臂运动控制")
+
+ # 画布
+ self.canvas = FigureCanvas(self.figure)
+ self.widget.setLayout(QVBoxLayout())
+ self.widget.layout().addWidget(self.canvas)
+
+ # 机械臂线
+ self.line, = self.ax.plot([], [], 'r-o', linewidth=2, markersize=6)
+
+ # 轨迹数据
+ self.x_list = []
+ self.y_list = []
+ self.ani = None
+
+ # 初始化电机(仅非 DEBUG)
+ self.init_hardware()
+
+ # 连接按钮
+ self.pushButton.clicked.connect(self.start_line_trajectory)
+ self.pushButton_2.clicked.connect(self.start_circle_trajectory)
+
+ def init_hardware(self):
+ global motor1, motor2, motor_control
+
+ if DEBUG_MODE:
+ print("【DEBUG】跳过 DM4310 电机初始化")
+ return
+
+ try:
+ ser = serial.Serial(CAN_PORT, CAN_BAUD, timeout=0.5)
+ motor_control = MotorControl(ser)
+ motor1 = Motor(DM_Motor_Type.DM4310, DM_MOTOR1_ID, 0x11)
+ motor2 = Motor(DM_Motor_Type.DM4310, DM_MOTOR2_ID, 0x14)
+
+ motor_control.addMotor(motor1)
+ motor_control.addMotor(motor2)
+ motor_control.switchControlMode(motor1, Control_Type.MIT)
+ motor_control.switchControlMode(motor2, Control_Type.MIT)
+ time.sleep(0.1)
+ motor_control.enable(motor1)
+ motor_control.enable(motor2)
+ print("✅ DM4310 电机已使能")
+ except Exception as e:
+ QMessageBox.critical(self, "电机初始化失败", str(e))
+ print(f"❌ 电机初始化失败: {e}")
+
+ def control_cutting_motor(self, enable=True):
+ """
+ 控制切割电机
+ - 仅在状态变化时调用
+ - 使用 qiege_motor.control() 接口
+ """
+ global current_cutting_state
+
+ if enable == current_cutting_state:
+ return # 状态未变,不重复操作
+
+ try:
+ cutting_control(enable)
+ print(f"✅ 切割电机: {'启动' if enable else '停止'}")
+ except Exception as e:
+ print(f"❌ 调用切割电机 control({enable}) 失败: {e}")
+
+ current_cutting_state = enable
+
+ def control_dm_motors(self, theta1, theta4):
+ """控制两个 DM4310 电机"""
+ pos1 = np.degrees(theta1)
+ pos4 = np.degrees(theta4)
+ vel = 0.8 # 可根据需要调整
+
+ if DEBUG_MODE:
+ print(f"[DEBUG] DM4310 -> M1:{pos1:.1f}°, M2:{pos4:.1f}°")
+ else:
+ try:
+ motor_control.controlMIT(motor1, KP, KD, pos1, vel, 0.0)
+ motor_control.controlMIT(motor2, KP, KD, pos4, vel, 0.0)
+ except Exception as e:
+ print(f"[DM电机] 控制失败: {e}")
+
+ def start_line_trajectory(self):
+ self.angle_model.clear()
+ self.ax.set_title("直线轨迹运动 + 电机控制")
+ self.x_list, self.y_list = line_trajectory(start=(125, 300), end=(125, 400))
+ self.start_animation()
+
+ def start_circle_trajectory(self):
+ self.angle_model.clear()
+ self.ax.set_title("圆形轨迹运动 + 电机控制")
+ self.x_list, self.y_list = circle_trajectory(center=(100, 200), radius=40)
+ self.start_animation()
+
+ def start_animation(self):
+ # 安全停止旧动画
+ try:
+ if self.ani is not None and self.ani.event_source is not None:
+ self.ani.event_source.stop()
+ except:
+ pass
+
+ # ✅ 开始前:重置切割状态
+ global current_cutting_state
+ current_cutting_state = False
+
+ def animate_frame(i):
+ x = self.x_list[i]
+ y = self.y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+
+ # 显示角度(角度制)
+ theta1_deg = np.degrees(theta1)
+ theta4_deg = np.degrees(theta4)
+ item_text = f"点 {i}: θ1={theta1_deg:.1f}°, θ4={theta4_deg:.1f}°"
+ if self.angle_model.rowCount() >= 10:
+ self.angle_model.removeRow(0)
+ self.angle_model.appendRow(QStandardItem(item_text))
+
+ # 计算五连杆坐标
+ x2 = L1 * np.cos(theta1)
+ y2 = L1 * np.sin(theta1)
+ x4 = L0 + L4 * np.cos(theta4)
+ y4 = L4 * np.sin(theta4)
+ x_coords = [0, x2, x, x4, L0]
+ y_coords = [0, y2, y, y4, 0]
+ self.line.set_data(x_coords, y_coords)
+
+ # --- 控制真实电机 ---
+ self.control_dm_motors(theta1, theta4)
+
+ # --- 切割控制(进入区域才切)---
+ in_cut_zone = y > CUTTING_MIN_Y
+ self.control_cutting_motor(enable=in_cut_zone)
+
+ except Exception as e:
+ print(f"第 {i} 帧错误: {e}")
+ return self.line,
+
+ # 创建动画
+ self.ani = FuncAnimation(
+ self.figure,
+ animate_frame,
+ frames=len(self.x_list),
+ interval=100,
+ repeat=False,
+ blit=False
+ )
+ self.canvas.draw()
+
+ # ✅ 动画结束后自动停止所有电机
+ from PyQt5.QtCore import QTimer
+ timer = QTimer(self)
+ timer.setSingleShot(True)
+ timer.timeout.connect(self.on_animation_finish)
+ timer.start(int(len(self.x_list) * 100 + 500)) # 动画总时长 + 0.5s 延迟
+
+ def on_animation_finish(self):
+ """动画结束后安全关闭所有电机"""
+ if not DEBUG_MODE:
+ try:
+ if 'motor_control' in globals() and motor_control:
+ motor_control.disable(motor1)
+ motor_control.disable(motor2)
+ print("✅ DM4310 电机已停用")
+ except Exception as e:
+ print(f"❌ DM 电机停用失败: {e}")
+
+ self.control_cutting_motor(False)
+ print("✅ 所有电机已安全关闭")
+
+ def closeEvent(self, event):
+ """窗口关闭时确保电机停止"""
+ self.control_cutting_motor(False)
+ if not DEBUG_MODE and 'motor_control' in globals():
+ try:
+ if motor_control:
+ motor_control.disable(motor1)
+ motor_control.disable(motor2)
+ print("【退出】所有电机已停用")
+ except Exception as e:
+ print(f"【退出】电机停用异常: {e}")
+ event.accept()
+
+
+if __name__ == "__main__":
+ app = QApplication(sys.argv)
+ dialog = MyDialog()
+ dialog.show()
+ sys.exit(app.exec_())
\ No newline at end of file
diff --git a/qt/untitled.py b/qt/untitled.py
new file mode 100644
index 0000000..78075de
--- /dev/null
+++ b/qt/untitled.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+
+# Form implementation generated from reading ui file 'untitled.ui'
+#
+# Created by: PyQt5 UI code generator 5.15.10
+#
+# WARNING: Any manual changes made to this file will be lost when pyuic5 is
+# run again. Do not edit this file unless you know what you are doing.
+
+
+from PyQt5 import QtCore, QtGui, QtWidgets
+
+
+class Ui_Dialog(object):
+ def setupUi(self, Dialog):
+ Dialog.setObjectName("Dialog")
+ Dialog.resize(851, 811)
+ self.pushButton = QtWidgets.QPushButton(Dialog)
+ self.pushButton.setGeometry(QtCore.QRect(10, 20, 111, 25))
+ self.pushButton.setObjectName("pushButton")
+ self.widget = QtWidgets.QWidget(Dialog)
+ self.widget.setGeometry(QtCore.QRect(10, 260, 801, 541))
+ self.widget.setObjectName("widget")
+ self.pushButton_2 = QtWidgets.QPushButton(Dialog)
+ self.pushButton_2.setGeometry(QtCore.QRect(140, 20, 111, 25))
+ self.pushButton_2.setObjectName("pushButton_2")
+ self.label = QtWidgets.QLabel(Dialog)
+ self.label.setGeometry(QtCore.QRect(10, 70, 131, 17))
+ self.label.setObjectName("label")
+ self.label_2 = QtWidgets.QLabel(Dialog)
+ self.label_2.setGeometry(QtCore.QRect(10, 240, 131, 21))
+ self.label_2.setObjectName("label_2")
+ self.listView = QtWidgets.QListView(Dialog)
+ self.listView.setGeometry(QtCore.QRect(10, 90, 801, 141))
+ self.listView.setObjectName("listView")
+
+ self.retranslateUi(Dialog)
+ QtCore.QMetaObject.connectSlotsByName(Dialog)
+
+ def retranslateUi(self, Dialog):
+ _translate = QtCore.QCoreApplication.translate
+ Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
+ self.pushButton.setText(_translate("Dialog", "直线轨迹移动"))
+ self.pushButton_2.setText(_translate("Dialog", "圆形轨迹移动"))
+ self.label.setText(_translate("Dialog", "电机转动角度"))
+ self.label_2.setText(_translate("Dialog", "轨迹可视化"))
diff --git a/qt/untitled.ui b/qt/untitled.ui
new file mode 100644
index 0000000..4572cfa
--- /dev/null
+++ b/qt/untitled.ui
@@ -0,0 +1,91 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 851
+ 811
+
+
+
+ Dialog
+
+
+
+
+ 10
+ 20
+ 111
+ 25
+
+
+
+ 直线轨迹移动
+
+
+
+
+
+ 10
+ 260
+ 801
+ 541
+
+
+
+
+
+
+ 140
+ 20
+ 111
+ 25
+
+
+
+ 圆形轨迹移动
+
+
+
+
+
+ 10
+ 70
+ 131
+ 17
+
+
+
+ 电机转动角度
+
+
+
+
+
+ 10
+ 240
+ 131
+ 21
+
+
+
+ 轨迹可视化
+
+
+
+
+
+ 10
+ 90
+ 801
+ 141
+
+
+
+
+
+
+
diff --git a/test1.py b/test1.py
new file mode 100644
index 0000000..aabf481
--- /dev/null
+++ b/test1.py
@@ -0,0 +1,160 @@
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.widgets import Slider, Button
+import matplotlib.pyplot as plt
+import matplotlib
+
+# 设置中文字体和解决负号显示问题
+plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong'] # 按优先级选择字体
+plt.rcParams['axes.unicode_minus'] = False # 显示负号 -
+
+# ======================= 对称五自由度机器人模型 =======================
+class Symmetric5DoFRobot:
+ def __init__(self):
+ self.L1 = 0.25 # 臂段长度
+ self.L2 = 0.3
+ self.Y0 = 0.25 # 左右臂起始点在 Y 轴上的偏移
+ self.tcp_x = 0.8 # 默认 TCP 目标位置
+ self.tcp_y = 0.0
+ self.tcp_theta = 0 # 夹具方向角度(弧度)
+
+ def inverse_kinematics(self, x, y, theta):
+ """给定末端位置和方向,返回左右臂各关节角度"""
+ # 计算左臂角度
+ try:
+ cos_q2_left = (x**2 + y**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)
+ sin_q2_left = np.sqrt(1 - cos_q2_left**2)
+ q2_left = np.arctan2(sin_q2_left, cos_q2_left)
+
+ k1 = self.L1 + self.L2 * np.cos(q2_left)
+ k2 = self.L2 * np.sin(q2_left)
+ q1_left = np.arctan2(y, x) - np.arctan2(k2, k1)
+
+ except:
+ q1_left = 0
+ q2_left = 0
+
+ # 右臂镜像求解
+ try:
+ cos_q2_right = (x**2 + y**2 - self.L1**2 - self.L2**2) / (2 * self.L1 * self.L2)
+ sin_q2_right = -np.sqrt(1 - cos_q2_right**2)
+ q2_right = np.arctan2(sin_q2_right, cos_q2_right)
+
+ k1 = self.L1 + self.L2 * np.cos(q2_right)
+ k2 = self.L2 * np.sin(q2_right)
+ q1_right = np.arctan2(y, x) - np.arctan2(k2, k1)
+
+ except:
+ q1_right = 0
+ q2_right = 0
+
+ return {
+ 'left': [q1_left, q2_left],
+ 'right': [q1_right, q2_right],
+ 'theta': theta
+ }
+
+ def forward_kinematics(self):
+ ik = self.inverse_kinematics(self.tcp_x, self.tcp_y, self.tcp_theta)
+ θ1, θ2 = ik['left']
+ θ4, θ3 = ik['right']
+
+ # 左臂坐标计算
+ left_base = np.array([0, -self.Y0])
+ j1_left = left_base + np.array([self.L1 * np.cos(θ1), self.L1 * np.sin(θ1)])
+ tcp_left = j1_left + np.array([
+ self.L2 * np.cos(θ1 + θ2),
+ self.L2 * np.sin(θ1 + θ2)
+ ])
+
+ # 右臂坐标计算
+ right_base = np.array([0, self.Y0])
+ j1_right = right_base + np.array([self.L1 * np.cos(θ4), self.L1 * np.sin(θ4)])
+ tcp_right = j1_right + np.array([
+ self.L2 * np.cos(θ4 + θ3),
+ self.L2 * np.sin(θ4 + θ3)
+ ])
+
+ # 确保末端相连
+ tcp_point = (tcp_left + tcp_right) / 2
+ gripper_dir = np.array([np.cos(self.tcp_theta), np.sin(self.tcp_theta)]) * 0.3
+
+ return {
+ 'left_arm': np.array([left_base, j1_left, tcp_point]),
+ 'right_arm': np.array([right_base, j1_right, tcp_point]),
+ 'gripper': [tcp_point, tcp_point + gripper_dir]
+ }
+
+# ======================= GUI界面 =======================
+class Symmetric5DoF_GUI:
+ def __init__(self):
+ self.robot = Symmetric5DoFRobot()
+
+ self.fig, self.ax = plt.subplots(figsize=(8, 6))
+ plt.subplots_adjust(left=0.1, right=0.9, top=0.85, bottom=0.3)
+ self.ax.set_title("非共基座对称五自由度机器人(末端相连 + 夹具方向)")
+ self.ax.set_xlim(-1.5, 1.5)
+ self.ax.set_ylim(-1.5, 1.5)
+ self.ax.set_aspect('equal')
+
+ # 初始化线条
+ self.left_line, = self.ax.plot([], [], 'b-o', lw=2, markersize=6, label="左臂")
+ self.right_line, = self.ax.plot([], [], 'r-o', lw=2, markersize=6, label="右臂")
+ self.gripper_line, = self.ax.plot([], [], 'g-->', lw=2, markersize=4, label="夹具方向")
+
+ plt.legend()
+
+ # 创建滑动条
+ self.sliders = []
+ self.slider_labels = ['X', 'Y', 'Theta']
+ self.slider_values = [self.robot.tcp_x, self.robot.tcp_y, self.robot.tcp_theta]
+
+ y_pos = 0.2
+ for i in range(3):
+ ax_slider = plt.axes([0.2, y_pos, 0.6, 0.03])
+ slider = Slider(ax_slider, self.slider_labels[i], -2, 2, valinit=self.slider_values[i])
+ slider.on_changed(lambda val, idx=i: self.update_tcp(idx, val))
+ self.sliders.append(slider)
+ y_pos -= 0.04
+
+ # 添加重置按钮
+ reset_ax = plt.axes([0.8, 0.02, 0.1, 0.04])
+ self.reset_button = Button(reset_ax, 'Reset', hovercolor='0.975')
+ self.reset_button.on_clicked(self.reset)
+
+ def update_tcp(self, idx, value):
+ if idx == 0:
+ self.robot.tcp_x = value
+ elif idx == 1:
+ self.robot.tcp_y = value
+ elif idx == 2:
+ self.robot.tcp_theta = value
+ self.update_plot()
+
+ def update_plot(self):
+ kinematics = self.robot.forward_kinematics()
+ self.left_line.set_data(kinematics['left_arm'][:, 0], kinematics['left_arm'][:, 1])
+ self.right_line.set_data(kinematics['right_arm'][:, 0], kinematics['right_arm'][:, 1])
+
+ gb = kinematics['gripper'][0]
+ gt = kinematics['gripper'][1]
+ self.gripper_line.set_data([gb[0], gt[0]], [gb[1], gt[1]])
+
+ self.fig.canvas.draw_idle()
+
+ def reset(self, event):
+ self.robot.tcp_x = 0.8
+ self.robot.tcp_y = 0.0
+ self.robot.tcp_theta = 0
+ for i, s in enumerate(self.sliders):
+ s.set_val([0.8, 0, 0][i])
+ self.update_plot()
+
+ def run(self):
+ self.update_plot()
+ plt.show()
+
+# ======================= 主程序入口 =======================
+if __name__ == "__main__":
+ gui = Symmetric5DoF_GUI()
+ gui.run()
\ No newline at end of file
diff --git a/test_chuankou.py b/test_chuankou.py
new file mode 100644
index 0000000..b675856
--- /dev/null
+++ b/test_chuankou.py
@@ -0,0 +1,55 @@
+from calculate.calculate_angle import main_of_5dof
+
+# control_with_serial.py
+
+
+# 📡 初始化串口连接
+def init_serial(port=SERIAL_PORT, baud_rate=BAUD_RATE):
+ try:
+ ser = serial.Serial(port, baud_rate, timeout=TIMEOUT)
+ print(f"串口已连接: {port} @ {baud_rate}")
+ return ser
+ except Exception as e:
+ print(f"无法打开串口 {port}: {e}")
+ return None
+
+# 📤 发送角度到串口
+def send_angles(ser, theta1_deg, theta4_deg):
+ # 格式:theta1_deg,theta4_deg\n
+ data = f"{int(theta1_deg)},{int(theta4_deg)}\n"
+ try:
+ ser.write(data.encode())
+ print(f"已发送角度: theta1 = {theta1_deg}°, theta4 = {theta4_deg}°")
+ except Exception as e:
+ print(f"发送失败: {e}")
+
+# 🚀 主程序入口
+if __name__ == "__main__":
+ # 调用你的轨迹规划函数
+ angles_list = main_of_5dof(
+ trajectory_type='line',
+ start=(0, 0),
+ end=(200, 300),
+ show_animation=False
+ )
+
+ # 初始化串口
+ ser = init_serial()
+ if ser is None:
+ exit(1)
+
+ # 遍历角度列表并发送
+ for i, (theta1, theta4) in enumerate(angles_list):
+ theta1_deg = np.degrees(theta1)
+ theta4_deg = np.degrees(theta4)
+
+ print(f"第 {i} 帧角度: theta1 = {theta1_deg:.2f}°, theta4 = {theta4_deg:.2f}°")
+ send_angles(ser, theta1_deg, theta4_deg)
+
+ # 可选:每帧之间加延迟,控制发送速度
+ import time
+ time.sleep(0.05) # 每帧间隔 50ms
+
+ # 关闭串口
+ ser.close()
+ print("串口已关闭")
\ No newline at end of file
diff --git a/test_duoji.py b/test_duoji.py
new file mode 100644
index 0000000..1f335f2
--- /dev/null
+++ b/test_duoji.py
@@ -0,0 +1,95 @@
+import time
+import numpy as np
+
+# 导入运动学函数
+from calculate.ik import inverseF
+
+# 导入串口控制函数
+from duoji_test.motor2 import send_hex_command, generate_position_command
+
+# 机械臂参数(和 qt_main.py 保持一致)
+L1 = 250
+L2 = 300
+L3 = 300
+L4 = 250
+L0 = 250
+
+# 串口配置
+MOTOR1_PORT = "/dev/ttyACM1" # 电机1使用的串口
+MOTOR2_PORT = "/dev/ttyACM0" # 电机2使用的串口
+BAUD_RATE = 115200
+
+# 控制参数
+SPEED_MOTOR1 = 10 # Rad/s
+SPEED_MOTOR2 = 10 # Rad/s
+DIRECTION_MOTOR1 = 1 # 0: 逆时针, 1: 顺时针
+DIRECTION_MOTOR2 = 1
+
+# 初始化电机角度为0
+current_angle_motor1 = 0
+current_angle_motor2 = 0
+
+
+def control_two_motors(theta1, theta4):
+ """
+ 根据 theta1 和 theta4 控制两个电机转动指定角度
+ :param theta1: 左臂角度(弧度)
+ :param theta4: 右臂角度(弧度)
+ """
+ global current_angle_motor1, current_angle_motor2
+
+ # 弧度转角度
+ angle1_target = np.degrees(theta1)
+ angle4_target = np.degrees(theta4)
+
+ # 计算相对角度
+ relative_angle1 = angle1_target - current_angle_motor1
+ relative_angle4 = angle4_target - current_angle_motor2
+
+ print(f"发送相对角度: 电机1={relative_angle1:.2f}°, 电机2={relative_angle4:.2f}°")
+
+ # 更新当前角度
+ current_angle_motor1 += relative_angle1
+ current_angle_motor2 += relative_angle4
+
+ # 生成指令
+ cmd1 = generate_position_command(angle=relative_angle1, speed=SPEED_MOTOR1, direction=DIRECTION_MOTOR1)
+ cmd2 = generate_position_command(angle=relative_angle4, speed=SPEED_MOTOR2, direction=DIRECTION_MOTOR2)
+
+ # 发送指令到两个电机
+ send_hex_command(MOTOR1_PORT, BAUD_RATE, cmd1)
+ send_hex_command(MOTOR2_PORT, BAUD_RATE, cmd2)
+
+
+def run_trajectory():
+ """
+ 主函数:模拟轨迹点并控制电机
+ """
+
+ # 模拟一个圆形轨迹(你可以替换成从文件读取或用户输入)
+ center = (150, 250)
+ radius = 60
+ num_points = 100
+ angles = np.linspace(0, 2 * np.pi, num_points)
+ x_list = center[0] + radius * np.cos(angles)
+ y_list = center[1] + radius * np.sin(angles)
+
+ print("开始执行轨迹控制...")
+
+ for i in range(len(x_list)):
+ x = x_list[i]
+ y = y_list[i]
+
+ try:
+ theta1, theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
+ control_two_motors(theta1, theta4)
+ except Exception as e:
+ print(f"第 {i} 点计算失败: {e}")
+
+ time.sleep(0.1) # 控制发送频率(可根据实际需求调整)
+
+ print("轨迹执行完成。")
+
+
+if __name__ == "__main__":
+ run_trajectory()
\ No newline at end of file