修复可能的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/registry *.yaml
recursive-include unilabos/app/web *.html recursive-include unilabos/app/static *
recursive-include unilabos/app/web *.css recursive-include unilabos/app/templates *
recursive-include unilabos/device_mesh/devices * recursive-include unilabos/device_mesh/devices *
recursive-include unilabos/device_mesh/resources * recursive-include unilabos/device_mesh/resources *

View File

@@ -1,6 +1,5 @@
import argparse import argparse
import asyncio import asyncio
import json
import os import os
import signal import signal
import sys import sys
@@ -10,7 +9,7 @@ from copy import deepcopy
import yaml 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__)) 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: if unilabos_dir not in sys.path:
sys.path.append(unilabos_dir) 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 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) 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(): def parse_args():
"""解析命令行参数""" """解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.") parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
parser.add_argument("-g", "--graph", help="Physical setup graph.") parser.add_argument("-g", "--graph", help="Physical setup graph.")
parser.add_argument("-d", "--devices", help="Devices config file.") # parser.add_argument("-d", "--devices", help="Devices config file.")
parser.add_argument("-r", "--resources", help="Resources 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("-c", "--controllers", default=None, help="Controllers config file.")
parser.add_argument( parser.add_argument(
"--registry_path", "--registry_path",
@@ -51,6 +60,12 @@ def parse_args():
action="append", action="append",
help="Path to the registry", help="Path to the registry",
) )
parser.add_argument(
"--working_dir",
type=str,
default=None,
help="Path to the working directory",
)
parser.add_argument( parser.add_argument(
"--backend", "--backend",
choices=["ros", "simple", "automancer"], choices=["ros", "simple", "automancer"],
@@ -92,12 +107,12 @@ def parse_args():
) )
parser.add_argument( parser.add_argument(
"--disable_browser", "--disable_browser",
action='store_true', action="store_true",
help="是否在启动时关闭信息页", help="是否在启动时关闭信息页",
) )
parser.add_argument( parser.add_argument(
"--2d_vis", "--2d_vis",
action='store_true', action="store_true",
help="是否在pylabrobot实例启动时同时启动可视化", help="是否在pylabrobot实例启动时同时启动可视化",
) )
parser.add_argument( parser.add_argument(
@@ -112,14 +127,15 @@ def parse_args():
default="", default="",
help="实验室唯一ID也可通过环境变量 UNILABOS.MQCONFIG.LABID 设置或传入--config设置", help="实验室唯一ID也可通过环境变量 UNILABOS.MQCONFIG.LABID 设置或传入--config设置",
) )
return parser.parse_args() return parser
def main(): def main():
"""主函数""" """主函数"""
# 解析命令行参数 # 解析命令行参数
args = parse_args() args = parse_args()
args_dict = vars(args) convert_argv_dashes_to_underscores(args)
args_dict = vars(args.parse_args())
# 加载配置文件优先加载config然后从env读取 # 加载配置文件优先加载config然后从env读取
config_path = args_dict.get("config") config_path = args_dict.get("config")
@@ -152,30 +168,30 @@ def main():
# 注册表 # 注册表
build_registry(args_dict["registry_path"]) build_registry(args_dict["registry_path"])
resource_edge_info = [] if args_dict["graph"] is None:
devices_and_resources = None request_startup_json = http_client.request_startup_json()
if args_dict["graph"] is not None: if not request_startup_json:
import unilabos.resources.graphio as graph_res 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"): if args_dict["graph"].endswith(".json"):
graph, data = read_node_link_json(args_dict["graph"]) graph, data = read_node_link_json(args_dict["graph"])
else: else:
graph, data = read_graphml(args_dict["graph"]) graph, data = read_graphml(args_dict["graph"])
graph_res.physical_setup_graph = graph import unilabos.resources.graphio as graph_res
resource_edge_info = modify_to_backend_format(data["links"])
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph) graph_res.physical_setup_graph = graph
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values())) resource_edge_info = modify_to_backend_format(data["links"])
args_dict["resources_config"] = list(devices_and_resources.values()) devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False) # args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["graph"] = graph_res.physical_setup_graph args_dict["resources_config"] = list(devices_and_resources.values())
else: args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False)
if args_dict["devices"] is None or args_dict["resources"] is None: args_dict["graph"] = graph_res.physical_setup_graph
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())
print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info") print_status(f"{len(args_dict['resources_config'])} Resources loaded:", "info")
for i in args_dict["resources_config"]: for i in args_dict["resources_config"]:
@@ -207,13 +223,22 @@ def main():
if args_dict["visual"] != "disable": if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz" enable_rviz = args_dict["visual"] == "rviz"
if devices_and_resources is not None: if devices_and_resources is not None:
from unilabos.device_mesh.resource_visalization import ResourceVisualization # 此处开启后logger会变更为INFO有需要请调整 from unilabos.device_mesh.resource_visalization import (
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz) 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 args_dict["resources_mesh_config"] = resource_visualization.resource_model
start_backend(**args_dict) start_backend(**args_dict)
server_thread = threading.Thread(target=start_server, kwargs=dict( server_thread = threading.Thread(
open_browser=not args_dict["disable_browser"], port=args_dict["port"], target=start_server,
)) kwargs=dict(
open_browser=not args_dict["disable_browser"],
port=args_dict["port"],
),
)
server_thread.start() server_thread.start()
asyncio.set_event_loop(asyncio.new_event_loop()) asyncio.set_event_loop(asyncio.new_event_loop())
resource_visualization.start() resource_visualization.start()
@@ -221,10 +246,16 @@ def main():
time.sleep(1) time.sleep(1)
else: else:
start_backend(**args_dict) 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: else:
start_backend(**args_dict) 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__": if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@ HTTP客户端模块
提供与远程服务器通信的客户端功能只有host需要用 提供与远程服务器通信的客户端功能只有host需要用
""" """
import json
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import requests import requests
@@ -170,6 +170,36 @@ class HTTPClient:
logger.error(f"注册资源失败: {response.status_code}, {response.text}") logger.error(f"注册资源失败: {response.status_code}, {response.text}")
return response 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() http_client = HTTPClient()

View File

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

View File

@@ -9,6 +9,7 @@ from typing import Any, Dict, List
import yaml import yaml
from unilabos.config.config import BasicConfig
from unilabos.resources.graphio import resource_plr_to_ulab, tree_to_list 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.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema, String
from unilabos.utils import logger from unilabos.utils import logger
@@ -46,6 +47,7 @@ class Registry:
) )
self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"") self.EmptyIn = self._replace_type_with_class("EmptyIn", "host_node", f"")
self.device_type_registry = {} self.device_type_registry = {}
self.device_module_to_registry = {}
self.resource_type_registry = {} self.resource_type_registry = {}
self._setup_called = False # 跟踪setup是否已调用 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}") logger.debug(f"[UniLab Registry] Path {i+1}/{len(self.registry_paths)}: {sys_path}")
sys.path.append(str(sys_path)) sys.path.append(str(sys_path))
self.load_device_types(path, complete_registry) 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] 注册表设置完成") logger.info("[UniLab Registry] 注册表设置完成")
# 标记setup已被调用 # 标记setup已被调用
self._setup_called = True self._setup_called = True

View File

@@ -1,7 +1,7 @@
import importlib import importlib
import inspect import inspect
import json import json
from typing import Union, Any from typing import Union, Any, Dict
import numpy as np import numpy as np
import networkx as nx import networkx as nx
from unilabos_msgs.msg import Resource 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]) 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 global physical_setup_graph
if isinstance(json_info, str):
data = json.load(open(json_file, encoding="utf-8")) data = json.load(open(json_info, encoding="utf-8"))
else:
data = json_info
data = canonicalize_nodes_data(data) data = canonicalize_nodes_data(data)
data = canonicalize_links_ports(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.device_node_wrapper import ros2_device_node
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError
from unilabos.utils import logger from unilabos.utils import logger
from unilabos.utils.exception import DeviceClassInvalid
from unilabos.utils.import_manager import default_manager from unilabos.utils.import_manager import default_manager
def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2DeviceNode]: def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2DeviceNode]:
"""Initializes a device based on its configuration. """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) original_device_config = copy.deepcopy(device_config)
device_class_config = device_config["class"] device_class_config = device_config["class"]
if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取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: 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"] device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"]
else: elif isinstance(device_class_config, dict):
raise ValueError("不再支持class为字典传入class必须为注册表中已经提供的设备您可以新增注册表并通过--registry传入") raise DeviceClassInvalid(f"Device [{device_id}] class config should be type 'str' but 'dict' got. {device_config}")
if isinstance(device_class_config, dict): if isinstance(device_class_config, dict):
DEVICE = default_manager.get_class(device_class_config["module"]) DEVICE = default_manager.get_class(device_class_config["module"])
# 不管是ros2的实例还是python的都必须包一次除了HostNode # 不管是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.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker
from unilabos.ros.nodes.presets.controller_node import ControllerNode from unilabos.ros.nodes.presets.controller_node import ControllerNode
from unilabos.utils.exception import DeviceClassInvalid
class HostNode(BaseROS2DeviceNode): class HostNode(BaseROS2DeviceNode):
@@ -459,7 +460,11 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node] Initializing device: {device_id}") self.lab_logger().info(f"[Host Node] Initializing device: {device_id}")
device_config_copy = copy.deepcopy(device_config) 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: if d is None:
return return
# noinspection PyProtectedMember # noinspection PyProtectedMember

View File

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