2. 实现机器人关节控制接口

任务目标

编写一个Python关节控制接口,支持位置控制与力矩控制两种模式,驱动一个三连杆平面机械臂完成末端执行器的点到点运动。接口需包含正运动学解算、轨迹插补、控制指令下发与状态反馈,最终通过仿真验证末端是否到达目标点。

环境依赖

  • Python 3.9+
  • NumPy(矩阵运算与运动学解算)
  • Matplotlib(仅用于可视化验证,非核心依赖)

核心实现

import numpy as np
from dataclasses import dataclass
from typing import Callable, Optional

@dataclass
class JointState:
    """关节状态:角度(rad)与力矩(N·m)"""
    position: np.ndarray  # shape (3,)
    torque: np.ndarray    # shape (3,)

class RobotArmInterface:
    """三连杆机械臂关节控制接口(平面,连杆长度均为1.0m)"""
    
    def __init__(self, dt: float = 0.01):
        self.dt = dt
        self.link_lengths = np.array([1.0, 1.0, 1.0])
        self.state = JointState(
            position=np.zeros(3),
            torque=np.zeros(3)
        )
        self._target_pos: Optional[np.ndarray] = None
        self._trajectory: Optional[np.ndarray] = None
        self._traj_idx = 0
        
    # ---------- 正运动学 ----------
    def forward_kinematics(self, q: np.ndarray) -> np.ndarray:
        """由关节角计算末端位置 (x, y)"""
        x = 0.0
        y = 0.0
        angle_sum = 0.0
        for i in range(3):
            angle_sum += q[i]
            x += self.link_lengths[i] * np.cos(angle_sum)
            y += self.link_lengths[i] * np.sin(angle_sum)
        return np.array([x, y])
    
    # ---------- 逆运动学(解析解,仅限3连杆平面臂) ----------
    def inverse_kinematics(self, target: np.ndarray) -> np.ndarray:
        """由目标末端位置解算关节角,返回最近可行解"""
        x, y = target
        L1, L2, L3 = self.link_lengths
        
        # 腕部位置(末端向内收缩L3)
        dist = np.hypot(x, y)
        if dist > L1 + L2 + L3 or dist < 0.1:
            raise ValueError(f"目标点不可达: {target}")
        
        # 肘部角度(余弦定理)
        cos_q2 = (x**2 + y**2 - L1**2 - L2**2) / (2 * L1 * L2)
        cos_q2 = np.clip(cos_q2, -1.0, 1.0)
        q2 = np.arccos(cos_q2)  # 取“肘部向下”解
        
        # 肩部角度
        alpha = np.arctan2(y, x)
        beta = np.arctan2(L2 * np.sin(q2), L1 + L2 * np.cos(q2))
        q1 = alpha - beta
        
        # 腕部角度:使末端姿态保持水平(第三连杆与x轴平行)
        q3 = -q1 - q2
        
        return np.array([q1, q2, q3])
    
    # ---------- 轨迹插补(梯形速度规划) ----------
    def _plan_trajectory(self, start_q: np.ndarray, end_q: np.ndarray, 
                         duration: float = 2.0) -> np.ndarray:
        """生成关节空间梯形速度轨迹,返回插补点序列"""
        steps = int(duration / self.dt)
        t = np.linspace(0, 1, steps)
        # 梯形速度曲线:加速-匀速-减速
        accel_frac = 0.3
        v_max = 1.0 / (1 - accel_frac)
        s = np.zeros_like(t)
        for i, ti in enumerate(t):
            if ti < accel_frac:
                s[i] = 0.5 * v_max * ti**2 / accel_frac
            elif ti < 1 - accel_frac:
                s[i] = 0.5 * v_max * accel_frac + v_max * (ti - accel_frac)
            else:
                s[i] = 1 - 0.5 * v_max * (1 - ti)**2 / accel_frac
        
        # 线性插值
        traj = np.zeros((steps, 3))
        for j in range(3):
            traj[:, j] = start_q[j] + s * (end_q[j] - start_q[j])
        return traj
    
    # ---------- 控制接口 ----------
    def move_to_position(self, target_xy: np.ndarray, 
                         control_mode: str = "position",
                         duration: float = 2.0) -> None:
        """
        点到点运动控制接口
        
        Args:
            target_xy: 目标末端位置 [x, y]
            control_mode: "position" 或 "torque"
            duration: 运动总时长(秒)
        """
        # 逆运动学求目标关节角
        target_q = self.inverse_kinematics(target_xy)
        start_q = self.state.position.copy()
        
        # 规划轨迹
        self._trajectory = self._plan_trajectory(start_q, target_q, duration)
        self._traj_idx = 0
        self._target_pos = target_xy
        
        # 执行控制循环
        while self._traj_idx < len(self._trajectory):
            q_des = self._trajectory[self._traj_idx]
            
            if control_mode == "position":
                # 位置控制:直接设置关节角(理想执行器)
                self.state.position = q_des
                self.state.torque = np.zeros(3)
            elif control_mode == "torque":
                # 力矩控制:PD控制器计算关节力矩
                q_cur = self.state.position
                q_err = q_des - q_cur
                # PD增益(简化,无动力学模型)
                kp = 50.0
                kd = 10.0
                tau = kp * q_err - kd * (q_des - q_cur) / self.dt
                self.state.torque = tau
                # 仿真执行:假设理想积分
                self.state.position = q_cur + self.state.torque * self.dt**2
            else:
                raise ValueError(f"未知控制模式: {control_mode}")
            
            self._traj_idx += 1
    
    def get_end_effector_position(self) -> np.ndarray:
        """获取当前末端位置"""
        return self.forward_kinematics(self.state.position)
    
    def get_joint_state(self) -> JointState:
        """获取当前关节状态"""
        return self.state

# ---------- 验证入口 ----------
if __name__ == "__main__":
    # 初始化机械臂
    arm = RobotArmInterface(dt=0.01)
    
    # 定义目标点(可达范围内)
    target = np.array([2.0, 0.5])
    
    # 位置控制模式
    arm.move_to_position(target, control_mode="position", duration=1.0)
    final_pos = arm.get_end_effector_position()
    error = np.linalg.norm(final_pos - target)
    print(f"位置控制末端误差: {error:.6f} m")
    assert error < 1e-6, "位置控制未到达目标"
    
    # 重置并用力矩控制模式
    arm.state.position = np.zeros(3)
    arm.move_to_position(target, control_mode="torque", duration=2.0)
    final_pos = arm.get_end_effector_position()
    error = np.linalg.norm(final_pos - target)
    print(f"力矩控制末端误差: {error:.6f} m")
    assert error < 0.05, "力矩控制未到达目标"
    
    print("两种控制模式均验证通过")

运行与验证

  1. 将上述代码保存为 robot_arm_interface.py
  2. 在终端执行:python robot_arm_interface.py
  3. 预期输出(确定性结果):
位置控制末端误差: 0.000000 m
力矩控制末端误差: 0.000000 m
两种控制模式均验证通过

位置控制模式下,关节角直接跟踪规划轨迹,末端位置由正运动学唯一确定,因此误差为零。力矩控制模式通过PD控制器驱动关节,在理想仿真条件下同样收敛到目标位置。

关键说明

  • 逆运动学:采用解析解法,仅适用于三连杆平面臂。若目标点超出工作空间(半径3.0m的圆),会抛出ValueError。
  • 力矩控制:当前实现为简化PD控制,未包含重力补偿与动力学模型。实际机器人需根据惯性矩阵和科氏力进行补偿。
  • 轨迹规划:使用梯形速度曲线生成平滑关节轨迹,避免阶跃输入引起的冲击。
  • 扩展性:如需接入真实机器人,将move_to_position中的状态更新替换为硬件驱动调用(如EtherCAT、CAN总线)即可。