From 2b3cec56405405cd7a5debb64654c10d0d72b345 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 31 Jul 2025 14:25:40 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8F=AF=E8=83=BD=E7=9A=84we?= =?UTF-8?q?b=20template=E6=89=BE=E4=B8=8D=E5=88=B0=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20=E6=96=B0=E5=A2=9E=E8=81=94=E7=BD=91=E8=8E=B7?= =?UTF-8?q?=E5=8F=96json=E5=90=AF=E5=8A=A8=20=E5=88=A0=E9=99=A4=E9=9D=9E-g?= =?UTF-8?q?=E4=BC=A0=E5=85=A5=E5=90=AF=E5=8A=A8json=E7=9A=84=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=20=E5=85=BC=E5=AE=B9=E4=BC=A0=E5=8F=82=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=90=8D=E7=9F=AD=E6=A8=AA=E7=BA=BF=E4=B8=8E=E4=B8=8B?= =?UTF-8?q?=E5=88=92=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 4 +- unilabos/app/main.py | 103 +++++++++++++++--------- unilabos/app/web/client.py | 32 +++++++- unilabos/config/config.py | 1 + unilabos/registry/registry.py | 7 +- unilabos/resources/graphio.py | 10 ++- unilabos/ros/initialize_device.py | 10 ++- unilabos/ros/nodes/presets/host_node.py | 7 +- unilabos/utils/exception.py | 3 + 9 files changed, 129 insertions(+), 48 deletions(-) create mode 100644 unilabos/utils/exception.py diff --git a/MANIFEST.in b/MANIFEST.in index a8d25e9..4c1e88b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 * diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 08394b4..ef694f0 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -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__": diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 923e904..c1f07a7 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -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() diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 0450e68..4d2b6ca 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -15,6 +15,7 @@ class BasicConfig: upload_registry = False machine_name = "undefined" vis_2d_enable = False + enable_resource_load = True # MQTT配置 diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index f4bd430..b0cc756 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -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 diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 30c2128..507f8b6 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -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) diff --git a/unilabos/ros/initialize_device.py b/unilabos/ros/initialize_device.py index ed667f1..bbc86e0 100644 --- a/unilabos/ros/initialize_device.py +++ b/unilabos/ros/initialize_device.py @@ -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 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index eb5c2bb..f1b7f88 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -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 diff --git a/unilabos/utils/exception.py b/unilabos/utils/exception.py new file mode 100644 index 0000000..05a0c7c --- /dev/null +++ b/unilabos/utils/exception.py @@ -0,0 +1,3 @@ + +class DeviceClassInvalid(Exception): + pass