1385 lines
77 KiB
Python
1385 lines
77 KiB
Python
#!/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()
|