Files
RC_WheelLeg/05_software/train/rc_mjlab/sim2sim/nav_sim2sim.py
T

1385 lines
77 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.
#!/usr/bin/env python3
"""
nav_sim2sim.py: 机器人高精度绝对惯导 2D 交互导航系统 & 实时遥测仪表盘。
特性:
- 动态 XML 地图解析引擎:自动读取并解析 scene_terrain.xml 中的所有 geom 障碍物,实现 100% 地图画面与物理环境精准对应。
- 极佳的可视化表现:支持旋转 box 顶点多边形解算、柱状 cylinder 绘制以及 hfield 区域半透明渲染,支持鼠标滚轮缩放与右键拖拽。
- 鼠标手绘轨迹巡航 (Path Drawing):长按左键即可在地图上画出任意曲线,系统智能离散化为航点,控制机器人顺畅驶完轨迹。
- 纯坐标触发自适应限高爬行:不依赖高度传感器,依据全局 IMU 坐标范围自动下蹲过杆。
- 关键点记录与指令微调:支持 R 键打点、TXT 文件导出以及命令行(如 "go 3.5 -6.0")直接发令控制。
- 全界面中文本地化。
"""
import os
import sys
import time
import math
import torch
import numpy as np
import pygame
import xml.etree.ElementTree as ET
from pathlib import Path
# 插入局部路径,确保模块导入顺畅
sys.path.append(str(Path(__file__).parent.absolute()))
from interface.mujoco_io import MuJoCoIO
from policy.policy_runner import PolicyRunner
import mujoco
import mujoco.viewer
# ==============================================================================
# HSL / RGB 科技感深色调配色系统
# ==============================================================================
COLOR_BG = (10, 15, 30) # Slate 950 深邃星空蓝
COLOR_GRID = (22, 29, 48) # 浅灰色网格线
COLOR_AXIS = (38, 50, 78) # 主轴线
COLOR_HUD_BG = (18, 24, 42) # 半透明磨砂控制面板背景
COLOR_HUD_BORDER = (38, 52, 84) # 边框线
COLOR_TEXT_LIGHT = (248, 250, 252) # 高对比度白色文字
COLOR_TEXT_MUTED = (148, 163, 184) # 灰色辅助说明文字
COLOR_EMERALD = (16, 185, 129) # 翡翠绿:自动模式 / 正常状态
COLOR_ROSE = (244, 63, 94) # 玫瑰红:手动模式 / 警告状态
COLOR_AMBER = (245, 158, 11) # 琥珀黄:高墙 / 爬坡提示
COLOR_GOLD = (234, 179, 8) # 亮金色:导航目标点
COLOR_CYAN = (6, 182, 212) # 霓虹青:机器人本体与行进轨迹
COLOR_PURPLE = (139, 92, 246) # 科技紫:关键点标记与 hfield 区域
COLOR_CONSOLE_BG = (7, 10, 19) # 控制台终端深黑色背景
# ==============================================================================
# 动态 XML 地图解析引擎
# ==============================================================================
def parse_xml_obstacles(xml_path):
"""
解析 scene_terrain.xml,动态提取所有的 geom 障碍物、平台、限高柱。
支持提取:pos, size, type, quat, rgba, name, 碰撞屏蔽(contype/conaffinity)。
"""
obstacles = []
hfields = {}
if not os.path.exists(xml_path):
print(f"[XML 解析器] 警告:未找到 XML 地图文件: {xml_path}")
return obstacles
try:
tree = ET.parse(xml_path)
root = tree.getroot()
# 1. 扫描并缓存所有 hfield 几何参数
for hf in root.iter('hfield'):
hf_name = hf.get('name')
hf_size_str = hf.get('size')
if hf_name and hf_size_str:
try:
hfields[hf_name] = [float(x) for x in hf_size_str.split()]
except ValueError:
continue
# 2. 解析所有的 geom 碰撞体
for geom in root.iter('geom'):
g_name = geom.get('name', 'geom')
# 过滤地面
if g_name == 'floor':
continue
g_type = geom.get('type')
pos_str = geom.get('pos')
size_str = geom.get('size')
quat_str = geom.get('quat', '1 0 0 0')
rgba_str = geom.get('rgba', '0.7 0.7 0.7 1')
contype = geom.get('contype', '1')
conaffinity = geom.get('conaffinity', '1')
hfield_ref = geom.get('hfield')
if not pos_str or not g_type:
continue
try:
pos = [float(x) for x in pos_str.split()]
quat = [float(x) for x in quat_str.split()]
rgba = [float(x) for x in rgba_str.split()]
# 如果是 hfield,则从缓存的 hfield 大小中提取尺寸
if g_type == 'hfield' and hfield_ref in hfields:
size = hfields[hfield_ref]
else:
size = [float(x) for x in size_str.split()] if size_str else []
except ValueError:
continue
# contype="0" 且 conaffinity="0" 表示仅渲染不发生碰撞的指示标
collidable = (contype != '0') and (conaffinity != '0')
obstacles.append({
'name': g_name,
'type': g_type,
'pos': pos,
'size': size,
'quat': quat,
'rgba': rgba,
'collidable': collidable
})
print(f"[XML 解析器] 成功加载地图:共解析出 {len(obstacles)} 个几何元素,已完成 100% 位置对应。")
except Exception as e:
print(f"[XML 解析器] 解析 XML 时发生错误: {e}")
return obstacles
# ==============================================================================
# 数学计算工具与姿态解算
# ==============================================================================
def quat_to_euler(quat_wxyz):
"""将 MuJoCo 的 wxyz 四元数转换为机身欧拉角 Roll, Pitch, Yaw (弧度)。"""
qw, qx, qy, qz = quat_wxyz
# 横滚角 Roll (x轴自转)
sinr_cosp = 2.0 * (qw * qx + qy * qz)
cosr_cosp = 1.0 - 2.0 * (qx * qx + qy * qy)
roll = math.atan2(sinr_cosp, cosr_cosp)
# 俯仰角 Pitch (y轴摆动)
sinp = 2.0 * (qw * qy - qz * qx)
pitch = math.asin(np.clip(sinp, -1.0, 1.0))
# 偏航角 Yaw (z轴航向)
siny_cosp = 2.0 * (qw * qz + qx * qy)
cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz)
yaw = math.atan2(siny_cosp, cosy_cosp)
return roll, pitch, yaw
# ==============================================================================
# 自动巡航控制器
# ==============================================================================
class NavController:
def __init__(self, io):
self.io = io
self.mode = "MANUAL" # "MANUAL" (手动键盘) 或 "AUTO" (自动导航)
# 导航目标点
self.target_x = None
self.target_y = None
self.target_z = 0.0
# 预设巡航任务状态
self.mission_name = "无"
self.waypoints = [] # list of (x, y, speed, policy)
self.waypoint_idx = 0
self.drawn_path = [] # 记录当前整条规划路径,用于 2D 高亮线段绘制
# 关键点记录表
self.recorded_keypoints = []
# APF 动态人工势场避障列表(将在主程序启动时根据 XML 解析结果动态填充)
self.obstacles = []
def start_mission(self, name, waypoints):
self.mode = "AUTO"
self.mission_name = name
self.waypoints = waypoints
self.waypoint_idx = 0
self.drawn_path = [(wp[0], wp[1]) for wp in waypoints]
if waypoints:
self.target_x, self.target_y = waypoints[0][0], waypoints[0][1]
print(f"[导航系统] 启动巡航任务:{name}。前往第 1 个航点: ({self.target_x:.2f}, {self.target_y:.2f})")
else:
self.target_x, self.target_y = None, None
def cancel_mission(self):
self.mode = "MANUAL"
self.mission_name = "无"
self.waypoints = []
self.waypoint_idx = 0
self.target_x, self.target_y = None, None
self.drawn_path = []
print("[导航系统] 自动巡航已取消,机器人切换回手动控制模式。")
def update_navigation(self, rx, ry, ryaw, runner, dt=0.02):
"""
核心控制计算逻辑:基于 PD 路径寻迹、自适应转向锁定与边界感知避障势场。
"""
if self.mode != "AUTO" or self.target_x is None or self.target_y is None:
return 0.0, 0.0, 0.0
# ----------------------------------------------------------------------
# 正常的 PD 路径跟随算法
# ----------------------------------------------------------------------
dx = self.target_x - rx
dy = self.target_y - ry
dist = math.sqrt(dx*dx + dy*dy)
# 判定抵达航点
if dist < 0.15:
if len(self.waypoints) > 0 and self.waypoint_idx < len(self.waypoints) - 1:
self.waypoint_idx += 1
next_wp = self.waypoints[self.waypoint_idx]
self.target_x, self.target_y = next_wp[0], next_wp[1]
print(f"[导航系统] 顺利抵达航点 WP {self.waypoint_idx}。下一个航点: ({self.target_x:.2f}, {self.target_y:.2f})")
return 0.0, 0.0, 0.0
else:
print("[导航系统] 自动巡航顺利走完!已平稳停留在终点。")
self.mode = "MANUAL"
self.mission_name = "无"
self.target_x, self.target_y = None, None
return 0.0, 0.0, 0.0
# 提取当前子航点的运行速度限制
speed_limit = 0.5
if self.waypoints and self.waypoint_idx < len(self.waypoints):
wp = self.waypoints[self.waypoint_idx]
if len(wp) >= 3:
speed_limit = wp[2]
# ----------------------------------------------------------------------
# 根据当前绝对地理坐标动态分配具体速度上限 (全局峰值限制为 1.2 m/s)
# ----------------------------------------------------------------------
dynamic_speed_limit = 0.6 # 默认平地过渡速度
# 1. 高越障墙冲越区 (拉满至峰值 1.2 m/s 以获取最大过墙动能)
if 1.0 <= rx <= 2.6 and -8.5 <= ry <= -5.0:
dynamic_speed_limit = 1.2
# 2. 限高低姿爬行区 (下蹲慢行,安全速度 0.3 m/s)
elif 4.8 <= rx <= 6.4 and -9.5 <= ry <= -8.5:
dynamic_speed_limit = 0.3
# 3. 蛇形绕障避险区 (精密控弯,平稳速度 0.4 m/s)
elif 0.5 <= rx <= 3.5 and -14.0 <= ry <= -9.5:
dynamic_speed_limit = 0.4
# 4. 台阶独木桥跨越区 (攀爬跨越,精细速度 0.4 m/s)
elif 1.0 <= rx <= 4.5 and -5.0 <= ry <= 0.0:
dynamic_speed_limit = 0.4
# 综合考虑航点限速、地理限速与 1.2 m/s 全局硬上限
speed_limit = min(speed_limit, dynamic_speed_limit, 1.2)
# ----------------------------------------------------------------------
# 直接路径寻迹 (已按需彻底移除人工势场避障功能)
# ----------------------------------------------------------------------
# 最终目标航向角 (直接朝着目标点行驶)
target_yaw = math.atan2(dy, dx)
# 航向偏差角度规范化至 [-pi, pi]
yaw_err = (target_yaw - ryaw + math.pi) % (2.0 * math.pi) - math.pi
# 自适应转向 PD 控制
Kp_yaw = 1.8
cmd_wz = Kp_yaw * yaw_err
cmd_wz = np.clip(cmd_wz, -1.0, 1.0)
# 转向优先自锁机制:当航向误差过大时(>45度),停止向前行进,全力原地自转对准
if abs(yaw_err) > 0.8:
cmd_vx = 0.0
else:
if "高墙" in self.mission_name or "charge" in self.mission_name.lower():
# 冲越复杂障碍物(如高越障墙)时,不随距离缩减速度,保持全速冲越!
cmd_vx = speed_limit
else:
Kp_dist = 0.8
cmd_vx = Kp_dist * dist * math.cos(yaw_err)
cmd_vx = np.clip(cmd_vx, -0.2, speed_limit)
return float(cmd_vx), 0.0, float(cmd_wz)
# ==============================================================================
# UI 组件:现代化圆角高光按钮
# ==============================================================================
class PygameButton:
def __init__(self, rect, text, bg_color, hover_color, text_color):
self.rect = pygame.Rect(rect)
self.text = text
self.bg_color = bg_color
self.hover_color = hover_color
self.text_color = text_color
def draw(self, surface, font):
mouse_pos = pygame.mouse.get_pos()
color = self.hover_color if self.rect.collidepoint(mouse_pos) else self.bg_color
pygame.draw.rect(surface, color, self.rect, border_radius=6)
pygame.draw.rect(surface, COLOR_HUD_BORDER, self.rect, width=1, border_radius=6)
text_surf = font.render(self.text, True, self.text_color)
text_rect = text_surf.get_rect(center=self.rect.center)
surface.blit(text_surf, text_rect)
def is_clicked(self, event):
if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
return self.rect.collidepoint(event.pos)
return False
# ==============================================================================
# 主循环与渲染主程序
# ==============================================================================
def main():
# 检测计算卡与渲染配置
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"[主程序] 正在使用计算设备: {device}")
# 路径解析
project_root = Path(__file__).parent.parent.absolute()
terrain_dir = Path(__file__).parent / "terrain"
terrain_xml = terrain_dir / "scene_terrain.xml"
robot_xml = project_root / "mjcf" / "wheelleg.xml"
rough_onnx = project_root / "model_6800.onnx"
crawl_onnx = project_root / "model_crawl.onnx"
policy_path = {
"rough": rough_onnx if rough_onnx.exists() else project_root / "model_rough.pt",
"crawl": crawl_onnx if crawl_onnx.exists() else project_root / "model_crawl.pt"
}
# 1. 解析 XML 地图障碍物,实现 100% 可视化精准对应
print("\n[主程序] 解析 scene_terrain.xml 实景物理元素...")
xml_obstacles = parse_xml_obstacles(terrain_xml)
# 2. 初始化 MuJoCo 物理接口与神经网络策略
print("\n[主程序] 正在编译 MuJoCo 物理环境模型...")
io = MuJoCoIO(terrain_xml, robot_xml, terrain_dir)
print("\n[主程序] 载入神经网络策略驱动模型...")
runner = PolicyRunner(policy_path, device)
# 初始化机器人姿态
io.reset_robot(runner.default_dof_pos)
runner.reset()
# 高频时序同步
control_dt = io.control_dt # 0.02s
sim_steps_per_control = int(round(control_dt / io.m.opt.timestep)) # 10
# 初始化自建巡航控制器
nav = NavController(io)
# 动态把从 XML 解析出来的碰撞体积 geom,自动注册装配到导航避障势场列表中(剔除翻越的高墙)
for obs in xml_obstacles:
# 1. 排除出发区
if obs['name'] == 'spawn_zone' or not obs['collidable']:
continue
# 2. 排除位于 Y = -7.0 处的 30cm 高越障墙,实现直接加速跨越冲锋
if abs(obs['pos'][1] - (-7.0)) < 0.1:
continue
# 根据几何形状计算其近似避障边界外切半径 o_rad
cx, cy = obs['pos'][0], obs['pos'][1]
if obs['type'] == 'cylinder':
o_rad = obs['size'][0]
elif obs['type'] == 'box':
o_rad = math.sqrt(obs['size'][0]**2 + obs['size'][1]**2)
else:
o_rad = 0.3 # 默认缺省值
nav.obstacles.append((cx, cy, o_rad, obs['name']))
print(f"[避障系统] 已成功动态注册 {len(nav.obstacles)} 个避障势场障碍体。高越障墙已做冲越通行处理。")
# ==========================================================================
# Pygame 图形化仪表盘窗口设置
# ==========================================================================
pygame.init()
pygame.font.init()
WINDOW_WIDTH, WINDOW_HEIGHT = 1024, 768
MAP_WIDTH = 680 # 稍微缩减地图面幅,给右侧 HUD 面板腾出充足排版空间
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Robot High-Precision Absolute Navigation & Real-time Telemetry Dashboard")
# 中文兼容与高解析度字体加载系统,直接加载 Windows 系统字体文件,杜绝方框(Tofu 乱码)现象
def get_chinese_font(size, bold=False):
# 优先读取系统物理路径下的字体文件,彻底避开 pygame.font.SysFont Silently-Fail Bug
font_paths = [
"C:/Windows/Fonts/msyhbd.ttc" if bold else "C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/simhei.ttf",
"C:/Windows/Fonts/simsun.ttc",
]
for path in font_paths:
if os.path.exists(path):
try:
return pygame.font.Font(path, size)
except Exception:
continue
# 备用 SysFont 尝试
for font_name in ["microsoftyahei", "微软雅黑", "simhei", "黑体", "simsun", "宋体"]:
try:
return pygame.font.SysFont(font_name, size, bold=bold)
except Exception:
continue
# 最终安全降级
return pygame.font.Font(None, size)
font_large = get_chinese_font(22, bold=True)
font_medium = get_chinese_font(16, bold=True)
font_small = get_chinese_font(13, bold=False)
font_mono = get_chinese_font(13, bold=False)
# 视口状态量
zoom = 30.0 # 物理单位到屏幕像素倍数 (像素/米)
pan_x = 0.0 # pan平移偏移量
pan_y = 0.0
is_dragging = False
drag_start_x = 0
drag_start_y = 0
auto_center = True
sim_speed_factor = 1.0 # 仿真物理流速倍率
# 轨迹面包屑历史缓存
robot_trail = []
max_trail_len = 250
# 手绘轨迹变量
draw_mode = False
is_drawing_path = False
drawn_points = []
# HUD 侧边交互按钮排版(垂直布局与位置微调,彻底消除重叠,完美对齐分界线)
# Column 1 X: 700, Column 2 X: 860. 宽度 140 像素
btn_slalom = PygameButton((700, 307, 140, 26), "S形绕杆", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_crawl = PygameButton((860, 307, 140, 26), "限高下蹲", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_gravel = PygameButton((700, 338, 140, 26), "砂砾碎石", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_wall = PygameButton((860, 338, 140, 26), "高墙越障", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_stairs = PygameButton((700, 369, 140, 26), "台阶攀爬", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_bridge = PygameButton((860, 369, 140, 26), "斜坡木桥", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
# 终极任务:大满贯 (科技紫圆角大按钮)
btn_grand = PygameButton((700, 400, 300, 26), "障碍赛大满贯 (总任务)", (139, 92, 246), (124, 58, 237), COLOR_TEXT_LIGHT)
# 系统与视图控制按钮
btn_clear_trg = PygameButton((700, 467, 140, 26), "清除与停止", (244, 63, 94, 100), (225, 29, 72), COLOR_TEXT_LIGHT)
btn_center_toggle = PygameButton((860, 467, 140, 26), "视角居中:开", (16, 185, 129), (5, 150, 105), COLOR_TEXT_LIGHT)
btn_draw_mode = PygameButton((700, 498, 140, 26), "手绘模式:关", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_speed_down = PygameButton((860, 498, 44, 26), "倍速-", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_speed_normal = PygameButton((908, 498, 44, 26), "标准", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_speed_up = PygameButton((956, 498, 44, 26), "倍速+", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
# 坐标直接微调控制发令键
btn_x_plus = PygameButton((700, 565, 65, 24), "X+", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_x_minus = PygameButton((775, 565, 65, 24), "X-", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_y_plus = PygameButton((860, 565, 65, 24), "Y+", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_y_minus = PygameButton((935, 565, 65, 24), "Y-", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
# 关键点记录按键
btn_rec_key = PygameButton((700, 628, 95, 24), "保存位置", (139, 92, 246), (124, 58, 237), COLOR_TEXT_LIGHT)
btn_exp_key = PygameButton((805, 628, 95, 24), "导出文件", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
btn_clear_key = PygameButton((910, 628, 90, 24), "清空位置", (30, 41, 59), (51, 65, 85), COLOR_TEXT_LIGHT)
# 动态指令命令行输入窗口
console_active = False
console_text = ""
console_rect = pygame.Rect(700, 708, 300, 22)
# 绝对坐标变换函数闭包
def world_to_screen(wx, wy):
sx = int(MAP_WIDTH / 2.0 + wx * zoom + pan_x)
sy = int(WINDOW_HEIGHT / 2.0 - wy * zoom + pan_y)
return sx, sy
def screen_to_world(sx, sy):
wx = (sx - MAP_WIDTH / 2.0 - pan_x) / zoom
wy = -(sy - WINDOW_HEIGHT / 2.0 - pan_y) / zoom
return wx, wy
# 锁频同步计数
next_exec_time = time.perf_counter()
clock = pygame.time.Clock()
viewer_counter = 0
print(f"\n[主程序] 高频控制与 2D 可视化仪表盘已启动 (采样步频: {control_dt:.3f}s)")
# 拉起 MuJoCo 被动渲染视口
with mujoco.viewer.launch_passive(io.m, io.d) as viewer:
viewer.cam.distance = 5.0
viewer.cam.elevation = -20.0
viewer.cam.azimuth = 45.0
running = True
while running and viewer.is_running():
step_start_time = time.perf_counter()
# ------------------------------------------------------------------
# 读取高精度绝对全局惯导系统 (IMU simulated readings)
# ------------------------------------------------------------------
rx = io.d.qpos[0]
ry = io.d.qpos[1]
rz = io.d.qpos[2]
# 读取四元数
quat = io.d.qpos[3:7].copy()
roll_rad, pitch_rad, yaw_rad = quat_to_euler(quat)
roll_deg = math.degrees(roll_rad)
pitch_deg = math.degrees(pitch_rad)
yaw_deg = math.degrees(yaw_rad)
# 读取线速度与角速度
vx_world = io.d.qvel[0]
vy_world = io.d.qvel[1]
linear_speed = math.sqrt(vx_world*vx_world + vy_world*vy_world)
wz_rate = io.d.qvel[5]
# 网格地图行迹更新
if not robot_trail or math.dist((rx, ry), robot_trail[-1]) > 0.05:
robot_trail.append((rx, ry))
if len(robot_trail) > max_trail_len:
robot_trail.pop(0)
# 居中锁定机身
if auto_center:
pan_x = -rx * zoom
pan_y = ry * zoom
# ------------------------------------------------------------------
# 统一自适应高度与神经网络控制策略切换决策系统
# ------------------------------------------------------------------
# 默认维持常规站立策略
desired_policy = "rough"
# 1. 自动导航模式下,且当前航点的期望策略为 crawl (触发限高下蹲)
if nav.mode == "AUTO" and nav.waypoints and nav.waypoint_idx < len(nav.waypoints):
wp = nav.waypoints[nav.waypoint_idx]
if len(wp) >= 4 and wp[3] == "crawl":
# 计算指向目标的方位角与偏角偏差,确保在对正后立即切换下蹲
dx = nav.target_x - rx
dy = nav.target_y - ry
target_yaw = math.atan2(dy, dx)
yaw_err = (target_yaw - yaw_rad + math.pi) % (2.0 * math.pi) - math.pi
# 只要车头对齐(偏角小于 10 度 / 0.18 弧度),立即在行进中提前下蹲准备过杆
if abs(yaw_err) < 0.18:
desired_policy = "crawl"
# 2. 绝对物理位置安全兜底:踏入限高栏区间 (Y in [-9.5, -8.5] 且 X in [4.8, 6.4]) 强制下蹲
if 4.8 <= rx <= 6.4 and -9.5 <= ry <= -8.5:
desired_policy = "crawl"
# 执行高度状态的敏捷切换
if runner.current_policy_name != desired_policy and not runner.transition_in_progress:
runner.trigger_transition(desired_policy)
# ------------------------------------------------------------------
# Pygame 键鼠事件与拖拽交互响应
# ------------------------------------------------------------------
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
# 滚轮缩放事件
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 4: # 向上滚:放大
zoom = min(120.0, zoom * 1.1)
elif event.button == 5: # 向下滚:缩小
zoom = max(8.0, zoom / 1.1)
# 鼠标左键点击地图设置目标点
elif event.button == 1:
if event.pos[0] < MAP_WIDTH:
wx, wy = screen_to_world(event.pos[0], event.pos[1])
if draw_mode:
is_drawing_path = True
drawn_points = [(wx, wy)]
nav.cancel_mission() # 清除以往规划
else:
# 正常的单点巡航目标发令
nav.mode = "AUTO"
nav.target_x = wx
nav.target_y = wy
nav.target_z = rz
nav.drawn_path = [(wx, wy)] # 生成一段直线指示
print(f"[导航系统] 用户手动指派新目标坐标: ({wx:.2f}, {wy:.2f})")
console_active = False
elif console_rect.collidepoint(event.pos):
console_active = True
else:
console_active = False
# 鼠标右键拖拽地图平移
elif event.button == 3:
if event.pos[0] < MAP_WIDTH:
is_dragging = True
auto_center = False
drag_start_x, drag_start_y = event.pos
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 3:
is_dragging = False
elif event.button == 1:
if is_drawing_path:
is_drawing_path = False
# 用户拖拽完鼠标,将所有坐标降采样过滤转化为离散巡航航点
if len(drawn_points) > 1:
wps = []
for pt in drawn_points:
if not wps or math.dist(pt, (wps[-1][0], wps[-1][1])) >= 0.25:
wps.append((pt[0], pt[1], 0.5, "rough"))
if wps:
nav.start_mission("手绘自定义轨迹", wps)
print(f"[导航系统] 成功加载手绘轨迹,已装配成 {len(wps)} 个精密航点。")
else:
nav.cancel_mission()
elif event.type == pygame.MOUSEMOTION:
if is_dragging:
dx_pix = event.pos[0] - drag_start_x
dy_pix = event.pos[1] - drag_start_y
pan_x += dx_pix
pan_y += dy_pix
drag_start_x, drag_start_y = event.pos
elif is_drawing_path:
if event.pos[0] < MAP_WIDTH:
wx, wy = screen_to_world(event.pos[0], event.pos[1])
# 间距大于 0.15m 时记录画线点,保证线条采样平滑
if not drawn_points or math.dist((wx, wy), drawn_points[-1]) > 0.15:
drawn_points.append((wx, wy))
# 键盘输入事件
elif event.type == pygame.KEYDOWN:
if console_active:
if event.key == pygame.K_RETURN:
cmd = console_text.strip().lower()
print(f"[终端控制] 捕获命令行输入: {cmd}")
if cmd.startswith("go "):
try:
parts = cmd.split()
cx = float(parts[1])
cy = float(parts[2])
nav.mode = "AUTO"
nav.target_x = cx
nav.target_y = cy
print(f"[终端控制] 执行导航,目的地坐标: ({cx}, {cy})")
except:
print("[终端控制] 指令语法错误!例:go <X坐标> <Y坐标>")
elif cmd == "clear" or cmd == "c":
nav.cancel_mission()
elif cmd == "record" or cmd == "r":
key_id = len(nav.recorded_keypoints) + 1
nav.recorded_keypoints.append({
'id': key_id, 'x': rx, 'y': ry, 'z': rz, 'yaw': yaw_deg
})
print(f"[关键点] 已记录 WP{key_id}({rx:.2f}, {ry:.2f})")
elif cmd == "crawl":
runner.trigger_transition("crawl")
elif cmd == "rough":
runner.trigger_transition("rough")
elif cmd == "slalom":
# 包含三处必经红色点并且留出宽裕避让余量的精准S形绕杆路径
slalom_wps = [
(2.9, -9.5, 0.4, "rough"), # 1. 安全过渡点,引导机器人向西南切入
(1.8, -9.95, 0.4, "rough"), # 2. 触发红点 1,与 Pole 1 保持 0.55m 超大安全间距
(0.9, -10.0, 0.4, "rough"), # 3. 绕过 Pole 1 左侧的保护航点,拉开间距
(0.8, -10.8, 0.4, "rough"), # 4. 左侧平滑过渡点,为对准 Pole 1-2 缝隙做准备
(0.9, -11.0, 0.4, "rough"), # 5. 缝隙入口前置对齐点
(2.7, -11.0, 0.4, "rough"), # 6. 横穿 Pole 1-2 缝隙,与上下两杆均保持 0.50m 绝对中心间距
(2.8, -11.8, 0.4, "rough"), # 7. 右侧平滑过渡点,为对准 Pole 2-3 缝隙做准备
(2.7, -12.0, 0.4, "rough"), # 8. 缝隙入口前置对齐点
(0.9, -12.0, 0.4, "rough"), # 9. 横穿 Pole 2-3 缝隙,与上下两杆均保持 0.50m 绝对中心间距
(0.9, -12.75, 0.4, "rough"), # 10. 左侧平滑过渡点,为侧向触发红点 3 做准备
(1.42, -12.82, 0.4, "rough"), # 11. 触发红点 3,与 Pole 3 保持 0.50m 超大安全间距
(2.3, -13.3, 0.4, "rough"), # 12. 向下切入 Pole 3-4 缝隙的正下方对齐点
(2.3, -11.5, 0.4, "rough"), # 13. 笔直向北穿过 Pole 3-4 缝隙,与两杆各留 0.50m 最大对称间距
(3.32, -12.44, 0.4, "rough"), # 14. 触发红点 2,与 Pole 4 保持 0.52m 超大安全间距
(3.7, -9.0, 0.6, "rough") # 15. 安全返回起点
]
nav.start_mission("S形绕行任务", slalom_wps)
elif cmd == "charge":
# 包含前置对位与直线冲刺的高墙全速越障路径
charge_wps = [
(3.3, -9.0, 0.5, "rough"), # 1. 移出起步区,向西对准 X = 3.3 轴线中途过渡
(1.8, -9.0, 0.5, "rough"), # 2. 轴线对齐入口,将 X 轴拉正到 1.8
(1.8, -8.0, 0.4, "rough"), # 3. 垂直逼近高墙第一步
(1.8, -7.5, 0.4, "rough"), # 4. 垂直逼近高墙第二步(距墙仅 0.5m,车身彻底正对)
(1.8, -5.8, 1.2, "rough"), # 5. 全速冲锋!越过高墙并在 Y = -5.8 处的安全平地上平稳落地(绝不碰台阶)
(3.3, -5.8, 0.5, "rough"), # 6. 安全东移折回,对齐到 X = 3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 7. 沿 X = 3.3 安全线直行南下,完全避开高墙
(3.7, -9.0, 0.6, "rough") # 8. 顺畅平准移入起点
]
nav.start_mission("高墙加速冲锋", charge_wps)
elif cmd == "bridge":
bridge_wps = [
(3.3, -9.0, 0.5, "rough"), # 1. 移出起步区,向西对准 X = 3.3 避开高墙通道
(3.3, -1.6, 0.5, "rough"), # 2. 沿 X = 3.3 笔直向北,完全避开所有高墙与台阶结构
(1.8, -1.6, 0.5, "rough"), # 3. 水平移入木桥 A 斜坡起点的平地
(1.8, -1.38, 0.4, "rough"), # 4. 对齐木桥 A 坡底起点
(1.8, 0.0, 0.4, "rough"), # 5. 稳定爬上木桥 A 斜坡登上平台
(3.2, 0.0, 0.4, "rough"), # 6. 沿独木桥 Y=0 轴线精细循迹
(4.85, 0.0, 0.4, "rough"), # 7. 沿独木桥 Y=0 轴线继续前进
(5.7, -0.5, 0.4, "rough"), # 8. 驶入东侧木桥转弯缓冲段
(5.7, -2.25, 0.4, "rough"), # 9. 驶上中间木桥平台
(5.7, -3.5, 0.4, "rough"), # 10. 到达木桥 B 斜坡起爬平整区
(5.7, -5.0, 0.4, "rough"), # 11. 稳健行进下斜坡 B
(5.7, -7.0, 0.4, "rough"), # 12. 平稳落地脱离木桥 B,在平地上放平车身
(3.3, -7.0, 0.5, "rough"), # 13. 横向西移,重新汇入 X=3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 14. 沿 X=3.3 安全走廊直行南下
(3.7, -9.0, 0.6, "rough") # 15. 顺畅平准移入起点
]
nav.start_mission("斜坡木桥任务", bridge_wps)
elif cmd == "gravel":
gravel_wps = [
(3.7, -12.5, 0.5, "rough"), # 1. 沿 X=3.7 轴线一路笔直向南,在 flat 地面上完全避开限高门柱
(4.84, -12.5, 0.4, "rough"), # 2. 横向东进,从西侧入口完美切入 1号砂砾平台 tip
(5.84, -12.0, 0.4, "rough"), # 3. 横跨转移至 2号砂砾平台
(5.84, -10.2, 0.4, "rough"), # 4. 笔直向北走下平台,平稳落于北侧平地 tip
(3.3, -10.2, 0.5, "rough"), # 5. 西向横移,重新汇入 X=3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 6. 沿着 X=3.3 直行北上
(3.7, -9.0, 0.6, "rough") # 7. 顺畅平准移入起点
]
nav.start_mission("砂砾碎石任务", gravel_wps)
elif cmd == "grand":
grand_wps = [
# 1. S形绕杆
(2.9, -9.5, 0.4, "rough"),
(1.8, -9.95, 0.4, "rough"),
(0.9, -10.0, 0.4, "rough"),
(0.8, -10.8, 0.4, "rough"),
(0.9, -11.0, 0.4, "rough"),
(2.7, -11.0, 0.4, "rough"),
(2.8, -11.8, 0.4, "rough"),
(2.7, -12.0, 0.4, "rough"),
(0.9, -12.0, 0.4, "rough"),
(0.9, -12.75, 0.4, "rough"),
(1.42, -12.82, 0.4, "rough"),
(2.3, -13.3, 0.4, "rough"),
(2.3, -11.5, 0.4, "rough"),
(3.32, -12.44, 0.4, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 2. 限高下蹲
(5.7, -10.2, 0.5, "rough"),
(5.7, -9.0, 0.3, "crawl"),
(5.7, -7.8, 0.3, "rough"),
(5.7, -7.0, 0.5, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 3. 砂砾碎石
(3.7, -12.5, 0.5, "rough"),
(4.84, -12.5, 0.4, "rough"),
(5.84, -12.0, 0.4, "rough"),
(5.84, -10.2, 0.4, "rough"),
(3.3, -10.2, 0.5, "rough"),
(3.3, -9.0, 0.5, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 4. 高墙越障
(3.3, -9.0, 0.5, "rough"),
(1.8, -9.0, 0.5, "rough"),
(1.8, -8.0, 0.4, "rough"),
(1.8, -7.5, 0.4, "rough"),
(1.8, -5.8, 1.2, "rough"),
# 5. 台阶攀爬
(1.8, -3.5, 0.4, "rough"),
(1.8, -1.6, 0.4, "rough"),
# 6. 斜坡木桥
(1.8, -1.38, 0.4, "rough"),
(1.8, 0.0, 0.4, "rough"),
(3.2, 0.0, 0.4, "rough"),
(4.85, 0.0, 0.4, "rough"),
(5.7, -0.5, 0.4, "rough"),
(5.7, -2.25, 0.4, "rough"),
(5.7, -3.5, 0.4, "rough"),
(5.7, -5.0, 0.4, "rough"),
(5.7, -7.0, 0.4, "rough"),
(3.3, -7.0, 0.5, "rough"), # 横向西移,重新汇入 X=3.3 安全通道
(3.3, -9.0, 0.5, "rough"), # 沿 X=3.3 通道南下返回
(3.7, -9.0, 0.6, "rough") # 顺畅平准移入起点
]
nav.start_mission("障碍赛大满贯", grand_wps)
console_text = ""
elif event.key == pygame.K_BACKSPACE:
console_text = console_text[:-1]
else:
console_text += event.unicode
else:
# 正常打点快捷键
if event.key == pygame.K_r:
key_id = len(nav.recorded_keypoints) + 1
nav.recorded_keypoints.append({
'id': key_id, 'x': rx, 'y': ry, 'z': rz, 'yaw': yaw_deg
})
print(f"[关键点] 快捷键记录 WP{key_id}({rx:.2f}, {ry:.2f})")
elif event.key == pygame.K_SPACE:
# 紧急刹车 / 停止
nav.cancel_mission()
# 动态交互按钮响应 (防止与终端输入冲突)
if not console_active:
if btn_slalom.is_clicked(event):
# 包含三处必经红色点并且留出宽裕避让余量的精准S形绕杆路径
slalom_wps = [
(2.9, -9.5, 0.4, "rough"), # 1. 安全过渡点,引导机器人向西南切入
(1.8, -9.95, 0.4, "rough"), # 2. 触发红点 1,与 Pole 1 保持 0.55m 超大安全间距
(0.9, -10.0, 0.4, "rough"), # 3. 绕过 Pole 1 左侧的保护航点,拉开间距
(0.8, -10.8, 0.4, "rough"), # 4. 左侧平滑过渡点,为对准 Pole 1-2 缝隙做准备
(0.9, -11.0, 0.4, "rough"), # 5. 缝隙入口前置对齐点
(2.7, -11.0, 0.4, "rough"), # 6. 横穿 Pole 1-2 缝隙,与上下两杆均保持 0.50m 绝对中心间距
(2.8, -11.8, 0.4, "rough"), # 7. 右侧平滑过渡点,为对准 Pole 2-3 缝隙做准备
(2.7, -12.0, 0.4, "rough"), # 8. 缝隙入口前置对齐点
(0.9, -12.0, 0.4, "rough"), # 9. 横穿 Pole 2-3 缝隙,与上下两杆均保持 0.50m 绝对中心间距
(0.9, -12.75, 0.4, "rough"), # 10. 左侧平滑过渡点,为侧向触发红点 3 做准备
(1.42, -12.82, 0.4, "rough"), # 11. 触发红点 3,与 Pole 3 保持 0.50m 超大安全间距
(2.3, -13.3, 0.4, "rough"), # 12. 向下切入 Pole 3-4 缝隙的正下方对齐点
(2.3, -11.5, 0.4, "rough"), # 13. 笔直向北穿过 Pole 3-4 缝隙,与两杆各留 0.50m 最大对称间距
(3.32, -12.44, 0.4, "rough"), # 14. 触发红点 2,与 Pole 4 保持 0.52m 超大安全间距
(3.7, -9.0, 0.6, "rough") # 15. 安全返回起点
]
nav.start_mission("S形绕行演练", slalom_wps)
elif btn_wall.is_clicked(event):
# 包含前置对位与直线冲刺的高墙全速越障路径
charge_wps = [
(3.3, -9.0, 0.5, "rough"), # 1. 移出起步区,向西对准 X = 3.3 轴线中途过渡
(1.8, -9.0, 0.5, "rough"), # 2. 轴线对齐入口,将 X 轴拉正到 1.8
(1.8, -8.0, 0.4, "rough"), # 3. 垂直逼近高墙第一步
(1.8, -7.5, 0.4, "rough"), # 4. 垂直逼近高墙第二步(距墙仅 0.5m,车身彻底正对)
(1.8, -5.8, 1.2, "rough"), # 5. 全速冲锋!越过高墙并在 Y = -5.8 处的安全平地上平稳落地(绝不碰台阶)
(3.3, -5.8, 0.5, "rough"), # 6. 安全东移折回,对齐到 X = 3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 7. 沿 X = 3.3 安全线直行南下,完全避开高墙
(3.7, -9.0, 0.6, "rough") # 8. 顺畅平准移入起点
]
nav.start_mission("高墙加速越障", charge_wps)
elif btn_crawl.is_clicked(event):
# 正确的 Y 轴方向对准跨越限高门
crawl_wps = [
(5.7, -10.2, 0.5, "rough"), # 首先对准限高门中心轴线前方
(5.7, -9.0, 0.3, "crawl"), # 弯腰通过限高门中心
(5.7, -7.8, 0.3, "rough"), # 只要过了门槛,直接恢复为常规高度
(3.7, -9.0, 0.5, "rough") # 顺畅返回起点
]
nav.start_mission("限高爬行任务", crawl_wps)
elif btn_stairs.is_clicked(event):
# 阶梯攀爬:利用 X=3.3 通道绕过高墙边缘,水平对齐轴线登顶,并完整从另一侧下台阶穿过
stairs_wps = [
(3.3, -9.0, 0.5, "rough"), # 1. 移出起步区,向西对准 X = 3.3 避开高墙通道
(3.3, -5.8, 0.5, "rough"), # 2. 沿 X = 3.3 笔直向北,完全绕开高墙右侧边缘(留出超大空间)
(1.8, -5.8, 0.5, "rough"), # 3. 水平移入台阶前方的平地/空地中心,完美对齐轴线
(1.8, -3.5, 0.4, "rough"), # 4. 直线攀爬登上顶部平台
(1.8, -1.6, 0.4, "rough"), # 5. 直线走下另一侧台阶,到 Y=-1.6m,后腿完全过台阶,前腿也不上桥,在平地平稳落地
(3.3, -1.6, 0.5, "rough"), # 6. 横向东移,重新对齐到 X = 3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 7. 沿 X = 3.3 安全线直行南下,完全避开高墙与台阶
(3.7, -9.0, 0.6, "rough") # 8. 顺畅平准移入起点
]
nav.start_mission("阶梯攀爬登顶", stairs_wps)
elif btn_bridge.is_clicked(event):
bridge_wps = [
(3.3, -9.0, 0.5, "rough"), # 1. 移出起步区,向西对准 X = 3.3 避开高墙通道
(3.3, -1.6, 0.5, "rough"), # 2. 沿 X = 3.3 笔直向北,完全绕开所有高墙与台阶结构
(1.8, -1.6, 0.5, "rough"), # 3. 水平移入木桥 A 斜坡起点的平地
(1.8, -1.38, 0.4, "rough"), # 4. 对齐木桥 A 坡底起点
(1.8, 0.0, 0.4, "rough"), # 5. 稳定爬上木桥 A 斜坡登上平台
(3.2, 0.0, 0.4, "rough"), # 6. 沿独木桥 Y=0 轴线精细循迹
(4.85, 0.0, 0.4, "rough"), # 7. 沿独木桥 Y=0 轴线继续前进
(5.7, -0.5, 0.4, "rough"), # 8. 驶入东侧木桥转弯缓冲段
(5.7, -2.25, 0.4, "rough"), # 9. 驶上中间木桥平台
(5.7, -3.5, 0.4, "rough"), # 10. 到达木桥 B 斜坡起爬平整区
(5.7, -5.0, 0.4, "rough"), # 11. 稳健行进下斜坡 B
(5.7, -7.0, 0.4, "rough"), # 12. 平稳落地脱离木桥 B,在平地上放平车身
(3.3, -7.0, 0.5, "rough"), # 13. 横向西移,重新汇入 X=3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 14. 沿 X=3.3 安全走廊直行南下
(3.7, -9.0, 0.6, "rough") # 15. 顺畅平准移入起点
]
nav.start_mission("斜坡木桥任务", bridge_wps)
elif btn_gravel.is_clicked(event):
gravel_wps = [
(3.7, -12.5, 0.5, "rough"), # 1. 沿 X=3.7 轴线一路笔直向南,在 flat 地面上完全避开限高门柱
(4.84, -12.5, 0.4, "rough"), # 2. 横向东进,从西侧入口完美切入 1号砂砾平台 tip
(5.84, -12.0, 0.4, "rough"), # 3. 横跨转移至 2号砂砾平台
(5.84, -10.2, 0.4, "rough"), # 4. 笔直向北走下平台,平稳落于北侧平地 tip
(3.3, -10.2, 0.5, "rough"), # 5. 西向横移,重新汇入 X=3.3 安全返航通道
(3.3, -9.0, 0.5, "rough"), # 6. 沿着 X=3.3 直行北上
(3.7, -9.0, 0.6, "rough") # 7. 顺畅平准移入起点
]
nav.start_mission("砂砾碎石任务", gravel_wps)
elif btn_grand.is_clicked(event):
grand_wps = [
# 1. S形绕杆
(2.9, -9.5, 0.4, "rough"),
(1.8, -9.95, 0.4, "rough"),
(0.9, -10.0, 0.4, "rough"),
(0.8, -10.8, 0.4, "rough"),
(0.9, -11.0, 0.4, "rough"),
(2.7, -11.0, 0.4, "rough"),
(2.8, -11.8, 0.4, "rough"),
(2.7, -12.0, 0.4, "rough"),
(0.9, -12.0, 0.4, "rough"),
(0.9, -12.75, 0.4, "rough"),
(1.42, -12.82, 0.4, "rough"),
(2.3, -13.3, 0.4, "rough"),
(2.3, -11.5, 0.4, "rough"),
(3.32, -12.44, 0.4, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 2. 限高下蹲
(5.7, -10.2, 0.5, "rough"),
(5.7, -9.0, 0.3, "crawl"),
(5.7, -7.8, 0.3, "rough"),
(5.7, -7.0, 0.5, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 3. 砂砾碎石
(3.7, -12.5, 0.5, "rough"),
(4.84, -12.5, 0.4, "rough"),
(5.84, -12.0, 0.4, "rough"),
(5.84, -10.2, 0.4, "rough"),
(3.3, -10.2, 0.5, "rough"),
(3.3, -9.0, 0.5, "rough"),
(3.7, -9.0, 0.5, "rough"),
# 4. 高墙越障
(3.3, -9.0, 0.5, "rough"),
(1.8, -9.0, 0.5, "rough"),
(1.8, -8.0, 0.4, "rough"),
(1.8, -7.5, 0.4, "rough"),
(1.8, -5.8, 1.2, "rough"),
# 5. 台阶攀爬
(1.8, -3.5, 0.4, "rough"),
(1.8, -1.6, 0.4, "rough"),
# 6. 斜坡木桥
(1.8, -1.38, 0.4, "rough"),
(1.8, 0.0, 0.4, "rough"),
(3.2, 0.0, 0.4, "rough"),
(4.85, 0.0, 0.4, "rough"),
(5.7, -0.5, 0.4, "rough"),
(5.7, -2.25, 0.4, "rough"),
(5.7, -3.5, 0.4, "rough"),
(5.7, -5.0, 0.4, "rough"),
(5.7, -7.0, 0.4, "rough"),
(3.3, -7.0, 0.5, "rough"), # 横向西移,重新汇入 X=3.3 安全通道
(3.3, -9.0, 0.5, "rough"), # 沿 X=3.3 通道南下返回
(3.7, -9.0, 0.6, "rough") # 顺畅平准移入起点
]
nav.start_mission("障碍赛大满贯", grand_wps)
elif btn_clear_trg.is_clicked(event):
nav.cancel_mission()
elif btn_center_toggle.is_clicked(event):
auto_center = not auto_center
btn_center_toggle.text = "视角居中:开" if auto_center else "视角居中:关"
btn_center_toggle.bg_color = (16, 185, 129) if auto_center else (244, 63, 94)
elif btn_draw_mode.is_clicked(event):
draw_mode = not draw_mode
btn_draw_mode.text = "手绘模式:开" if draw_mode else "手绘模式:关"
btn_draw_mode.bg_color = (16, 185, 129) if draw_mode else (30, 41, 59)
elif btn_speed_down.is_clicked(event):
# 物理减速
if sim_speed_factor > 1.0:
if sim_speed_factor >= 5.0: sim_speed_factor = 3.0
elif sim_speed_factor >= 3.0: sim_speed_factor = 2.0
elif sim_speed_factor >= 2.0: sim_speed_factor = 1.5
else: sim_speed_factor = 1.0
else:
if sim_speed_factor >= 1.0: sim_speed_factor = 0.8
elif sim_speed_factor >= 0.8: sim_speed_factor = 0.5
elif sim_speed_factor >= 0.5: sim_speed_factor = 0.2
print(f"[仿真控制] 物理减速。当前加速倍率: {sim_speed_factor:.1f}x")
elif btn_speed_normal.is_clicked(event):
sim_speed_factor = 1.0
print(f"[仿真控制] 恢复标准时间流速 (1x)")
elif btn_speed_up.is_clicked(event):
# 物理加速
if sim_speed_factor < 1.0:
if sim_speed_factor <= 0.2: sim_speed_factor = 0.5
elif sim_speed_factor <= 0.5: sim_speed_factor = 0.8
else: sim_speed_factor = 1.0
else:
if sim_speed_factor <= 1.0: sim_speed_factor = 1.5
elif sim_speed_factor <= 1.5: sim_speed_factor = 2.0
elif sim_speed_factor <= 2.0: sim_speed_factor = 3.0
elif sim_speed_factor <= 3.0: sim_speed_factor = 5.0
print(f"[仿真控制] 物理加速。当前加速倍率: {sim_speed_factor:.1f}x")
elif btn_x_plus.is_clicked(event):
nav.mode = "AUTO"
nav.target_x = (nav.target_x if nav.target_x is not None else rx) + 0.5
nav.target_y = nav.target_y if nav.target_y is not None else ry
elif btn_x_minus.is_clicked(event):
nav.mode = "AUTO"
nav.target_x = (nav.target_x if nav.target_x is not None else rx) - 0.5
nav.target_y = nav.target_y if nav.target_y is not None else ry
elif btn_y_plus.is_clicked(event):
nav.mode = "AUTO"
nav.target_x = nav.target_x if nav.target_x is not None else rx
nav.target_y = (nav.target_y if nav.target_y is not None else ry) + 0.5
elif btn_y_minus.is_clicked(event):
nav.mode = "AUTO"
nav.target_x = nav.target_x if nav.target_x is not None else rx
nav.target_y = (nav.target_y if nav.target_y is not None else ry) - 0.5
elif btn_rec_key.is_clicked(event):
key_id = len(nav.recorded_keypoints) + 1
nav.recorded_keypoints.append({
'id': key_id, 'x': rx, 'y': ry, 'z': rz, 'yaw': yaw_deg
})
print(f"[关键点] 已记录 WP{key_id}({rx:.2f}, {ry:.2f})")
elif btn_exp_key.is_clicked(event):
try:
exp_path = Path(__file__).parent / "recorded_keypoints.txt"
with open(exp_path, "w", encoding="utf-8") as f:
f.write("序号, X坐标(米), Y坐标(米), Z高度(米), 机身航向角(度)\n")
for kp in nav.recorded_keypoints:
f.write(f"WP{kp['id']}, {kp['x']:.4f}, {kp['y']:.4f}, {kp['z']:.4f}, {kp['yaw']:.2f}\n")
print(f"[关键点] 数据文件已成功导出至: {exp_path}")
except Exception as ex:
print(f"[关键点] 导出出错: {ex}")
elif btn_clear_key.is_clicked(event):
nav.recorded_keypoints = []
print("[关键点] 所有已记录的关键点已清空。")
# ------------------------------------------------------------------
# 自动路径寻寻航与物理指令步进 (50Hz 控制频率)
# ------------------------------------------------------------------
if nav.mode == "AUTO":
cmd_vx, cmd_vy, cmd_wz = nav.update_navigation(rx, ry, yaw_rad, runner, dt=control_dt)
command_input = np.array([cmd_vx, cmd_vy, cmd_wz], dtype=np.float32)
else:
# 读取键盘输入控制:方向键控制 x (前后) 和 yaw (旋转),A/D 键控制 y (左右横移)
keys = pygame.key.get_pressed()
cmd_vx, cmd_vy, cmd_wz = 0.0, 0.0, 0.0
# 方向键控制 x (前后,峰值限制为 1.2m/s)
if keys[pygame.K_UP]: cmd_vx = 1.2
elif keys[pygame.K_DOWN]: cmd_vx = -1.2
# A/D 键控制 y (左右横移)
if keys[pygame.K_a]: cmd_vy = 0.8
elif keys[pygame.K_d]: cmd_vy = -0.8
# 方向键控制 yaw (旋转,峰值限制为 1.2r/s)
if keys[pygame.K_LEFT]: cmd_wz = 1.2
elif keys[pygame.K_RIGHT]: cmd_wz = -1.2
command_input = np.array([cmd_vx, cmd_vy, cmd_wz], dtype=np.float32)
# 融合观测数据
obs = io.get_obs_53d(command_input, runner.default_dof_pos, runner.last_actions)
# 策略网络推理
scaled_actions, raw_actions = runner.step(obs)
# 向仿真器下发关节控制指令
io.send_actions(scaled_actions, runner.default_dof_pos)
# 推进物理步进
for _ in range(sim_steps_per_control):
mujoco.mj_step(io.m, io.d)
# 控制视口镜头跟随
viewer_counter += 1
if viewer_counter >= 2:
base_id = mujoco.mj_name2id(io.m, mujoco.mjtObj.mjOBJ_BODY, "base_link")
if base_id != -1:
viewer.cam.lookat[:] = io.d.xpos[base_id]
viewer.sync()
viewer_counter = 0
# ------------------------------------------------------------------
# 画面绘制:左侧网格物理元素实景图 (100% 实景对应解析绘制)
# ------------------------------------------------------------------
pygame.draw.rect(screen, COLOR_BG, (0, 0, MAP_WIDTH, WINDOW_HEIGHT))
# 计算当前网格绘制边界
grid_min_w, grid_min_h = screen_to_world(0, WINDOW_HEIGHT)
grid_max_w, grid_max_h = screen_to_world(MAP_WIDTH, 0)
start_grid_x = int(math.floor(grid_min_w))
end_grid_x = int(math.ceil(grid_max_w))
start_grid_y = int(math.floor(grid_min_h))
end_grid_y = int(math.ceil(grid_max_h))
# 绘制纵轴网格线
for gx in range(start_grid_x, end_grid_x + 1):
sx, _ = world_to_screen(gx, 0)
color = COLOR_AXIS if gx == 0 else COLOR_GRID
width = 2 if gx == 0 else 1
pygame.draw.line(screen, color, (sx, 0), (sx, WINDOW_HEIGHT), width)
if gx % 2 == 0 and 0 < sx < MAP_WIDTH - 20:
lbl = font_mono.render(f"{gx}m", True, COLOR_TEXT_MUTED)
screen.blit(lbl, (sx + 3, WINDOW_HEIGHT - 20))
# 绘制横轴网格线
for gy in range(start_grid_y, end_grid_y + 1):
_, sy = world_to_screen(0, gy)
color = COLOR_AXIS if gy == 0 else COLOR_GRID
width = 2 if gy == 0 else 1
pygame.draw.line(screen, color, (0, sy), (MAP_WIDTH, sy), width)
if gy % 2 == 0 and 0 < sy < WINDOW_HEIGHT - 20:
lbl = font_mono.render(f"{gy}m", True, COLOR_TEXT_MUTED)
screen.blit(lbl, (5, sy + 3))
# ------------------------------------------------------------------
# 🌟 动态地图绘制引擎 —— 解析并精确渲染 scene_terrain.xml 所有实景元素
# ------------------------------------------------------------------
for geom in xml_obstacles:
g_type = geom['type']
pos = geom['pos']
size = geom['size']
quat = geom['quat']
rgba = geom['rgba']
name = geom['name']
# 转换色彩至 0-255 并增加发光度
color = (int(rgba[0]*255), int(rgba[1]*255), int(rgba[2]*255))
cx, cy = pos[0], pos[1]
# 1. 矩形盒子渲染(支持任意偏航角 Z轴自转 顶点多边形解算)
if g_type == 'box':
qw, qx, qy, qz = quat
siny_cosp = 2.0 * (qw * qz + qx * qy)
cosy_cosp = 1.0 - 2.0 * (qy * qy + qz * qz)
yaw = math.atan2(siny_cosp, cosy_cosp)
sx, sy = size[0], size[1]
local_corners = [
(-sx, -sy), (sx, -sy), (sx, sy), (-sx, sy)
]
screen_corners = []
for lx, ly in local_corners:
# 空间旋转
wx = cx + lx * math.cos(yaw) - ly * math.sin(yaw)
wy = cy + lx * math.sin(yaw) + ly * math.cos(yaw)
screen_corners.append(world_to_screen(wx, wy))
# 填充半透明色,勾画发光边界
pygame.draw.polygon(screen, color, screen_corners)
border_color = (min(color[0]+40, 255), min(color[1]+40, 255), min(color[2]+40, 255))
pygame.draw.polygon(screen, border_color, screen_corners, width=1)
# 如果是高越障墙,渲染额外标识
if abs(cy - (-7.0)) < 0.1:
wall_lbl = font_small.render("Obstacle Wall", True, COLOR_TEXT_LIGHT)
screen.blit(wall_lbl, (screen_corners[0][0] + 5, screen_corners[0][1] - 18))
# 2. 圆柱体渲染
elif g_type == 'cylinder':
sx, sy = world_to_screen(cx, cy)
rad = max(int(size[0] * zoom), 3)
pygame.draw.circle(screen, color, (sx, sy), rad)
border_color = (min(color[0]+40, 255), min(color[1]+40, 255), min(color[2]+40, 255))
pygame.draw.circle(screen, border_color, (sx, sy), rad, width=1)
# 如果是绕杆立柱,加注底图安全边界环
if "Slalom" in name:
pygame.draw.circle(screen, (6, 182, 212, 30), (sx, sy), int(0.1 * zoom))
pygame.draw.circle(screen, COLOR_CYAN, (sx, sy), int(0.1 * zoom), width=1)
# 3. 半透明 hfield 高原区域渲染
elif g_type == 'hfield':
sx = size[0] if len(size) > 0 else 1.0
sy = size[1] if len(size) > 1 else 1.0
x1, y1 = world_to_screen(cx - sx, cy + sy)
x2, y2 = world_to_screen(cx + sx, cy - sy)
w = x2 - x1
h = y2 - y1
if w > 0 and h > 0:
surf = pygame.Surface((w, h), pygame.SRCALPHA)
surf.fill((139, 92, 246, 35)) # 紫色高原提示
pygame.draw.rect(surf, COLOR_PURPLE, (0, 0, w, h), width=1)
screen.blit(surf, (x1, y1))
hf_lbl = font_mono.render("HField Platform", True, COLOR_PURPLE)
screen.blit(hf_lbl, (x1 + 6, y1 + 4))
# ------------------------------------------------------------------
# 规划与运动行迹线绘制
# ------------------------------------------------------------------
# 绘制机器人面包屑行迹线
if len(robot_trail) > 1:
trail_points = [world_to_screen(pt[0], pt[1]) for pt in robot_trail]
pygame.draw.lines(screen, COLOR_CYAN, False, trail_points, 2)
# 绘制正在实时手划的绿色轨迹
if is_drawing_path and len(drawn_points) > 1:
draw_pts = [world_to_screen(pt[0], pt[1]) for pt in drawn_points]
pygame.draw.lines(screen, COLOR_EMERALD, False, draw_pts, 3)
# 绘制已加载的自动路径寻迹线
if len(nav.drawn_path) > 1:
path_pts = [world_to_screen(pt[0], pt[1]) for pt in nav.drawn_path]
pygame.draw.lines(screen, COLOR_EMERALD, False, path_pts, 2)
# 渲染每一个航点节点小球
for pt in nav.drawn_path:
sx, sy = world_to_screen(pt[0], pt[1])
pygame.draw.circle(screen, COLOR_CYAN, (sx, sy), 4)
# 绘制关键点数据打点
for kp in nav.recorded_keypoints:
kx, ky = world_to_screen(kp['x'], kp['y'])
pygame.draw.circle(screen, COLOR_PURPLE, (kx, ky), 7)
pygame.draw.circle(screen, COLOR_TEXT_LIGHT, (kx, ky), 7, width=1)
kp_lbl = font_mono.render(f"WP{kp['id']}", True, COLOR_TEXT_LIGHT)
screen.blit(kp_lbl, (kx - 8, ky - 18))
# 绘制当前正在行进指向的目标点
if nav.target_x is not None and nav.target_y is not None:
tx, ty = world_to_screen(nav.target_x, nav.target_y)
pulse_rad = int(8 + 4 * math.sin(time.time() * 8.0))
pygame.draw.circle(screen, COLOR_GOLD, (tx, ty), pulse_rad, width=2)
pygame.draw.circle(screen, COLOR_GOLD, (tx, ty), 3)
pygame.draw.line(screen, COLOR_GOLD, (tx - pulse_rad - 4, ty), (tx + pulse_rad + 4, ty), 1)
pygame.draw.line(screen, COLOR_GOLD, (tx, ty - pulse_rad - 4), (tx, ty + pulse_rad + 4), 1)
trg_lbl = font_mono.render(f"Target: ({nav.target_x:.2f}, {nav.target_y:.2f})", True, COLOR_GOLD)
screen.blit(trg_lbl, (tx + 12, ty - 6))
# ------------------------------------------------------------------
# 渲染机器人本体图标 (多边形轮骨,指示航向)
# ------------------------------------------------------------------
rx_pix, ry_pix = world_to_screen(rx, ry)
robot_rad = max(int(0.25 * zoom), 6)
# 画机身盘
pygame.draw.circle(screen, COLOR_CYAN, (rx_pix, ry_pix), robot_rad)
pygame.draw.circle(screen, COLOR_TEXT_LIGHT, (rx_pix, ry_pix), robot_rad, width=2)
# 航向箭头鼻线
nose_wx = rx + 0.35 * math.cos(yaw_rad)
nose_wy = ry + 0.35 * math.sin(yaw_rad)
nose_x_pix, nose_y_pix = world_to_screen(nose_wx, nose_wy)
pygame.draw.line(screen, COLOR_EMERALD, (rx_pix, ry_pix), (nose_x_pix, nose_y_pix), 3)
pygame.draw.circle(screen, COLOR_EMERALD, (nose_x_pix, nose_y_pix), 3)
# 四轮足点映射
wheel_offset_w = 0.2
for angle_offset in [-0.7, 0.7, -2.4, 2.4]:
w_angle = yaw_rad + angle_offset
w_wx = rx + wheel_offset_w * math.cos(w_angle)
w_wy = ry + wheel_offset_w * math.sin(w_angle)
w_x_pix, w_y_pix = world_to_screen(w_wx, w_wy)
pygame.draw.rect(screen, (0, 0, 0), (w_x_pix - 4, w_y_pix - 4, 8, 8), border_radius=4)
# ------------------------------------------------------------------
# 渲染右侧 HUD 磨砂玻璃科技控制面板
# ------------------------------------------------------------------
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH, 0), (MAP_WIDTH, WINDOW_HEIGHT), 2)
pygame.draw.rect(screen, COLOR_HUD_BG, (MAP_WIDTH, 0, WINDOW_WIDTH - MAP_WIDTH, WINDOW_HEIGHT))
# 面板标题
screen.blit(font_large.render("数据与控制面板", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, 15))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, 42), (WINDOW_WIDTH - 20, 42), 1)
# 自动模式状态指示
y_offset = 55
screen.blit(font_medium.render("当前状态:", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, y_offset))
if nav.mode == "AUTO":
pygame.draw.rect(screen, (16, 185, 129, 40), (MAP_WIDTH + 110, y_offset - 2, 85, 22), border_radius=4)
screen.blit(font_medium.render("自动巡航", True, COLOR_EMERALD), (MAP_WIDTH + 120, y_offset))
else:
pygame.draw.rect(screen, (244, 63, 94, 40), (MAP_WIDTH + 110, y_offset - 2, 85, 22), border_radius=4)
screen.blit(font_medium.render("手动接管", True, COLOR_ROSE), (MAP_WIDTH + 120, y_offset))
# 当前驱动神经网络控制策略
y_offset = 85
screen.blit(font_medium.render("当前高度:", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, y_offset))
p_name = runner.current_policy_name.upper()
p_color = COLOR_CYAN if p_name == "CRAWL" else COLOR_AMBER
cn_pname = "低姿爬行" if p_name == "CRAWL" else "常规站立"
screen.blit(font_medium.render(cn_pname, True, p_color), (MAP_WIDTH + 110, y_offset))
# (已移除自动脱困及卡死报警提醒)
# 当前仿真时间倍速
y_offset = 115
screen.blit(font_medium.render("仿真倍速:", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, y_offset))
speed_display_str = f"{sim_speed_factor:.1f}x"
screen.blit(font_medium.render(speed_display_str, True, COLOR_EMERALD), (MAP_WIDTH + 110, y_offset))
# 全局惯导 telemetry 数据读取
y_offset = 145
screen.blit(font_small.render("实时全局位置(m)", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, y_offset))
y_offset += 18
pygame.draw.rect(screen, COLOR_CONSOLE_BG, (MAP_WIDTH + 20, y_offset, 285, 45), border_radius=4)
pos_text = f"X: {rx:6.3f} m Y: {ry:6.3f} m Z: {rz:5.3f} m"
screen.blit(font_mono.render(pos_text, True, COLOR_EMERALD), (MAP_WIDTH + 30, y_offset + 5))
speed_text = f"速度: {linear_speed:4.2f} m/s 转向: {wz_rate:5.2f} r/s"
screen.blit(font_mono.render(speed_text, True, COLOR_EMERALD), (MAP_WIDTH + 30, y_offset + 22))
# Euler 机身姿态
y_offset = 218
screen.blit(font_small.render("机身姿态角(deg)", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, y_offset))
y_offset += 18
pygame.draw.rect(screen, COLOR_CONSOLE_BG, (MAP_WIDTH + 20, y_offset, 285, 30), border_radius=4)
euler_text = f"横滚: {roll_deg:5.1f}° 俯仰: {pitch_deg:5.1f}° 航向: {yaw_deg:5.1f}°"
screen.blit(font_medium.render(euler_text, True, COLOR_EMERALD), (MAP_WIDTH + 25, y_offset + 6))
# PRESET routines title
y_offset = 280
screen.blit(font_large.render("预设任务列表", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, y_offset))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, y_offset + 25), (WINDOW_WIDTH - 20, y_offset + 25), 1)
# 绘制所有功能按钮
btn_slalom.draw(screen, font_small)
btn_crawl.draw(screen, font_small)
btn_gravel.draw(screen, font_small)
btn_wall.draw(screen, font_small)
btn_stairs.draw(screen, font_small)
btn_bridge.draw(screen, font_small)
btn_grand.draw(screen, font_small)
# 系统与视图控制
y_offset = 440
screen.blit(font_large.render("系统与视图控制", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, y_offset))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, y_offset + 22), (WINDOW_WIDTH - 20, y_offset + 22), 1)
btn_clear_trg.draw(screen, font_small)
btn_center_toggle.draw(screen, font_small)
btn_draw_mode.draw(screen, font_small)
btn_speed_down.draw(screen, font_small)
btn_speed_normal.draw(screen, font_small)
btn_speed_up.draw(screen, font_small)
# 发令命令
y_offset = 542
screen.blit(font_medium.render("目标微调", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, y_offset))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, y_offset + 18), (WINDOW_WIDTH - 20, y_offset + 18), 1)
btn_x_plus.draw(screen, font_small)
btn_x_minus.draw(screen, font_small)
btn_y_plus.draw(screen, font_small)
btn_y_minus.draw(screen, font_small)
# 打点打点打点
y_offset = 605
screen.blit(font_medium.render("关键点记录 (R键)", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, y_offset))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, y_offset + 18), (WINDOW_WIDTH - 20, y_offset + 18), 1)
# 渲染最新记录位置
if nav.recorded_keypoints:
kp = nav.recorded_keypoints[-1]
kp_str = f"最新记录: WP{kp['id']}: X:{kp['x']:5.2f} Y:{kp['y']:5.2f} YAW:{kp['yaw']:4.0f}°"
screen.blit(font_mono.render(kp_str, True, COLOR_PURPLE), (MAP_WIDTH + 20, 661))
else:
screen.blit(font_small.render("最新记录: 暂无记录位置 (按R键保存)", True, COLOR_TEXT_MUTED), (MAP_WIDTH + 20, 661))
btn_rec_key.draw(screen, font_small)
btn_exp_key.draw(screen, font_small)
btn_clear_key.draw(screen, font_small)
# 命令行
y_offset = 685
screen.blit(font_medium.render("命令行输入", True, COLOR_TEXT_LIGHT), (MAP_WIDTH + 20, y_offset))
pygame.draw.line(screen, COLOR_HUD_BORDER, (MAP_WIDTH + 20, y_offset + 18), (WINDOW_WIDTH - 20, y_offset + 18), 1)
# 指令框
con_color = COLOR_EMERALD if console_active else COLOR_HUD_BORDER
pygame.draw.rect(screen, COLOR_CONSOLE_BG, console_rect, border_radius=4)
pygame.draw.rect(screen, con_color, console_rect, width=1, border_radius=4)
cursor = "_" if (int(time.time() * 2.0) % 2 == 0 and console_active) else ""
con_str = f" > {console_text}{cursor}"
screen.blit(font_mono.render(con_str, True, COLOR_EMERALD), (console_rect.x + 5, console_rect.y + 4))
# 地图横条
pygame.draw.rect(screen, COLOR_CONSOLE_BG, (0, WINDOW_HEIGHT - 25, MAP_WIDTH, 25))
pygame.draw.line(screen, COLOR_HUD_BORDER, (0, WINDOW_HEIGHT - 25), (MAP_WIDTH, WINDOW_HEIGHT - 25), 1)
status_str = f"Target: X: {nav.target_x if nav.target_x is not None else 0.0:.2f} m, Y: {nav.target_y if nav.target_y is not None else 0.0:.2f} m | Mission: {nav.mission_name}"
screen.blit(font_mono.render(status_str, True, COLOR_TEXT_MUTED), (10, WINDOW_HEIGHT - 20))
pygame.display.flip()
# 50Hz 物理锁帧同步计算
dt_real = control_dt / sim_speed_factor
next_exec_time += dt_real
now = time.perf_counter()
sleep_time = next_exec_time - now
if sleep_time > 0:
time.sleep(sleep_time)
elif sleep_time < -dt_real:
next_exec_time = now
clock.tick(int(50 * sim_speed_factor))
print("\n[主程序] 退出网格主循环。正在关闭物理引擎环境...")
pygame.quit()
if __name__ == "__main__":
main()