Files
5dof/main.py
2025-09-17 16:39:51 +08:00

532 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ==================== 五连杆机械臂轨迹控制(直接角度发送版)====================
# 功能:轨迹生成 + 逆解 + 直接控制 + 动画显示 + 关节角度可视化 + FK 验证
# ==============================================================================
import time
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from time import perf_counter # 高精度计时
# 设置中文字体和负号显示
plt.rcParams['font.sans-serif'] = ['SimHei', 'WenQuanYi Zen Hei', 'FangSong']
plt.rcParams['axes.unicode_minus'] = False
# ------------------------ 调试开关 ------------------------
DEBUG_MODE = True # <<< 设为 False 控制真实电机
# 导入运动学和轨迹函数(确保路径正确)
try:
from calculate.ik import inverseF
from calculate.fk import forwardF
from calculate.trajectory import (
circle_trajectory,
line_trajectory,
line_trajectory_fix,
ellipse_trajectory,
square_trajectory,
triangle_trajectory
)
except ImportError as e:
print(f"【警告】无法导入运动学模块: {e}, 使用 DEBUG_MODE")
DEBUG_MODE = True
# -------------------- 非 Debug 模式导入硬件库 --------------------
if not DEBUG_MODE:
try:
from DM_CAN.DM_CAN import Motor, MotorControl, DM_Motor_Type, Control_Type
import serial
except ImportError as e:
print(f"硬件库导入失败: {e}")
exit(1)
else:
print("【DEBUG MODE】: 已启用调试模式,不连接真实硬件。")
# ------------------------ 机械臂参数 ------------------------
L1 = 250 # 左臂长度 (mm)
L2 = 300
L3 = 300
L4 = 250 # 右臂长度
L0 = 250 # 基座右端偏移
# ------------------------ 电机与通信配置 ------------------------
MOTOR1_ID = 0x01
MOTOR2_ID = 0x02
CAN_SERIAL_PORT = '/dev/ttyACM0'
BAUD_RATE = 921600
KP = 50.0
KD = 1.0
DT = 0.01# 控制周期 10ms每点延时
# ------------------------ 全局变量 ------------------------
motor1 = motor2 = motor_control = None
current_theta1 = current_theta4 = 0.0
x_list = y_list = [] # 轨迹点
line = None
# ------------------------ 精确延时 ------------------------
def busy_wait(dt):
"""高精度延时"""
start = perf_counter()
while perf_counter() - start < dt:
pass
# ------------------------ 角度连续性调整 ------------------------
def adjust_angle_continuity(new_angle, prev_angle):
"""
防止角度跳变(如 3.14 → -3.14),保持连续
"""
diff = new_angle - prev_angle
while diff > np.pi:
diff -= 2 * np.pi
while diff < -np.pi:
diff += 2 * np.pi
return prev_angle + diff
# ------------------------ 初始化电机 ------------------------
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,
'refresh_motor_status': lambda m: None,
'switchControlMode': lambda m, mode: None,
'save_motor_param': lambda m: 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, 0x12)
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
# 电机1、4安全角度范围弧度
THETA1_MIN = -np.pi/2 # -60°
THETA1_MAX = np.pi/2 # 60°
THETA4_MIN = -np.pi/2 # -60°
THETA4_MAX = np.pi/2 # 60°
# ------------------------ MIT 控制函数(带限位 + 回零逻辑) ------------------------
def control_two_motors_mit(theta1_rad, theta4_rad):
"""
发送 MIT 控制指令(角度单位:弧度)
带限位检测:如果超限,自动触发回零。
"""
global current_theta1, current_theta4
# --- 限位检测 ---
if not (THETA1_MIN <= theta1_rad <= THETA1_MAX):
print(f"θ1 超出限位 [{THETA1_MIN}, {THETA1_MAX}],执行回零...")
move_motors_to_zero()
return
if not (THETA4_MIN <= theta4_rad <= THETA4_MAX):
print(f"θ4 超出限位 [{THETA4_MIN}, {THETA4_MAX}],执行回零...")
move_motors_to_zero()
return
# --- 正常控制 ---
pos1 = theta1_rad
pos4 = theta4_rad
vel = 0.1
torq = 0.0
if not DEBUG_MODE:
motor_control.controlMIT(motor1, KP, KD, pos1, vel, torq)
motor_control.controlMIT(motor2, KP, KD, pos4, vel, torq)
else:
print(f"[DEBUG] 控制 -> θ1={np.degrees(theta1_rad):.2f}°, θ4={np.degrees(theta4_rad):.2f}°")
current_theta1 = theta1_rad
current_theta4 = theta4_rad
# ------------------------ 直接轨迹执行函数(无插值)------------------------
from time import perf_counter
import numpy as np
# 假设其他所需函数和全局变量已定义,例如 inverseF, forwardF, adjust_angle_continuity, control_two_motors_mit, 等等.
def execute_direct_trajectory(x_list, y_list, dt=DT):
"""
直接执行轨迹:对每个 (x,y) 计算逆解并立即发送角度,不做插值或速度规划
执行前:先把电机从零点平滑移动到轨迹起点
返回: (time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
"""
global motor_control, motor1, motor2
ANGLE_OFFSET_RAD = np.pi / 2 # 90° 偏移量
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = [], [], [], [], []
l1, l2, l3, l4, l5 = L1, L2, L3, L4, L0
omega1 = omega4 = 0.0
alpha1 = alpha4 = 0.0
# --- 轨迹起点逆解 ---
x0, y0 = x_list[0], y_list[0]
try:
raw_theta1, raw_theta4 = inverseF(x0, y0, L1, L2, L3, L4, L0)
theta1_start = float(raw_theta1) - ANGLE_OFFSET_RAD
theta4_start = float(raw_theta4) - ANGLE_OFFSET_RAD
print(f"起点逆解成功 -> θ1={np.degrees(theta1_start):.2f}°, θ4={np.degrees(theta4_start):.2f}°")
except Exception as e:
print(f"❌ 起点逆解失败: {e}")
return [], [], [], [], []
# --- 电机移动到起点 ---
print("电机从零点移动到轨迹起点...")
move_motors_to_target(theta1_start, theta4_start, duration=1.5, dt=dt)
current_theta1, current_theta4 = theta1_start, theta4_start
# --- 轨迹执行 ---
start_time = perf_counter()
for i, (x, y) in enumerate(zip(x_list, y_list)):
try:
raw_theta1, raw_theta4 = inverseF(x, y, L1, L2, L3, L4, L0)
target_theta1 = float(raw_theta1) - ANGLE_OFFSET_RAD
target_theta4 = float(raw_theta4) - ANGLE_OFFSET_RAD
target_theta1 = adjust_angle_continuity(target_theta1, current_theta1)
target_theta4 = adjust_angle_continuity(target_theta4, current_theta4)
except Exception as e:
print(f"{i} 点逆解失败 ({x:.2f}, {y:.2f}): {e}")
target_theta1, target_theta4 = current_theta1, current_theta4
else:
current_theta1, current_theta4 = target_theta1, target_theta4
# 控制电机
control_two_motors_mit(target_theta1, target_theta4)
# FK 验证
try:
xc, yc, *_ = forwardF(
u1=target_theta1 + ANGLE_OFFSET_RAD,
u4=target_theta4 + ANGLE_OFFSET_RAD,
omega1=omega1, omega4=omega4,
l1=l1, l2=l2, l3=l3, l4=l4, l5=l5,
alpha1=alpha1, alpha4=alpha4
)
x_fk_log.append(xc)
y_fk_log.append(yc)
except:
x_fk_log.append(np.nan)
y_fk_log.append(np.nan)
# 记录日志
elapsed = perf_counter() - start_time
time_log.append(elapsed)
theta1_log.append(np.degrees(target_theta1))
theta4_log.append(np.degrees(target_theta4))
busy_wait(dt)
return time_log, theta1_log, theta4_log, x_fk_log, y_fk_log
# ------------------------ 动画绘制函数 ------------------------
def draw_frame(i):
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)
except:
line.set_data([], [])
return line,
# ------------------------ 轨迹动画与执行 + 可视化 + FK 验证 ------------------------
# ------------------------ 轨迹动画预览 ------------------------
def preview_trajectory(trajectory_func, **params):
global x_list, y_list, line
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(-50, L0 + 100)
ax.set_ylim(0, 500)
ax.set_aspect('equal')
ax.grid(True, alpha=0.6)
ax.set_title(f"五连杆机械臂 - 轨迹: {trajectory_func.__name__}")
ax.plot(x_list, y_list, 'b--', alpha=0.5, label='目标轨迹')
line, = ax.plot([], [], 'r-o', lw=3, ms=6, label='机械臂')
ax.legend()
ani = FuncAnimation(fig, draw_frame, frames=len(x_list),
interval=50, blit=True, repeat=False)
plt.show()
return x_list, y_list
# ------------------------ 执行轨迹 ------------------------
def execute_trajectory(x_list, y_list, dt=DT):
print("开始执行轨迹(直接发送角度)...")
return execute_direct_trajectory(x_list, y_list, dt=dt)
# ------------------------ 可视化与误差统计 ------------------------
# ------------------------ 可视化与误差统计 ------------------------
def visualize_results(x_list, y_list, time_log, theta1_log, theta4_log, x_fk_log, y_fk_log):
# --- 绘制关节角度 ---
fig2, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
ax1.plot(time_log, theta1_log, 'b-o', markersize=3, linewidth=1.5, label='θ₁ (电机1)')
ax1.set_ylabel('关节角 θ₁ (°)')
ax1.set_title('电机关节角度随时间变化')
ax1.grid(True, alpha=0.5)
ax1.legend()
ax2.plot(time_log, theta4_log, 'r-o', markersize=3, linewidth=1.5, label='θ₄ (电机2)')
ax2.set_xlabel('时间 (秒)')
ax2.set_ylabel('关节角 θ₄ (°)')
ax2.grid(True, alpha=0.5)
ax2.legend()
plt.tight_layout()
plt.show()
# --- FK 验证 ---
fig3, ax3 = plt.subplots(figsize=(10, 8))
ax3.plot(x_list, y_list, 'b--', linewidth=2, label='目标轨迹', alpha=0.8)
ax3.plot(x_fk_log, y_fk_log, 'g-', linewidth=2, marker='o', markersize=3, label='FK 重建轨迹', alpha=0.7)
ax3.scatter(x_fk_log[0], y_fk_log[0], c='green', s=100, marker='s', label='起点')
ax3.scatter(x_fk_log[-1], y_fk_log[-1], c='red', s=100, marker='x', label='终点')
ax3.set_xlabel('X (mm)')
ax3.set_ylabel('Y (mm)')
ax3.set_title('FK 验证:目标 vs 实际轨迹')
ax3.grid(True, alpha=0.5)
ax3.legend()
ax3.set_aspect('equal')
plt.tight_layout()
plt.show()
# --- 误差统计 ---
mask = ~np.isnan(x_fk_log) & ~np.isnan(y_fk_log)
x_fk_valid = np.array(x_fk_log)[mask]
y_fk_valid = np.array(y_fk_log)[mask]
if len(x_fk_valid) == 0:
print("【警告】FK 数据全为 nan无法计算误差")
return
t_target = np.linspace(0, 1, len(x_list))
t_fk = np.linspace(0, 1, len(x_fk_valid))
from scipy.interpolate import interp1d
try:
f_x = interp1d(t_fk, x_fk_valid, kind='linear', fill_value='extrapolate')
f_y = interp1d(t_fk, y_fk_valid, kind='linear', fill_value='extrapolate')
x_interp = f_x(t_target)
y_interp = f_y(t_target)
errors = np.sqrt((x_interp - x_list)**2 + (y_interp - y_list)**2)
print("\n=== FK 验证误差统计 ===")
print(f"平均误差: {np.mean(errors):.3f} mm")
print(f"最大误差: {np.max(errors):.3f} mm")
print(f"标准差: {np.std(errors):.3f} mm")
except Exception as e:
print(f"误差计算失败: {e}")
# --- 关节角度统计与保存 ---
print("\n=== 关节角度范围统计 ===")
print(f"θ₁ 范围: {min(theta1_log):.2f}° ~ {max(theta1_log):.2f}°")
print(f"θ₄ 范围: {min(theta4_log):.2f}° ~ {max(theta4_log):.2f}°")
# 角度转换为弧度,并组合成配对列表
theta1_rad = np.radians(theta1_log)
theta4_rad = np.radians(theta4_log)
# 构建嵌套列表:[[θ1_0, θ4_0], [θ1_1, θ41], ...]
angles_rad_list = [[t1, t4] for t1, t4 in zip(theta1_rad, theta4_rad)]
# 格式化为字符串保留6位小数用逗号分隔
nested_str = ",\n ".join([f"[{t1:.6f}, {t4:.6f}]" for t1, t4 in angles_rad_list])
nested_str = f"[\n {nested_str}\n]"
# 保存为 txt 文件
filename = 'joint_angles_rad_nested.txt'
with open(filename, 'w') as f:
f.write("angles_rad_list = ")
f.write(nested_str)
f.write("\n")
print(f"\n✅ 嵌套列表格式的弧度数据已保存至: {filename}")
# ------------------------ 主流程 ------------------------
def run_full_trajectory(trajectory_func, **params):
x_list, y_list = preview_trajectory(trajectory_func, **params)
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = execute_trajectory(x_list, y_list, dt=DT)
visualize_results(x_list, y_list, time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
# ==================== 获取电机当前位置 =====================
def get_current_motor_position():
"""
获取当前电机角度(电机原始单位,通常是度),并打印。
返回theta1_current, theta4_current
"""
if not DEBUG_MODE:
try:
motor_control.refresh_motor_status(motor1)
motor_control.refresh_motor_status(motor2)
time.sleep(0.05)
theta1_current = motor1.getPosition()
theta4_current = motor2.getPosition()
except Exception:
theta1_current = theta4_current = 0.0
else:
theta1_current = motor1.getPosition()
theta4_current = motor2.getPosition()
print(f"当前电机弧度 -> θ1={theta1_current:.2f}, θ4={theta4_current:.2f}")
return theta1_current, theta4_current
# ==================== MIT 模式回零函数 =====================
def move_motors_to_zero(duration=1.0, dt=DT):
"""
使用 MIT 模式将电机从当前位置平滑回到零点
duration: 总运动时间 (秒)
dt: 控制周期 (秒)
"""
global motor1, motor2, motor_control, current_theta1, current_theta4
# 获取当前位置
theta1_start, theta4_start = get_current_motor_position()
# 计算步数
steps = int(duration / dt)
for i in range(steps + 1):
t = i / steps
# 线性插值
theta1_target = theta1_start * (1 - t)
theta4_target = theta4_start * (1 - t)
# 发送 MIT 控制
control_two_motors_mit(theta1_target, theta4_target)
busy_wait(dt)
# 确保最终位置为零
control_two_motors_mit(0.0, 0.0)
current_theta1 = current_theta4 = 0.0
print("电机已回零完成。")
# ==================== MIT 模式移动到指定角度(任意两点) =====================
def move_motors_to_target(theta1_target_rad, theta4_target_rad, duration=1.0, dt=DT,
theta1_start_rad=None, theta4_start_rad=None):
"""
使用 MIT 模式将电机从起点平滑移动到指定目标角度
参数:
theta1_target_rad: 电机1目标角度弧度
theta4_target_rad: 电机4目标角度弧度
duration: 总运动时间 (秒)
dt: 控制周期 (秒)
theta1_start_rad: 电机1起始角度弧度默认 None 表示从零点
theta4_start_rad: 电机4起始角度弧度默认 None 表示从零点
"""
global motor1, motor2, motor_control, current_theta1, current_theta4
# 默认从零点出发
if theta1_start_rad is None:
theta1_start_rad = 0.0
if theta4_start_rad is None:
theta4_start_rad = 0.0
steps = int(duration / dt)
for i in range(steps + 1):
t = i / steps
# 线性插值
theta1_now = theta1_start_rad + (theta1_target_rad - theta1_start_rad) * t
theta4_now = theta4_start_rad + (theta4_target_rad - theta4_start_rad) * t
# 发送 MIT 控制
control_two_motors_mit(theta1_now, theta4_now)
busy_wait(dt)
# 确保最终位置精确到目标
control_two_motors_mit(theta1_target_rad, theta4_target_rad)
current_theta1 = theta1_target_rad
current_theta4 = theta4_target_rad
print(f"电机已移动到目标角度: θ1={np.degrees(theta1_target_rad):.2f}°, θ4={np.degrees(theta4_target_rad):.2f}°")
if __name__ == "__main__":
try:
# ---------------- 初始化电机 ----------------
init_motors()
# --- 获取当前位置测试 ---
print("\n=== 测试:获取电机当前位置 ===")
#theta1, theta4 = get_current_motor_position()
# --- MIT 模式回零 ---
print("\n=== MIT 模式回零 ===")
#move_motors_to_zero(duration=1.0) # 主函数里执行回零
# ---------------- 选择轨迹 ----------------
'''
#直线轨迹:
trajectory_config = {
'func': line_trajectory_fix,
'params': {'start': (25, 300), 'end': (200, 300), 'num_points': 100}
}
#圆形轨迹
trajectory_config = {
'func': circle_trajectory,
'params': {'center': (100, 300), 'radius': 100}
}
trajectory_config = {
'func': square_trajectory,
'params': {'side':60, 'num_points': 200}
}
#椭圆轨迹
trajectory_config = {
'func': ellipse_trajectory,
'params': {'center': (100, 250), 'rx': 100, 'ry': 60, 'num_points': 200}
}
'''
trajectory_config = {
'func': circle_trajectory,
'params': {'center': (100, 300), 'radius': 100, 'num_points': 100}
}
run_full_trajectory(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】程序结束")