修复可能的web template找不到的问题

新增联网获取json启动
删除非-g传入启动json的方式
兼容传参参数名短横线与下划线
This commit is contained in:
Xuwznln
2025-07-31 14:25:40 +08:00
parent c6ac32c115
commit 2b3cec5640
9 changed files with 129 additions and 48 deletions

View File

@@ -1,5 +1,5 @@
recursive-include unilabos/registry *.yaml
recursive-include unilabos/app/web *.html
recursive-include unilabos/app/web *.css
recursive-include unilabos/app/static *
recursive-include unilabos/app/templates *
recursive-include unilabos/device_mesh/devices *
recursive-include unilabos/device_mesh/resources *

View File

@@ -1,6 +1,5 @@
import argparse
import asyncio
import json
import os
import signal
import sys
@@ -10,7 +9,7 @@ from copy import deepcopy
import yaml
from unilabos.resources.graphio import tree_to_list, modify_to_backend_format
from unilabos.resources.graphio import modify_to_backend_format
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -18,7 +17,7 @@ unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
if unilabos_dir not in sys.path:
sys.path.append(unilabos_dir)
from unilabos.config.config import load_config, BasicConfig, _update_config_from_env
from unilabos.config.config import load_config, BasicConfig
from unilabos.utils.banner_print import print_status, print_unilab_banner
@@ -37,12 +36,22 @@ def load_config_from_file(config_path, override_labid=None):
load_config(config_path, override_labid)
def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser):
# easier for user input, easier for dev search code
option_strings = list(args._option_string_actions.keys())
for i, arg in enumerate(sys.argv):
for option_string in option_strings:
if arg.startswith(option_string):
new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):]
sys.argv[i] = new_arg
break
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
parser.add_argument("-g", "--graph", help="Physical setup graph.")
parser.add_argument("-d", "--devices", help="Devices config file.")
parser.add_argument("-r", "--resources", help="Resources config file.")
# parser.add_argument("-d", "--devices", help="Devices config file.")
# parser.add_argument("-r", "--resources", help="Resources config file.")
parser.add_argument("-c", "--controllers", default=None, help="Controllers config file.")
parser.add_argument(
"--registry_path",
@@ -51,6 +60,12 @@ def parse_args():
action="append",
help="Path to the registry",
)
parser.add_argument(
"--working_dir",
type=str,
default=None,
help="Path to the working directory",
)
parser.add_argument(
"--backend",
choices=["ros", "simple", "automancer"],
@@ -92,12 +107,12 @@ def parse_args():
)
parser.add_argument(
"--disable_browser",
action='store_true',
action="store_true",
help="是否在启动时关闭信息页",
)
parser.add_argument(
"--2d_vis",
action='store_true',
action="store_true",
help="是否在pylabrobot实例启动时同时启动可视化",
)
parser.add_argument(
@@ -112,14 +127,15 @@ def parse_args():
default="",
help="实验室唯一ID也可通过环境变量 UNILABOS.MQCONFIG.LABID 设置或传入--config设置",
)
return parser.parse_args()
return parser
def main():
"""主函数"""
# 解析命令行参数
args = parse_args()
args_dict = vars(args)
convert_argv_dashes_to_underscores(args)
args_dict = vars(args.parse_args())
# 加载配置文件优先加载config然后从env读取
config_path = args_dict.get("config")
@@ -152,30 +168,30 @@ def main():
# 注册表
build_registry(args_dict["registry_path"])
resource_edge_info = []
devices_and_resources = None
if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res
if args_dict["graph"] is None:
request_startup_json = http_client.request_startup_json()
if not request_startup_json:
print_status(
"未指定设备加载文件路径尝试从HTTP获取失败请检查网络或者使用-g参数指定设备加载文件路径", "error"
)
os._exit(1)
else:
print_status("联网获取设备加载文件成功", "info")
graph, data = read_node_link_json(request_startup_json)
else:
if args_dict["graph"].endswith(".json"):
graph, data = read_node_link_json(args_dict["graph"])
else:
graph, data = read_graphml(args_dict["graph"])
graph_res.physical_setup_graph = graph
resource_edge_info = modify_to_backend_format(data["links"])
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["resources_config"] = list(devices_and_resources.values())
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph
else:
if args_dict["devices"] is None or args_dict["resources"] is None:
print_status("Either graph or devices and resources must be provided.", "error")
sys.exit(1)
args_dict["devices_config"] = json.load(open(args_dict["devices"], encoding="utf-8"))
# args_dict["resources_config"] = initialize_resources(
# list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
# )
args_dict["resources_config"] = list(json.load(open(args_dict["resources"], encoding="utf-8")).values())
import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = graph
resource_edge_info = modify_to_backend_format(data["links"])
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["resources_config"] = list(devices_and_resources.values())
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
args_dict["graph"] = graph_res.physical_setup_graph
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
for i in args_dict["resources_config"]:
@@ -207,13 +223,22 @@ def main():
if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz"
if devices_and_resources is not None:
from unilabos.device_mesh.resource_visalization import ResourceVisualization # 此处开启后logger会变更为INFO有需要请调整
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
from unilabos.device_mesh.resource_visalization import (
ResourceVisualization,
) # 此处开启后logger会变更为INFO有需要请调整
resource_visualization = ResourceVisualization(
devices_and_resources, args_dict["resources_config"], enable_rviz=enable_rviz
)
args_dict["resources_mesh_config"] = resource_visualization.resource_model
start_backend(**args_dict)
server_thread = threading.Thread(target=start_server, kwargs=dict(
open_browser=not args_dict["disable_browser"], port=args_dict["port"],
))
server_thread = threading.Thread(
target=start_server,
kwargs=dict(
open_browser=not args_dict["disable_browser"],
port=args_dict["port"],
),
)
server_thread.start()
asyncio.set_event_loop(asyncio.new_event_loop())
resource_visualization.start()
@@ -221,10 +246,16 @@ def main():
time.sleep(1)
else:
start_backend(**args_dict)
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
start_server(
open_browser=not args_dict["disable_browser"],
port=args_dict["port"],
)
else:
start_backend(**args_dict)
start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],)
start_server(
open_browser=not args_dict["disable_browser"],
port=args_dict["port"],
)
if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@ HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用
"""
import json
from typing import List, Dict, Any, Optional
import requests
@@ -170,6 +170,36 @@ class HTTPClient:
logger.error(f"注册资源失败: {response.status_code}, {response.text}")
return response
def request_startup_json(self) -> Optional[Dict[str, Any]]:
"""
请求启动配置
Args:
startup_json: 启动配置JSON数据
Returns:
Response: API响应对象
"""
response = requests.get(
f"{self.remote_addr}/lab/resource/graph_info/",
headers={"Authorization": f"lab {self.auth}"},
timeout=(3, 30),
)
if response.status_code != 200:
logger.error(f"请求启动配置失败: {response.status_code}, {response.text}")
else:
try:
with open("startup_config.json", "w", encoding="utf-8") as f:
f.write(response.text)
target_dict = json.loads(response.text)
if "data" in target_dict:
target_dict = target_dict["data"]
return target_dict
except json.JSONDecodeError as e:
logger.error(f"解析启动配置JSON失败: {str(e.args)}\n响应内容: {response.text}")
logger.error(f"响应内容: {response.text}")
return None
# 创建默认客户端实例
http_client = HTTPClient()

View File

@@ -15,6 +15,7 @@ class BasicConfig:
upload_registry = False
machine_name = "undefined"
vis_2d_enable = False
enable_resource_load = True
# MQTT配置

View File

@@ -9,6 +9,7 @@ from typing import Any, Dict, List
import yaml
from unilabos.config.config import BasicConfig
from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list
from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema, String
from unilabos.utils import logger
@@ -46,6 +47,7 @@ class Registry:
)
self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"")
self.device_type_registry = {}
self.device_module_to_registry = {}
self.resource_type_registry = {}
self._setup_called = False # 跟踪setup是否已调用
# 其他状态变量
@@ -157,7 +159,10 @@ class Registry:
logger.debug(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
sys.path.append(str(sys_path))
self.load_device_types(path, complete_registry)
self.load_resource_types(path, complete_registry)
if BasicConfig.enable_resource_load:
self.load_resource_types(path, complete_registry)
else:
logger.warning("跳过了资源注册表加载!")
logger.info("[UniLab Registry] 注册表设置完成")
# 标记setup已被调用
self._setup_called = True

View File

@@ -1,7 +1,7 @@
import importlib
import inspect
import json
from typing import Union, Any
from typing import Union, Any, Dict
import numpy as np
import networkx as nx
from unilabos_msgs.msg import Resource
@@ -150,10 +150,12 @@ def handle_communications(G: nx.Graph):
G.nodes[device]["config"]["io_device_port"] = int(edata["port"][device_comm])
def read_node_link_json(json_file):
def read_node_link_json(json_info: Union[str, Dict[str, Any]]) -> tuple[nx.Graph, dict]:
global physical_setup_graph
data = json.load(open(json_file, encoding="utf-8"))
if isinstance(json_info, str):
data = json.load(open(json_info, encoding="utf-8"))
else:
data = json_info
data = canonicalize_nodes_data(data)
data = canonicalize_links_ports(data)

View File

@@ -5,9 +5,11 @@ from unilabos.registry.registry import lab_registry
from unilabos.ros.device_node_wrapper import ros2_device_node
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError
from unilabos.utils import logger
from unilabos.utils.exception import DeviceClassInvalid
from unilabos.utils.import_manager import default_manager
def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2DeviceNode]:
"""Initializes a device based on its configuration.
@@ -25,11 +27,13 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
original_device_config = copy.deepcopy(device_config)
device_class_config = device_config["class"]
if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取class
if len(device_class_config) == 0:
raise DeviceClassInvalid(f"Device [{device_id}] class cannot be an empty string. {device_config}")
if device_class_config not in lab_registry.device_type_registry:
raise ValueError(f"Device class {device_class_config} not found.")
raise DeviceClassInvalid(f"Device [{device_id}] class {device_class_config} not found. {device_config}")
device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"]
else:
raise ValueError("不再支持class为字典传入class必须为注册表中已经提供的设备您可以新增注册表并通过--registry传入")
elif isinstance(device_class_config, dict):
raise DeviceClassInvalid(f"Device [{device_id}] class config should be type 'str' but 'dict' got. {device_config}")
if isinstance(device_class_config, dict):
DEVICE = default_manager.get_class(device_class_config["module"])
# 不管是ros2的实例还是python的都必须包一次除了HostNode

View File

@@ -36,6 +36,7 @@ from unilabos.ros.msgs.message_converter import (
)
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker
from unilabos.ros.nodes.presets.controller_node import ControllerNode
from unilabos.utils.exception import DeviceClassInvalid
class HostNode(BaseROS2DeviceNode):
@@ -459,7 +460,11 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node] Initializing device: {device_id}")
device_config_copy = copy.deepcopy(device_config)
d = initialize_device_from_dict(device_id, device_config_copy)
try:
d = initialize_device_from_dict(device_id, device_config_copy)
except DeviceClassInvalid as e:
self.lab_logger().error(f"[Host Node] Device class invalid: {e}")
d = None
if d is None:
return
# noinspection PyProtectedMember

View File

@@ -0,0 +1,3 @@
class DeviceClassInvalid(Exception):
pass