시리즈: Humanoid
python
519 줄
· 업데이트 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)
관련 글
Humanoid
python
업데이트 2026-03-12
utils.py
utils.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/deploy/utils.py).
글 읽기 →
Humanoid
python
업데이트 2026-03-12
agent.py
agent.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/agent.py).
글 읽기 →
Humanoid
python
업데이트 2026-03-12
planner.py
planner.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/planner.py).
글 읽기 →
Humanoid
python
업데이트 2026-05-12
prompts.py
prompts.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/agents/prompts.py).
글 읽기 →
Humanoid
python
업데이트 2026-05-12
run.py
run.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/run.py).
글 읽기 →
Humanoid
python
업데이트 2026-05-12
__init__.py
__init__.py — python source code from the Humanoid learning materials (Humanoid/May_12/RoboOS/master/task_manager/__init__.py).
글 읽기 →