From 4dbb6649b44cafa5faa0087afabd48eafd9b44d8 Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:48:25 +0800 Subject: [PATCH 1/6] fix: device.class possible null --- unilabos/app/web/templates/status.html | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/unilabos/app/web/templates/status.html b/unilabos/app/web/templates/status.html index f4ef011a..e1105b7b 100644 --- a/unilabos/app/web/templates/status.html +++ b/unilabos/app/web/templates/status.html @@ -333,8 +333,13 @@
-
{{ device.class|tojson(indent=4) }}
- + {% if device.class %} +
{{ device.class | tojson(indent=4) }}
+ {% else %} + +
// No data
+ {% endif %} + {% if device.is_online %}
在线
{% endif %} @@ -366,7 +371,12 @@
From 8ab108c4894afcf8ba59c05dd20a3b79e3fe3f28 Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:53:10 +0800 Subject: [PATCH 2/6] fix: HPLC additions with online service --- test/experiments/HPLC.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/experiments/HPLC.json b/test/experiments/HPLC.json index 51433f67..6d866a9a 100644 --- a/test/experiments/HPLC.json +++ b/test/experiments/HPLC.json @@ -19,8 +19,8 @@ }, { "id": "BottlesRack3", - "name": "Revvity上样盘3", - "parent": "Revvity", + "name": "上样盘3", + "parent": "HPLC", "type": "plate", "class": null, "position": { From 2d034f728a91a1d118c0feedd7c92d35e74cc834 Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:21:29 +0800 Subject: [PATCH 3/6] fix: slave mode spin not working --- unilabos/ros/main_slave_run.py | 28 +++++++--------------------- unilabos/ros/utils/driver_creator.py | 2 -- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 974359a3..60dc189e 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -1,10 +1,12 @@ import copy import json import os +import threading import traceback from typing import Optional, Dict, Any, List import rclpy +from rclpy.callback_groups import ReentrantCallbackGroup from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore from rclpy.executors import MultiThreadedExecutor @@ -17,7 +19,6 @@ from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, ) 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 @@ -63,16 +64,8 @@ def main( discovery_interval, ) - executor.add_node(host_node) - # run_event_loop_in_thread() - - try: - executor.spin() - except Exception as e: - logger.error(traceback.format_exc()) - print(f"Exception caught: {e}") - finally: - exit() + thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread") + thread.start() def slave( @@ -102,6 +95,9 @@ def slave( n = Node(f"slaveMachine_{machine_name}", parameter_overrides=[]) executor.add_node(n) + thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread") + thread.start() + if not BasicConfig.slave_no_host: sclient = n.create_client(SerialCommand, "/node_info_update") sclient.wait_for_service() @@ -125,15 +121,5 @@ def slave( logger.info(f"Slave resource added.") - run_event_loop_in_thread() - - try: - executor.spin() - except Exception as e: - print(f"Exception caught: {e}") - finally: - exit() - - if __name__ == "__main__": main() diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index b2402b5e..24c98b21 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -224,8 +224,6 @@ class PyLabRobotCreator(DeviceClassCreator[T]): if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")): from unilabos.ros.nodes.base_device_node import ROS2DeviceNode ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(lambda x: logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")) -# 2486229810384 -#2486232539792 class ProtocolNodeCreator(DeviceClassCreator[T]): """ From 7a51b2adc1bf6ca810f2bf39aba91150f474f612 Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 30 Apr 2025 15:48:33 +0800 Subject: [PATCH 4/6] fix: slave mode spin not working --- unilabos/ros/main_slave_run.py | 7 +++++-- unilabos/ros/utils/driver_creator.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 60dc189e..15265c4d 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -2,11 +2,9 @@ import copy import json import os import threading -import traceback from typing import Optional, Dict, Any, List import rclpy -from rclpy.callback_groups import ReentrantCallbackGroup from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore from rclpy.executors import MultiThreadedExecutor @@ -67,6 +65,9 @@ def main( thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread") thread.start() + while True: + input() + def slave( devices_config: Dict[str, Any] = {}, @@ -120,6 +121,8 @@ def slave( response = rclient.call_async(request).result() logger.info(f"Slave resource added.") + while True: + input() if __name__ == "__main__": main() diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 24c98b21..2ea30856 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -225,6 +225,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]): from unilabos.ros.nodes.base_device_node import ROS2DeviceNode ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(lambda x: logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")) + class ProtocolNodeCreator(DeviceClassCreator[T]): """ ProtocolNode设备类创建器 From 9d2bfec1dd867cab2c3d687236c4d9cbe46ce0ba Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 1 May 2025 14:36:15 +0800 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20=E5=A4=9AProtocolNode=20=E5=85=81?= =?UTF-8?q?=E8=AE=B8=E5=AD=90=E8=AE=BE=E5=A4=87ID=E7=9B=B8=E5=90=8C=20feat?= =?UTF-8?q?:=20=E4=B8=8A=E6=8A=A5=E5=8F=91=E7=8E=B0=E7=9A=84ActionClient?= =?UTF-8?q?=20feat:=20Host=E9=87=8D=E5=90=AF=E5=8A=A8=EF=BC=8C=E9=80=9A?= =?UTF-8?q?=E8=BF=87discover=E6=9C=BA=E5=88=B6=E8=A6=81=E6=B1=82slaveNode?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=B3=A8=E5=86=8C=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=E5=8F=8A=E6=97=B6=E4=B8=8A=E6=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/app/main.py | 3 + unilabos/app/mq.py | 7 +- unilabos/app/web/utils/host_utils.py | 10 +-- unilabos/config/config.py | 1 + unilabos/ros/main_slave_run.py | 6 +- unilabos/ros/nodes/base_device_node.py | 58 +++++++++++++--- unilabos/ros/nodes/presets/host_node.py | 88 ++++++++++++++++--------- 7 files changed, 119 insertions(+), 54 deletions(-) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 8b87736f..f75a3295 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -96,6 +96,9 @@ def main(): # 设置BasicConfig参数 BasicConfig.is_host_mode = not args_dict.get("without_host", False) BasicConfig.slave_no_host = args_dict.get("slave_no_host", False) + machine_name = os.popen("hostname").read().strip() + machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) + BasicConfig.machine_name = machine_name from unilabos.resources.graphio import ( read_node_link_json, diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index f604c72d..a6123fb2 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -168,11 +168,8 @@ class MQTTClient: if self.mqtt_disable: return address = f"labs/{MQConfig.lab_id}/actions/" - action_type_name = action_info["title"] - action_info["title"] = action_id - action_data = json.dumps({action_type_name: action_info}, ensure_ascii=False) - self.client.publish(address, action_data, qos=2) - logger.debug(f"Action data published: address: {address}, {action_data}") + self.client.publish(address, json.dumps(action_info), qos=2) + logger.debug(f"Action data published: address: {address}, {action_id}, {action_info}") mqtt_client = MQTTClient() diff --git a/unilabos/app/web/utils/host_utils.py b/unilabos/app/web/utils/host_utils.py index 6ceb068a..a9070486 100644 --- a/unilabos/app/web/utils/host_utils.py +++ b/unilabos/app/web/utils/host_utils.py @@ -30,13 +30,13 @@ def get_host_node_info() -> Dict[str, Any]: return host_info host_info["available"] = True host_info["devices"] = { - device_id: { + edge_device_id: { "namespace": namespace, - "is_online": f"{namespace}/{device_id}" in host_node._online_devices, - "key": f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}", - "machine_name": host_node.device_machine_names.get(device_id, "未知"), + "is_online": f"{namespace}/{edge_device_id}" in host_node._online_devices, + "key": f"{namespace}/{edge_device_id}" if namespace.startswith("/") else f"/{namespace}/{edge_device_id}", + "machine_name": host_node.device_machine_names.get(edge_device_id, "未知"), } - for device_id, namespace in host_node.devices_names.items() + for edge_device_id, namespace in host_node.devices_names.items() } # 获取已订阅的主题 host_info["subscribed_topics"] = sorted(list(host_node._subscribed_topics)) diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 5c1f7b9c..3f3d8dd7 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -12,6 +12,7 @@ class BasicConfig: config_path = "" is_host_mode = True # 从registry.py移动过来 slave_no_host = False # 是否跳过rclient.wait_for_service() + machine_name = "undefined" # MQTT配置 diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 15265c4d..9ac96748 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -91,9 +91,7 @@ def slave( # else: # print(f"Warning: Device {device_id} could not be initialized or is not a valid Node") - machine_name = os.popen("hostname").read().strip() - machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) - n = Node(f"slaveMachine_{machine_name}", parameter_overrides=[]) + n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[]) executor.add_node(n) thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread") @@ -105,7 +103,7 @@ def slave( request = SerialCommand.Request() request.command = json.dumps({ - "machine_name": machine_name, + "machine_name": BasicConfig.machine_name, "type": "slave", "devices_config": devices_config_copy, "registry_config": lab_registry.obtain_registry_device_info() diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 0ff03a68..7e032064 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1,3 +1,4 @@ +import json import threading import time import traceback @@ -13,15 +14,17 @@ from rclpy.action import ActionServer from rclpy.action.server import ServerGoalHandle from rclpy.client import Client from rclpy.callback_groups import ReentrantCallbackGroup +from rclpy.service import Service from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, convert_from_ros_msg, convert_from_ros_msg_with_mapping, - convert_to_ros_msg_with_mapping, + convert_to_ros_msg_with_mapping, ros_action_to_json_schema, ) -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 unilabos_msgs.msg import Resource # type: ignore from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker @@ -29,7 +32,7 @@ from unilabos.ros.x.rclpyx import get_event_loop from unilabos.ros.utils.driver_creator import ProtocolNodeCreator, PyLabRobotCreator, DeviceClassCreator from unilabos.utils.async_util import run_async_func from unilabos.utils.log import info, debug, warning, error, critical, logger -from unilabos.utils.type_check import get_type_class +from unilabos.utils.type_check import get_type_class, TypeEncoder T = TypeVar("T") @@ -44,19 +47,17 @@ class ROSLoggerAdapter: @property def identifier(self): - return f"{self.namespace}/{self.node_name}" + return f"{self.namespace}" - def __init__(self, ros_logger, node_name, namespace): + def __init__(self, ros_logger, namespace): """ 初始化日志适配器 Args: ros_logger: ROS2日志记录器 - node_name: 节点名称 namespace: 命名空间 """ self.ros_logger = ros_logger - self.node_name = node_name self.namespace = namespace self.level_2_logger_func = { "info": info, @@ -258,9 +259,9 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.lab_logger().critical("资源跟踪器未初始化,请检查") # 创建自定义日志记录器 - self._lab_logger = ROSLoggerAdapter(self.get_logger(), self.node_name, self.namespace) + self._lab_logger = ROSLoggerAdapter(self.get_logger(), self.namespace) - self._action_servers = {} + self._action_servers: Dict[str, ActionServer] = {} self._property_publishers = {} self._status_types = status_types self._action_value_mappings = action_value_mappings @@ -284,7 +285,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.create_ros_action_server(action_name, action_value_mapping) # 创建线程池执行器 - self._executor = ThreadPoolExecutor(max_workers=max(len(action_value_mappings), 1)) + self._executor = ThreadPoolExecutor(max_workers=max(len(action_value_mappings), 1), thread_name_prefix=f"ROSDevice{self.device_id}") # 创建资源管理客户端 self._resource_clients: Dict[str, Client] = { @@ -295,6 +296,18 @@ class BaseROS2DeviceNode(Node, Generic[T]): "resource_list": self.create_client(ResourceList, "/resources/list"), } + def query_host_name_cb(req, res): + self.register_device() + self.lab_logger().info("Host要求重新注册当前节点") + res.response = "" + return res + + self._service_server: Dict[str, Service] = { + "query_host_name": self.create_service( + SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group + ), + } + # 向全局在线设备注册表添加设备信息 self.register_device() rclpy.get_global_executor().add_node(self) @@ -318,6 +331,31 @@ class BaseROS2DeviceNode(Node, Generic[T]): ) # 加入全局注册表 registered_devices[self.device_id] = device_info + from unilabos.config.config import BasicConfig + if not BasicConfig.is_host_mode: + sclient = self.create_client(SerialCommand, "/node_info_update") + # 启动线程执行发送任务 + threading.Thread( + target=self.send_slave_node_info, + args=(sclient,), + daemon=True, + name=f"ROSDevice{self.device_id}_send_slave_node_info" + ).start() + + def send_slave_node_info(self, sclient): + sclient.wait_for_service() + request = SerialCommand.Request() + from unilabos.config.config import BasicConfig + request.command = json.dumps({ + "SYNC_SLAVE_NODE_INFO": { + "machine_name": BasicConfig.machine_name, + "type": "slave", + "edge_device_id": self.device_id + }}, ensure_ascii=False, cls=TypeEncoder) + + # 发送异步请求并等待结果 + future = sclient.call_async(request) + response = future.result() def lab_logger(self): """ diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 2c114412..5a739773 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -27,6 +27,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.type_check import TypeEncoder class HostNode(BaseROS2DeviceNode): @@ -98,7 +99,7 @@ class HostNode(BaseROS2DeviceNode): # 创建设备、动作客户端和目标存储 self.devices_names: Dict[str, str] = {} # 存储设备名称和命名空间的映射 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 - self.device_machine_names: Dict[str, str] = {} # 存储设备ID到机器名称的映射 + self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射 self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例 self._action_value_mappings: Dict[str, Dict] = ( {} @@ -160,6 +161,13 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().info("[Host Node] Host node initialized.") HostNode._ready_event.set() + def _send_re_register(self, sclient): + sclient.wait_for_service() + request = SerialCommand.Request() + request.command = "" + future = sclient.call_async(request) + response = future.result() + def _discover_devices(self) -> None: """ 发现网络中的设备 @@ -176,23 +184,37 @@ class HostNode(BaseROS2DeviceNode): current_devices = set() for device_id, namespace in nodes_and_names: - if not namespace.startswith("/devices"): + if not namespace.startswith("/devices/"): continue - + edge_device_id = namespace[9:] # 将设备添加到当前设备集合 - device_key = f"{namespace}/{device_id}" + device_key = f"{namespace}/{edge_device_id}" # namespace已经包含device_id了,这里复写一遍 current_devices.add(device_key) # 如果是新设备,记录并创建ActionClient - if device_id not in self.devices_names: + if edge_device_id not in self.devices_names: self.lab_logger().info(f"[Host Node] Discovered new device: {device_key}") - self.devices_names[device_id] = namespace + self.devices_names[edge_device_id] = namespace self._create_action_clients_for_device(device_id, namespace) self._online_devices.add(device_key) + sclient = self.create_client(SerialCommand, f"/srv{namespace}/query_host_name") + threading.Thread( + target=self._send_re_register, + args=(sclient,), + daemon=True, + name=f"ROSDevice{self.device_id}_query_host_name_{namespace}" + ).start() elif device_key not in self._online_devices: # 设备重新上线 self.lab_logger().info(f"[Host Node] Device reconnected: {device_key}") self._online_devices.add(device_key) + sclient = self.create_client(SerialCommand, f"/srv{namespace}/query_host_name") + threading.Thread( + target=self._send_re_register, + args=(sclient,), + daemon=True, + name=f"ROSDevice{self.device_id}_query_host_name_{namespace}" + ).start() # 检测离线设备 offline_devices = self._online_devices - current_devices @@ -234,17 +256,22 @@ class HostNode(BaseROS2DeviceNode): self._action_clients[action_id] = ActionClient( self, action_type, action_id, callback_group=self.callback_group ) - self.lab_logger().debug(f"[Host Node] Created ActionClient: {action_id}") + self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}") + action_name = action_id[len(namespace) + 1:] + edge_device_id = namespace[9:] 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) + mqtt_client.publish_actions(action_name, { + "device_id": edge_device_id, + "action_name": action_name, + "schema": info_with_schema, + }) except Exception as e: self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}") def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: """ - 根据配置初始化设备 + 根据配置初始化设备, 此函数根据提供的设备配置动态导入适当的设备类并创建其实例。 同时为设备的动作值映射设置动作客户端。 @@ -260,7 +287,7 @@ class HostNode(BaseROS2DeviceNode): if d is None: return # noinspection PyProtectedMember - self.devices_names[device_id] = d._ros_node.namespace + self.devices_names[device_id] = d._ros_node.namespace # 这里不涉及二级device_id self.device_machine_names[device_id] = "本地" self.devices_instances[device_id] = d # noinspection PyProtectedMember @@ -269,14 +296,17 @@ class HostNode(BaseROS2DeviceNode): if action_id not in self._action_clients: action_type = action_value_mapping["type"] self._action_clients[action_id] = ActionClient(self, action_type, action_id) - self.lab_logger().debug(f"[Host Node] Created ActionClient: {action_id}") + self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的 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) + mqtt_client.publish_actions(action_name, { + "device_id": device_id, + "action_name": action_name, + "schema": info_with_schema, + }) else: self.lab_logger().warning(f"[Host Node] ActionClient {action_id} already exists.") - device_key = f"{self.devices_names[device_id]}/{device_id}" + device_key = f"{self.devices_names[device_id]}/{device_id}" # 这里不涉及二级device_id # 添加到在线设备列表 self._online_devices.add(device_key) @@ -298,8 +328,8 @@ class HostNode(BaseROS2DeviceNode): # 解析设备名和属性名 parts = topic.split("/") - if len(parts) >= 4: - device_id = parts[-2] + if len(parts) >= 4: # 可能有ProtocolNode,创建更长的设备 + device_id = "/".join(parts[2:-1]) property_name = parts[-1] # 初始化设备状态字典 @@ -526,21 +556,19 @@ class HostNode(BaseROS2DeviceNode): from unilabos.app.mq import mqtt_client info = json.loads(request.command) - machine_name = info["machine_name"] - devices_config = info["devices_config"] - registry_config = info["registry_config"] - - # 更新设备机器名称映射 - for device_id in devices_config.keys(): - self.device_machine_names[device_id] = machine_name - self.lab_logger().debug(f"[Host Node] Updated machine name for device {device_id}: {machine_name}") - - for device_config in registry_config: - mqtt_client.publish_registry(device_config["id"], device_config) - self.lab_logger().info(f"[Host Node] Node info update: {info}") + if "SYNC_SLAVE_NODE_INFO" in info: + info = info["SYNC_SLAVE_NODE_INFO"] + machine_name = info["machine_name"] + edge_device_id = info["edge_device_id"] + self.device_machine_names[edge_device_id] = machine_name + else: + registry_config = info["registry_config"] + for device_config in registry_config: + mqtt_client.publish_registry(device_config["id"], device_config) + self.lab_logger().debug(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)}") + self.lab_logger().error(f"[Host Node] Error updating node info: {e.args}") response.response = "ERROR" return response From 752442cb37d9c6607abb523b0ce9feacfc767e20 Mon Sep 17 00:00:00 2001 From: wznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 1 May 2025 14:52:50 +0800 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81env=E8=AE=BE?= =?UTF-8?q?=E7=BD=AEconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/app/main.py | 6 ++-- unilabos/config/config.py | 61 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index f75a3295..89462822 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -12,7 +12,7 @@ ilabos_dir = os.path.dirname(os.path.dirname(current_dir)) if ilabos_dir not in sys.path: sys.path.append(ilabos_dir) -from unilabos.config.config import load_config, BasicConfig +from unilabos.config.config import load_config, BasicConfig, _update_config_from_env from unilabos.utils.banner_print import print_status, print_unilab_banner @@ -80,8 +80,10 @@ def main(): args = parse_args() args_dict = vars(args) - # 加载配置文件 - 这里保持最先加载配置的逻辑 + # 加载配置文件,优先加载config,然后从env读取 config_path = args_dict.get("config") + if config_path is None: + config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None) if config_path: if not os.path.exists(config_path): print_status(f"配置文件 {config_path} 不存在", "error") diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 3f3d8dd7..12ed4f6e 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -101,26 +101,79 @@ def _update_config_from_module(module): logger.warning("Skipping key file loading, key_file is empty") +def _update_config_from_env(): + prefix = "UNILABOS." + for env_key, env_value in os.environ.items(): + if not env_key.startswith(prefix): + continue + try: + key_path = env_key[len(prefix):] # Remove UNILAB_ prefix + class_field = key_path.upper().split(".", 1) + if len(class_field) != 2: + logger.warning(f"[ENV] 环境变量格式不正确:{env_key}") + continue + + class_key, field_key = class_field + # 遍历 globals 找匹配类(不区分大小写) + matched_cls = None + for name, obj in globals().items(): + if name.upper() == class_key and isinstance(obj, type): + matched_cls = obj + break + + if matched_cls is None: + logger.warning(f"[ENV] 未找到类:{class_key}") + continue + + # 查找类属性(不区分大小写) + matched_field = None + for attr in dir(matched_cls): + if attr.upper() == field_key: + matched_field = attr + break + + if matched_field is None: + logger.warning(f"[ENV] 类 {matched_cls.__name__} 中未找到字段:{field_key}") + continue + + current_value = getattr(matched_cls, matched_field) + attr_type = type(current_value) + if attr_type == bool: + value = env_value.lower() in ("true", "1", "yes") + elif attr_type == int: + value = int(env_value) + elif attr_type == float: + value = float(env_value) + else: + value = env_value + setattr(matched_cls, matched_field, value) + logger.info(f"[ENV] 设置 {matched_cls.__name__}.{matched_field} = {value}") + except Exception as e: + logger.warning(f"[ENV] 解析环境变量 {env_key} 失败: {e}") + + + def load_config(config_path=None): # 如果提供了配置文件路径,从该文件导入配置 if config_path: + _update_config_from_env() # 允许config_path被env设定后读取 BasicConfig.config_path = os.path.abspath(os.path.dirname(config_path)) if not os.path.exists(config_path): - logger.error(f"配置文件 {config_path} 不存在") + logger.error(f"[ENV] 配置文件 {config_path} 不存在") exit(1) try: module_name = "lab_" + os.path.basename(config_path).replace(".py", "") spec = importlib.util.spec_from_file_location(module_name, config_path) if spec is None: - logger.error(f"配置文件 {config_path} 错误") + logger.error(f"[ENV] 配置文件 {config_path} 错误") return module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # type: ignore _update_config_from_module(module) - logger.info(f"配置文件 {config_path} 加载成功") + logger.info(f"[ENV] 配置文件 {config_path} 加载成功") except Exception as e: - logger.error(f"加载配置文件 {config_path} 失败: {e}") + logger.error(f"[ENV] 加载配置文件 {config_path} 失败") traceback.print_exc() exit(1) else: