diff --git a/unilabos/app/main.py b/unilabos/app/main.py index d418c5c1..dab1b347 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -58,6 +58,18 @@ def parse_args(): default=None, help="配置文件路径,支持.py格式的Python配置文件", ) + parser.add_argument( + "--port", + type=int, + default=None, + help="信息页web服务的启动端口", + ) + parser.add_argument( + "--open_browser", + type=bool, + default=True, + help="是否在启动时打开信息页", + ) return parser.parse_args() @@ -151,7 +163,7 @@ def main(): mqtt_client.start() start_backend(**args_dict) - start_server() + start_server(port=args_dict.get("port", 8002), open_browser=args_dict.get("open_browser", False)) if __name__ == "__main__": diff --git a/unilabos/app/web/pages.py b/unilabos/app/web/pages.py index 7216a66c..add64c6a 100644 --- a/unilabos/app/web/pages.py +++ b/unilabos/app/web/pages.py @@ -92,19 +92,7 @@ def setup_web_pages(router: APIRouter) -> None: # 获取已加载的设备 if lab_registry: - # 设备类型 - for device_id, device_info in lab_registry.device_type_registry.items(): - msg = { - "id": device_id, - "name": device_info.get("name", "未命名"), - "file_path": device_info.get("file_path", ""), - "class_json": json.dumps( - device_info.get("class", {}), indent=4, ensure_ascii=False, cls=TypeEncoder - ), - } - mqtt_client.publish_registry(device_id, device_info) - devices.append(msg) - + devices = lab_registry.obtain_registry_device_info() # 资源类型 for resource_id, resource_info in lab_registry.resource_type_registry.items(): resources.append( diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 9c7a95b9..b8120330 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,3 +1,4 @@ +import json import os import sys from pathlib import Path @@ -8,6 +9,7 @@ import yaml from unilabos.utils import logger from unilabos.ros.msgs.message_converter import msg_converter_manager from unilabos.utils.decorator import singleton +from unilabos.utils.type_check import TypeEncoder DEFAULT_PATHS = [Path(__file__).absolute().parent] @@ -143,6 +145,20 @@ class Registry: f"[UniLab Registry] Device File-{i+1}/{len(files)} Not Valid YAML File: {file.absolute()}" ) + def obtain_registry_device_info(self): + devices = [] + for device_id, device_info in self.device_type_registry.items(): + msg = { + "id": device_id, + "name": device_info.get("name", "未命名"), + "file_path": device_info.get("file_path", ""), + "class_json": json.dumps( + device_info.get("class", {}), indent=4, ensure_ascii=False, cls=TypeEncoder + ), + } + devices.append(msg) + return devices + # 全局单例实例 lab_registry = Registry() diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index e5c16bc4..71e4aaeb 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -1,4 +1,5 @@ import importlib +import inspect import json from typing import Union import numpy as np @@ -384,7 +385,11 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR": d = resource_ulab_to_plr_inner(resource) """无法通过Resource进行反序列化,例如TipSpot必须内部序列化好,直接用TipSpot序列化会多参数,导致出错""" from pylabrobot.utils.object_parsing import find_subclass - resource_plr = find_subclass(d["type"], ResourcePLR).deserialize(d, allow_marshal=True) + sub_cls = find_subclass(d["type"], ResourcePLR) + spect = inspect.signature(sub_cls) + if "category" not in spect.parameters: + d.pop("category") + resource_plr = sub_cls.deserialize(d, allow_marshal=True) resource_plr.load_all_state(all_states) return resource_plr diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 1ae37906..5c038bdc 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -1,14 +1,16 @@ +import json import os import traceback from typing import Optional, Dict, Any, List import rclpy from unilabos_msgs.msg import Resource # type: ignore -from unilabos_msgs.srv import ResourceAdd # type: ignore +from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore from rclpy.executors import MultiThreadedExecutor from rclpy.node import Node from rclpy.timer import Timer +from unilabos.registry.registry import lab_registry from unilabos.ros.initialize_device import initialize_device_from_dict from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, @@ -17,6 +19,7 @@ from unilabos.ros.nodes.presets.host_node import HostNode from unilabos.ros.x.rclpyx import run_event_loop_in_thread from unilabos.utils import logger from unilabos.config.config import BasicConfig +from unilabos.utils.type_check import TypeEncoder def exit() -> None: @@ -98,17 +101,29 @@ def slave( n = Node(f"slaveMachine_{machine_name}", parameter_overrides=[]) executor.add_node(n) - if BasicConfig.slave_no_host: - # 确保ResourceAdd存在 - if "ResourceAdd" in globals(): - rclient = n.create_client(ResourceAdd, "/resources/add") - rclient.wait_for_service() # FIXME 可能一直等待,加一个参数 + if not BasicConfig.slave_no_host: + sclient = n.create_client(SerialCommand, "/node_info_update") + sclient.wait_for_service() + + request = SerialCommand.Request() + request.command = json.dumps({ + "machine_name": machine_name, + "type": "slave", + "devices_config": devices_config, + "registry_config": lab_registry.obtain_registry_device_info() + }, ensure_ascii=False, cls=TypeEncoder) + response = sclient.call_async(request) + logger.info(f"Slave node info update response: {response}") + + rclient = n.create_client(ResourceAdd, "/resources/add") + rclient.wait_for_service() # FIXME 可能一直等待,加一个参数 + + request = ResourceAdd.Request() + request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] + response = rclient.call_async(request) + logger.info(f"Slave resource add response: {response}") + - request = ResourceAdd.Request() - request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] - response = rclient.call_async(request) - else: - print("Warning: ResourceAdd service not available") run_event_loop_in_thread() diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 34132b35..8e07e05c 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -1,4 +1,5 @@ import copy +import json import threading import time import traceback @@ -7,7 +8,7 @@ from typing import Optional, Dict, Any, List, ClassVar, Set from action_msgs.msg import GoalStatus from unilabos_msgs.msg import Resource # type: ignore -from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList # type: ignore +from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, SerialCommand # type: ignore from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.service import Service @@ -20,7 +21,8 @@ from unilabos.ros.msgs.message_converter import ( get_ros_type_by_msgname, convert_from_ros_msg, convert_to_ros_msg, - msg_converter_manager, ros_action_to_json_schema, + msg_converter_manager, + ros_action_to_json_schema, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.presets.controller_node import ControllerNode @@ -106,7 +108,7 @@ class HostNode(BaseROS2DeviceNode): self._subscribed_topics = set() # 用于跟踪已订阅的话题 # 创建物料增删改查服务(非客户端) - self._init_resource_service() + self._init_host_service() self.device_status = {} # 用来存储设备状态 self.device_status_timestamps = {} # 用来存储设备状态最后更新时间 @@ -117,7 +119,9 @@ class HostNode(BaseROS2DeviceNode): # 初始化所有本机设备节点,多一次过滤,防止重复初始化 for device_id, device_config in devices_config.items(): if device_config.get("type", "device") != "device": - self.lab_logger().debug(f"[Host Node] Skipping type {device_config['type']} {device_id} already existed, skipping.") + self.lab_logger().debug( + f"[Host Node] Skipping type {device_config['type']} {device_id} already existed, skipping." + ) continue if device_id not in self.devices_names: self.initialize_device(device_id, device_config) @@ -226,6 +230,7 @@ class HostNode(BaseROS2DeviceNode): ) self.lab_logger().debug(f"[Host Node] Created ActionClient: {action_id}") from unilabos.app.mq import mqtt_client + info_with_schema = ros_action_to_json_schema(action_type) mqtt_client.publish_actions(action_id, info_with_schema) except Exception as e: @@ -259,6 +264,7 @@ class HostNode(BaseROS2DeviceNode): self._action_clients[action_id] = ActionClient(self, action_type, action_id) self.lab_logger().debug(f"[Host Node] Created ActionClient: {action_id}") from unilabos.app.mq import mqtt_client + info_with_schema = ros_action_to_json_schema(action_type) mqtt_client.publish_actions(action_id, info_with_schema) else: @@ -473,7 +479,7 @@ class HostNode(BaseROS2DeviceNode): """Resource""" - def _init_resource_service(self): + def _init_host_service(self): self._resource_services: Dict[str, Service] = { "resource_add": self.create_service( ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=ReentrantCallbackGroup() @@ -496,8 +502,28 @@ class HostNode(BaseROS2DeviceNode): "resource_list": self.create_service( ResourceList, "/resources/list", self._resource_list_callback, callback_group=ReentrantCallbackGroup() ), + "node_info_update": self.create_service( + SerialCommand, + "/node_info_update", + self._node_info_update_callback, + callback_group=ReentrantCallbackGroup(), + ), } + def _node_info_update_callback(self, request, response): + """ + 更新节点信息回调 + """ + self.lab_logger().info(f"[Host Node] Node info update request received: {request}") + try: + info = json.loads(request.command) + self.lab_logger().info(f"[Host Node] Node info update: {info}") + response.response = "OK" + except Exception as e: + self.lab_logger().error(f"[Host Node] Error updating node info: {str(e)}") + response.response = "ERROR" + return response + def _resource_add_callback(self, request, response): """ 添加资源回调