[software] 添加16DOF早期训练仿真与Sim2Real闭环

This commit is contained in:
2026-07-21 16:15:14 +08:00
parent 9bd22225f9
commit e9e2c946b3
681 changed files with 137221 additions and 8 deletions
+9
View File
@@ -0,0 +1,9 @@
# IK 真机控制探索
该目录保存强化学习部署前的逆运动学真机控制代码。
- `sim2real_control_api.py`:真机控制接口
- `trajectory_interpolator.py`:关节/姿态轨迹插值
- `sim_to_real_deploy_beifen.py`:早期部署脚本备份
文件名中的 `beifen` 来自原始资料。为保持早期版本可追溯性,本次归档不修改源码和文件名。
@@ -0,0 +1,274 @@
"""Sim-to-real control/IK API extracted from mature mujoco_sim controller.
This module provides a deployment-friendly wrapper around:
- wheel mode posture control
- trot swing-leg IK + wheel assist
- differential wheel speed mapping
No MuJoCo runtime is required for using the API itself.
For trot IK, Pinocchio model is used via Dynamics.
"""
from dataclasses import dataclass
from typing import Dict, Optional, Tuple
import numpy as np
from config import (
LEG_NAMES,
LEG_JOINTS,
WHEEL_JOINT,
DEFAULT_JOINT_ANGLES,
WHEEL_RADIUS,
WHEEL_TRACK,
WHEEL_VEL_MAX,
KP_ROLL,
KP_PITCH,
GAIT_FREQ,
GAIT_DUTY,
SWING_HEIGHT,
PHASE_OFFSETS,
)
from dynamics import Dynamics
@dataclass
class DeployState:
"""Minimal state for deployment control."""
rpy: np.ndarray # (3,) roll, pitch, yaw
class Sim2RealControlAPI:
"""Deployment-friendly control and IK API.
Supported modes:
- wheel: wheel differential drive + leg posture hold
- trot: swing foot IK + stance posture + wheel assist
"""
def __init__(self):
self.mode = "wheel"
self.prone = False
self.vel_x = 0.0
self.vel_y = 0.0
self.yaw_rate = 0.0
self.height = 0.33
self._gait_phase = 0.0
self._smooth_vx = 0.0
self._smooth_vy = 0.0
self._smooth_yaw = 0.0
self._default_q = np.array([
DEFAULT_JOINT_ANGLES["hip_abduction"],
DEFAULT_JOINT_ANGLES["hip_pitch"],
DEFAULT_JOINT_ANGLES["knee"],
])
self.dynamics = Dynamics()
self._swing_start_foot = {leg: np.zeros(3) for leg in LEG_NAMES}
self._last_contact = {leg: True for leg in LEG_NAMES}
def set_mode(self, mode: str):
if mode not in ("wheel", "trot"):
raise ValueError("mode must be one of: wheel, trot")
self.mode = mode
def set_command(self, vel_x: float, vel_y: float, yaw_rate: float, height: Optional[float] = None):
self.vel_x = float(vel_x)
self.vel_y = float(vel_y)
self.yaw_rate = float(yaw_rate)
if height is not None:
self.height = float(height)
def compute(
self,
state: DeployState,
dt: float,
q_pin: Optional[np.ndarray] = None,
dq_pin: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, np.ndarray]:
"""Compute leg and wheel commands.
Returns:
leg_targets: (12,) [fl(3), fr(3), rl(3), rr(3)]
wheel_targets: (4,) [fl, fr, rl, rr] in rad/s
Notes:
- wheel mode does not require q_pin/dq_pin
- trot mode requires q_pin/dq_pin for IK/FK through Pinocchio
"""
alpha = min(float(dt) * 3.0, 1.0)
self._smooth_vx += alpha * (self.vel_x - self._smooth_vx)
self._smooth_vy += alpha * (self.vel_y - self._smooth_vy)
self._smooth_yaw += alpha * (self.yaw_rate - self._smooth_yaw)
if self.prone:
return self._prone_mode()
if self.mode == "wheel":
return self._wheel_mode(state)
if q_pin is None or dq_pin is None:
raise ValueError("trot mode requires q_pin and dq_pin")
return self._trot_mode(state, float(dt), q_pin, dq_pin)
def to_joint_dict(self, leg_targets: np.ndarray, wheel_targets: np.ndarray) -> Dict[str, float]:
"""Convert array commands to named joint-command dictionary."""
out: Dict[str, float] = {}
for i, leg in enumerate(LEG_NAMES):
out[f"{leg}_{LEG_JOINTS[0]}"] = float(leg_targets[i * 3 + 0])
out[f"{leg}_{LEG_JOINTS[1]}"] = float(leg_targets[i * 3 + 1])
out[f"{leg}_{LEG_JOINTS[2]}"] = float(leg_targets[i * 3 + 2])
out[f"{leg}_{WHEEL_JOINT}"] = float(wheel_targets[i])
return out
def _prone_mode(self):
leg_targets = np.zeros(12)
for i, leg in enumerate(LEG_NAMES):
side = 1.0 if leg[1] == "l" else -1.0
leg_targets[i * 3 + 0] = side * 0.3
leg_targets[i * 3 + 1] = 1.5
leg_targets[i * 3 + 2] = -2.65
return leg_targets, np.zeros(4)
def _wheel_mode(self, state: DeployState):
wheel_targets = self._differential_drive(self._smooth_vx, self._smooth_yaw)
leg_targets = self._posture_control(state)
return leg_targets, wheel_targets
def _posture_control(self, state: DeployState) -> np.ndarray:
leg_targets = np.zeros(12)
_H = [0.157, 0.248, 0.311, 0.366, 0.411, 0.448]
_HIP = [1.5, 1.2, 1.0, 0.8, 0.6, 0.4]
_KNEE = [-2.5, -2.1, -1.8, -1.5, -1.2, -0.9]
h_clamp = np.clip(self.height, _H[0], _H[-1])
q_hip_base = float(np.interp(h_clamp, _H, _HIP))
q_knee_base = float(np.interp(h_clamp, _H, _KNEE))
roll_corr = -KP_ROLL * float(state.rpy[0])
pitch_corr = -KP_PITCH * float(state.rpy[1])
lateral_lean = 0.3 * self.vel_y
for i, leg in enumerate(LEG_NAMES):
side = 1.0 if leg[1] == "l" else -1.0
leg_targets[i * 3 + 0] = np.clip(side * roll_corr + lateral_lean, -0.5, 0.5)
leg_targets[i * 3 + 1] = np.clip(q_hip_base + pitch_corr, -1.0, 2.5)
leg_targets[i * 3 + 2] = np.clip(q_knee_base, -2.6, -0.3)
return leg_targets
def _trot_mode(self, state: DeployState, dt: float, q_pin: np.ndarray, dq_pin: np.ndarray):
self._gait_phase = (self._gait_phase + dt * GAIT_FREQ) % 1.0
contacts: Dict[str, bool] = {}
for leg in LEG_NAMES:
phase = (self._gait_phase + PHASE_OFFSETS[leg]) % 1.0
contacts[leg] = bool(phase < GAIT_DUTY)
self.dynamics.update(q_pin, dq_pin)
leg_targets = np.zeros(12)
wheel_targets = np.zeros(4)
for i, leg in enumerate(LEG_NAMES):
if contacts[leg]:
leg_targets[i * 3:(i + 1) * 3] = self._stance_leg_target(state, leg)
self._swing_start_foot[leg] = self.dynamics.get_foot_pos(leg)
self._last_contact[leg] = True
wheel_targets[i] = self._differential_drive_single(self._smooth_vx, self._smooth_yaw, leg)
else:
swing_phase = self._get_swing_phase(leg)
target_foot = self._compute_swing_target(leg, state, swing_phase)
q_ik = self.dynamics.inverse_kinematics(leg, target_foot, q_pin)
leg_targets[i * 3:(i + 1) * 3] = q_ik
self._last_contact[leg] = False
wheel_targets[i] = 0.0
return leg_targets, wheel_targets
def _stance_leg_target(self, state: DeployState, leg: str) -> np.ndarray:
_H = [0.157, 0.248, 0.311, 0.366, 0.411, 0.448]
_HIP = [1.5, 1.2, 1.0, 0.8, 0.6, 0.4]
_KNEE = [-2.5, -2.1, -1.8, -1.5, -1.2, -0.9]
h_clamp = np.clip(self.height, _H[0], _H[-1])
q_hip = float(np.interp(h_clamp, _H, _HIP))
q_knee = float(np.interp(h_clamp, _H, _KNEE))
roll_corr = -KP_ROLL * float(state.rpy[0])
pitch_corr = -KP_PITCH * float(state.rpy[1])
side = 1.0 if leg[1] == "l" else -1.0
lateral_lean = 0.3 * self.vel_y
return np.array([
np.clip(side * roll_corr + lateral_lean, -0.5, 0.5),
np.clip(q_hip + pitch_corr, -1.0, 2.5),
np.clip(q_knee, -2.6, -0.3),
])
def _differential_drive(self, vel_x: float, yaw_rate: float) -> np.ndarray:
vel_left = (vel_x - 0.5 * WHEEL_TRACK * yaw_rate) / WHEEL_RADIUS
vel_right = (vel_x + 0.5 * WHEEL_TRACK * yaw_rate) / WHEEL_RADIUS
targets = np.zeros(4)
for i, leg in enumerate(LEG_NAMES):
targets[i] = vel_left if leg[1] == "l" else vel_right
return np.clip(targets, -WHEEL_VEL_MAX, WHEEL_VEL_MAX)
def _differential_drive_single(self, vel_x: float, yaw_rate: float, leg: str) -> float:
if leg[1] == "l":
v = (vel_x - 0.5 * WHEEL_TRACK * yaw_rate) / WHEEL_RADIUS
else:
v = (vel_x + 0.5 * WHEEL_TRACK * yaw_rate) / WHEEL_RADIUS
return float(np.clip(v, -WHEEL_VEL_MAX, WHEEL_VEL_MAX))
def _get_swing_phase(self, leg: str) -> float:
phase = (self._gait_phase + PHASE_OFFSETS[leg]) % 1.0
if phase < GAIT_DUTY:
return 0.0
return (phase - GAIT_DUTY) / (1.0 - GAIT_DUTY)
def _compute_swing_target(self, leg: str, state: DeployState, swing_phase: float) -> np.ndarray:
p_start = self._swing_start_foot[leg]
p_end = self._compute_touchdown(leg, state)
s = swing_phase
s_mj = 10 * s**3 - 15 * s**4 + 6 * s**5
pos = p_start + (p_end - p_start) * s_mj
z_lift = 64.0 * s**3 * (1.0 - s)**3
pos[2] = p_start[2] + SWING_HEIGHT * z_lift
return pos
def _compute_touchdown(self, leg: str, state: DeployState) -> np.ndarray:
td = self._swing_start_foot[leg].copy()
t_stance = (1.0 / GAIT_FREQ) * GAIT_DUTY
yaw = float(state.rpy[2])
c, s = np.cos(yaw), np.sin(yaw)
R_z = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
cmd_vel_world = R_z @ np.array([self._smooth_vx, self._smooth_vy, 0.0])
td[0] += cmd_vel_world[0] * t_stance * 0.5
td[1] += cmd_vel_world[1] * t_stance * 0.5
td[2] = WHEEL_RADIUS
return td
if __name__ == "__main__":
api = Sim2RealControlAPI()
api.set_mode("wheel")
api.set_command(vel_x=0.3, vel_y=0.0, yaw_rate=0.0, height=0.33)
state = DeployState(rpy=np.array([0.0, 0.0, 0.0]))
leg, wheel = api.compute(state=state, dt=0.004)
cmd = api.to_joint_dict(leg, wheel)
print("Example wheel-mode command:")
for k, v in sorted(cmd.items()):
print(f"{k}: {v:.6f}")
@@ -0,0 +1,234 @@
#!/usr/bin/env python3
import argparse, sys, threading, time
from dataclasses import dataclass
from pathlib import Path
import numpy as np
sys.path.append('/home/rc2/work/rcwork/control')
from drivers.motor_driver import RobStrideDriver
ROOT = Path('/home/rc2/work/rcwork/wheelleg_deploy_swj/wheelleg_deploy/wheelleg_mjlab/beifen')
sys.path += [str(ROOT), str(ROOT / 'mujoco_sim')]
from sim2real_control_api import DeployState, Sim2RealControlAPI # type: ignore
sys.path.append('/home/rc2/work/rcwork')
from trajectory_interpolator import TrajectoryInterpolator
LEGS = ('fl','fr','rl','rr')
LJ = ('hip_abduction_joint','hip_pitch_joint','knee_joint')
WJ = 'wheel_joint'
JNS = [f'{l}_{j}' for l in LEGS for j in (*LJ, WJ)]
@dataclass
class Cfg:
mid:int; model:str; sign:float; off:float; bus:str
class Deploy:
def __init__(self, can1, can2, hz=100.0, use_interpolation=True, interp_method='quintic', interp_time=1.5):
self.d1, self.d2 = RobStrideDriver(can1, False), RobStrideDriver(can2, False)
self.api = Sim2RealControlAPI(); self.dt = 1.0/hz; self.lk = threading.Lock()
self.run = False; self.enabled = False; self.estop = True
self.mode='stand'; self.vx=0.0; self.vy=0.0; self.yaw=0.0; self.h=0.33; self.roll=0.0; self.pitch=0.0
self.prone = False
self.kp_leg, self.kd_leg, self.kd_wheel = 80.0, 2.5, 2.0
self.cfg = self._cfg(); self.q=np.zeros(23); self.dq=np.zeros(22)
self.use_interpolation = use_interpolation
if self.use_interpolation:
self.interpolator = TrajectoryInterpolator(method=interp_method, transition_time=interp_time)
print(f'[Interpolation] Enabled: method={interp_method}, transition_time={interp_time}s')
else:
self.interpolator = None
print('[Interpolation] Disabled')
def _cfg(self):
sign={'fl_hip_abduction_joint':-1,'fl_hip_pitch_joint':-1,'fl_knee_joint':-1,'fl_wheel_joint':-1,'fr_hip_abduction_joint':-1,'fr_hip_pitch_joint':1,'fr_knee_joint':1,'fr_wheel_joint':1,'rl_hip_abduction_joint':1,'rl_hip_pitch_joint':-1,'rl_knee_joint':-1,'rl_wheel_joint':-1,'rr_hip_abduction_joint':1,'rr_hip_pitch_joint':1,'rr_knee_joint':1,'rr_wheel_joint':1}
off={'fl_hip_abduction_joint':0.003,'fl_hip_pitch_joint':0.030,'fl_knee_joint':0.028,'fl_wheel_joint':0.0,'fr_hip_abduction_joint':0.004,'fr_hip_pitch_joint':0.038,'fr_knee_joint':0.011,'fr_wheel_joint':0.0,'rl_hip_abduction_joint':0.019,'rl_hip_pitch_joint':-0.034,'rl_knee_joint':0.025,'rl_wheel_joint':0.0,'rr_hip_abduction_joint':-0.001,'rr_hip_pitch_joint':0.039,'rr_knee_joint':0.018,'rr_wheel_joint':0.0}
ids={'fl_hip_abduction_joint':1,'fl_hip_pitch_joint':2,'fl_knee_joint':3,'fl_wheel_joint':4,'fr_hip_abduction_joint':5,'fr_hip_pitch_joint':6,'fr_knee_joint':7,'fr_wheel_joint':8,'rl_hip_abduction_joint':1,'rl_hip_pitch_joint':2,'rl_knee_joint':3,'rl_wheel_joint':4,'rr_hip_abduction_joint':5,'rr_hip_pitch_joint':6,'rr_knee_joint':7,'rr_wheel_joint':8}
bus={k:('can1' if k.startswith('f') else 'can2') for k in JNS}
return {jn:Cfg(ids[jn],'rs-06',float(sign[jn]),float(off[jn]),bus[jn]) for jn in JNS}
def _drv(self, jn): return self.d1 if self.cfg[jn].bus=='can1' else self.d2
def connect(self):
self.d1.connect(); self.d2.connect()
for jn,c in self.cfg.items(): self._drv(jn).add_motor(jn,c.mid,c.model)
def enable_all(self):
for jn in JNS: self._drv(jn).enable(jn)
with self.lk: self.enabled=True; self.estop=False; self.vx=self.vy=self.yaw=0.0
def disable_all(self):
for jn in JNS: self._drv(jn).disable(jn)
with self.lk: self.enabled=False
def clear(self):
for jn in JNS: self._drv(jn).clear_warnings(jn)
def set_estop(self,on):
with self.lk:
self.estop=on
if on: self.vx=self.vy=self.yaw=0.0
if on: self.disable_all()
def set_prone(self,on):
with self.lk: self.prone=on; self.api.prone=on
def _update_pin(self,leg,wheel):
q=np.zeros(23); dq=np.zeros(22); q[2]=self.h; q[6]=1.0
for i,_ in enumerate(LEGS):
b=7+i*4; q[b:b+3]=leg[i*3:i*3+3]; dq[6+i*4+3]=wheel[i]
self.q,self.dq=q,dq
def step(self):
with self.lk:
if self.estop or (not self.enabled): return
m=self.mode; vx=float(np.clip(self.vx,-0.8,0.8)); vy=float(np.clip(self.vy,-0.5,0.5)); yaw=float(np.clip(self.yaw,-3,3)); h=float(np.clip(self.h,0.157,0.448)); r=float(np.clip(self.roll,-0.4,0.4)); p=float(np.clip(self.pitch,-0.4,0.4)); prone=self.prone
cm='trot' if m=='trot' else 'wheel'
if m=='stand': vx=vy=yaw=0.0
self.api.prone=prone; self.api.set_mode(cm); self.api.set_command(vx,vy,yaw,height=h)
st=DeployState(rpy=np.array([r,p,0.0]))
leg,wheel = self.api.compute(st,self.dt,self.q,self.dq) if cm=='trot' else self.api.compute(st,self.dt)
self._update_pin(leg,wheel); cmd=self.api.to_joint_dict(leg,wheel)
if self.use_interpolation and self.interpolator is not None:
leg_cmd = {jn: cmd[jn] for jn in JNS if not jn.endswith(WJ)}
self.interpolator.set_target(leg_cmd)
smooth_cmd = self.interpolator.update(self.dt)
for jn in leg_cmd:
cmd[jn] = smooth_cmd[jn]
for jn in JNS:
d=self._drv(jn); c=self.cfg[jn]
if jn.endswith(WJ): d.control_mit(jn,0.0,c.sign*float(cmd.get(jn,0.0)),0.0,self.kd_wheel,0.0)
else: d.control_mit(jn,c.sign*float(cmd[jn])+c.off,0.0,self.kp_leg,self.kd_leg,0.0)
def loop(self):
while self.run:
t=time.time()
try: self.step()
except Exception as e: print('[control]',e)
time.sleep(max(0.0,self.dt-(time.time()-t)))
def start(self): self.run=True; threading.Thread(target=self.loop,daemon=True).start()
def stop(self):
self.run=False; time.sleep(0.05)
try: self.disable_all()
finally: self.d1.disconnect(); self.d2.disconnect()
def status(self):
with self.lk: return f'mode={self.mode} en={self.enabled} estop={self.estop} prone={self.prone} vx={self.vx:.2f} vy={self.vy:.2f} yaw={self.yaw:.2f} h={self.h:.3f}'
class CLI:
def __init__(self,d): self.d=d
def run(self):
print('enable disable clear estop_on estop_off prone_on prone_off status')
print('mode stand|wheel|trot, vx vy yaw h roll pitch, stop, quit')
print('interp_on interp_off interp_time <sec>, interp_method linear|cubic|quintic|cosine')
while True:
try: s=input('cmd> ').strip().lower()
except (EOFError,KeyboardInterrupt): s='quit'
if s in ('quit','exit'): break
if s=='enable': self.d.enable_all(); continue
if s=='disable': self.d.disable_all(); continue
if s=='clear': self.d.clear(); continue
if s=='estop_on': self.d.set_estop(True); continue
if s=='estop_off': self.d.set_estop(False); continue
if s=='prone_on': self.d.set_prone(True); continue
if s=='prone_off': self.d.set_prone(False); continue
if s=='status': print(self.d.status()); continue
if s=='stop':
with self.d.lk: self.d.vx=self.d.vy=self.d.yaw=0.0
continue
if s=='interp_on':
with self.d.lk: self.d.use_interpolation=True
print('Interpolation enabled'); continue
if s=='interp_off':
with self.d.lk: self.d.use_interpolation=False
print('Interpolation disabled'); continue
if s.startswith('interp_time '):
try:
t=float(s.split()[1])
if self.d.interpolator: self.d.interpolator.set_transition_time(t)
print(f'Interpolation time set to {t}s')
except Exception as e: print(f'Error: {e}')
continue
if s.startswith('interp_method '):
try:
method=s.split()[1]
if self.d.interpolator: self.d.interpolator.set_method(method)
print(f'Interpolation method set to {method}')
except Exception as e: print(f'Error: {e}')
continue
if s.startswith('mode '):
m=s.split()[1]
if m in ('stand','wheel','trot'):
with self.d.lk: self.d.mode=m
else: print('bad mode')
continue
try:
k,v=s.split()[0],float(s.split()[1])
with self.d.lk:
if k=='vx': self.d.vx=v
elif k=='vy': self.d.vy=v
elif k=='yaw': self.d.yaw=v
elif k=='h': self.d.h=v
elif k=='roll': self.d.roll=v
elif k=='pitch': self.d.pitch=v
else: print('unknown')
except Exception: print('unknown/bad')
class GUI:
def __init__(self,d):
import tkinter as tk
from tkinter import ttk
self.d=d; self.root=tk.Tk(); self.root.title('WheelLeg Deploy')
f=ttk.Frame(self.root,padding=8); f.grid(row=0,column=0,sticky='nsew')
self.state=tk.StringVar(value='E-STOP ON'); ttk.Label(f,textvariable=self.state).grid(row=0,column=0,columnspan=4,sticky='w')
ttk.Button(f,text='Enable',command=self.en).grid(row=1,column=0)
ttk.Button(f,text='Disable',command=self.dis).grid(row=1,column=1)
ttk.Button(f,text='E-STOP ON',command=lambda:self.es(True)).grid(row=1,column=2)
ttk.Button(f,text='E-STOP OFF',command=lambda:self.es(False)).grid(row=1,column=3)
ttk.Button(f,text='Prone ON',command=lambda:self.pr(True)).grid(row=2,column=2)
ttk.Button(f,text='Prone OFF',command=lambda:self.pr(False)).grid(row=2,column=3)
self.mode=tk.StringVar(value='stand'); self.vx=tk.DoubleVar(value=0.0); self.vy=tk.DoubleVar(value=0.0); self.yaw=tk.DoubleVar(value=0.0); self.h=tk.DoubleVar(value=0.33)
self.roll=tk.DoubleVar(value=0.0); self.pitch=tk.DoubleVar(value=0.0)
cb=ttk.Combobox(f,textvariable=self.mode,values=['stand','wheel','trot'],state='readonly'); cb.grid(row=3,column=0,columnspan=2,sticky='ew'); cb.bind('<<ComboboxSelected>>',lambda _:self.sync())
ttk.Button(f,text='Stop',command=self.stp).grid(row=3,column=3)
self.sl(f,4,'vx',self.vx,-0.8,0.8); self.sl(f,5,'vy',self.vy,-0.5,0.5); self.sl(f,6,'yaw',self.yaw,-3,3); self.sl(f,7,'height',self.h,0.157,0.448); self.sl(f,8,'roll',self.roll,-0.4,0.4); self.sl(f,9,'pitch',self.pitch,-0.4,0.4)
self.info=tk.StringVar(value=''); ttk.Label(f,textvariable=self.info).grid(row=10,column=0,columnspan=4,sticky='w'); self.tick()
def sl(self,f,r,n,v,lo,hi):
from tkinter import ttk
ttk.Label(f,text=n).grid(row=r,column=0,sticky='w'); ttk.Scale(f,from_=lo,to=hi,variable=v,command=lambda _:self.sync()).grid(row=r,column=1,columnspan=3,sticky='ew')
def sync(self):
with self.d.lk:
self.d.mode=self.mode.get(); self.d.vx=float(self.vx.get()); self.d.vy=float(self.vy.get()); self.d.yaw=float(self.yaw.get()); self.d.h=float(self.h.get()); self.d.roll=float(self.roll.get()); self.d.pitch=float(self.pitch.get())
def en(self): self.d.enable_all(); self.state.set('Enabled')
def dis(self): self.d.disable_all(); self.state.set('Disabled')
def es(self,on): self.d.set_estop(on); self.state.set('E-STOP ON' if on else 'E-STOP OFF')
def pr(self,on): self.d.set_prone(on)
def stp(self):
with self.d.lk: self.d.vx=self.d.vy=self.d.yaw=0.0
self.vx.set(0.0); self.vy.set(0.0); self.yaw.set(0.0)
def tick(self): self.info.set(self.d.status()); self.root.after(150,self.tick)
def run(self): self.root.mainloop()
def main():
ap=argparse.ArgumentParser()
ap.add_argument('--port-can1',default='/dev/can1')
ap.add_argument('--port-can2',default='/dev/can2')
ap.add_argument('--hz',type=float,default=100.0)
ap.add_argument('--no-gui',action='store_true')
ap.add_argument('--no-interp',action='store_true',help='Disable trajectory interpolation')
ap.add_argument('--interp-method',default='quintic',choices=['linear','cubic','quintic','cosine'],help='Interpolation method')
ap.add_argument('--interp-time',type=float,default=0.3,help='Interpolation transition time (seconds)')
a=ap.parse_args()
d=Deploy(a.port_can1,a.port_can2,a.hz,use_interpolation=not a.no_interp,interp_method=a.interp_method,interp_time=a.interp_time); d.connect(); d.start()
try:
cli=CLI(d); t=threading.Thread(target=cli.run,daemon=True); t.start()
if a.no_gui:
while t.is_alive(): time.sleep(0.2)
else: GUI(d).run()
finally: d.stop()
if __name__=='__main__': main()
@@ -0,0 +1,237 @@
#!/usr/bin/env python3
"""
轨迹插值模块 - 用于平滑关节角度过渡,避免突变和冲击
支持多种插值方法:
- linear: 线性插值
- cubic: 三次多项式(速度连续)
- quintic: 五次多项式(速度和加速度连续,最平滑)
- cosine: 余弦 S 曲线
使用示例:
interp = TrajectoryInterpolator(method='quintic', transition_time=0.5)
# 设置新目标
interp.set_target({'joint1': 1.5, 'joint2': 0.8})
# 每个控制周期调用
smooth_q = interp.update(dt=0.01, current_q={'joint1': 0.5, 'joint2': 0.3})
"""
import time
from typing import Dict, Optional
import numpy as np
class TrajectoryInterpolator:
def __init__(self, method: str = 'quintic', transition_time: float = 0.5):
"""
初始化轨迹插值器
Args:
method: 插值方法 ('linear', 'cubic', 'quintic', 'cosine')
transition_time: 过渡时间(秒)
"""
self.method = method
self.transition_time = transition_time
self.q_start: Dict[str, float] = {}
self.q_target: Dict[str, float] = {}
self.q_current: Dict[str, float] = {}
self.transition_start_time: Optional[float] = None
self.is_transitioning = False
self._interpolation_funcs = {
'linear': self._linear,
'cubic': self._cubic,
'quintic': self._quintic,
'cosine': self._cosine,
}
if method not in self._interpolation_funcs:
raise ValueError(f"Unknown interpolation method: {method}. "
f"Available: {list(self._interpolation_funcs.keys())}")
def set_target(self, q_target: Dict[str, float], force_restart: bool = False):
"""
设置新的目标角度,开始新的过渡
Args:
q_target: 目标关节角度字典 {joint_name: angle}
force_restart: 是否强制重新开始过渡(即使已经在过渡中)
"""
if not self.q_current:
self.q_current = q_target.copy()
self.q_target = q_target.copy()
self.q_start = q_target.copy()
self.is_transitioning = False
return
if not force_restart and self.is_transitioning:
self.q_target = q_target.copy()
return
self.q_start = self.q_current.copy()
self.q_target = q_target.copy()
self.transition_start_time = time.time()
self.is_transitioning = True
def update(self, dt: float, current_q: Optional[Dict[str, float]] = None) -> Dict[str, float]:
"""
更新插值状态,返回当前应该下发的平滑角度
Args:
dt: 时间步长(秒)
current_q: 可选的当前实际角度(用于初始化或同步)
Returns:
平滑后的关节角度字典
"""
if current_q is not None and not self.q_current:
self.q_current = current_q.copy()
self.q_start = current_q.copy()
self.q_target = current_q.copy()
return self.q_current.copy()
if not self.is_transitioning:
return self.q_target.copy()
elapsed = time.time() - self.transition_start_time
if elapsed >= self.transition_time:
self.q_current = self.q_target.copy()
self.is_transitioning = False
return self.q_current.copy()
s = elapsed / self.transition_time
alpha = self._interpolation_funcs[self.method](s)
self.q_current = {}
for joint_name in self.q_target:
start_val = self.q_start.get(joint_name, 0.0)
target_val = self.q_target[joint_name]
self.q_current[joint_name] = start_val + (target_val - start_val) * alpha
return self.q_current.copy()
def reset(self, q_init: Optional[Dict[str, float]] = None):
"""
重置插值器状态
Args:
q_init: 初始角度,如果为 None 则清空所有状态
"""
if q_init is None:
self.q_start = {}
self.q_target = {}
self.q_current = {}
else:
self.q_start = q_init.copy()
self.q_target = q_init.copy()
self.q_current = q_init.copy()
self.transition_start_time = None
self.is_transitioning = False
def is_done(self) -> bool:
"""返回是否已完成当前过渡"""
return not self.is_transitioning
def set_transition_time(self, t: float):
"""动态修改过渡时间"""
self.transition_time = max(0.01, t)
def set_method(self, method: str):
"""动态修改插值方法"""
if method not in self._interpolation_funcs:
raise ValueError(f"Unknown method: {method}")
self.method = method
@staticmethod
def _linear(s: float) -> float:
"""线性插值:alpha = s"""
return np.clip(s, 0.0, 1.0)
@staticmethod
def _cubic(s: float) -> float:
"""三次多项式:alpha = 3s² - 2s³"""
s = np.clip(s, 0.0, 1.0)
return 3.0 * s**2 - 2.0 * s**3
@staticmethod
def _quintic(s: float) -> float:
"""五次多项式:alpha = 10s³ - 15s⁴ + 6s⁵"""
s = np.clip(s, 0.0, 1.0)
return 10.0 * s**3 - 15.0 * s**4 + 6.0 * s**5
@staticmethod
def _cosine(s: float) -> float:
"""余弦 S 曲线:alpha = (1 - cos(πs)) / 2"""
s = np.clip(s, 0.0, 1.0)
return (1.0 - np.cos(np.pi * s)) / 2.0
if __name__ == '__main__':
import matplotlib.pyplot as plt
print("轨迹插值模块测试")
print("=" * 60)
methods = ['linear', 'cubic', 'quintic', 'cosine']
colors = ['blue', 'green', 'red', 'purple']
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
for method, color in zip(methods, colors):
interp = TrajectoryInterpolator(method=method, transition_time=1.0)
interp.reset({'joint1': 0.0})
interp.set_target({'joint1': 1.0})
times = []
positions = []
velocities = []
t = 0.0
dt = 0.01
last_pos = 0.0
while t <= 1.0:
q = interp.update(dt)
pos = q['joint1']
vel = (pos - last_pos) / dt if t > 0 else 0.0
times.append(t)
positions.append(pos)
velocities.append(vel)
last_pos = pos
t += dt
ax1.plot(times, positions, label=method, color=color, linewidth=2)
ax2.plot(times, velocities, label=method, color=color, linewidth=2)
ax1.set_xlabel('时间 (s)')
ax1.set_ylabel('位置 (rad)')
ax1.set_title('不同插值方法的位置曲线')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax2.set_xlabel('时间 (s)')
ax2.set_ylabel('速度 (rad/s)')
ax2.set_title('不同插值方法的速度曲线')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('/home/rc2/work/rcwork/trajectory_interpolation_comparison.png', dpi=150)
print("已保存对比图到: trajectory_interpolation_comparison.png")
print("\n测试完成")
print("=" * 60)
print("推荐使用:")
print(" - quintic: 最平滑,速度和加速度连续")
print(" - cosine: 平滑且计算简单")
print(" - cubic: 速度连续,比 quintic 稍快")
print(" - linear: 最简单但速度会突变")