2025-09-15 10:45:15 +08:00
|
|
|
|
# ==================== 五连杆机械臂轨迹控制(直接角度发送版)====================
|
|
|
|
|
|
# 功能:轨迹生成 + 逆解 + 直接控制 + 动画显示 + 关节角度可视化 + 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
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------ 调试开关 ------------------------
|
2025-09-17 16:39:51 +08:00
|
|
|
|
DEBUG_MODE = True # <<< 设为 False 控制真实电机
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
|
|
|
|
|
# 导入运动学和轨迹函数(确保路径正确)
|
|
|
|
|
|
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
|
2025-09-17 16:39:51 +08:00
|
|
|
|
DT = 0.01# 控制周期 10ms(每点延时)
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
|
|
|
|
|
# ------------------------ 全局变量 ------------------------
|
|
|
|
|
|
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】跳过电机初始化")
|
2025-09-16 15:17:32 +08:00
|
|
|
|
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
|
|
|
|
|
|
})()
|
2025-09-15 10:45:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# 电机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 控制函数(带限位 + 回零逻辑) ------------------------
|
2025-09-15 10:45:15 +08:00
|
|
|
|
def control_two_motors_mit(theta1_rad, theta4_rad):
|
|
|
|
|
|
"""
|
|
|
|
|
|
发送 MIT 控制指令(角度单位:弧度)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
带限位检测:如果超限,自动触发回零。
|
2025-09-15 10:45:15 +08:00
|
|
|
|
"""
|
|
|
|
|
|
global current_theta1, current_theta4
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 限位检测 ---
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
# --- 正常控制 ---
|
2025-09-15 10:45:15 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-09-16 11:43:04 +08:00
|
|
|
|
|
2025-09-15 10:45:15 +08:00
|
|
|
|
# ------------------------ 直接轨迹执行函数(无插值)------------------------
|
|
|
|
|
|
from time import perf_counter
|
|
|
|
|
|
import numpy as np
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# 假设其他所需函数和全局变量已定义,例如 inverseF, forwardF, adjust_angle_continuity, control_two_motors_mit, 等等.
|
|
|
|
|
|
|
|
|
|
|
|
def execute_direct_trajectory(x_list, y_list, dt=DT):
|
2025-09-15 10:45:15 +08:00
|
|
|
|
"""
|
2025-09-16 15:17:32 +08:00
|
|
|
|
直接执行轨迹:对每个 (x,y) 计算逆解并立即发送角度,不做插值或速度规划
|
|
|
|
|
|
执行前:先把电机从零点平滑移动到轨迹起点
|
2025-09-15 10:45:15 +08:00
|
|
|
|
返回: (time_log, theta1_log, theta4_log, x_fk_log, y_fk_log)
|
|
|
|
|
|
"""
|
|
|
|
|
|
global motor_control, motor1, motor2
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ANGLE_OFFSET_RAD = np.pi / 2 # 90° 偏移量
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
time_log, theta1_log, theta4_log, x_fk_log, y_fk_log = [], [], [], [], []
|
2025-09-15 10:45:15 +08:00
|
|
|
|
l1, l2, l3, l4, l5 = L1, L2, L3, L4, L0
|
|
|
|
|
|
omega1 = omega4 = 0.0
|
|
|
|
|
|
alpha1 = alpha4 = 0.0
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 轨迹起点逆解 ---
|
|
|
|
|
|
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 [], [], [], [], []
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 电机移动到起点 ---
|
|
|
|
|
|
print("电机从零点移动到轨迹起点...")
|
|
|
|
|
|
move_motors_to_target(theta1_start, theta4_start, duration=1.5, dt=dt)
|
|
|
|
|
|
current_theta1, current_theta4 = theta1_start, theta4_start
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 轨迹执行 ---
|
|
|
|
|
|
start_time = perf_counter()
|
|
|
|
|
|
for i, (x, y) in enumerate(zip(x_list, y_list)):
|
2025-09-15 10:45:15 +08:00
|
|
|
|
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}")
|
2025-09-16 15:17:32 +08:00
|
|
|
|
target_theta1, target_theta4 = current_theta1, current_theta4
|
2025-09-15 10:45:15 +08:00
|
|
|
|
else:
|
|
|
|
|
|
current_theta1, current_theta4 = target_theta1, target_theta4
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# 控制电机
|
2025-09-15 10:45:15 +08:00
|
|
|
|
control_two_motors_mit(target_theta1, target_theta4)
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# FK 验证
|
2025-09-15 10:45:15 +08:00
|
|
|
|
try:
|
2025-09-16 11:43:04 +08:00
|
|
|
|
xc, yc, *_ = forwardF(
|
2025-09-15 10:45:15 +08:00
|
|
|
|
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)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
except:
|
2025-09-15 10:45:15 +08:00
|
|
|
|
x_fk_log.append(np.nan)
|
|
|
|
|
|
y_fk_log.append(np.nan)
|
|
|
|
|
|
|
|
|
|
|
|
# 记录日志
|
|
|
|
|
|
elapsed = perf_counter() - start_time
|
|
|
|
|
|
time_log.append(elapsed)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
theta1_log.append(np.degrees(target_theta1))
|
2025-09-16 11:43:04 +08:00
|
|
|
|
theta4_log.append(np.degrees(target_theta4))
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
|
|
|
|
|
busy_wait(dt)
|
|
|
|
|
|
|
|
|
|
|
|
return time_log, theta1_log, theta4_log, x_fk_log, y_fk_log
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
|
2025-09-15 10:45:15 +08:00
|
|
|
|
# ------------------------ 动画绘制函数 ------------------------
|
|
|
|
|
|
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)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
except:
|
2025-09-15 10:45:15 +08:00
|
|
|
|
line.set_data([], [])
|
|
|
|
|
|
return line,
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------ 轨迹动画与执行 + 可视化 + FK 验证 ------------------------
|
2025-09-17 16:39:51 +08:00
|
|
|
|
# ------------------------ 轨迹动画预览 ------------------------
|
|
|
|
|
|
def preview_trajectory(trajectory_func, **params):
|
2025-09-15 10:45:15 +08:00
|
|
|
|
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()
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ani = FuncAnimation(fig, draw_frame, frames=len(x_list),
|
|
|
|
|
|
interval=50, blit=True, repeat=False)
|
2025-09-15 10:45:15 +08:00
|
|
|
|
plt.show()
|
2025-09-17 16:39:51 +08:00
|
|
|
|
return x_list, y_list
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
2025-09-17 16:39:51 +08:00
|
|
|
|
|
|
|
|
|
|
# ------------------------ 执行轨迹 ------------------------
|
|
|
|
|
|
def execute_trajectory(x_list, y_list, dt=DT):
|
2025-09-16 15:17:32 +08:00
|
|
|
|
print("开始执行轨迹(直接发送角度)...")
|
2025-09-17 16:39:51 +08:00
|
|
|
|
return execute_direct_trajectory(x_list, y_list, dt=dt)
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
|
2025-09-17 16:39:51 +08:00
|
|
|
|
# ------------------------ 可视化与误差统计 ------------------------
|
|
|
|
|
|
# ------------------------ 可视化与误差统计 ------------------------
|
|
|
|
|
|
def visualize_results(x_list, y_list, time_log, theta1_log, theta4_log, x_fk_log, y_fk_log):
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 绘制关节角度 ---
|
2025-09-15 10:45:15 +08:00
|
|
|
|
fig2, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ax1.plot(time_log, theta1_log, 'b-o', markersize=3, linewidth=1.5, label='θ₁ (电机1)')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
ax1.set_ylabel('关节角 θ₁ (°)')
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ax1.set_title('电机关节角度随时间变化')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
ax1.grid(True, alpha=0.5)
|
|
|
|
|
|
ax1.legend()
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ax2.plot(time_log, theta4_log, 'r-o', markersize=3, linewidth=1.5, label='θ₄ (电机2)')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
ax2.set_xlabel('时间 (秒)')
|
|
|
|
|
|
ax2.set_ylabel('关节角 θ₄ (°)')
|
|
|
|
|
|
ax2.grid(True, alpha=0.5)
|
|
|
|
|
|
ax2.legend()
|
|
|
|
|
|
plt.tight_layout()
|
|
|
|
|
|
plt.show()
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- FK 验证 ---
|
2025-09-15 10:45:15 +08:00
|
|
|
|
fig3, ax3 = plt.subplots(figsize=(10, 8))
|
|
|
|
|
|
ax3.plot(x_list, y_list, 'b--', linewidth=2, label='目标轨迹', alpha=0.8)
|
2025-09-16 15:17:32 +08:00
|
|
|
|
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='终点')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
ax3.set_xlabel('X (mm)')
|
|
|
|
|
|
ax3.set_ylabel('Y (mm)')
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ax3.set_title('FK 验证:目标 vs 实际轨迹')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
ax3.grid(True, alpha=0.5)
|
|
|
|
|
|
ax3.legend()
|
2025-09-16 15:17:32 +08:00
|
|
|
|
ax3.set_aspect('equal')
|
2025-09-15 10:45:15 +08:00
|
|
|
|
plt.tight_layout()
|
|
|
|
|
|
plt.show()
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 误差统计 ---
|
|
|
|
|
|
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}")
|
|
|
|
|
|
|
2025-09-17 16:39:51 +08:00
|
|
|
|
# --- 关节角度统计与保存 ---
|
2025-09-16 15:17:32 +08:00
|
|
|
|
print("\n=== 关节角度范围统计 ===")
|
|
|
|
|
|
print(f"θ₁ 范围: {min(theta1_log):.2f}° ~ {max(theta1_log):.2f}°")
|
|
|
|
|
|
print(f"θ₄ 范围: {min(theta4_log):.2f}° ~ {max(theta4_log):.2f}°")
|
|
|
|
|
|
|
2025-09-17 16:39:51 +08:00
|
|
|
|
# 角度转换为弧度,并组合成配对列表
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# ==================== 获取电机当前位置 =====================
|
|
|
|
|
|
def get_current_motor_position():
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取当前电机角度(电机原始单位,通常是度),并打印。
|
|
|
|
|
|
返回:theta1_current, theta4_current(度)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not DEBUG_MODE:
|
2025-09-16 11:43:04 +08:00
|
|
|
|
try:
|
2025-09-16 15:17:32 +08:00
|
|
|
|
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}°")
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
try:
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# ---------------- 初始化电机 ----------------
|
2025-09-15 10:45:15 +08:00
|
|
|
|
init_motors()
|
|
|
|
|
|
|
2025-09-16 15:17:32 +08:00
|
|
|
|
# --- 获取当前位置测试 ---
|
|
|
|
|
|
print("\n=== 测试:获取电机当前位置 ===")
|
2025-09-17 16:39:51 +08:00
|
|
|
|
#theta1, theta4 = get_current_motor_position()
|
2025-09-16 15:17:32 +08:00
|
|
|
|
|
|
|
|
|
|
# --- MIT 模式回零 ---
|
|
|
|
|
|
print("\n=== MIT 模式回零 ===")
|
2025-09-17 16:39:51 +08:00
|
|
|
|
#move_motors_to_zero(duration=1.0) # 主函数里执行回零
|
2025-09-16 15:17:32 +08:00
|
|
|
|
|
|
|
|
|
|
# ---------------- 选择轨迹 ----------------
|
2025-09-17 16:39:51 +08:00
|
|
|
|
'''
|
|
|
|
|
|
#直线轨迹:
|
2025-09-15 10:45:15 +08:00
|
|
|
|
trajectory_config = {
|
|
|
|
|
|
'func': line_trajectory_fix,
|
|
|
|
|
|
'params': {'start': (25, 300), 'end': (200, 300), 'num_points': 100}
|
|
|
|
|
|
}
|
2025-09-17 16:39:51 +08:00
|
|
|
|
|
|
|
|
|
|
#圆形轨迹
|
|
|
|
|
|
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'])
|
2025-09-15 10:45:15 +08:00
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\n【用户中断】")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"程序异常: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if not DEBUG_MODE:
|
2025-09-16 15:17:32 +08:00
|
|
|
|
motor_control.disable(motor1)
|
|
|
|
|
|
motor_control.disable(motor2)
|
|
|
|
|
|
print("电机已停机。")
|
2025-09-15 10:45:15 +08:00
|
|
|
|
else:
|
2025-09-16 11:43:04 +08:00
|
|
|
|
print("【DEBUG】程序结束")
|
2025-09-16 15:17:32 +08:00
|
|
|
|
|