diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 7fab0588..dcf9dcb4 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -45,7 +45,7 @@ def convert_argv_dashes_to_underscores(args: argparse.ArgumentParser): 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) :] + new_arg = arg[:2] + arg[2:len(option_string)].replace("-", "_") + arg[len(option_string):] sys.argv[i] = new_arg break @@ -335,12 +335,11 @@ def main(): resource_edge_info.pop(edge_info - ind - 1) continue - # 如果从远端获取了物料信息,则与本地物料进行同步 - if request_startup_json and "nodes" in request_startup_json: - print_status("开始同步远端物料到本地...", "info") - remote_tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"]) - resource_tree_set.merge_remote_resources(remote_tree_set) - print_status("远端物料同步完成", "info") + tree_set = ResourceTreeSet.from_raw_list(request_startup_json["nodes"]) + for root_node in tree_set.root_nodes: 希望和本地的resources_config进行同步,根节点device + id能对上的且是resource的,则自动添加进来,根节点上不是device的,也包含进来 + device_id = root_node. + # tree_set.all_nodes # 使用 ResourceTreeSet 代替 list args_dict["resources_config"] = resource_tree_set diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index f08e3d11..b2c3ac2d 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -155,11 +155,12 @@ class BioyondWorkstation(WorkstationBase): "resources": [self.deck] }) - def transfer_resource_to_another(self, resource: ResourceSlot, mount_device_id: DeviceSlot, mount_resource: ResourceSlot): + def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot): ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{ - "plr_resources": [resource], + "plr_resources": resource, "target_device_id": mount_device_id, - "target_resource_uuid": getattr(mount_resource, "unilabos_uuid", None), + "target_resources": mount_resource, + "sites": sites, }) def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None: diff --git a/unilabos/registry/devices/dispensing_station_bioyond.yaml b/unilabos/registry/devices/dispensing_station_bioyond.yaml index a8bf6699..11c234ef 100644 --- a/unilabos/registry/devices/dispensing_station_bioyond.yaml +++ b/unilabos/registry/devices/dispensing_station_bioyond.yaml @@ -889,6 +889,7 @@ dispensing_station.bioyond: mount_device_id: null mount_resource: null resource: null + sites: null handles: {} placeholder_keys: mount_device_id: unilabos_devices @@ -904,155 +905,164 @@ dispensing_station.bioyond: mount_device_id: type: object mount_resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: mount_resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: mount_resource + type: object + type: array resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: resource + type: object + type: array + sites: + items: + type: string + type: array required: - resource - - mount_device_id - mount_resource + - sites + - mount_device_id type: object result: {} required: diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 97cbe51b..712108bd 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -883,6 +883,7 @@ reaction_station.bioyond: mount_device_id: null mount_resource: null resource: null + sites: null handles: {} placeholder_keys: mount_device_id: unilabos_devices @@ -898,155 +899,164 @@ reaction_station.bioyond: mount_device_id: type: object mount_resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: mount_resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: mount_resource + type: object + type: array resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: resource + type: object + type: array + sites: + items: + type: string + type: array required: - resource - - mount_device_id - mount_resource + - sites + - mount_device_id type: object result: {} required: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 947b0560..94250a50 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -7134,6 +7134,7 @@ workstation.bioyond: mount_device_id: null mount_resource: null resource: null + sites: null handles: {} placeholder_keys: mount_device_id: unilabos_devices @@ -7149,155 +7150,164 @@ workstation.bioyond: mount_device_id: type: object mount_resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: mount_resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: mount_resource + type: object + type: array resource: - properties: - category: - type: string - children: - items: + items: + properties: + category: type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: resource - type: object + children: + items: + type: string + type: array + config: + type: string + data: + type: string + id: + type: string + name: + type: string + parent: + type: string + pose: + properties: + orientation: + properties: + w: + type: number + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + - w + title: orientation + type: object + position: + properties: + x: + type: number + y: + type: number + z: + type: number + required: + - x + - y + - z + title: position + type: object + required: + - position + - orientation + title: pose + type: object + sample_id: + type: string + type: + type: string + required: + - id + - name + - sample_id + - children + - parent + - type + - category + - pose + - config + - data + title: resource + type: object + type: array + sites: + items: + type: string + type: array required: - resource - - mount_device_id - mount_resource + - sites + - mount_device_id type: object result: {} required: diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index fa18459b..b4e1b16f 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -9,6 +9,8 @@ from typing import Any, Dict, List, Union, Tuple import msgcenterpy import yaml +from rosidl_parser.definition import UnboundedSequence +from unilabos_msgs.action import LiquidHandlerTransfer from unilabos_msgs.msg import Resource from unilabos.config.config import BasicConfig @@ -431,6 +433,11 @@ class Registry: param_required = arg_info.get("required", True) if param_type == "unilabos.registry.placeholder_type:ResourceSlot": schema["properties"][param_name] = ros_message_to_json_schema(Resource, param_name) + elif param_type == ("list", "unilabos.registry.placeholder_type:ResourceSlot"): + schema["properties"][param_name] = { + "items": ros_message_to_json_schema(Resource, param_name), + "type": "array" + } else: schema["properties"][param_name] = self._generate_schema_from_info(param_name, param_type, param_default) if param_required: @@ -543,9 +550,9 @@ class Registry: "goal_default": {i["name"]: i["default"] for i in v["args"]}, "handles": [], "placeholder_keys": { - i["name"]: "unilabos_resources" if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" else "unilabos_devices" + i["name"]: "unilabos_resources" if i["type"] == "unilabos.registry.placeholder_type:ResourceSlot" or i["type"] == ("list", "unilabos.registry.placeholder_type:ResourceSlot") else "unilabos_devices" for i in v["args"] - if i.get("type", "") in ["unilabos.registry.placeholder_type:ResourceSlot", "unilabos.registry.placeholder_type:DeviceSlot"] + if i.get("type", "") in ["unilabos.registry.placeholder_type:ResourceSlot", "unilabos.registry.placeholder_type:DeviceSlot", ("list", "unilabos.registry.placeholder_type:ResourceSlot"), ("list", "unilabos.registry.placeholder_type:DeviceSlot")] } } # 不生成已配置action的动作 diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 797f6e17..5f2feea6 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -9,7 +9,10 @@ from unilabos_msgs.msg import Resource from unilabos.resources.container import RegularContainer from unilabos.ros.msgs.message_converter import convert_to_ros_msg -from unilabos.ros.nodes.resource_tracker import ResourceTreeSet +from unilabos.ros.nodes.resource_tracker import ( + ResourceDictInstance, + ResourceTreeSet, +) from unilabos.utils.banner_print import print_status try: @@ -51,20 +54,51 @@ def canonicalize_nodes_data( if child in id2idx: nodes[id2idx[child]]["parent"] = parent - # 第三步:打印节点信息(用于调试) + # 第三步:使用 ResourceInstanceDictFlatten 标准化每个节点 + standardized_instances = [] + known_nodes: Dict[str, ResourceDictInstance] = {} # {node_id: ResourceDictInstance} + uuid_to_instance: Dict[str, ResourceDictInstance] = {} # {uuid: ResourceDictInstance} + for node in nodes: try: print_status(f"DeviceId: {node['id']}, Class: {node['class']}", "info") + # 使用标准化方法 + resource_instance = ResourceDictInstance.get_resource_instance_from_dict(node) + known_nodes[node["id"]] = resource_instance + uuid_to_instance[resource_instance.res_content.uuid] = resource_instance + standardized_instances.append(resource_instance) except Exception as e: - print_status(f"Failed to read node {node.get('id', 'unknown')}: {e}", "error") + print_status(f"Failed to standardize node {node.get('id', 'unknown')}:\n{traceback.format_exc()}", "error") + continue - # 第四步:使用 from_raw_list 创建 ResourceTreeSet(自动处理标准化、parent-children关系) - try: - resource_tree_set = ResourceTreeSet.from_raw_list(nodes) - except Exception as e: - print_status(f"Failed to create ResourceTreeSet:\n{traceback.format_exc()}", "error") - raise + # 第四步:建立 parent 和 children 关系 + for node in nodes: + node_id = node["id"] + if node_id not in known_nodes: + continue + current_instance = known_nodes[node_id] + + # 优先使用 parent_uuid 进行匹配,如果不存在则使用 parent + parent_uuid = node.get("parent_uuid") + parent_id = node.get("parent") + parent_instance = None + + # 优先用 parent_uuid 匹配 + if parent_uuid and parent_uuid in uuid_to_instance: + parent_instance = uuid_to_instance[parent_uuid] + # 否则用 parent_id 匹配 + elif parent_id and parent_id in known_nodes: + parent_instance = known_nodes[parent_id] + + # 设置 parent 引用 + if parent_instance: + current_instance.res_content.parent = parent_instance.res_content + # 将当前节点添加到父节点的 children 列表 + parent_instance.children.append(current_instance) + + # 第五步:创建 ResourceTreeSet + resource_tree_set = ResourceTreeSet.from_nested_list(standardized_instances) return resource_tree_set diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index c3b96065..36fe0ee6 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1,4 +1,5 @@ import copy +import inspect import io import json import threading @@ -332,7 +333,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 创建资源管理客户端 self._resource_clients: Dict[str, Client] = { "resource_add": self.create_client(ResourceAdd, "/resources/add"), - "resource_get": self.create_client(ResourceGet, "/resources/get"), + "resource_get": self.create_client(SerialCommand, "/resources/get"), "resource_delete": self.create_client(ResourceDelete, "/resources/delete"), "resource_update": self.create_client(ResourceUpdate, "/resources/update"), "resource_list": self.create_client(ResourceList, "/resources/list"), @@ -578,6 +579,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): for i in data: action = i.get("action") # remove, add, update resources_uuid: List[str] = i.get("data") # 资源数据 + additional_add_params = i.get("additional_add_params", {}) # 额外参数 self.lab_logger().info( f"[Resource Tree Update] Processing {action} operation, " f"resources count: {len(resources_uuid)}" @@ -609,7 +611,13 @@ class BaseROS2DeviceNode(Node, Generic[T]): f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在") else: try: - parent_resource.assign_child_resource(plr_resource, location=None) + # 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步 + additional_params = {} + site = additional_add_params.get("site", None) + spec = inspect.signature(parent_resource.assign_child_resource) + if "spot" in spec.parameters: + additional_params["spot"] = site + parent_resource.assign_child_resource(plr_resource, location=None, **additional_params) except Exception as e: self.lab_logger().warning( f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}") @@ -666,14 +674,20 @@ class BaseROS2DeviceNode(Node, Generic[T]): return res - async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id, target_resource_uuid: str): + async def transfer_resource_to_another(self, plr_resources: List["ResourcePLR"], target_device_id: str, target_resources: List["ResourcePLR"], sites: List[str]): # 准备工作 uids = [] + target_uids = [] for plr_resource in plr_resources: uid = getattr(plr_resource, "unilabos_uuid", None) if uid is None: - raise ValueError(f"物料{plr_resource}没有unilabos_uuid属性,无法转运") + raise ValueError(f"来源物料{plr_resource}没有unilabos_uuid属性,无法转运") uids.append(uid) + for target_resource in target_resources: + uid = getattr(target_resource, "unilabos_uuid", None) + if uid is None: + raise ValueError(f"目标物料{target_resource}没有unilabos_uuid属性,无法转运") + target_uids.append(uid) srv_address = f"/srv{target_device_id}/s2c_resource_tree" sclient = self.create_client(SerialCommand, srv_address) # 等待服务可用(设置超时) @@ -688,31 +702,33 @@ class BaseROS2DeviceNode(Node, Generic[T]): }], ensure_ascii=False)), SerialCommand_Response()) # 通知云端转运资源 - tree_set = ResourceTreeSet.from_plr_resources(plr_resources) - for root_node in tree_set.root_nodes: - root_node.res_content.parent = None - root_node.res_content.parent_uuid = target_resource_uuid - r = SerialCommand.Request() - r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致 - response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore - self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}") + for plr_resource, target_uid, site in zip(plr_resources, target_uids, sites): + tree_set = ResourceTreeSet.from_plr_resources([plr_resource]) + for root_node in tree_set.root_nodes: + root_node.res_content.parent = None + root_node.res_content.parent_uuid = target_uid + r = SerialCommand.Request() + r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) # 和Update Resource一致 + response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore + self.lab_logger().info(f"资源云端转运到{target_device_id}结果: {response.response}") - # 创建请求 - request = SerialCommand.Request() - request.command = json.dumps([{ - "action": "add", - "data": tree_set.all_nodes_uuid # 只添加父节点,子节点会自动添加 - }], ensure_ascii=False) + # 创建请求 + request = SerialCommand.Request() + request.command = json.dumps([{ + "action": "add", + "data": tree_set.all_nodes_uuid, # 只添加父节点,子节点会自动添加 + "additional_add_params": {"site": site} + }], ensure_ascii=False) - future = sclient.call_async(request) - timeout = 30.0 - start_time = time.time() - while not future.done(): - if time.time() - start_time > timeout: - self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}") - return False - time.sleep(0.05) - self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}") + future = sclient.call_async(request) + timeout = 30.0 + start_time = time.time() + while not future.done(): + if time.time() - start_time > timeout: + self.lab_logger().error(f"[{self.device_id} Node-Resource] Timeout waiting for response from {target_device_id}") + return False + time.sleep(0.05) + self.lab_logger().info(f"资源本地增加到{target_device_id}结果: {response.response}") return None def register_device(self): @@ -872,7 +888,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): for k, v in goal.get_fields_and_field_types().items(): if v in ["unilabos_msgs/Resource", "sequence"]: self.lab_logger().info(f"{action_name} 查询资源状态: Key: {k} Type: {v}") - current_resources: Union[List[Resource], List[List[Resource]]] = [] + current_resources: List[List[Dict[str, Any]]] = [] # TODO: resource后面需要分组 only_one_resource = False try: @@ -881,8 +897,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): r = ResourceGet.Request() r.id = i["id"] # splash optional r.with_children = True - response = await self._resource_clients["resource_get"].call_async(r) - current_resources.append(response.resources) + response: SerialCommand_Response = await self._resource_clients["resource_get"].call_async(r) + current_resources.append(json.loads(response.response)) else: only_one_resource = True r = ResourceGet.Request() @@ -893,23 +909,21 @@ class BaseROS2DeviceNode(Node, Generic[T]): ) r.with_children = True response = await self._resource_clients["resource_get"].call_async(r) - current_resources.extend(response.resources) + current_resources.append(json.loads(response.response)) except Exception: logger.error(f"资源查询失败,默认使用本地资源") # 删除对response.resources的检查,因为它总是存在 type_hint = action_paramtypes[k] final_type = get_type_class(type_hint) if only_one_resource: - resources_list: List[Dict[str, Any]] = [convert_from_ros_msg(rs) for rs in current_resources] # type: ignore - self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") - final_resource = convert_resources_to_type(resources_list, final_type) + tree_set = ResourceTreeSet.from_raw_list(current_resources[0]) + self.lab_logger().debug(f"资源查询结果: {len(tree_set.all_nodes)} 个资源") + final_resource: List[ResourcePLR] | ResourcePLR = tree_set.to_plr_resources()[0] # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource,并做转换 else: - resources_list: List[List[Dict[str, Any]]] = [[convert_from_ros_msg(rs) for rs in sub_res_list] for sub_res_list in current_resources] # type: ignore - final_resource = [ - convert_resources_to_type(sub_res_list, final_type)[0] - for sub_res_list in resources_list - ] + final_resource: List[ResourcePLR] | ResourcePLR = [] + for entry in current_resources: + final_resource.append(ResourceTreeSet.from_raw_list(entry)[0]) # type: ignore try: action_kwargs[k] = self.resource_tracker.figure_resource(final_resource, try_mode=False) except Exception as e: diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index cb801222..07e35ee6 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -29,9 +29,11 @@ from unilabos.utils.type_check import serialize_result_info, get_result_info_str if TYPE_CHECKING: from unilabos.devices.workstation.workstation_base import WorkstationBase + class ROS2WorkstationNodeTempError(Exception): pass + class ROS2WorkstationNode(BaseROS2DeviceNode): """ ROS2WorkstationNode代表管理ROS2环境中设备通信和动作的协议节点。 @@ -63,10 +65,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): driver_instance=driver_instance, device_id=device_id, status_types=status_types, - action_value_mappings={ - **action_value_mappings, - **self.protocol_action_mappings - }, + action_value_mappings={**action_value_mappings, **self.protocol_action_mappings}, hardware_interface=hardware_interface, print_publish=print_publish, resource_tracker=resource_tracker, @@ -89,7 +88,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): d = self.initialize_device(device_id, device_config) except Exception as ex: self.lab_logger().error( - f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}") + f"[Protocol Node] Failed to initialize device {device_id}: {ex}\n{traceback.format_exc()}" + ) d = None if d is None: continue @@ -109,10 +109,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): if d: hardware_interface = d.ros_node_instance._hardware_interface if ( - hasattr(d.driver_instance, hardware_interface["name"]) - and hasattr(d.driver_instance, hardware_interface["write"]) - and ( - hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"])) + hasattr(d.driver_instance, hardware_interface["name"]) + and hasattr(d.driver_instance, hardware_interface["write"]) + and (hardware_interface["read"] is None or hasattr(d.driver_instance, hardware_interface["read"])) ): name = getattr(d.driver_instance, hardware_interface["name"]) @@ -160,7 +159,8 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): node.resource_tracker = self.resource_tracker # 站内应当共享资源跟踪器 for action_name, action_mapping in node._action_value_mappings.items(): if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith( - "UniLabJsonCommand"): + "UniLabJsonCommand" + ): continue action_id = f"/devices/{device_id_abs}/{action_name}" if action_id not in self._action_clients: @@ -245,8 +245,10 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): logs.append(step) elif isinstance(step, list): logs.append(step) - self.lab_logger().info(f"Goal received: {protocol_kwargs}, running steps: " - f"{json.dumps(logs, indent=4, ensure_ascii=False)}") + self.lab_logger().info( + f"Goal received: {protocol_kwargs}, running steps: " + f"{json.dumps(logs, indent=4, ensure_ascii=False)}" + ) time_start = time.time() time_overall = 100 @@ -268,7 +270,9 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): if not ret_info.get("suc", False): raise RuntimeError(f"Step {i + 1} failed.") except ROS2WorkstationNodeTempError as ex: - step_results.append({"step": i + 1, "action": action["action_name"], "result": ex.args[0]}) + step_results.append( + {"step": i + 1, "action": action["action_name"], "result": ex.args[0]} + ) elif isinstance(action, list): # 如果是并行动作,同时执行 actions = action @@ -307,8 +311,12 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): except Exception as e: # 捕获并记录错误信息 str_step_results = [ - {k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v for k, v in - i.items()} for i in step_results] + { + k: dict(message_to_ordereddict(v)) if k == "result" and hasattr(v, "SLOT_TYPES") else v + for k, v in i.items() + } + for i in step_results + ] execution_error = f"{traceback.format_exc()}\n\nStep Result: {pformat(str_step_results)}" execution_success = False self.lab_logger().error(f"协议 {protocol_name} 执行出错: {str(e)} \n{traceback.format_exc()}") @@ -381,7 +389,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): """还没有改过的部分""" def _setup_hardware_proxy( - self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method + self, device: ROS2DeviceNode, communication_device: ROS2DeviceNode, read_method, write_method ): """为设备设置硬件接口代理""" # extra_info = [getattr(device.driver_instance, info) for info in communication_device.ros_node_instance._hardware_interface.get("extra_info", [])] @@ -405,17 +413,3 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): if write_method: # bound_write = MethodType(_write, device.driver_instance) setattr(device.driver_instance, write_method, _write) - - async def _update_resources(self, goal, protocol_kwargs): - """更新资源状态""" - for k, v in goal.get_fields_and_field_types().items(): - if v in ["unilabos_msgs/Resource", "sequence"]: - if protocol_kwargs[k] is not None: - try: - r = ResourceUpdate.Request() - r.resources = [ - convert_to_ros_msg(Resource, rs) for rs in nested_dict_to_list(protocol_kwargs[k]) - ] - await self._resource_clients["resource_update"].call_async(r) - except Exception as e: - self.lab_logger().error(f"更新资源失败: {e}") \ No newline at end of file