commit 9477f25a5117e66f7673fc1a3424d5a2c6cc0f3e
Author: 琉璃月光 <15630071+llyg777@user.noreply.gitee.com>
Date: Wed Aug 13 12:36:04 2025 +0800
feat: 初始化项目,添加电机控制、CAN通信、QT界面等模块
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(' {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 0000000..d83a184
Binary files /dev/null and b/can_test_motor/__pycache__/can_main.cpython-38.pyc differ
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 0000000..330d00e
Binary files /dev/null and b/can_test_motor/__pycache__/motor_control_can_all.cpython-38.pyc differ
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 0000000..6e8ee4b
Binary files /dev/null and b/motor_control/__pycache__/can_main.cpython-38.pyc differ
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(' {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