diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 33136de3..316ff59b 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -208,7 +208,6 @@ def main(): os.path.join(os.path.dirname(os.path.dirname(__file__)), "config", "example_config.py"), config_path ) print_status(f"已创建 local_config.py 路径: {config_path}", "info") - os._exit(1) else: os._exit(1) # 加载配置文件 diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index 3659bc06..1a859671 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -384,7 +384,7 @@ class MessageProcessor: """停止消息处理线程""" self.is_running = False if self.thread and self.thread.is_alive(): - self.thread.join(timeout=5) + self.thread.join(timeout=2) logger.info("[MessageProcessor] Stopped") def _run(self): @@ -832,7 +832,7 @@ class QueueProcessor: """停止队列处理线程""" self.is_running = False if self.thread and self.thread.is_alive(): - self.thread.join(timeout=5) + self.thread.join(timeout=2) logger.info("[QueueProcessor] Stopped") def _run(self): diff --git a/unilabos/registry/device_comms/communication_devices.yaml b/unilabos/registry/device_comms/communication_devices.yaml index 6b21394d..a35e0556 100644 --- a/unilabos/registry/device_comms/communication_devices.yaml +++ b/unilabos/registry/device_comms/communication_devices.yaml @@ -94,7 +94,7 @@ serial: port: type: string resource_tracker: - type: string + type: object required: - device_id - port diff --git a/unilabos/registry/devices/camera.yaml b/unilabos/registry/devices/camera.yaml index 8d7b09fb..3e50be0e 100644 --- a/unilabos/registry/devices/camera.yaml +++ b/unilabos/registry/devices/camera.yaml @@ -63,7 +63,7 @@ camera.USB: default: 0.1 type: number resource_tracker: - type: string + type: object required: [] type: object data: diff --git a/unilabos/registry/devices/characterization_chromatic.yaml b/unilabos/registry/devices/characterization_chromatic.yaml index 527c6828..e22deb1b 100644 --- a/unilabos/registry/devices/characterization_chromatic.yaml +++ b/unilabos/registry/devices/characterization_chromatic.yaml @@ -170,7 +170,7 @@ hplc.agilent: module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver status_types: could_run: bool - data_file: list + data_file: String device_status: str driver_init_ok: bool finish_status: str @@ -195,6 +195,8 @@ hplc.agilent: could_run: type: boolean data_file: + items: + type: string type: array device_status: type: string diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 2392f969..abf10658 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -4508,14 +4508,22 @@ liquid_handler.biomek: bind_parent_id: type: string liquid_input_slot: + items: + type: integer type: array liquid_type: + items: + type: string type: array liquid_volume: + items: + type: integer type: array resource_tracker: - type: string + type: object resources: + items: + type: object type: array slot_on_deck: type: integer @@ -4559,10 +4567,16 @@ liquid_handler.biomek: id: type: string liquid_input_wells: + items: + type: string type: array liquid_type: + items: + type: string type: array liquid_volume: + items: + type: integer type: array parent: type: string @@ -6039,6 +6053,8 @@ liquid_handler.prcxi: properties: none_keys: default: [] + items: + type: string type: array protocol_author: default: '' @@ -6139,7 +6155,7 @@ liquid_handler.prcxi: default: 0 type: number well: - type: string + type: object required: - well type: object @@ -8358,7 +8374,7 @@ liquid_handler.prcxi: default: false type: string deck: - type: string + type: object host: type: string matrix_id: diff --git a/unilabos/registry/devices/pump_and_valve.yaml b/unilabos/registry/devices/pump_and_valve.yaml index 352e841e..5a333a56 100644 --- a/unilabos/registry/devices/pump_and_valve.yaml +++ b/unilabos/registry/devices/pump_and_valve.yaml @@ -832,7 +832,7 @@ syringe_pump_with_valve.runze.SY03B-T06: default: 25.0 type: number mode: - type: string + type: object port: type: string required: @@ -1352,7 +1352,7 @@ syringe_pump_with_valve.runze.SY03B-T08: default: 25.0 type: number mode: - type: string + type: object port: type: string required: diff --git a/unilabos/registry/devices/robot_arm.yaml b/unilabos/registry/devices/robot_arm.yaml index cf72c766..b4a48b00 100644 --- a/unilabos/registry/devices/robot_arm.yaml +++ b/unilabos/registry/devices/robot_arm.yaml @@ -133,7 +133,7 @@ robotic_arm.SCARA_with_slider.virtual: goal: properties: ros_node: - type: string + type: object required: - ros_node type: object @@ -753,7 +753,7 @@ robotic_arm.elite: module: unilabos.devices.arm.elite_robot:EliteRobot status_types: actual_joint_positions: String - arm_pose: list + arm_pose: String type: python config_info: [] description: Elite robot arm @@ -775,6 +775,8 @@ robotic_arm.elite: actual_joint_positions: type: string arm_pose: + items: + type: number type: array required: - arm_pose diff --git a/unilabos/registry/devices/robot_linear_motion.yaml b/unilabos/registry/devices/robot_linear_motion.yaml index 1aeb8612..c789259c 100644 --- a/unilabos/registry/devices/robot_linear_motion.yaml +++ b/unilabos/registry/devices/robot_linear_motion.yaml @@ -37,7 +37,7 @@ linear_motion.grbl: goal: properties: position: - type: string + type: object required: - position type: object @@ -450,6 +450,8 @@ linear_motion.grbl: - 0 - -80 - 0 + items: + type: integer type: array port: type: string @@ -459,7 +461,7 @@ linear_motion.grbl: data: properties: position: - type: string + type: object spindle_speed: type: number status: @@ -605,7 +607,7 @@ linear_motion.toyo_xyz.sim: goal: properties: ros_node: - type: string + type: object required: - ros_node type: object diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 5f20f450..bdcf830f 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -6129,7 +6129,7 @@ workstation: protocol_type: type: string resource_tracker: - type: string + type: object required: - device_id - children @@ -6171,14 +6171,22 @@ workstation.example: bind_parent_id: type: string liquid_input_slot: + items: + type: integer type: array liquid_type: + items: + type: string type: array liquid_volume: + items: + type: integer type: array resource_tracker: - type: string + type: object resources: + items: + type: object type: array slot_on_deck: type: integer @@ -6213,9 +6221,9 @@ workstation.example: goal: properties: base_plate: - type: string + type: object tip_rack: - type: string + type: object required: - tip_rack - base_plate @@ -6241,9 +6249,9 @@ workstation.example: goal: properties: from_plate: - type: string + type: object to_base_plate: - type: string + type: object required: - from_plate - to_base_plate @@ -6271,7 +6279,7 @@ workstation.example: protocol_type: type: string resource_tracker: - type: string + type: object required: - device_id - children diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 45240960..cff7614e 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -5,7 +5,7 @@ import sys import inspect import importlib from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Union, Tuple import yaml @@ -294,7 +294,7 @@ class Registry: logger.warning(f"[UniLab Registry] 设备 {device_id} 的 {field_name} 类型为空,跳过替换") return type_name convert_manager = { # 将python基本对象转为ros2基本对象 - "str": "String", + "builtins:str": "String", "bool": "Bool", "int": "Int64", "float": "Float64", @@ -310,37 +310,73 @@ class Registry: logger.error(f"[UniLab Registry] 无法找到类型 '{type_name}' 用于设备 {device_id} 的 {field_name}") sys.exit(1) + def _get_json_schema_type(self, type_str: str) -> str: + """ + 根据类型字符串返回对应的JSON Schema类型 + + Args: + type_str: 类型字符串 + + Returns: + JSON Schema类型字符串 + """ + type_lower = type_str.lower() + type_mapping = { + ("str", "string"): "string", + ("int", "integer"): "integer", + ("float", "number"): "number", + ("bool", "boolean"): "boolean", + ("list", "array"): "array", + ("dict", "object"): "object", + } + + # 遍历映射找到匹配的类型 + for type_variants, json_type in type_mapping.items(): + if type_lower in type_variants: + return json_type + + # 特殊处理包含冒号的类型(如ROS消息类型) + if ":" in type_lower: + return "object" + + # 默认返回字符串类型 + return "string" + def _generate_schema_from_info( self, param_name: str, - param_type: str, + param_type: Union[str, Tuple[str]], param_default: Any, ) -> Dict[str, Any]: """ 根据参数信息生成JSON Schema """ prop_schema = {} - # 根据类型设置schema FIXME 不完整 - if param_type: - param_type_lower = param_type.lower() - if param_type_lower in ["str", "string"]: - prop_schema["type"] = "string" - elif param_type_lower in ["int", "integer"]: - prop_schema["type"] = "integer" - elif param_type_lower in ["float", "number"]: - prop_schema["type"] = "number" - elif param_type_lower in ["bool", "boolean"]: - prop_schema["type"] = "boolean" - elif param_type_lower in ["list", "array"]: - prop_schema["type"] = "array" - elif param_type_lower in ["dict", "object"]: - prop_schema["type"] = "object" + + # 处理嵌套类型(Tuple[str]) + if isinstance(param_type, tuple): + if len(param_type) == 2: + outer_type, inner_type = param_type + outer_json_type = self._get_json_schema_type(outer_type) + inner_json_type = self._get_json_schema_type(inner_type) + + prop_schema["type"] = outer_json_type + + # 根据外层类型设置内层类型信息 + if outer_json_type == "array": + prop_schema["items"] = {"type": inner_json_type} + elif outer_json_type == "object": + prop_schema["additionalProperties"] = {"type": inner_json_type} else: - # 默认为字符串类型 + # 不是标准的嵌套类型,默认为字符串 prop_schema["type"] = "string" else: - # 如果没有类型信息,默认为字符串 - prop_schema["type"] = "string" + # 处理非嵌套类型 + if param_type: + prop_schema["type"] = self._get_json_schema_type(param_type) + else: + # 如果没有类型信息,默认为字符串 + prop_schema["type"] = "string" # 设置默认值 if param_default is not None: @@ -456,7 +492,7 @@ class Registry: {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()} ) for status_name, status_type in device_config["class"]["status_types"].items(): - if status_type in ["Any", "None", "Unknown"]: + if isinstance(status_type, tuple) or status_type in ["Any", "None", "Unknown"]: status_type = "String" # 替换成ROS的String,便于显示 device_config["class"]["status_types"][status_name] = status_type target_type = self._replace_type_with_class(status_type, device_id, f"状态 {status_name}") diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 42af1ca4..cf116faa 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -697,7 +697,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): else: final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list] try: - action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=True) + action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False) except Exception as e: self.lab_logger().error(f"物料实例获取失败: {e}\n{traceback.format_exc()}") error_skip = True diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index c27a7a2b..18811920 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -199,22 +199,18 @@ class HostNode(BaseROS2DeviceNode): "children": [], }, ) - resource_with_parent_name = [] + resource_with_dirs_name = [] resource_ids_to_instance = {i["id"]: i for i in resources_config} - resource_name_to_with_parent_name = {} for res in resources_config: - # if res.get("parent") and res.get("type") == "device" and res.get("class"): - # parent_id = res.get("parent") - # parent_res = resource_ids_to_instance[parent_id] - # if parent_res.get("type") == "device" and parent_res.get("class"): - # resource_with_parent_name.append(copy.deepcopy(res)) - # resource_name_to_with_parent_name[resource_with_parent_name[-1]["id"]] = f"{parent_res['id']}/{res['id']}" - # resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}" - # continue - resource_with_parent_name.append(copy.deepcopy(res)) - # for edge in self.resources_edge_config: - # edge["source"] = resource_name_to_with_parent_name.get(edge.get("source"), edge.get("source")) - # edge["target"] = resource_name_to_with_parent_name.get(edge.get("target"), edge.get("target")) + temp_res = res + res_paths = [res] + while temp_res.get("parent"): + temp_res = resource_ids_to_instance[temp_res.get("parent")] + res_paths.append(temp_res) + dirs = "/" + "/".join([res["id"] for res in res_paths[::-1]]) + new_res = copy.deepcopy(res) + new_res["data"]["unilabos_dirs"] = dirs + resource_with_dirs_name.append(new_res) try: for bridge in self.bridges: if hasattr(bridge, "resource_add"): @@ -222,7 +218,12 @@ class HostNode(BaseROS2DeviceNode): client: HTTPClient = bridge resource_start_time = time.time() - resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False) + resource_add_res = client.resource_add(add_schema(resources_config), False) + # DEBUG ONLY + # for i in resource_with_dirs_name: + # http_req = self.bridges[-1].resource_get(i["data"]["unilabos_dirs"], True) + # res = self._resource_get_process(http_req) + # print(res) resource_end_time = time.time() self.lab_logger().info( f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" @@ -871,6 +872,12 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().info(f"[Host Node-Resource] Add request completed, success: {success}") return response + def _resource_get_process(self, data: Dict[str, Any]): + r = data["data"] + self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources") + resources = [convert_to_ros_msg(Resource, resource) for resource in r] + return resources + def _resource_get_callback(self, request: ResourceGet.Request, response: ResourceGet.Response): """ 获取资源回调 @@ -884,22 +891,14 @@ class HostNode(BaseROS2DeviceNode): Returns: 响应对象,包含查询到的资源 """ - self.lab_logger().info(f"[Host Node-Resource] Get request for ID: {request.id}") - - if len(self.bridges) > 0: - # 云上物料服务,根据 id 查询物料 - try: - r = self.bridges[-1].resource_get(request.id, request.with_children)["data"] - self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources") - except Exception as e: - self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}") - r = [resource for resource in self.resources_config if resource.get("id") == request.id] - self.lab_logger().warning(f"[Host Node-Resource] Retrieved from local: {len(r)} resources") - else: - # 本地物料服务,根据 id 查询物料 - r = [resource for resource in self.resources_config if resource.get("id") == request.id] - self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources") - + try: + http_req = self.bridges[-1].resource_get(request.id, request.with_children) + response.resources = self._resource_get_process(http_req) + return response + except Exception as e: + self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}") + r = [resource for resource in self.resources_config if resource.get("id") == request.id] + self.lab_logger().debug(f"[Host Node-Resource] Retrieved from local: {len(r)} resources") response.resources = [convert_to_ros_msg(Resource, resource) for resource in r] return response diff --git a/unilabos/utils/import_manager.py b/unilabos/utils/import_manager.py index 7a3066f9..4b873386 100644 --- a/unilabos/utils/import_manager.py +++ b/unilabos/utils/import_manager.py @@ -12,8 +12,7 @@ import traceback import ast import os from pathlib import Path -from typing import Dict, List, Any, Optional, Callable, Type - +from typing import Dict, List, Any, Optional, Callable, Type, Union, Tuple __all__ = [ "ImportManager", @@ -383,7 +382,7 @@ class ImportManager: signature = inspect.signature(method) return self._get_type_string(signature.return_annotation) - def _get_type_string(self, annotation) -> str: + def _get_type_string(self, annotation) -> Union[str, Tuple[str, Any]]: """将类型注解转换为Class Library中可搜索的类名""" if annotation == inspect.Parameter.empty: return "Any" # 如果没有注解,返回Any @@ -400,7 +399,7 @@ class ImportManager: return "Int64MultiArray" elif isinstance(arg0, float): return "Float64MultiArray" - return "list" + return "list", self._get_type_string(arg0) elif origin is dict: return "dict" elif origin is Optional: