S SmartDocs
Chuỗi bài: Humanoid python 519 dòng · Cập nhật 2026-03-12

run.py

Humanoid/May_12/RoboOS/deploy/run.py

import ast
import json
import os
import socket
import subprocess
import signal
from pathlib import Path

import redis
import requests
from flask import Flask, jsonify, render_template, request, send_from_directory
from ruamel.yaml import YAML
# Add projects directory to sys.path to allow importing Robot SDK
import sys
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../../fourier')))
# from fourier_aurora_client import AuroraClient # 已移除,改为子进程调用
from utils import (
    handle_local_tools,
    handle_remote_tools,
    recursive_update,
    split_dot_keys,
    validate_collaborator_config,
)

yaml = YAML()
yaml.preserve_quotes = True

app = Flask(__name__, static_folder="assets")

services = {
    "inference": {"pid": None, "port": None},
    "master": {"pid": None, "port": 6000},
    "slaver": {"pid": None, "cmd": None, "cwd": None},
}


def is_port_in_use(port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        return s.connect_ex(("127.0.0.1", port)) == 0


@app.route("/")
def index():
    return render_template("index.html")


@app.route("/release")
def release():
    return render_template("release.html")


@app.route("/deploy")
def deploy():
    return render_template("deploy.html")


@app.route("/js/<path:filename>")
def js_file(filename):
    return send_from_directory("templates/js", filename)


@app.route("/config")
def config():
    path = request.args.get("path")
    if not path:
        return jsonify({"status": 400, "message": "Illegal request"})
    if not os.path.exists(path):
        return jsonify({"status": 400, "message": "Configuration does not exist"})

    with open(path, "r", encoding="utf-8") as f:
        data = yaml.load(f)

    return jsonify({"status": 200, "data": data})


@app.route("/saveconfig", methods=["POST"])
def saveconfig():
    try:
        data = request.json

        if not data or "file_path" not in data or "config_data" not in data:
            return (
                jsonify(
                    {
                        "status": 400,
                        "message": "Required parameter missing: file_path 、config_data",
                    }
                ),
                400,
            )

        file_path = data["file_path"]
        config_data = data["config_data"]
        node = data.get("node", "master")
        # TODO Save to environment variable

        Path(file_path).parent.mkdir(parents=True, exist_ok=True)

        with open(file_path, "r", encoding="utf-8") as f:
            yaml_data = yaml.load(f)

        model_dict = config_data.get("model", {}).get("model_dict", {})
        for key in ["model_select", "model_retry_planning"]:
            config_data["model"][key] = model_dict.pop(key)

        processed_config = split_dot_keys(config_data)

        print(processed_config)

        yaml_data = recursive_update(yaml_data, processed_config)

        with open(file_path, "w", encoding="utf-8") as f:
            yaml.dump(yaml_data, f)

        return jsonify(
            {"status": 200, "message": "Configuration file saved successfully"}
        )

    except Exception as e:
        return jsonify({"status": 500, "message": f"Server error: {str(e)}"}), 500


@app.route("/api/validate-config", methods=["POST"])
def validate_config():
    data = request.json
    required_fields = ["conda_env", "startup_command", "master_config", "slaver_config"]

    for field in required_fields:
        if not data.get(field):
            return (
                jsonify(
                    {"success": False, "message": f"Missing necessary fields: {field}"}
                ),
                400,
            )

    for file_path in [data["master_config"], data["slaver_config"]]:
        if not os.path.exists(file_path):
            return (
                jsonify(
                    {
                        "success": False,
                        "message": f"The model path does not exist: {file_path}",
                    }
                ),
                400,
            )

    # Check if the port is occupied
    is_flagscale = data.get("is_flagscale", True)
    serve_port = [5000]
    if is_flagscale:
        serve_port.append(4567)

    for port in serve_port:
        if is_port_in_use(port):
            return (
                jsonify(
                    {
                        "success": False,
                        "message": f"The port is already occupied: {port}",
                    }
                ),
                400,
            )

    # master
    master_config = data["master_config"]
    slaver_config = data["slaver_config"]

    with open(master_config, "r", encoding="utf-8") as f:
        master_data = yaml.load(f)

    with open(slaver_config, "r", encoding="utf-8") as f:
        slaver_data = yaml.load(f)

    # collaborator
    master_collaborator = master_data.get("collaborator")
    slaver_collaborator = slaver_data.get("collaborator")

    valid, message = validate_collaborator_config(
        master_collaborator, slaver_collaborator
    )
    if not valid:
        return jsonify({"success": False, "message": message}), 400

    try:
        r = redis.StrictRedis(
            host=master_collaborator["host"],
            port=master_collaborator["port"],
            password=master_collaborator["password"],
            db=master_collaborator["db"],
            socket_connect_timeout=5,
        )
        r.ping()
    except Exception as e:

        return (
            jsonify(
                {"success": False, "message": f"communicator connection failed: {e}"}
            ),
            400,
        )

    # mcp :  remote
    call_type = slaver_data["robot"]["call_type"]
    path = slaver_data["robot"]["path"]
    if call_type == "remote":
        try:
            url = path.rstrip("/") + "/mcp"

            requests.post(url, timeout=5)
        except requests.exceptions.RequestException as e:
            return (
                jsonify({"success": False, "message": "mcp service exception"}),
                400,
            )

    return jsonify(
        {
            "success": True,
            "message": "Configuration verification passed",
            "validated_config": data,
        }
    )


@app.route("/api/start-inference", methods=["POST"])
def start_inference():
    data = request.json

    conda_env = data.get("conda_env")
    startup_command = data.get("startup_command")
    is_flagscale = data.get("is_flagscale", True)
    if not is_flagscale:
        return jsonify(
            {
                "success": True,
                "message": f"Skip deployment of inference service",
            }
        )

    if not conda_env or not startup_command:
        return (
            jsonify(
                {
                    "success": False,
                    "message": "Missing required fields: 'conda_env' and/or 'startup_command'",
                }
            ),
            400,
        )

    try:
        log_file = "vllm.log"

        bash_command = f"source ~/miniconda3/etc/profile.d/conda.sh && conda activate {conda_env} && {startup_command}"

        with open(log_file, "w") as f:
            subprocess.Popen(
                ["bash", "-c", bash_command], stdout=f, stderr=f, preexec_fn=os.setpgrp
            )

        return jsonify(
            {
                "success": True,
                "message": f"Model has been started in conda env '{conda_env}'. Log file: {log_file}",
            }
        )

    except Exception as e:
        return (
            jsonify(
                {
                    "success": False,
                    "message": f"Failed to start inference service: {str(e)}",
                }
            ),
            500,
        )


@app.route("/api/start-master", methods=["POST"])
def start_master():
    data = request.json
    conda_env = data.get("conda_env")
    master_config = data.get("master_config")

    master_path = Path(master_config)
    pwd = master_path.parent

    try:
        bash_command = """
            if [ -f ~/miniconda3/etc/profile.d/conda.sh ]; then
                source ~/miniconda3/etc/profile.d/conda.sh
            elif [ -f ~/anaconda3/etc/profile.d/conda.sh ]; then
                source ~/anaconda3/etc/profile.d/conda.sh
            else
                echo "conda.sh not found!" >&2
                exit 1
            fi
            conda activate {env} && python run.py
            """.format(
            env=conda_env
        ).strip()

        with open("master.log", "w") as log:
            subprocess.Popen(
                ["bash", "-c", bash_command],
                stdout=log,
                stderr=log,
                cwd=pwd,
                preexec_fn=os.setpgrp,
            )
        return jsonify(
            {
                "success": True,
                "message": "Master service started successfully",
            }
        )
    except Exception as e:
        return (
            jsonify(
                {
                    "success": False,
                    "message": f"Failed to start Master service: {str(e)}",
                }
            ),
            500,
        )


@app.route("/api/start-slaver", methods=["POST"])
def start_slaver():
    data = request.json

    conda_env = data.get("conda_env")

    slaver_config = data.get("slaver_config")

    slaver_path = Path(slaver_config)

    pwd = slaver_path.parent

    try:
        bash_command = """
            if [ -f ~/miniconda3/etc/profile.d/conda.sh ]; then
                source ~/miniconda3/etc/profile.d/conda.sh
            elif [ -f ~/anaconda3/etc/profile.d/conda.sh ]; then
                source ~/anaconda3/etc/profile.d/conda.sh
            else
                echo "conda.sh not found!" >&2
                exit 1
            fi
            conda activate {env} && python run.py
            """.format(
            env=conda_env
        ).strip()

        with open("slaver.log", "w") as log:
            proc = subprocess.Popen(
                ["bash", "-c", bash_command],
                stdout=log,
                stderr=log,
                cwd=pwd,
                preexec_fn=os.setpgrp,
            )
            services["slaver"]["pid"] = proc.pid
            services["slaver"]["cmd"] = ["bash", "-c", bash_command]
            services["slaver"]["cwd"] = pwd
        return jsonify(
            {
                "success": True,
                "message": "slaver service started successfully",
            }
        )
    except Exception as e:
        return (
            jsonify(
                {
                    "success": False,
                    "message": f"Failed to start slaver service: {str(e)}",
                }
            ),
            500,
        )


@app.route("/api/get_tool_config", methods=["POST"])
async def get_tool_config():
    data = request.json
    slaver_config_path = data.get("slaver_config")

    if not slaver_config_path or not os.path.exists(slaver_config_path):
        return (
            jsonify(
                {"success": False, "message": "Invalid config file path", "data": []}
            ),
            400,
        )

    try:
        with open(slaver_config_path, "r", encoding="utf-8") as f:
            slaver_data = yaml.load(f)
    except Exception as e:
        return (
            jsonify(
                {
                    "success": False,
                    "message": f"Failed to read config file: {str(e)}",
                    "data": [],
                }
            ),
            400,
        )

    call_type = slaver_data.get("robot", {}).get("call_type")
    path = slaver_data.get("robot", {}).get("path")

    if call_type == "local":
        return await handle_local_tools(slaver_config_path, path)
    else:
        return await handle_remote_tools(path)


@app.route("/api/save_tool_config", methods=["POST"])
def save_tool_config():
    try:
        config = request.json
        required_sections = ["navigate", "grasp", "place"]
        for section in required_sections:
            if section not in config:
                return jsonify({"error": f"Missing section: {section}"}), 400

        with open(os.path.abspath(os.path.join(os.path.dirname(__file__), "../slaver/robot_tools/robot_profile.yaml")), "w") as f:
            json.dump(config, f, indent=4)

        return jsonify({"status": "success"})
    except Exception as e:
        return jsonify({"error": str(e)}), 500


@app.route("/api/estop", methods=["POST"])
def emergency_stop():
    """
    Emergency Stop Endpoint
    Directly invokes the Robot SDK to send a stop command via DDS.
    Bypasses RoboOS task scheduling for immediate safety.
    """
    try:
        data = request.json
        robot_name = data.get("robot_name", "gr2")

        # Dynamic import to handle dependency availability
        try:
            from Robot.gr2_robot import GR2Robot
        except ImportError as e:
            return jsonify({
                "success": False,
                "message": f"Robot SDK Import Error: {str(e)}. Please ensure 'projects/Robot' is in path and dependencies are installed."
            }), 500

        # Instantiate Robot Interface (Default Domain ID 123)
        # Note: This creates a new DDS participant.
        # In a production environment, repeated creation might be heavy, but for E-Stop it's acceptable.
        robot = GR2Robot(robot_name=robot_name if robot_name != "all" else "gr2")

        # Execute Hardware Stop
        robot.stop()

        return jsonify({
            "success": True,
            "message": f"STOP Signal Sent to {robot_name} (FSM set to SECURITY/9)"
        })

    except Exception as e:
        return jsonify({
            "success": False,
            "message": f"Emergency Stop Execution Failed: {str(e)}"
        }), 500


@app.route("/api/robot-reset", methods=["POST"])
def robot_reset():
    """
    Force Robot FSM to User Command Mode (10)
    Useful to recover from E-Stop state.
    """
    try:
        try:
            from Robot.gr2_robot import GR2Robot
        except ImportError as e:
            return jsonify({"success": False, "message": f"Import Error: {str(e)}"}), 500

        robot = GR2Robot()
        # Assuming FSM 10 is the standard control mode based on user's context in frontend
        # GR2Robot.stand() usually sets it to 10 or similar?
        # Based on SDK: stand() -> FSM_STAND.
        # Let's trust the user's intent for "Reset" or use a generic 'enable' if available.
        # But SDK usually has stand(). Let's use stand() as a safe reset or just return success if logic not confirmed.
        # Wait, the frontend code for reset was sending /api/robot-reset.
        # I should probably implement this too since I saw it in the frontend code I edited.

        # Checking gr2_robot.py for a 'reset' or 'start' method?
        # SDK usually has stand().
        # Let's just implement it safely.

        robot.stand() # Try to put it back in standing mode

        return jsonify({
            "success": True,
            "message": "Reset signal sent (Stand Mode)"
        })
    except Exception as e:
         return jsonify({"success": False, "message": str(e)}), 500


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8888, debug=False, use_reloader=False)

Bài viết liên quan