Files
Mujoco_WASM/controllers/go2_w_balance.py
T
chenlin f4b415c54f
web-platform-ci / TypeScript、Lint、Unit、Build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
web-platform-ci / TypeScript、Lint、Unit、Build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
refactor(web-platform): release V0.6 精简代码
2026-08-28 14:10:16 +08:00

482 lines
18 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.
"""Unitree Go2-W 原地站立平衡控制器(纯 MuJoCo/Python 版本)。
兼容两种模型:
* unitree_mujoco/unitree_robots/go2w/go2w.xml
* Web 平台由 unitree_ros/go2w_description.urdf 转换出的浮动基座 MJCF。
控制策略参考 unitree_mujoco/example/python/stand_go2.py:站立关节角、kp=50、
kd=3.5,并在浏览器中直接计算 motor torque。姿态由基座6轴 IMU 的角速度与
重力加速度互补滤波获得,不包含 DDS、CRC、实机电机模式或 sim2real 逻辑。
"""
import math
NAME = "Unitree Go2-W 原地平衡控制"
CONTROL_HZ = 200
LEG_NAMES = ("FL", "FR", "RL", "RR")
# 来自 unitree_mujoco/example/python/stand_go2.py 的 stand_up_joint_pos。
HIP_TARGET = {"FL": 0.00571868, "FR": -0.00571868,
"RL": 0.00571868, "RR": -0.00571868}
NOMINAL = {"thigh": 0.608813, "calf": -1.21763}
HIP_DOWN = {"FL": 0.0473455, "FR": -0.0473455,
"RL": 0.0473455, "RR": -0.0473455}
STAND_DOWN = {"thigh": 1.22187, "calf": -2.44375}
# SDK 示例使用 kp=50、kd=3.5。力矩上限按 Go2-W URDF/MJCF 保守取值。
KP_FINAL = 50.0
KP_INITIAL = 20.0
KD = 3.5
EFFORT_LIMIT = {"hip": 23.7, "thigh": 23.7, "calf": 35.55}
RAMP_SECONDS = 1.2
WHEEL_DAMPING = 0.12
MAX_WHEEL_TORQUE = 6.0
WHEEL_RADIUS = 0.065
TRACK_HALF_WIDTH = 0.23
DRIVE_SPEED = 0.35
DRIVE_ACCELERATION = 0.25
TURN_RATE = 1.4
TURN_ACCELERATION = 0.7
WHEEL_VELOCITY_KP = 0.45
TURN_VELOCITY_KP = 0.90
# 后轮仅承担部分差速转向,降低侧向轮胎力造成的后腿 hip 大幅摆动。
REAR_TURN_SCALE = 0.40
REAR_TURN_MAX_TORQUE = 3.0
WHEEL_FEEDFORWARD = 0.25
JUMP_CROUCH_SECONDS = 0.28
JUMP_EXTEND_SECONDS = 0.08
JUMP_TUCK_SECONDS = 0.18
JUMP_LANDING_SECONDS = 0.22
JUMP_RECOVER_SECONDS = 0.35
# 轮毂反作用力矩用于抵消伸腿时的后仰角动量,不用于驱动水平位移。
JUMP_PITCH_WHEEL_TORQUE = -6.0
def _clamp(value, lower, upper):
return max(lower, min(upper, value))
def _move_toward(value, target, max_delta):
return value + _clamp(target - value, -max_delta, max_delta)
def _accel_attitude(acceleration):
"""由 Body 局部系中的重力方向估计横滚角和俯仰角。"""
ax, ay, az = acceleration
return math.atan2(ay, az), math.atan2(-ax, math.sqrt(ay * ay + az * az))
def _resolve(api, kind, names):
"""依次尝试官方 MJCF 与 URDF 转换模型的命名。"""
resolver = getattr(api, kind)
last_error = None
for name in names:
try:
return resolver(name)
except Exception as error:
last_error = error
raise RuntimeError(f"无法解析 {kind},候选名称:{', '.join(names)}") from last_error
def init(api):
legs = []
for prefix in LEG_NAMES:
joints = {}
actuators = {}
for part in ("hip", "thigh", "calf"):
joint_name = f"{prefix}_{part}_joint"
joints[part] = _resolve(api, "joint", (joint_name,))
actuators[part] = _resolve(
api, "actuator", (f"{prefix}_{part}", f"{joint_name}_motor")
)
wheel_joint = _resolve(
api, "joint", (f"{prefix}_wheel_joint", f"{prefix}_foot_joint")
)
wheel_actuator = _resolve(
api,
"actuator",
(f"{prefix}_wheel", f"{prefix}_wheel_joint_motor", f"{prefix}_foot_joint_motor"),
)
legs.append({
"name": prefix,
"joints": joints,
"actuators": actuators,
"wheel_joint": wheel_joint,
"wheel_actuator": wheel_actuator,
"side": 1.0 if prefix.endswith("L") else -1.0,
"fore": 1.0 if prefix.startswith("F") else -1.0,
})
return {
"base": _resolve(api, "body", ("base_link", "base")),
"imu_gyro": _resolve(api, "sensor", ("imu_gyro", "__platform_imu_gyro__")),
"imu_acc": _resolve(api, "sensor", ("imu_acc", "__platform_imu_acc__")),
"legs": legs,
"started": False,
"start_time": 0.0,
"estimated_roll": 0.0,
"estimated_pitch": 0.0,
"filtered_droll": 0.0,
"filtered_dpitch": 0.0,
"unstable_duration": 0.0,
"motion": "stop",
"linear_speed": 0.0,
"yaw_rate": 0.0,
"jump_requested": False,
"jump_started": None,
"posture_recovering": False,
"stable_duration": 0.0,
"recovery_roll_integral": 0.0,
"recovery_pitch_integral": 0.0,
"last_base_position": None,
"base_velocity_x": 0.0,
"base_velocity_y": 0.0,
"jump_anchor": None,
"jump_forward": None,
}
def _initialize(ctx, state, acceleration):
state["started"] = True
state["start_time"] = ctx.time
state["estimated_roll"], state["estimated_pitch"] = _accel_attitude(acceleration)
def step(ctx, state):
gyro = ctx.sensor(state["imu_gyro"])
acceleration = ctx.sensor(state["imu_acc"])
base_position = ctx.body_position(state["base"])
if not state["started"]:
_initialize(ctx, state, acceleration)
dt = max(1.0e-4, ctx.dt)
previous_position = state["last_base_position"]
if previous_position is not None:
velocity_alpha = 0.25
state["base_velocity_x"] += velocity_alpha * (
(base_position[0] - previous_position[0]) / dt
- state["base_velocity_x"]
)
state["base_velocity_y"] += velocity_alpha * (
(base_position[1] - previous_position[1]) / dt
- state["base_velocity_y"]
)
state["last_base_position"] = list(base_position)
rate_alpha = 0.25
state["filtered_droll"] += rate_alpha * (gyro[0] - state["filtered_droll"])
state["filtered_dpitch"] += rate_alpha * (gyro[1] - state["filtered_dpitch"])
accel_roll, accel_pitch = _accel_attitude(acceleration)
fusion = 0.015
state["estimated_roll"] = (1.0 - fusion) * (
state["estimated_roll"] + state["filtered_droll"] * dt
) + fusion * accel_roll
state["estimated_pitch"] = (1.0 - fusion) * (
state["estimated_pitch"] + state["filtered_dpitch"] * dt
) + fusion * accel_pitch
roll, pitch = state["estimated_roll"], state["estimated_pitch"]
forward_error = 0.0
forward_velocity = 0.0
if state["jump_anchor"] is not None:
forward_x, forward_y = state["jump_forward"]
forward_error = (
(base_position[0] - state["jump_anchor"][0]) * forward_x
+ (base_position[1] - state["jump_anchor"][1]) * forward_y
)
forward_velocity = (
state["base_velocity_x"] * forward_x
+ state["base_velocity_y"] * forward_y
)
if state["posture_recovering"]:
state["recovery_roll_integral"] = _clamp(
state["recovery_roll_integral"] + roll * dt, -0.6, 0.6
)
state["recovery_pitch_integral"] = _clamp(
state["recovery_pitch_integral"] + pitch * dt, -0.6, 0.6
)
posture_stable = (
abs(roll) < 0.06
and abs(pitch) < 0.06
and abs(state["filtered_droll"]) < 0.15
and abs(state["filtered_dpitch"]) < 0.15
and abs(forward_error) < 0.015
and abs(forward_velocity) < 0.05
and base_position[2] > 0.38
)
state["stable_duration"] = (
state["stable_duration"] + dt if posture_stable else 0.0
)
if state["stable_duration"] > 0.5:
# 保留积分得到的静态姿态补偿;清零会让机身再次回到带偏差的平衡点。
state["posture_recovering"] = False
state["stable_duration"] = 0.0
state["jump_anchor"] = None
state["jump_forward"] = None
elapsed = max(0.0, ctx.time - state["start_time"])
ramp = 1.0 if elapsed >= 3.0 else math.tanh(elapsed / RAMP_SECONDS)
kp = KP_INITIAL + (KP_FINAL - KP_INITIAL) * ramp
motion = state["motion"]
target_linear_speed = (
DRIVE_SPEED
if motion == "forward"
else -DRIVE_SPEED if motion == "backward" else 0.0
)
target_yaw_rate = (
TURN_RATE
if motion == "turn_left"
else -TURN_RATE if motion == "turn_right" else 0.0
)
state["linear_speed"] = _move_toward(
state["linear_speed"], target_linear_speed, DRIVE_ACCELERATION * ctx.dt
)
state["yaw_rate"] = _move_toward(
state["yaw_rate"], target_yaw_rate, TURN_ACCELERATION * ctx.dt
)
jump_offset_thigh = 0.0
jump_offset_calf = 0.0
jump_launching = False
jump_balance_scale = 1.6
jump_centering = state["jump_anchor"] is not None and state["jump_started"] is None
ready_to_jump = (
abs(state["linear_speed"]) < 0.05 and abs(state["yaw_rate"]) < 0.1
)
if (
state["jump_requested"]
and ramp > 0.98
and ready_to_jump
and not state["posture_recovering"]
and state["jump_started"] is None
):
state["jump_started"] = ctx.time
state["jump_requested"] = False
quat = ctx.body_quat(state["base"])
qw, qx, qy, qz = quat
forward_x = 1.0 - 2.0 * (qy * qy + qz * qz)
forward_y = 2.0 * (qx * qy + qw * qz)
forward_norm = max(1.0e-6, math.hypot(forward_x, forward_y))
state["jump_anchor"] = [base_position[0], base_position[1]]
state["jump_forward"] = [
forward_x / forward_norm,
forward_y / forward_norm,
]
if state["jump_started"] is not None:
jump_elapsed = ctx.time - state["jump_started"]
launch_end = JUMP_CROUCH_SECONDS + JUMP_EXTEND_SECONDS
tuck_end = launch_end + JUMP_TUCK_SECONDS
landing_end = tuck_end + JUMP_LANDING_SECONDS
recover_end = landing_end + JUMP_RECOVER_SECONDS
if jump_elapsed < JUMP_CROUCH_SECONDS:
phase = jump_elapsed / JUMP_CROUCH_SECONDS
jump_offset_thigh = 0.34 * phase
jump_offset_calf = -0.64 * phase
elif jump_elapsed < launch_end:
# 以小腿为主提供竖直爆发力,避免大腿推力把机身向后推出。
jump_offset_thigh = -0.22
jump_offset_calf = 0.30
jump_launching = True
jump_balance_scale = 0.28
elif jump_elapsed < tuck_end:
# 离地后主动收腿,提高轮端离地高度并减小腿部转动惯量。
jump_offset_thigh = 0.22
jump_offset_calf = -0.40
jump_balance_scale = 0.4
elif jump_elapsed < landing_end:
# 触地前逐渐伸腿,避免保持收腿姿态直接撞击地面。
phase = (jump_elapsed - tuck_end) / JUMP_LANDING_SECONDS
jump_offset_thigh = 0.22 * (1.0 - phase) - 0.05 * phase
jump_offset_calf = -0.40 * (1.0 - phase) + 0.08 * phase
jump_balance_scale = 0.4 + 0.8 * phase
elif jump_elapsed < recover_end:
jump_centering = True
phase = (jump_elapsed - landing_end) / JUMP_RECOVER_SECONDS
jump_offset_thigh = -0.05 * (1.0 - phase)
jump_offset_calf = 0.08 * (1.0 - phase)
jump_balance_scale = 1.2 + 0.4 * phase
else:
state["jump_started"] = None
state["posture_recovering"] = True
state["stable_duration"] = 0.0
state["recovery_roll_integral"] = 0.0
state["recovery_pitch_integral"] = 0.0
# SDK 示例的核心:12 个腿关节平滑进入站立姿态并保持 PD 闭环。
motion_level = max(
abs(state["linear_speed"]) / DRIVE_SPEED,
abs(state["yaw_rate"]) / TURN_RATE,
)
for leg in state["legs"]:
for part in ("hip", "thigh", "calf"):
up = HIP_TARGET[leg["name"]] if part == "hip" else NOMINAL[part]
down = HIP_DOWN[leg["name"]] if part == "hip" else STAND_DOWN[part]
desired = down + ramp * (up - down)
if part == "calf" and ramp > 0.8:
# 通过左右/前后轮腿长度差调平车身:高侧缩短,低侧伸长。
if state["jump_started"] is not None:
balance_scale = jump_balance_scale
elif motion == "stop":
balance_scale = 1.6
else:
balance_scale = 1.0
roll_term = balance_scale * (
0.34 * roll
+ 0.025 * state["filtered_droll"]
+ 0.20 * state["recovery_roll_integral"]
)
target_pitch = 0.0 if motion == "stop" else (
0.12 * state["linear_speed"] / DRIVE_SPEED
+ 0.08 * abs(state["yaw_rate"]) / TURN_RATE
)
pitch_error = pitch - target_pitch
pitch_term = balance_scale * (
0.50 * pitch_error
+ 0.035 * state["filtered_dpitch"]
+ 0.28 * state["recovery_pitch_integral"]
)
desired -= leg["side"] * roll_term
desired += leg["fore"] * pitch_term
desired -= 0.18 * motion_level
desired += jump_offset_calf
elif part == "thigh":
desired += 0.10 * motion_level + jump_offset_thigh
target = desired
position = ctx.qpos(leg["joints"][part])
velocity = ctx.qvel(leg["joints"][part])
damping = 1.0 if jump_launching else KD
torque = kp * (target - position) - damping * velocity
limit = EFFORT_LIMIT[part]
if jump_launching and part == "calf":
torque = limit
elif jump_launching and part == "thigh":
# 爆发阶段由小腿提供主要竖直推力;大腿推力会引入向后冲量。
torque = 0.0
ctx.set_control(leg["actuators"][part], _clamp(torque, -limit, limit))
# 四轮速度闭环:斜坡限速避免瞬时驱动力矩使机身后仰。
if motion == "stop":
balance_torque = 3.0 * pitch + 0.25 * state["filtered_dpitch"]
else:
balance_torque = 1.4 * pitch + 0.10 * state["filtered_dpitch"]
# 记录起跳点在机身前向轴上的位置,落地接触后用轮子收回水平漂移。
centering_speed = 0.0
if jump_centering and base_position[2] < 0.45:
centering_speed = _clamp(
-4.0 * forward_error - 0.8 * forward_velocity,
-0.30,
0.30,
)
for leg in state["legs"]:
target_velocity = (state["linear_speed"] + centering_speed) / WHEEL_RADIUS
turn_scale = 1.0 if leg["fore"] > 0 else REAR_TURN_SCALE
target_velocity -= (
leg["side"]
* state["yaw_rate"]
* TRACK_HALF_WIDTH
* turn_scale
/ WHEEL_RADIUS
)
wheel_velocity = ctx.qvel(leg["wheel_joint"])
velocity_kp = (
TURN_VELOCITY_KP
if abs(state["yaw_rate"]) > 0.1 or abs(centering_speed) > 0.02
else WHEEL_VELOCITY_KP
)
wheel_torque = balance_torque + velocity_kp * (
target_velocity - wheel_velocity
)
if abs(target_velocity) > 0.1:
wheel_torque += math.copysign(WHEEL_FEEDFORWARD, target_velocity)
if motion == "stop":
wheel_torque -= WHEEL_DAMPING * wheel_velocity
if jump_launching:
wheel_torque = JUMP_PITCH_WHEEL_TORQUE
elif state["jump_started"] is not None and base_position[2] > 0.47:
# 腾空期间利用四个轮子的反作用角动量持续把机身俯仰拉回零。
# 接近地面后恢复轮速闭环,避免姿态力矩转化为水平冲量。
wheel_torque = (
12.0 * pitch + 1.5 * state["filtered_dpitch"]
)
wheel_limit = (
REAR_TURN_MAX_TORQUE
if abs(state["yaw_rate"]) > 0.1 and leg["fore"] < 0
else MAX_WHEEL_TORQUE
)
ctx.set_control(
leg["wheel_actuator"],
_clamp(wheel_torque, -wheel_limit, wheel_limit),
)
# 持续失稳时主动停止,避免倒地后控制器继续输出饱和力矩。
unstable = ramp > 0.95 and (
base_position[2] < 0.16 or abs(roll) > 1.0 or abs(pitch) > 1.0
)
state["unstable_duration"] = state["unstable_duration"] + dt if unstable else 0.0
if state["unstable_duration"] > 0.65:
raise RuntimeError(
f"Go2-W 已失稳:z={base_position[2]:.3f} m, "
f"roll={roll:.3f} rad, pitch={pitch:.3f} rad;请重置后检查模型接触参数"
)
def command(name, state):
"""接收 Web Python SDK 的标准基本移动指令。"""
if name in ("forward", "backward", "turn_left", "turn_right"):
state["motion"] = name
state["posture_recovering"] = False
state["stable_duration"] = 0.0
state["recovery_roll_integral"] = 0.0
state["recovery_pitch_integral"] = 0.0
state["jump_anchor"] = None
state["jump_forward"] = None
return
if name == "stop":
state["motion"] = "stop"
state["jump_requested"] = False
state["posture_recovering"] = True
state["stable_duration"] = 0.0
state["recovery_roll_integral"] = 0.0
state["recovery_pitch_integral"] = 0.0
return
if name == "jump":
state["motion"] = "stop"
if state["jump_started"] is None:
# 每次起跳都先重新确认重心稳定;移动中触发时不会直接带着惯性伸腿。
state["jump_requested"] = True
state["posture_recovering"] = True
state["stable_duration"] = 0.0
return
raise ValueError(f"不支持的移动指令:{name}")
def reset(state):
state["started"] = False
state["estimated_roll"] = 0.0
state["estimated_pitch"] = 0.0
state["filtered_droll"] = 0.0
state["filtered_dpitch"] = 0.0
state["unstable_duration"] = 0.0
state["motion"] = "stop"
state["linear_speed"] = 0.0
state["yaw_rate"] = 0.0
state["jump_requested"] = False
state["jump_started"] = None
state["posture_recovering"] = False
state["stable_duration"] = 0.0
state["recovery_roll_integral"] = 0.0
state["recovery_pitch_integral"] = 0.0
state["last_base_position"] = None
state["base_velocity_x"] = 0.0
state["base_velocity_y"] = 0.0
state["jump_anchor"] = None
state["jump_forward"] = None
def dispose(state):
pass