| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """ |
| Kuavo机器人控制示例脚本 (Python版) |
| 完全等价于原 Bash 脚本,支持交互控制、暂停、恢复、停止、日志查看等功能。 |
| """ |
|
|
| import os |
| import sys |
| import signal |
| import subprocess |
| import yaml |
| from pathlib import Path |
| from time import sleep |
| import select, time, threading, queue |
| |
| current_proc = None |
| LOG_DIR = None |
|
|
|
|
| |
| def cleanup(signum, frame): |
| global current_proc |
| print("\n⏹️ 捕获到 Ctrl+C,开始终止任务") |
| if current_proc and current_proc.poll() is None: |
| print(f"⏹️ 正在终止任务 (PID: {current_proc.pid})...") |
| current_proc.terminate() |
| try: |
| current_proc.wait(timeout=3) |
| except subprocess.TimeoutExpired: |
| current_proc.kill() |
| print("✅ 任务已终止") |
| sys.exit(130) |
|
|
|
|
| signal.signal(signal.SIGINT, cleanup) |
| signal.signal(signal.SIGTERM, cleanup) |
|
|
|
|
| |
| def print_header(): |
| print("=== Kuavo机器人控制示例 ===") |
| print("此脚本展示如何使用命令行参数控制不同的任务") |
| print("支持暂停、继续、停止功能\n") |
| print("📋 控制功能说明:") |
| print(" 🔄 暂停/恢复: 发送 SIGUSR1 信号") |
| print(" ⏹️ 停止任务: 发送 SIGUSR2 信号") |
| print(" 📊 查看日志: tail -f log/kuavo_deploy/kuavo_deploy.log\n") |
|
|
|
|
| def get_script_paths(): |
| script_dir = Path(__file__).resolve().parent |
| script = script_dir / "src" / "scripts" / "script.py" |
| auto_test = script_dir / "src" / "scripts" / "script_auto_test.py" |
| offline_bag_eval = script_dir / "offline_bag_eval.py" |
| return script_dir, script, auto_test, offline_bag_eval |
|
|
|
|
| def ensure_log_dir(script_dir): |
| log_root = script_dir.parent / "log" / "kuavo_deploy" |
| log_root.mkdir(parents=True, exist_ok=True) |
| return log_root |
|
|
|
|
| |
| def input_listener(input_queue, stop_event): |
| """后台线程:持续监听用户输入""" |
| sys.stdout.write(f"\r🟢 任务运行中,输入命令以暂停/停止等(p/s/l/h): >") |
| sys.stdout.flush() |
| while not stop_event.is_set(): |
| |
| inputok, _, _ = select.select([sys.stdin], [], [], 0.1) |
| if inputok: |
| line = sys.stdin.readline() |
| |
| input_queue.put(line.strip().lower()) |
|
|
| def interactive_controller(): |
| global current_proc, LOG_DIR |
|
|
| print("🎮 交互式控制器已启动") |
| print(f"任务PID: {current_proc.pid}\n") |
| print("📋 可用命令:") |
| print(" p/pause - 暂停/恢复任务") |
| print(" s/stop - 停止任务") |
| print(" l/log - 查看实时日志") |
| print(" h/help - 显示帮助\n") |
|
|
| input_queue = queue.Queue() |
| stop_event = threading.Event() |
|
|
| |
| threading.Thread( |
| target=input_listener, args=(input_queue, stop_event), daemon=True |
| ).start() |
|
|
| while True: |
| |
| if current_proc.poll() is not None: |
| retcode = current_proc.returncode |
|
|
| if retcode == 0: |
| print("\n✅ 任务已正常结束") |
| else: |
| print(f"\n❌ 任务异常退出,错误码:{retcode}") |
| print("📄 请查看日志文件:log/kuavo_deploy/kuavo_deploy.log") |
|
|
| current_proc = None |
| stop_event.set() |
| break |
|
|
|
|
| try: |
| cmd = input_queue.get(timeout=0.5) |
| except queue.Empty: |
| continue |
| |
| |
| |
| if cmd in ("p", "pause"): |
| print("🔄 发送暂停/恢复信号...") |
| os.kill(current_proc.pid, signal.SIGUSR1) |
|
|
| elif cmd in ("s", "stop"): |
| print("⏹️ 发送停止信号...") |
| os.kill(current_proc.pid, signal.SIGUSR2) |
| try: |
| current_proc.wait(timeout=3) |
| except subprocess.TimeoutExpired: |
| current_proc.kill() |
| print("✅ 任务已强制停止") |
| stop_event.set() |
| break |
|
|
| elif cmd in ("l", "log"): |
| log_path = LOG_DIR / "kuavo_deploy.log" |
| if log_path.exists(): |
| print("📊 显示最新日志 (Ctrl+C 返回):") |
| os.system(f"tail -n 20 {log_path}") |
| else: |
| print("❌ 日志文件不存在") |
|
|
| elif cmd in ("h", "help"): |
| print("📋 可用命令:") |
| print(" p/pause - 暂停/恢复任务") |
| print(" s/stop - 停止任务") |
| print(" l/log - 查看实时日志") |
| print(" h/help - 显示帮助\n") |
|
|
| else: |
| print(f"❌ 未知命令: {cmd}") |
|
|
|
|
| |
| def parse_config(config_path): |
| try: |
| with open(config_path, "r") as f: |
| cfg = yaml.safe_load(f) |
| inf = cfg.get("inference", {}) |
| pretrained_path = inf.get("pretrained_path", "") |
| task = inf.get("task", "N/A") |
| method = inf.get("method", "N/A") |
| timestamp = inf.get("timestamp", "N/A") |
| epoch = inf.get("epoch", "N/A") |
|
|
| if pretrained_path: |
| model_path = Path(pretrained_path) |
| else: |
| model_path = Path(f"outputs/train/{task}/{method}/{timestamp}/epoch{epoch}") |
| print("📋 模型配置信息:") |
| if pretrained_path: |
| print(f" Pretrained Path: {pretrained_path}") |
| else: |
| print(f" Task: {task}") |
| print(f" Method: {method}") |
| print(f" Timestamp: {timestamp}") |
| print(f" Epoch: {epoch}") |
| print(f"📂 完整模型路径: {model_path}") |
| if model_path.exists(): |
| print("✅ 模型路径存在") |
| else: |
| print("❌ 模型路径不存在") |
| return cfg |
| except Exception as e: |
| print(f"❌ 解析配置文件失败: {e}") |
| sys.exit(1) |
|
|
|
|
| def print_task_menu(config_path="<config_path>", use_color=True): |
| """ |
| 打印 Kuavo 任务菜单,说明在前,统一在最后输出命令行模板。 |
| |
| Args: |
| config_path (str): 默认配置文件路径,用于命令行模板显示。 |
| use_color (bool): 是否使用终端颜色,默认 True。 |
| """ |
| |
| GREEN = "\033[32m" if use_color else "" |
| BLUE = "\033[34m" if use_color else "" |
| YELLOW = "\033[33m" if use_color else "" |
| RESET = "\033[0m" if use_color else "" |
|
|
| tasks = [ |
| ("go", "普通任务: 先插值到bag第一帧的位置, 再回放bag包前往工作位置"), |
| ("run", "普通任务: 从当前位置直接运行模型"), |
| ("auto_test", "自动测试任务:仿真中自动测试模型,执行 eval_episodes 次"), |
| ("offline_bag", "离线任务:从一条 bag 读取 obs,跑完整推理链路并和 bag action 做误差验证"), |
| ("退出", ""), |
| ] |
|
|
| print(f"\n🟢 可选择的任务示例如下:") |
| for idx, (name, desc) in enumerate(tasks, 1): |
| if desc: |
| print(f"{GREEN}{idx}. {name:<15}{RESET} : {BLUE}{desc}{RESET}") |
| else: |
| print(f"{GREEN}{idx}. {name}{RESET}") |
|
|
| |
| print(f"📋 数字选择后,会自动执行的命令示例:{RESET}") |
| print(f"普通任务:{RESET}") |
| print(f"{YELLOW} python kuavo_deploy/src/scripts/script.py --task <chosen_task> --config {config_path}{RESET}") |
| print(f"自动测试任务:{RESET}") |
| print(f"{YELLOW} python kuavo_deploy/src/scripts/script_auto_test.py --task auto_test --config {config_path}{RESET}") |
| print(f"离线bag推理:{RESET}") |
| print(f"{YELLOW} python kuavo_deploy/offline_bag_eval.py --config {config_path} --bag <bag_path> --plot{RESET}") |
|
|
|
|
|
|
| |
| def main(): |
| global current_proc, LOG_DIR |
|
|
| print_header() |
| script_dir, script, auto_test, offline_bag_eval = get_script_paths() |
| LOG_DIR = ensure_log_dir(script_dir) |
|
|
| if not script.exists(): |
| print(f"错误: 找不到 script.py 文件: {script}") |
| sys.exit(1) |
| if not auto_test.exists(): |
| print(f"错误: 找不到 script_auto_test.py 文件: {auto_test}") |
| sys.exit(1) |
| if not offline_bag_eval.exists(): |
| print(f"错误: 找不到 offline_bag_eval.py 文件: {offline_bag_eval}") |
| sys.exit(1) |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| config_path = sys.argv[1] if len(sys.argv) > 1 else "configs/deploy/deploy.yaml" |
| if not Path(config_path).exists(): |
| print(f"❌ 配置文件不存在: {config_path}") |
| sys.exit(1) |
|
|
| parse_config(config_path) |
|
|
| while True: |
| print_task_menu(config_path=config_path, use_color=True) |
|
|
| sub_choice = input("请选择要执行的示例 (1-5): ").strip() |
|
|
| def start_task(cmd): |
| global current_proc |
| log_path = LOG_DIR / "kuavo_deploy.log" |
| with open(log_path, "w") as f: |
| current_proc = subprocess.Popen(cmd, stdout=f, stderr=subprocess.STDOUT) |
| print(f"任务已启动,PID: {current_proc.pid}") |
| interactive_controller() |
|
|
| if sub_choice == "1": |
| start_task(["python3", str(script), "--task", "go", "--config", config_path]) |
| elif sub_choice == "2": |
| start_task(["python3", str(script), "--task", "run", "--config", config_path]) |
| elif sub_choice == "3": |
| start_task(["python3", str(auto_test), "--task", "auto_test", "--config", config_path]) |
| elif sub_choice == "4": |
| bag_path = input("请输入 bag 路径(留空则使用 inference.go_bag_path): ").strip() |
| |
| cmd = ["python3", "-u", str(offline_bag_eval), "--config", config_path, "--log-step-times"] |
| if bag_path: |
| cmd.extend(["--bag", bag_path]) |
| |
| cmd.append("--plot") |
| start_task(cmd) |
| elif sub_choice == "5": |
| print("退出") |
| break |
| else: |
| print("❌ 无效选择: ", sub_choice) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|