From d1a16851bb2af42f4c1e52c6bd609557f16c8be9 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:16:56 +0800 Subject: [PATCH 01/33] Fix ResourceTreeSet load error --- unilabos/ros/nodes/resource_tracker.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 30567aec..0859e0a3 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -715,16 +715,9 @@ def load(cls, data: List[List[Dict[str, Any]]]) -> "ResourceTreeSet": Returns: ResourceTreeSet: 反序列化后的资源树集合 """ - # 将每个字典转换为 ResourceInstanceDict - # FIXME: 需要重新确定parent关系 nested_lists = [] for tree_data in data: - flatten_instances = [ - ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in tree_data - ] - nested_lists.append(flatten_instances) - - # 使用现有的构造函数创建 ResourceTreeSet + nested_lists.extend(ResourceTreeSet.from_raw_list(tree_data).trees) return cls(nested_lists) From ac57a37bb709d724fe4e16bdf62238cf6b3bd8cf Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 13 Oct 2025 15:20:30 +0800 Subject: [PATCH 02/33] Raise error when using unsupported type to create ResourceTreeSet --- unilabos/ros/nodes/resource_tracker.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 0859e0a3..3ad29c63 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -289,8 +289,6 @@ def __init__(self, resource_list: List[List[ResourceDictInstance]] | List[Resour elif isinstance(resource_list[0], ResourceTreeInstance): # 已经是ResourceTree列表 self.trees = cast(List[ResourceTreeInstance], resource_list) - elif isinstance(resource_list[0], list): - pass else: raise TypeError( f"不支持的类型: {type(resource_list[0])}。" From 4a88eb35a09676801e0e0b0dcd9893a1a0b5f6b0 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:20:42 +0800 Subject: [PATCH 03/33] Fix children key error --- unilabos/resources/graphio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index b9b63f34..bb317393 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -67,7 +67,7 @@ def canonicalize_nodes_data( if z is not None: node["position"]["position"]["z"] = z for k in list(node.keys()): - if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data"]: + if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children"]: v = node.pop(k) node["config"][k] = v From 921b83734c0cc05c0e83bc5280cf7f6b3f6b3a8b Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 13 Oct 2025 23:34:17 +0800 Subject: [PATCH 04/33] Fix children key error --- unilabos/resources/graphio.py | 2 +- unilabos/ros/nodes/resource_tracker.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index bb317393..c6780001 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -52,7 +52,7 @@ def canonicalize_nodes_data( if not node.get("type"): node["type"] = "device" print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning") - if not node.get("name"): + if node.get("name", None) is None: node["name"] = node.get("id") print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning") if not isinstance(node.get("position"), dict): diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 3ad29c63..4ab31f0f 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -140,7 +140,7 @@ def get_resource_instance_from_dict(cls, content: Dict[str, Any]) -> "ResourceDi def get_nested_dict(self) -> Dict[str, Any]: """获取资源实例的嵌套字典表示""" res_dict = self.res_content.model_dump(by_alias=True) - res_dict["children"] = {child.res_content.name: child.get_nested_dict() for child in self.children} + res_dict["children"] = {child.res_content.id: child.get_nested_dict() for child in self.children} res_dict["parent"] = self.res_content.parent_instance_name res_dict["position"] = self.res_content.position.position.model_dump() return res_dict From 5221d12c0669c93c2bb5c24ee09a1093c22961c7 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:05:41 +0800 Subject: [PATCH 05/33] Fix workstation resource not tracking --- unilabos/devices/workstation/workstation_base.py | 1 - unilabos/ros/nodes/resource_tracker.py | 10 ++++++++-- unilabos/ros/utils/driver_creator.py | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/unilabos/devices/workstation/workstation_base.py b/unilabos/devices/workstation/workstation_base.py index 1988249f..97db1505 100644 --- a/unilabos/devices/workstation/workstation_base.py +++ b/unilabos/devices/workstation/workstation_base.py @@ -171,7 +171,6 @@ def __init__( def post_init(self, ros_node: ROS2WorkstationNode) -> None: # 初始化物料系统 self._ros_node = ros_node - self._ros_node.update_resource([self.deck]) def _build_resource_mappings(self, deck: Deck): """递归构建资源映射""" diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 4ab31f0f..bd5b9184 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -1037,13 +1037,19 @@ def loop_find_resource( ) -> List[Tuple[Any, Any]]: res_list = [] # print(resource, target_resource_cls_type, identifier_key, compare_value) - children = getattr(resource, "children", []) + children = [] + if not isinstance(resource, dict): + children = getattr(resource, "children", []) + else: + children = resource.get("children") + if children is not None: + children = list(children.values()) if isinstance(children, dict) else children for child in children: res_list.extend( self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource) ) if issubclass(type(resource), target_resource_cls_type): - if target_resource_cls_type == dict: + if type(resource) == dict: # 对于字典类型,直接检查 identifier_key if identifier_key in resource: if resource[identifier_key] == compare_value: diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index f72edf29..9481ce31 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -336,6 +336,9 @@ def create_instance(self, data: Dict[str, Any]) -> T: try: # 创建实例,额外补充一个给protocol node的字段,后面考虑取消 data["children"] = self.children + for material_id, child in self.children.items(): + if child["type"] != "device": + self.resource_tracker.add_resource(self.children[material_id]) deck_dict = data.get("deck") if deck_dict: from pylabrobot.resources import Deck, Resource From 011c4fbce30da5df8bfa87e08243fb6fe32c53a9 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:21:16 +0800 Subject: [PATCH 06/33] Fix workstation deck & children resource dupe --- unilabos/ros/nodes/resource_tracker.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index bd5b9184..4d59ffd0 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -907,6 +907,14 @@ def add_resource(self, resource): for r in self.resources: if id(r) == id(resource): return + uid = None + if isinstance(resource, dict): + uid = resource["uuid"] + else: + uid = getattr(resource, "unilabos_uuid", None) + if uid and uid in self.uuid_to_resources: + self.remove_resource(self.uuid_to_resources[uid]) + logger.warning(f"资源 UUID {uid} 已存在,覆盖为: {resource}") self.resources.append(resource) # 递归收集uuid映射 self._collect_uuid_mapping(resource) From 5753cff039a5fb20f91812a226ea17dff7617933 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 00:21:37 +0800 Subject: [PATCH 07/33] Fix workstation deck & children resource dupe --- unilabos/ros/nodes/resource_tracker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 4d59ffd0..1ac233d2 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -907,9 +907,8 @@ def add_resource(self, resource): for r in self.resources: if id(r) == id(resource): return - uid = None if isinstance(resource, dict): - uid = resource["uuid"] + uid = resource.get("uuid", None) else: uid = getattr(resource, "unilabos_uuid", None) if uid and uid in self.uuid_to_resources: From 81351e46e594f23a1164b2cc310a7953358c5d61 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:45:08 +0800 Subject: [PATCH 08/33] Fix multiple resource error --- unilabos/ros/nodes/presets/host_node.py | 2 +- unilabos/ros/nodes/resource_tracker.py | 10 ++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 3d5bd165..c217e509 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -266,7 +266,7 @@ def __init__( old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid) if old_uuid: # 找到旧UUID,使用UUID查找 - resource_instance = device_tracker.figure_resource({"uuid": old_uuid}) + resource_instance = device_tracker.uuid_to_resources.get(old_uuid) else: # 未找到旧UUID,使用name查找 resource_instance = device_tracker.figure_resource( diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 1ac233d2..9104eb57 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -868,8 +868,9 @@ def _collect_uuid_mapping(self, resource): def process(res): current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid") if current_uuid: + old = self.uuid_to_resources.get(current_uuid) self.uuid_to_resources[current_uuid] = res - logger.debug(f"收集资源UUID映射: {current_uuid} -> {res}") + logger.debug(f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}") return 0 self._traverse_and_process(resource, process) @@ -907,13 +908,6 @@ def add_resource(self, resource): for r in self.resources: if id(r) == id(resource): return - if isinstance(resource, dict): - uid = resource.get("uuid", None) - else: - uid = getattr(resource, "unilabos_uuid", None) - if uid and uid in self.uuid_to_resources: - self.remove_resource(self.uuid_to_resources[uid]) - logger.warning(f"资源 UUID {uid} 已存在,覆盖为: {resource}") self.resources.append(resource) # 递归收集uuid映射 self._collect_uuid_mapping(resource) From 374b701329a5861fb711b8e7b30e34b846abed0a Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:53:04 +0800 Subject: [PATCH 09/33] Fix resource tree update --- unilabos/ros/nodes/presets/host_node.py | 31 +++++++++++++++---------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index c217e509..567bd275 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -932,18 +932,25 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm from unilabos.app.web.client import http_client - resource_start_time = time.time() - uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False) - success = bool(uuid_mapping) - resource_end_time = time.time() - self.lab_logger().info( - f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" - ) - if uuid_mapping: - self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点") - # 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式 - response.response = json.dumps(uuid_mapping) - self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") + uuid_to_trees = collections.defaultdict(list) + for root_node in resource_tree_set.root_nodes: + uuid_to_trees[root_node.res_content.uuid].append(root_node) + + for uuid, trees in uuid_to_trees.items(): + + new_tree_set = ResourceTreeSet(trees) + resource_start_time = time.time() + uuid_mapping = http_client.resource_tree_add(new_tree_set, uuid, False) + success = bool(uuid_mapping) + resource_end_time = time.time() + self.lab_logger().info( + f"[Host Node-Resource] 挂载 {uuid} 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" + ) + if uuid_mapping: + self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点") + # 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式 + response.response = json.dumps(uuid_mapping) + self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response): """ From 4d53adbcc2f9fd370c86e28f26059b6f62406e51 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 01:55:29 +0800 Subject: [PATCH 10/33] Fix resource tree update --- unilabos/ros/nodes/presets/host_node.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 567bd275..f0fff728 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -933,14 +933,14 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm from unilabos.app.web.client import http_client uuid_to_trees = collections.defaultdict(list) - for root_node in resource_tree_set.root_nodes: - uuid_to_trees[root_node.res_content.uuid].append(root_node) + for tree in resource_tree_set.trees: + uuid_to_trees[tree.root_node.res_content.uuid].append(tree) - for uuid, trees in uuid_to_trees.items(): + for uid, trees in uuid_to_trees.items(): new_tree_set = ResourceTreeSet(trees) resource_start_time = time.time() - uuid_mapping = http_client.resource_tree_add(new_tree_set, uuid, False) + uuid_mapping = http_client.resource_tree_add(new_tree_set, uid, False) success = bool(uuid_mapping) resource_end_time = time.time() self.lab_logger().info( From 6ba55553a7d7c47795a4e8e78e9773d31236471f Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:22:39 +0800 Subject: [PATCH 11/33] Force confirm uuid --- unilabos/ros/nodes/resource_tracker.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 9104eb57..15f4b8ca 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -1,3 +1,4 @@ +import traceback import uuid from pydantic import BaseModel, field_serializer, field_validator from pydantic import Field @@ -213,7 +214,7 @@ def validate_node(node: ResourceDictInstance): if node.res_content.uuid: known_uuids.add(node.res_content.uuid) else: - print(f"警告: 资源 {node.res_content.id} 没有uuid") + logger.warning(f"警告: 资源 {node.res_content.id} 没有uuid") # 验证并递归处理子节点 for child in node.children: @@ -318,7 +319,12 @@ def replace_plr_type(source: str): def build_uuid_mapping(res: "PLRResource", uuid_list: list): """递归构建uuid映射字典""" - uuid_list.append(getattr(res, "unilabos_uuid", "")) + uid = getattr(res, "unilabos_uuid", "") + if not uid: + uid = str(uuid.uuid4()) + res.unilabos_uuid = uid + logger.warning(f"{res}没有uuid,请设置后再传入,默认填充{uid}!\n{traceback.format_exc()}") + uuid_list.append(uid) for child in res.children: build_uuid_mapping(child, uuid_list) From 1584b84980f888bc15ce11c95096097e6d26ef62 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:29:14 +0800 Subject: [PATCH 12/33] Tip more error log --- unilabos/ros/nodes/presets/host_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index f0fff728..1b9a84b8 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -932,7 +932,7 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm from unilabos.app.web.client import http_client - uuid_to_trees = collections.defaultdict(list) + uuid_to_trees: Dict[str, List[ResourceTreeInstance]] = collections.defaultdict(list) for tree in resource_tree_set.trees: uuid_to_trees[tree.root_node.res_content.uuid].append(tree) @@ -944,7 +944,7 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm success = bool(uuid_mapping) resource_end_time = time.time() self.lab_logger().info( - f"[Host Node-Resource] 挂载 {uuid} 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" + f"[Host Node-Resource] 物料 {[root_node.res_content.id for root_node in new_tree_set.root_nodes]} 挂载 {uid} P{trees[0].root_node.res_content.parent} 更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" ) if uuid_mapping: self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点") From f36e8628a8218b88f42c43e37c5698e8e3807f0b Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Tue, 14 Oct 2025 02:46:31 +0800 Subject: [PATCH 13/33] Refactor Bioyond workstation and experiment workflow (#105) Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods. --- .../dispensing_station_bioyond.json | 11 +- .../experiments/reaction_station_bioyond.json | 11 +- .../bioyond_studio/dispensing_station.py | 11 +- .../workstation/bioyond_studio/experiment.py | 194 +++---- .../bioyond_studio/reaction_station.py | 502 ++++++++++++++++-- .../workstation/bioyond_studio/station.py | 22 +- unilabos/resources/graphio.py | 2 + 7 files changed, 599 insertions(+), 154 deletions(-) diff --git a/test/experiments/dispensing_station_bioyond.json b/test/experiments/dispensing_station_bioyond.json index b2f79c80..745e1289 100644 --- a/test/experiments/dispensing_station_bioyond.json +++ b/test/experiments/dispensing_station_bioyond.json @@ -8,7 +8,7 @@ ], "parent": null, "type": "device", - "class": "dispensing_station.bioyond", + "class": "workstation.bioyond_dispensing_station", "config": { "config": { "api_key": "DE9BDDA0", @@ -20,13 +20,6 @@ "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck" } }, - "station_config": { - "station_type": "dispensing_station", - "enable_dispensing_station": true, - "enable_reaction_station": false, - "station_name": "DispensingStation_001", - "description": "Bioyond配液工作站" - }, "protocol_type": [] }, "data": {} @@ -57,4 +50,4 @@ "data": {} } ] -} +} \ No newline at end of file diff --git a/test/experiments/reaction_station_bioyond.json b/test/experiments/reaction_station_bioyond.json index 2a18d90a..f09aeb91 100644 --- a/test/experiments/reaction_station_bioyond.json +++ b/test/experiments/reaction_station_bioyond.json @@ -24,9 +24,13 @@ "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" }, "material_type_mappings": { - "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", - "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", - "样品板": "BIOYOND_PolymerStation_6VialCarrier" + "烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"], + "试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""], + "样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"], + "分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"], + "样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"], + "90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"], + "10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"] } }, "deck": { @@ -42,7 +46,6 @@ { "id": "Bioyond_Deck", "name": "Bioyond_Deck", - "sample_id": null, "children": [ ], "parent": "reaction_station_bioyond", diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py index b1820d6c..11b011cc 100644 --- a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py @@ -6,8 +6,15 @@ class BioyondDispensingStation(BioyondWorkstation): - def __init__(self, config): - super().__init__(config) + def __init__( + self, + config, + # 桌子 + deck, + *args, + **kwargs, + ): + super().__init__(config, deck, *args, **kwargs) # self.config = config # self.api_key = config["api_key"] # self.host = config["api_host"] diff --git a/unilabos/devices/workstation/bioyond_studio/experiment.py b/unilabos/devices/workstation/bioyond_studio/experiment.py index ae3111b8..92e52b45 100644 --- a/unilabos/devices/workstation/bioyond_studio/experiment.py +++ b/unilabos/devices/workstation/bioyond_studio/experiment.py @@ -1,203 +1,205 @@ -# experiment_workflow.py """ 实验流程主程序 """ import json -from bioyond_rpc import BioyondV1RPC -from config import API_CONFIG, WORKFLOW_MAPPINGS +from reaction_station import BioyondReactionStation +from config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS def run_experiment(): """运行实验流程""" - + # 初始化Bioyond客户端 config = { **API_CONFIG, - "workflow_mappings": WORKFLOW_MAPPINGS + "workflow_mappings": WORKFLOW_MAPPINGS, + "material_type_mappings": MATERIAL_TYPE_MAPPINGS } - - Bioyond = BioyondV1RPC(config) - + + # 创建BioyondReactionStation实例,传入deck配置 + Bioyond = BioyondReactionStation( + config=config, + deck=DECK_CONFIG + ) + print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") - + # 显示可用的材料名称(前20个) - available_materials = Bioyond.get_available_materials() + available_materials = Bioyond.hardware_interface.get_available_materials() print(f"可用材料名称(前20个): {available_materials[:20]}") print(f"总共有 {len(available_materials)} 个材料可用\n") - + # 1. 反应器放入 print("1. 添加反应器放入工作流,带参数...") Bioyond.reactor_taken_in( - assign_material_name="BTDA-DD", - cutoff="10000", + assign_material_name="BTDA-DD", + cutoff="10000", temperature="-10" ) - + # 2. 液体投料-烧杯 (第一个) print("2. 添加液体投料-烧杯,带参数...") Bioyond.liquid_feeding_beaker( - volume="34768.7", + volume="34768.7", assign_material_name="ODA", - time="0", - torque_variation="1", - titrationType="1", + time="0", + torque_variation="1", + titrationType="1", temperature=-10 ) - + # 3. 液体投料-烧杯 (第二个) print("3. 添加液体投料-烧杯,带参数...") Bioyond.liquid_feeding_beaker( - volume="34080.9", + volume="34080.9", assign_material_name="MPDA", - time="5", - torque_variation="2", - titrationType="1", + time="5", + torque_variation="2", + titrationType="1", temperature=0 ) - + # 4. 液体投料-小瓶非滴定 print("4. 添加液体投料-小瓶非滴定,带参数...") Bioyond.liquid_feeding_vials_non_titration( - volumeFormula="639.5", - assign_material_name="SIDA", - titration_type="1", - time="0", - torque_variation="1", + volumeFormula="639.5", + assign_material_name="SIDA", + titration_type="1", + time="0", + torque_variation="1", temperature=-10 ) - + # 5. 液体投料溶剂 print("5. 添加液体投料溶剂,带参数...") Bioyond.liquid_feeding_solvents( assign_material_name="NMP", - volume="19000", - titration_type="1", - time="5", - torque_variation="2", + volume="19000", + titration_type="1", + time="5", + torque_variation="2", temperature=-10 ) - + # 6-8. 固体进料小瓶 (三个) print("6. 添加固体进料小瓶,带参数...") Bioyond.solid_feeding_vials( - material_id="3", - time="180", + material_id="3", + time="180", torque_variation="2", - assign_material_name="BTDA-1", + assign_material_name="BTDA1", temperature=-10.00 ) - +#二杆,样品版90 print("7. 添加固体进料小瓶,带参数...") Bioyond.solid_feeding_vials( - material_id="3", - time="180", + material_id="3", + time="180", torque_variation="2", - assign_material_name="BTDA-2", + assign_material_name="BTDA2", temperature=25.00 ) - +#二杆,样品版90 print("8. 添加固体进料小瓶,带参数...") Bioyond.solid_feeding_vials( - material_id="3", - time="480", + material_id="3", + time="480", torque_variation="2", - assign_material_name="BTDA-3", + assign_material_name="BTDA3", temperature=25.00 ) - + # 液体投料滴定(第一个) print("9. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="1000", + volume_formula="{{6-0-5}}+{{7-0-5}}+{{8-0-5}}", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) - + # 液体投料滴定(第二个) print("10. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="500", + volume_formula="500", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) # 液体投料滴定(第三个) print("11. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="500", + volume_formula="500", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) - + print("12. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="500", + volume_formula="500", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) - + print("13. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="500", + volume_formula="500", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) - + print("14. 添加液体投料滴定,带参数...") # ODPA Bioyond.liquid_feeding_titration( - volume_formula="500", + volume_formula="500", assign_material_name="BTDA-DD", - titration_type="1", - time="360", - torque_variation="2", + titration_type="1", + time="360", + torque_variation="2", temperature="25.00" ) - - print("15. 添加液体投料溶剂,带参数...") Bioyond.liquid_feeding_solvents( assign_material_name="PGME", - volume="16894.6", - titration_type="1", - time="360", - torque_variation="2", + volume="16894.6", + titration_type="1", + time="360", + torque_variation="2", temperature=25.00 ) - + # 16. 反应器取出 print("16. 添加反应器取出工作流...") Bioyond.reactor_taken_out() - + # 显示当前工作流序列 sequence = Bioyond.get_workflow_sequence() print("\n当前工作流执行顺序:") print(sequence) - + # 执行process_and_execute_workflow,合并工作流并创建任务 print("\n4. 执行process_and_execute_workflow...") - + result = Bioyond.process_and_execute_workflow( - workflow_name="test3_86", - task_name="实验3_86" + workflow_name="test3_8", + task_name="实验3_8" ) - + # 显示执行结果 print("\n5. 执行结果:") if isinstance(result, str): @@ -220,16 +222,16 @@ def run_experiment(): print(f"- 任务结果: {result.get('task')}") else: print(f"任务创建失败: {result.get('error')}") - + # 可选:启动调度器 # Bioyond.scheduler_start() - + return Bioyond def prepare_materials(bioyond): """准备实验材料(可选)""" - + # 样品板材料数据定义 material_data_yp_1 = { "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", @@ -288,7 +290,7 @@ def prepare_materials(bioyond): ], "Parameters": "{}" } - + material_data_yp_2 = { "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", "name": "样品板-2", @@ -338,7 +340,7 @@ def prepare_materials(bioyond): ], "Parameters": "{}" } - + # 烧杯材料数据定义 beaker_materials = [ { @@ -377,12 +379,12 @@ def prepare_materials(bioyond): "parameters": "{\"DeviceMaterialType\":\"NMP\"}" } ] - + # 如果需要,可以在这里调用add_material方法添加材料 # 例如: # result = bioyond.add_material(json.dumps(material_data_yp_1)) # print(f"添加材料结果: {result}") - + return { "sample_plates": [material_data_yp_1, material_data_yp_2], "beakers": beaker_materials @@ -392,7 +394,7 @@ def prepare_materials(bioyond): if __name__ == "__main__": # 运行主实验流程 bioyond_client = run_experiment() - + # 可选:准备材料数据 # materials = prepare_materials(bioyond_client) # print(f"\n准备的材料数据: {materials}") diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station.py index e35c657f..5bb8709c 100644 --- a/unilabos/devices/workstation/bioyond_studio/reaction_station.py +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station.py @@ -1,30 +1,67 @@ import json - +from typing import List, Dict, Any from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation from unilabos.devices.workstation.bioyond_studio.config import ( - API_CONFIG, WORKFLOW_MAPPINGS, WORKFLOW_STEP_IDS, MATERIAL_TYPE_MAPPINGS, - STATION_TYPES, DEFAULT_STATION_CONFIG + WORKFLOW_STEP_IDS, + WORKFLOW_TO_SECTION_MAP ) class BioyondReactionStation(BioyondWorkstation): - def __init__(self, config: dict = None): - super().__init__(config) + """Bioyond反应站类 + + 继承自BioyondWorkstation,提供反应站特定的业务方法 + """ + + def __init__(self, config: dict = None, deck=None): + """初始化反应站 + + Args: + config: 配置字典,应包含workflow_mappings等配置 + deck: Deck对象 + """ + # 如果 deck 作为独立参数传入,使用它;否则尝试从 config 中提取 + if deck is None and config: + deck = config.get('deck') + + # 调试信息:检查传入的config + print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}") + if config and 'workflow_mappings' in config: + print(f"workflow_mappings内容: {config['workflow_mappings']}") + + # 将 config 作为 bioyond_config 传递给父类 + super().__init__(bioyond_config=config, deck=deck) + + # 调试信息:检查初始化后的workflow_mappings + print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}") + print(f"workflow_mappings长度: {len(self.workflow_mappings)}") + + # ==================== 工作流方法 ==================== - # 工作流方法 def reactor_taken_out(self): """反应器取出""" - self.hardware_interface.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') reactor_taken_out_params = {"param_values": {}} - self.hardware_interface.pending_task_params.append(reactor_taken_out_params) + self.pending_task_params.append(reactor_taken_out_params) print(f"成功添加反应器取出工作流") - print(f"当前队列长度: {len(self.hardware_interface.pending_task_params)}") + print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00): - """反应器放入""" + def reactor_taken_in( + self, + assign_material_name: str, + cutoff: str = "900000", + temperature: float = -10.00 + ): + """反应器放入 + + Args: + assign_material_name: 物料名称 + cutoff: 截止参数 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') - material_id = self._get_material_id_by_name(assign_material_name) + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -45,11 +82,25 @@ def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", te print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", - assign_material_name: str = None, temperature: float = 25.00): - """固体进料小瓶""" + def solid_feeding_vials( + self, + material_id: str, + time: str = "0", + torque_variation: str = "1", + assign_material_name: str = None, + temperature: float = 25.00 + ): + """固体进料小瓶 + + Args: + material_id: 物料ID + time: 时间 + torque_variation: 扭矩变化 + assign_material_name: 物料名称 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') - material_id_m = self._get_material_id_by_name(assign_material_name) + material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -76,12 +127,27 @@ def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variatio print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str, - titration_type: str = "1", time: str = "0", - torque_variation: str = "1", temperature: float = 25.00): - """液体进料小瓶(非滴定)""" + def liquid_feeding_vials_non_titration( + self, + volumeFormula: str, + assign_material_name: str, + titration_type: str = "1", + time: str = "0", + torque_variation: str = "1", + temperature: float = 25.00 + ): + """液体进料小瓶(非滴定) + + Args: + volumeFormula: 体积公式 + assign_material_name: 物料名称 + titration_type: 滴定类型 + time: 时间 + torque_variation: 扭矩变化 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') - material_id = self._get_material_id_by_name(assign_material_name) + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -109,11 +175,27 @@ def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titration_type: str = "1", - time: str = "360", torque_variation: str = "2", temperature: float = 25.00): - """液体进料溶剂""" + def liquid_feeding_solvents( + self, + assign_material_name: str, + volume: str, + titration_type: str = "1", + time: str = "360", + torque_variation: str = "2", + temperature: float = 25.00 + ): + """液体进料溶剂 + + Args: + assign_material_name: 物料名称 + volume: 体积 + titration_type: 滴定类型 + time: 时间 + torque_variation: 扭矩变化 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') - material_id = self._get_material_id_by_name(assign_material_name) + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -141,11 +223,27 @@ def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titrat print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def liquid_feeding_titration(self, volume_formula: str, assign_material_name: str, titration_type: str = "1", - time: str = "90", torque_variation: int = 2, temperature: float = 25.00): - """液体进料(滴定)""" + def liquid_feeding_titration( + self, + volume_formula: str, + assign_material_name: str, + titration_type: str = "1", + time: str = "90", + torque_variation: int = 2, + temperature: float = 25.00 + ): + """液体进料(滴定) + + Args: + volume_formula: 体积公式 + assign_material_name: 物料名称 + titration_type: 滴定类型 + time: 时间 + torque_variation: 扭矩变化 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') - material_id = self._get_material_id_by_name(assign_material_name) + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -173,12 +271,27 @@ def liquid_feeding_titration(self, volume_formula: str, assign_material_name: st print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP", - time: str = "0", torque_variation: str = "1", titrationType: str = "1", - temperature: float = 25.00): - """液体进料烧杯""" + def liquid_feeding_beaker( + self, + volume: str = "35000", + assign_material_name: str = "BAPP", + time: str = "0", + torque_variation: str = "1", + titrationType: str = "1", + temperature: float = 25.00 + ): + """液体进料烧杯 + + Args: + volume: 体积 + assign_material_name: 物料名称 + time: 时间 + torque_variation: 扭矩变化 + titrationType: 滴定类型 + temperature: 温度 + """ self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') - material_id = self._get_material_id_by_name(assign_material_name) + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if isinstance(temperature, str): temperature = float(temperature) @@ -204,4 +317,323 @@ def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str self.pending_task_params.append(params) print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") - return json.dumps({"suc": True}) \ No newline at end of file + return json.dumps({"suc": True}) + + # ==================== 工作流管理方法 ==================== + + def get_workflow_sequence(self) -> List[str]: + """获取当前工作流执行顺序 + + Returns: + 工作流名称列表 + """ + id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()} + workflow_names = [] + for workflow_id in self.workflow_sequence: + workflow_names.append(id_to_name.get(workflow_id, workflow_id)) + return workflow_names + + def workflow_step_query(self, workflow_id: str) -> dict: + """查询工作流步骤参数 + + Args: + workflow_id: 工作流ID + + Returns: + 工作流步骤参数字典 + """ + return self.hardware_interface.workflow_step_query(workflow_id) + + def create_order(self, json_str: str) -> dict: + """创建订单 + + Args: + json_str: 订单参数的JSON字符串 + + Returns: + 创建结果 + """ + return self.hardware_interface.create_order(json_str) + + # ==================== 工作流执行核心方法 ==================== + + # 发布任务 + def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: + web_workflow_list = self.get_workflow_sequence() + workflow_name = workflow_name + + pending_params_backup = self.pending_task_params.copy() + print(f"保存pending_task_params副本,共{len(pending_params_backup)}个参数") + + # 1. 处理网页工作流列表 + print(f"处理网页工作流列表: {web_workflow_list}") + web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) + workflows_result = self.process_web_workflows(web_workflow_json) + + if not workflows_result: + error_msg = "处理网页工作流列表失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "process_web_workflows"}) + return result + + # 2. 合并工作流序列 + print(f"合并工作流序列,名称: {workflow_name}") + merge_json = json.dumps({"name": workflow_name}) + merged_workflow = self.merge_sequence_workflow(merge_json) + print(f"合并工作流序列结果: {merged_workflow}") + + if not merged_workflow: + error_msg = "合并工作流序列失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "merge_sequence_workflow"}) + return result + + # 3. 合并所有参数并创建任务 + # 新API只返回状态信息,需要适配处理 + if isinstance(merged_workflow, dict) and merged_workflow.get("code") == 1: + # 新API返回格式:{code: 1, message: "", timestamp: 0} + # 使用传入的工作流名称和生成的临时ID + final_workflow_name = workflow_name + workflow_id = f"merged_{workflow_name}_{self.hardware_interface.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}" + print(f"新API合并成功,使用工作流创建任务: {final_workflow_name} (临时ID: {workflow_id})") + else: + # 旧API返回格式:包含详细工作流信息 + final_workflow_name = merged_workflow.get("name", workflow_name) + workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") + print(f"旧API格式,使用工作流创建任务: {final_workflow_name} (ID: {workflow_id})") + + if not workflow_id: + error_msg = "无法获取工作流ID" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "get_workflow_id"}) + return result + + workflow_query_json = json.dumps({"workflow_id": workflow_id}) + workflow_params_structure = self.workflow_step_query(workflow_query_json) + + self.pending_task_params = pending_params_backup + print(f"恢复pending_task_params,共{len(self.pending_task_params)}个参数") + + param_values = self.generate_task_param_values(workflow_params_structure) + + task_params = [{ + "orderCode": f"BSO{self.hardware_interface.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}", + "orderName": f"实验-{self.hardware_interface.get_current_time_iso8601()[:10].replace('-', '')}", + "workFlowId": workflow_id, + "borderNumber": 1, + "paramValues": param_values, + "extendProperties": "" + }] + + task_json = json.dumps(task_params) + print(f"创建任务参数: {type(task_json)}") + result = self.create_order(task_json) + + if not result: + error_msg = "创建任务失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"}) + return result + + print(f"任务创建成功: {result}") + self.pending_task_params.clear() + print("已清空pending_task_params") + + return { + "success": True, + "workflow": {"name": final_workflow_name, "id": workflow_id}, + "task": result, + "method": "process_and_execute_workflow" + } + + def merge_sequence_workflow(self, json_str: str) -> dict: + """合并当前工作流序列 + + Args: + json_str: 包含name等参数的JSON字符串 + + Returns: + 合并结果 + """ + try: + data = json.loads(json_str) + name = data.get("name", "合并工作流") + step_parameters = data.get("stepParameters", {}) + variables = data.get("variables", {}) + except json.JSONDecodeError: + return {} + + if not self.workflow_sequence: + print("工作流序列为空,无法合并") + return {} + + # 将工作流ID列表转换为新API要求的格式 + workflows = [{"id": workflow_id} for workflow_id in self.workflow_sequence] + + # 构建新的API参数格式 + params = { + "name": name, + "workflows": workflows, + "stepParameters": step_parameters, + "variables": variables + } + + # 使用新的API接口 + response = self.hardware_interface.post( + url=f'{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters', + params={ + "apiKey": self.hardware_interface.api_key, + "requestTime": self.hardware_interface.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def generate_task_param_values(self, workflow_params_structure: dict) -> dict: + """生成任务参数值 + + 根据工作流参数结构和待处理的任务参数,生成最终的任务参数值 + + Args: + workflow_params_structure: 工作流参数结构 + + Returns: + 任务参数值字典 + """ + if not workflow_params_structure: + print("workflow_params_structure为空") + return {} + + data = workflow_params_structure + + # 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织 + pending_params_by_section = {} + print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组") + + # 获取工作流执行顺序,用于按顺序匹配参数 + workflow_sequence = self.get_workflow_sequence() + print(f"工作流执行顺序: {workflow_sequence}") + + workflow_index = 0 + + # 遍历所有待处理的任务参数 + for i, task_param in enumerate(self.pending_task_params): + if 'param_values' in task_param: + print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤") + + if workflow_index < len(workflow_sequence): + current_workflow = workflow_sequence[workflow_index] + section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow) + print(f" 匹配到工作流: {current_workflow} -> {section_name}") + workflow_index += 1 + else: + print(f" 警告: 参数组{i+1}超出了工作流序列范围") + continue + + if not section_name: + print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName") + continue + + if section_name not in pending_params_by_section: + pending_params_by_section[section_name] = {} + + # 处理每个步骤的参数 + for step_id, param_list in task_param['param_values'].items(): + print(f" 步骤ID: {step_id},参数数量: {len(param_list)}") + + for param_item in param_list: + key = param_item.get('Key', '') + value = param_item.get('Value', '') + m = param_item.get('m', 0) + n = param_item.get('n', 0) + print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}") + + param_key = f"{section_name}.{key}" + if param_key not in pending_params_by_section[section_name]: + pending_params_by_section[section_name][param_key] = [] + + pending_params_by_section[section_name][param_key].append({ + 'value': value, + 'm': m, + 'n': n + }) + + print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组") + + # 收集所有参数,过滤TaskDisplayable为0的项 + filtered_params = [] + + for step_id, step_info in data.items(): + if isinstance(step_info, list): + for step_item in step_info: + param_list = step_item.get("parameterList", []) + for param in param_list: + if param.get("TaskDisplayable") == 0: + continue + + param_with_step = param.copy() + param_with_step['step_id'] = step_id + param_with_step['step_name'] = step_item.get("name", "") + param_with_step['step_m'] = step_item.get("m", 0) + param_with_step['step_n'] = step_item.get("n", 0) + filtered_params.append(param_with_step) + + # 按DisplaySectionIndex排序 + filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0)) + + # 生成参数映射 + param_mapping = {} + step_params = {} + for param in filtered_params: + step_id = param['step_id'] + if step_id not in step_params: + step_params[step_id] = [] + step_params[step_id].append(param) + + # 为每个步骤生成参数 + for step_id, params in step_params.items(): + param_list = [] + for param in params: + key = param.get('Key', '') + display_section_index = param.get('DisplaySectionIndex', 0) + step_m = param.get('step_m', 0) + step_n = param.get('step_n', 0) + + section_name = param.get('DisplaySectionName', '') + param_key = f"{section_name}.{key}" + + if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]: + pending_param_list = pending_params_by_section[section_name][param_key] + if pending_param_list: + pending_param = pending_param_list[0] + value = pending_param['value'] + m = step_m + n = step_n + print(f" 匹配成功: {section_name}.{key} = {value} (m={m}, n={n})") + pending_param_list.pop(0) + else: + value = "1" + m = step_m + n = step_n + print(f" 匹配失败: {section_name}.{key},参数列表为空,使用默认值 = {value}") + else: + value = "1" + m = display_section_index + n = step_n + print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") + + param_item = { + "m": m, + "n": n, + "key": key, + "value": str(value).strip() + } + param_list.append(param_item) + + if param_list: + param_mapping[step_id] = param_list + + print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤") + return param_mapping \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index f415a363..910fdb3a 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -129,7 +129,6 @@ def __init__( self, bioyond_config: Optional[Dict[str, Any]] = None, deck: Optional[Any] = None, - station_config: Optional[Dict[str, Any]] = None, *args, **kwargs, ): @@ -152,9 +151,6 @@ def __init__( if isinstance(resource, WareHouse): self.deck.warehouses[resource.name] = resource - # 配置站点类型 - self._configure_station_type(station_config) - # 创建通信模块 self._create_communication_module(bioyond_config) self.resource_synchronizer = BioyondResourceSynchronizer(self) @@ -167,8 +163,6 @@ def __init__( self.workflow_mappings = {} self.workflow_sequence = [] self.pending_task_params = [] - self.material_cache = {} - self._load_material_cache() if "workflow_mappings" in bioyond_config: self._set_workflow_mappings(bioyond_config["workflow_mappings"]) @@ -325,10 +319,22 @@ def merge_workflow_with_parameters(self, json_str: str) -> dict: } def append_to_workflow_sequence(self, web_workflow_name: str) -> bool: - workflow_id = self._get_workflow(web_workflow_name) + # 检查是否为JSON格式的字符串 + actual_workflow_name = web_workflow_name + if web_workflow_name.startswith('{') and web_workflow_name.endswith('}'): + try: + data = json.loads(web_workflow_name) + actual_workflow_name = data.get("web_workflow_name", web_workflow_name) + print(f"解析JSON格式工作流名称: {web_workflow_name} -> {actual_workflow_name}") + except json.JSONDecodeError: + print(f"JSON解析失败,使用原始字符串: {web_workflow_name}") + + workflow_id = self._get_workflow(actual_workflow_name) if workflow_id: self.workflow_sequence.append(workflow_id) - print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}") + print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}") + return True + return False def set_workflow_sequence(self, json_str: str) -> List[str]: try: diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index c6780001..a8cd6152 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -4,6 +4,7 @@ import os.path import traceback from typing import Union, Any, Dict, List, Tuple +import uuid import networkx as nx from pylabrobot.resources import ResourceHolder from unilabos_msgs.msg import Resource @@ -629,6 +630,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st {"name": material["name"], "class": className}, resource_type=ResourcePLR ) plr_material.code = material.get("code", "") and material.get("barCode", "") or "" + plr_material.unilabos_uuid = str(uuid.uuid4()) # 处理子物料(detail) if material.get("detail") and len(material["detail"]) > 0: From e59f4692b51923cbd959694a00cac5ab1b907c22 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 17:24:41 +0800 Subject: [PATCH 14/33] Fix resource get. Fix resource parent not found. Mapping uuid for all resources. --- unilabos/app/web/client.py | 4 + unilabos/ros/initialize_device.py | 3 +- unilabos/ros/nodes/base_device_node.py | 15 +- unilabos/ros/nodes/presets/host_node.py | 197 +++++++++++----------- unilabos/ros/nodes/presets/workstation.py | 41 +++-- unilabos/ros/nodes/resource_tracker.py | 19 ++- 6 files changed, 164 insertions(+), 115 deletions(-) diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index d070e690..b8c8bea3 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -73,6 +73,8 @@ def resource_tree_add(self, resources: ResourceTreeSet, mount_uuid: str, first_a Returns: Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid} """ + with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_add.json"), "w", encoding="utf-8") as f: + f.write(json.dumps({"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, indent=4)) # 从序列化数据中提取所有节点的UUID(保存旧UUID) old_uuids = {n.res_content.uuid: n for n in resources.all_nodes} if not self.initialized or first_add: @@ -92,6 +94,8 @@ def resource_tree_add(self, resources: ResourceTreeSet, mount_uuid: str, first_a timeout=100, ) + with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f: + f.write(f"{response.status_code}" + "\n" + response.text) # 处理响应,构建UUID映射 uuid_mapping = {} if response.status_code == 200: diff --git a/unilabos/ros/initialize_device.py b/unilabos/ros/initialize_device.py index bbc86e04..a92a9f50 100644 --- a/unilabos/ros/initialize_device.py +++ b/unilabos/ros/initialize_device.py @@ -26,6 +26,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device d = None original_device_config = copy.deepcopy(device_config) device_class_config = device_config["class"] + uid = device_config["uuid"] 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}") @@ -50,7 +51,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device ) try: d = DEVICE( - device_id=device_id, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {}) + device_id=device_id, device_uuid=uid, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {}) ) except DeviceInitError as ex: return d diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index b62ad2d9..d3f224bc 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -132,6 +132,7 @@ def critical(self, msg, *args, **kwargs): def init_wrapper( self, device_id: str, + device_uuid: str, driver_class: type[T], device_config: Dict[str, Any], status_types: Dict[str, Any], @@ -150,6 +151,7 @@ def init_wrapper( if children is None: children = [] kwargs["device_id"] = device_id + kwargs["device_uuid"] = device_uuid kwargs["driver_class"] = driver_class kwargs["device_config"] = device_config kwargs["driver_params"] = driver_params @@ -266,6 +268,7 @@ def __init__( self, driver_instance: T, device_id: str, + device_uuid: str, status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], hardware_interface: Dict[str, Any], @@ -278,6 +281,7 @@ def __init__( Args: driver_instance: 设备实例 device_id: 设备标识符 + device_uuid: 设备标识符 status_types: 需要发布的状态和传感器信息 action_value_mappings: 设备动作 hardware_interface: 硬件接口配置 @@ -285,7 +289,7 @@ def __init__( """ self.driver_instance = driver_instance self.device_id = device_id - self.uuid = str(uuid.uuid4()) + self.uuid = device_uuid self.publish_high_frequency = False self.callback_group = ReentrantCallbackGroup() self.resource_tracker = resource_tracker @@ -554,6 +558,11 @@ def done_cb(*args): async def update_resource(self, resources: List["ResourcePLR"]): r = SerialCommand.Request() tree_set = ResourceTreeSet.from_plr_resources(resources) + for tree in tree_set.trees: + root_node = tree.root_node + if not root_node.res_content.uuid_parent: + logger.warning(f"更新无父节点物料{root_node},自动以当前设备作为根节点") + root_node.res_content.parent_uuid = self.uuid r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore try: @@ -1347,6 +1356,7 @@ def ros_node_instance(self): def __init__( self, device_id: str, + device_uuid: str, driver_class: Type[T], device_config: Dict[str, Any], driver_params: Dict[str, Any], @@ -1362,6 +1372,7 @@ def __init__( Args: device_id: 设备标识符 + device_uuid: 设备uuid driver_class: 设备类 device_config: 原始初始化的json driver_params: driver初始化的参数 @@ -1436,6 +1447,7 @@ def __init__( children=children, driver_instance=self._driver_instance, # type: ignore device_id=device_id, + device_uuid=device_uuid, status_types=status_types, action_value_mappings=action_value_mappings, hardware_interface=hardware_interface, @@ -1446,6 +1458,7 @@ def __init__( self._ros_node = BaseROS2DeviceNode( driver_instance=self._driver_instance, device_id=device_id, + device_uuid=device_uuid, status_types=status_types, action_value_mappings=action_value_mappings, hardware_interface=hardware_interface, diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 1b9a84b8..58a20e6f 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -18,7 +18,7 @@ ResourceDelete, ResourceUpdate, ResourceList, - SerialCommand, + SerialCommand, ResourceGet, ) # type: ignore from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unique_identifier_msgs.msg import UUID @@ -41,6 +41,7 @@ ResourceTreeSet, ResourceTreeInstance, ) +from unilabos.utils import logger from unilabos.utils.exception import DeviceClassInvalid from unilabos.utils.type_check import serialize_result_info from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot @@ -99,17 +100,6 @@ def __init__( """ if self._instance is not None: self._instance.lab_logger().critical("[Host Node] HostNode instance already exists.") - # 初始化Node基类,传递空参数覆盖列表 - BaseROS2DeviceNode.__init__( - self, - driver_instance=self, - device_id=device_id, - status_types={}, - action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"], - hardware_interface={}, - print_publish=False, - resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的 - ) # 设置单例实例 self.__class__._instance = self @@ -127,6 +117,91 @@ def __init__( bridges = [] self.bridges = bridges + # 创建 host_node 作为一个单独的 ResourceTree + host_node_dict = { + "id": "host_node", + "uuid": str(uuid.uuid4()), + "parent_uuid": "", + "name": "host_node", + "type": "device", + "class": "host_node", + "config": {}, + "data": {}, + "children": [], + "description": "", + "schema": {}, + "model": {}, + "icon": "", + } + + # 创建 host_node 的 ResourceTree + host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict) + host_node_tree = ResourceTreeInstance(host_node_instance) + resources_config.trees.insert(0, host_node_tree) + try: + for bridge in self.bridges: + if hasattr(bridge, "resource_tree_add") and resources_config: + from unilabos.app.web.client import HTTPClient + + client: HTTPClient = bridge + resource_start_time = time.time() + # 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射 + uuid_mapping = client.resource_tree_add(resources_config, "", True) + device_uuid = resources_config.root_nodes[0].res_content.uuid + resource_end_time = time.time() + logger.info( + f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" + ) + for edge in self.resources_edge_config: + edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"]) + edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"]) + resource_add_res = client.resource_edge_add(self.resources_edge_config) + resource_edge_end_time = time.time() + logger.info( + f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms" + ) + # resources_config 通过各个设备的 resource_tracker 进行uuid更新,利用uuid_mapping + # resources_config 的 root node 是 + # # 创建反向映射:new_uuid -> old_uuid + # reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()} + # for tree in resources_config.trees: + # node = tree.root_node + # if node.res_content.type == "device": + # if node.res_content.id == "host_node": + # continue + # # slave节点走c2s更新接口,拿到add自行update uuid + # device_tracker = self.devices_instances[node.res_content.id].resource_tracker + # old_uuid = reverse_uuid_mapping.get(node.res_content.uuid) + # if old_uuid: + # # 找到旧UUID,使用UUID查找 + # resource_instance = device_tracker.uuid_to_resources.get(old_uuid) + # else: + # # 未找到旧UUID,使用name查找 + # resource_instance = device_tracker.figure_resource( + # {"name": node.res_content.name} + # ) + # device_tracker.loop_update_uuid(resource_instance, uuid_mapping) + # else: + # try: + # for plr_resource in ResourceTreeSet([tree]).to_plr_resources(): + # self.resource_tracker.add_resource(plr_resource) + # except Exception as ex: + # self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!") + except Exception as ex: + logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}") + # 初始化Node基类,传递空参数覆盖列表 + BaseROS2DeviceNode.__init__( + self, + driver_instance=self, + device_id=device_id, + device_uuid=host_node_dict["uuid"], + status_types={}, + action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"], + hardware_interface={}, + print_publish=False, + resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的 + ) + # 创建设备、动作客户端和目标存储 self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 @@ -207,81 +282,7 @@ def __init__( ].items(): controller_config["update_rate"] = update_rate self.initialize_controller(controller_id, controller_config) - # 创建 host_node 作为一个单独的 ResourceTree - host_node_dict = { - "id": "host_node", - "uuid": str(uuid.uuid4()), - "parent_uuid": "", - "name": "host_node", - "type": "device", - "class": "host_node", - "config": {}, - "data": {}, - "children": [], - "description": "", - "schema": {}, - "model": {}, - "icon": "", - } - - # 创建 host_node 的 ResourceTree - host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict) - host_node_tree = ResourceTreeInstance(host_node_instance) - resources_config.trees.insert(0, host_node_tree) - try: - for bridge in self.bridges: - if hasattr(bridge, "resource_tree_add") and resources_config: - from unilabos.app.web.client import HTTPClient - - client: HTTPClient = bridge - resource_start_time = time.time() - # 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射 - uuid_mapping = client.resource_tree_add(resources_config, "", True) - resource_end_time = time.time() - self.lab_logger().info( - f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" - ) - for edge in self.resources_edge_config: - edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"]) - edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"]) - resource_add_res = client.resource_edge_add(self.resources_edge_config) - resource_edge_end_time = time.time() - self.lab_logger().info( - f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms" - ) - # resources_config 通过各个设备的 resource_tracker 进行uuid更新,利用uuid_mapping - # resources_config 的 root node 是 - # 创建反向映射:new_uuid -> old_uuid - reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()} - for tree in resources_config.trees: - node = tree.root_node - if node.res_content.type == "device": - for sub_node in node.children: - # 只有二级子设备 - if sub_node.res_content.type != "device": - # slave节点走c2s更新接口,拿到add自行update uuid - device_tracker = self.devices_instances[node.res_content.id].resource_tracker - # sub_node.res_content.uuid 已经是新UUID,需要用旧UUID去查找 - old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid) - if old_uuid: - # 找到旧UUID,使用UUID查找 - resource_instance = device_tracker.uuid_to_resources.get(old_uuid) - else: - # 未找到旧UUID,使用name查找 - resource_instance = device_tracker.figure_resource( - {"name": sub_node.res_content.name} - ) - device_tracker.loop_update_uuid(resource_instance, uuid_mapping) - else: - try: - for plr_resource in ResourceTreeSet([tree]).to_plr_resources(): - self.resource_tracker.add_resource(plr_resource) - except Exception as ex: - self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!") - except Exception as ex: - self.lab_logger().error("[Host Node-Resource] 添加物料出错!") - self.lab_logger().error(traceback.format_exc()) # 创建定时器,定期发现设备 self._discovery_timer = self.create_timer( discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup() @@ -862,7 +863,7 @@ def _init_host_service(self): ), } - def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK + async def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK resource_tree_set = ResourceTreeSet.load(data["data"]) mount_uuid = data["mount_uuid"] first_add = data["first_add"] @@ -903,7 +904,7 @@ def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand response.response = json.dumps(uuid_mapping) if success else "FAILED" self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") - def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK + async def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK uuid_list: List[str] = data["data"] with_children: bool = data["with_children"] from unilabos.app.web.client import http_client @@ -911,7 +912,7 @@ def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand resource_response = http_client.resource_tree_get(uuid_list, with_children) response.response = json.dumps(resource_response) - def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response): + async def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response): """ 子节点通知Host物料树删除 """ @@ -919,7 +920,7 @@ def _resource_tree_action_remove_callback(self, data: dict, response: SerialComm response.response = "OK" self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed") - def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response): + async def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response): """ 子节点通知Host物料树更新 """ @@ -937,14 +938,16 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm uuid_to_trees[tree.root_node.res_content.uuid].append(tree) for uid, trees in uuid_to_trees.items(): - new_tree_set = ResourceTreeSet(trees) resource_start_time = time.time() + self.lab_logger().info( + f"[Host Node-Resource] 物料 {[root_node.res_content.id for root_node in new_tree_set.root_nodes]} {uid} 挂载 {trees[0].root_node.res_content.parent_uuid} 请求更新上传" + ) uuid_mapping = http_client.resource_tree_add(new_tree_set, uid, False) success = bool(uuid_mapping) resource_end_time = time.time() self.lab_logger().info( - f"[Host Node-Resource] 物料 {[root_node.res_content.id for root_node in new_tree_set.root_nodes]} 挂载 {uid} P{trees[0].root_node.res_content.parent} 更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" + f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms" ) if uuid_mapping: self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点") @@ -952,7 +955,7 @@ def _resource_tree_action_update_callback(self, data: dict, response: SerialComm response.response = json.dumps(uuid_mapping) self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") - def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response): + async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response): """ 子节点通知Host物料树更新 @@ -965,13 +968,13 @@ def _resource_tree_update_callback(self, request: SerialCommand_Request, respons action = data["action"] data = data["data"] if action == "add": - self._resource_tree_action_add_callback(data, response) + await self._resource_tree_action_add_callback(data, response) elif action == "get": - self._resource_tree_action_get_callback(data, response) + await self._resource_tree_action_get_callback(data, response) elif action == "update": - self._resource_tree_action_update_callback(data, response) + await self._resource_tree_action_update_callback(data, response) elif action == "remove": - self._resource_tree_action_remove_callback(data, response) + await self._resource_tree_action_remove_callback(data, response) else: self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}") response.response = "ERROR" diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index 07e35ee6..5a1eaa75 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -11,8 +11,7 @@ from rclpy.action import ActionServer, ActionClient from rclpy.action.server import ServerGoalHandle from rclpy.callback_groups import ReentrantCallbackGroup -from unilabos_msgs.msg import Resource # type: ignore -from unilabos_msgs.srv import ResourceGet, ResourceUpdate # type: ignore +from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos.compile import action_protocol_generators from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list @@ -20,11 +19,11 @@ from unilabos.ros.msgs.message_converter import ( get_action_type, convert_to_ros_msg, - convert_from_ros_msg, convert_from_ros_msg_with_mapping, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode -from unilabos.utils.type_check import serialize_result_info, get_result_info_str +from unilabos.ros.nodes.resource_tracker import ResourceTreeSet +from unilabos.utils.type_check import get_result_info_str if TYPE_CHECKING: from unilabos.devices.workstation.workstation_base import WorkstationBase @@ -50,6 +49,7 @@ def __init__( *, driver_instance: "WorkstationBase", device_id: str, + device_uuid: str, status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], hardware_interface: Dict[str, Any], @@ -64,6 +64,7 @@ def __init__( super().__init__( driver_instance=driver_instance, device_id=device_id, + device_uuid=device_uuid, status_types=status_types, action_value_mappings={**action_value_mappings, **self.protocol_action_mappings}, hardware_interface=hardware_interface, @@ -222,16 +223,28 @@ async def execute_protocol(goal_handle: ServerGoalHandle): # 向Host查询物料当前状态 for k, v in goal.get_fields_and_field_types().items(): if v in ["unilabos_msgs/Resource", "sequence"]: - r = ResourceGet.Request() - resource_id = ( - protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] - ) - r.id = resource_id - r.with_children = True - response = await self._resource_clients["resource_get"].call_async(r) - protocol_kwargs[k] = list_to_nested_dict( - [convert_from_ros_msg(rs) for rs in response.resources] - ) + self.lab_logger().info(f"{protocol_name} 查询资源状态: Key: {k} Type: {v}") + + try: + # 统一处理单个或多个资源 + resource_id = ( + protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] + ) + r = SerialCommand_Request() + r.command = json.dumps({"id": resource_id, "with_children": True}) + # 发送请求并等待响应 + response: SerialCommand_Response = await self._resource_clients[ + "resource_get" + ].call_async( + r + ) # type: ignore + raw_data = json.loads(response.response) + tree_set = ResourceTreeSet.from_raw_list(raw_data) + target = tree_set.dump() + protocol_kwargs[k] = target[0] + except Exception as ex: + self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}") + raise self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 15f4b8ca..e6c1beea 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -849,6 +849,7 @@ def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int: def process(res): current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid") + replaced = 0 if current_uuid and current_uuid in uuid_map: new_uuid = uuid_map[current_uuid] if current_uuid != new_uuid: @@ -858,8 +859,8 @@ def process(res): self.uuid_to_resources.pop(current_uuid) self.uuid_to_resources[new_uuid] = res logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}") - return 1 - return 0 + replaced = 1 + return replaced return self._traverse_and_process(resource, process) @@ -911,9 +912,23 @@ def add_resource(self, resource): Args: resource: 资源对象(可以是dict或实例) """ + root_uuids = {} for r in self.resources: + res_uuid = r.get("uuid") if isinstance(r, dict) else getattr(r, "unilabos_uuid", None) + if res_uuid: + root_uuids[res_uuid] = r if id(r) == id(resource): return + + # 这里只做uuid的根节点比较 + if isinstance(resource, dict): + res_uuid = resource.get("uuid") + else: + res_uuid = getattr(resource, "unilabos_uuid", None) + if res_uuid in root_uuids: + old_res = root_uuids[res_uuid] + # self.remove_resource(old_res) + logger.warning(f"资源{resource}已存在,旧资源: {old_res}") self.resources.append(resource) # 递归收集uuid映射 self._collect_uuid_mapping(resource) From 6f4889ac58a2d52b27205b283d5c804db74433d7 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 14 Oct 2025 18:07:59 +0800 Subject: [PATCH 15/33] mount parent uuid --- unilabos/ros/nodes/presets/host_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 58a20e6f..d265e13a 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -935,7 +935,7 @@ async def _resource_tree_action_update_callback(self, data: dict, response: Seri uuid_to_trees: Dict[str, List[ResourceTreeInstance]] = collections.defaultdict(list) for tree in resource_tree_set.trees: - uuid_to_trees[tree.root_node.res_content.uuid].append(tree) + uuid_to_trees[tree.root_node.res_content.parent_uuid].append(tree) for uid, trees in uuid_to_trees.items(): new_tree_set = ResourceTreeSet(trees) From 1807bb1578cbe9afa763a9978db90cd859b59e28 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:02:15 +0800 Subject: [PATCH 16/33] Add logging configuration based on BasicConfig in main function --- unilabos/app/main.py | 9 +++++++++ unilabos/config/config.py | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 110ca040..b65da8e9 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -180,6 +180,7 @@ def main(): working_dir = os.path.abspath(os.getcwd()) else: working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data")) + if args_dict.get("working_dir"): working_dir = args_dict.get("working_dir", "") if config_path and not os.path.exists(config_path): @@ -211,6 +212,14 @@ def main(): # 加载配置文件 print_status(f"当前工作目录为 {working_dir}", "info") load_config_from_file(config_path) + + # 根据配置重新设置日志级别 + from unilabos.utils.log import configure_logger, logger + + if hasattr(BasicConfig, "log_level"): + logger.info(f"Log level set to '{BasicConfig.log_level}' from config file.") + configure_logger(loglevel=BasicConfig.log_level) + if args_dict["addr"] == "test": print_status("使用测试环境地址", "info") HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" diff --git a/unilabos/config/config.py b/unilabos/config/config.py index e0664449..b5bc6191 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -2,7 +2,7 @@ import traceback import os import importlib.util -from typing import Optional +from typing import Optional, Literal from unilabos.utils import logger @@ -18,6 +18,7 @@ class BasicConfig: vis_2d_enable = False enable_resource_load = True communication_protocol = "websocket" + log_level: Literal['TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] = "DEBUG" # 'TRACE', 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL' @classmethod def auth_secret(cls): From 7078d6346390cb79c89e95652a63441972a644ff Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:58:15 +0800 Subject: [PATCH 17/33] fix workstation node error --- unilabos/ros/nodes/presets/workstation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index 5a1eaa75..f8fbe97d 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -241,7 +241,7 @@ async def execute_protocol(goal_handle: ServerGoalHandle): raw_data = json.loads(response.response) tree_set = ResourceTreeSet.from_raw_list(raw_data) target = tree_set.dump() - protocol_kwargs[k] = target[0] + protocol_kwargs[k] = target[0][0] if v == "unilabos_msgs/Resource" else target except Exception as ex: self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}") raise From 5e397eef68aab17e9a40f2f87630d90a5dc06261 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 01:59:48 +0800 Subject: [PATCH 18/33] fix workstation node error --- unilabos/ros/nodes/presets/workstation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index f8fbe97d..745ec196 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -6,6 +6,8 @@ import rclpy from rosidl_runtime_py import message_to_ordereddict +from unilabos_msgs.msg import Resource +from unilabos_msgs.srv import ResourceUpdate from unilabos.messages import * # type: ignore # protocol names from rclpy.action import ActionServer, ActionClient From a08f5d84a16bedc6d5a999e63bd943e3cf9637e5 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 02:33:15 +0800 Subject: [PATCH 19/33] Update boot example --- docs/boot_examples/organic_synthesis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/boot_examples/organic_synthesis.md b/docs/boot_examples/organic_synthesis.md index 31b83df7..23b970fd 100644 --- a/docs/boot_examples/organic_synthesis.md +++ b/docs/boot_examples/organic_synthesis.md @@ -91,7 +91,7 @@ 使用以下命令启动模拟反应器: ```bash -unilab -g test/experiments/mock_reactor.json --app_bridges "" +unilab -g test/experiments/mock_reactor.json ``` ### 2. 执行抽真空和充气操作 From 2b4a9e13d4e3a8dea8cb292bcc5bea3296b0af9e Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 03:15:56 +0800 Subject: [PATCH 20/33] temp fix for resource get --- unilabos/resources/graphio.py | 5 +++++ unilabos/ros/nodes/base_device_node.py | 28 +++++++++++++++++++------- unilabos/ros/nodes/resource_tracker.py | 7 ++++--- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index a8cd6152..b1b5a611 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -17,6 +17,7 @@ ResourceDictInstance, ResourceTreeSet, ) +from unilabos.utils import logger from unilabos.utils.banner_print import print_status try: @@ -67,6 +68,10 @@ def canonicalize_nodes_data( z = node.pop("z", None) if z is not None: node["position"]["position"]["z"] = z + if "sample_id" in node: + sample_id = node.pop("sample_id") + if sample_id: + logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}") for k in list(node.keys()): if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children"]: v = node.pop(k) diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index d3f224bc..2a1b5b74 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -6,7 +6,7 @@ import time import traceback import uuid -from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING +from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING, Union from concurrent.futures import ThreadPoolExecutor import asyncio @@ -657,15 +657,27 @@ async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand results.append({"success": True, "action": "update"}) elif action == "remove": # 移除资源 - plr_resources: List[ResourcePLR] = [ - self.resource_tracker.uuid_to_resources[i] for i in resources_uuid - ] + found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource( + [{"uuid": uid} for uid in resources_uuid], try_mode=True + ) + found_plr_resources = [] + other_plr_resources = [] + for res_list in found_resources: + for res in res_list: + if issubclass(res.__class__, ResourcePLR): + found_plr_resources.append(res) + else: + other_plr_resources.append(res) func = getattr(self.driver_instance, "resource_tree_remove", None) if callable(func): - func(plr_resources) - for plr_resource in plr_resources: + func(found_plr_resources) + for plr_resource in found_plr_resources: plr_resource.parent.unassign_child_resource(plr_resource) self.resource_tracker.remove_resource(plr_resource) + self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点") + for res in other_plr_resources: + self.resource_tracker.remove_resource(res) + self.lab_logger().info(f"移除物料 {res} 及其子节点") results.append({"success": True, "action": "remove"}) except Exception as e: error_msg = f"Error processing {action} operation: {str(e)}" @@ -945,7 +957,9 @@ def ACTION(**kwargs): # 通过资源跟踪器获取本地实例 final_resources = queried_resources if is_sequence else queried_resources[0] - action_kwargs[k] = self.resource_tracker.figure_resource(final_resources, try_mode=False) + final_resources = self.resource_tracker.figure_resource({"name": final_resources.id}, try_mode=False) if not is_sequence else [ + self.resource_tracker.figure_resource({"name": res.id}, try_mode=False) for res in queried_resources + ] except Exception as e: self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}") diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index e6c1beea..79d8e616 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -774,7 +774,8 @@ def _get_resource_attr(self, resource, attr_name: str, uuid_attr: Optional[str] else: return getattr(resource, uuid_attr, None) - def _set_resource_uuid(self, resource, new_uuid: str): + @classmethod + def set_resource_uuid(cls, resource, new_uuid: str): """ 设置资源的 uuid,统一处理 dict 和 instance 两种类型 @@ -827,7 +828,7 @@ def process(res): resource_name = self._get_resource_attr(res, "name") if resource_name and resource_name in name_to_uuid_map: new_uuid = name_to_uuid_map[resource_name] - self._set_resource_uuid(res, new_uuid) + self.set_resource_uuid(res, new_uuid) self.uuid_to_resources[new_uuid] = res logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}") return 1 @@ -853,7 +854,7 @@ def process(res): if current_uuid and current_uuid in uuid_map: new_uuid = uuid_map[current_uuid] if current_uuid != new_uuid: - self._set_resource_uuid(res, new_uuid) + self.set_resource_uuid(res, new_uuid) # 更新uuid_to_resources映射 if current_uuid in self.uuid_to_resources: self.uuid_to_resources.pop(current_uuid) From 56d32508c2a342963398d69c175fdec5092f2372 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 03:20:37 +0800 Subject: [PATCH 21/33] temp fix for resource get --- unilabos/ros/nodes/base_device_node.py | 5 +++-- unilabos/ros/nodes/resource_tracker.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 2a1b5b74..5900acac 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -957,9 +957,10 @@ def ACTION(**kwargs): # 通过资源跟踪器获取本地实例 final_resources = queried_resources if is_sequence else queried_resources[0] - final_resources = self.resource_tracker.figure_resource({"name": final_resources.id}, try_mode=False) if not is_sequence else [ - self.resource_tracker.figure_resource({"name": res.id}, try_mode=False) for res in queried_resources + final_resources = self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False) if not is_sequence else [ + self.resource_tracker.figure_resource({"name": res.name}, try_mode=False) for res in queried_resources ] + action_kwargs[k] = final_resources except Exception as e: self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}") diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 79d8e616..0f1ba55e 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -840,7 +840,7 @@ def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int: """ 递归遍历资源树,更新所有节点的uuid - Args: + Args:0 resource: 资源对象(可以是dict或实例) uuid_map: uuid映射字典,{old_uuid: new_uuid} From 4bc3abf5397ffd2f2134661c34da3d971836559d Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:05:44 +0800 Subject: [PATCH 22/33] provide error info when cant find plr type --- unilabos/ros/nodes/resource_tracker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index 0f1ba55e..e1b98486 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -439,7 +439,7 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): try: sub_cls = find_subclass(plr_dict["type"], PLRResource) if sub_cls is None: - raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类") + raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}") spec = inspect.signature(sub_cls) if "category" not in spec.parameters: plr_dict.pop("category", None) From ebd9bd643ae56ae418d42c909ef882cc0e68e3c1 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:06:13 +0800 Subject: [PATCH 23/33] pack repo info --- .github/workflows/conda-pack-build.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/conda-pack-build.yml b/.github/workflows/conda-pack-build.yml index 0c119759..7e278a9b 100644 --- a/.github/workflows/conda-pack-build.yml +++ b/.github/workflows/conda-pack-build.yml @@ -242,6 +242,10 @@ jobs: echo Adding: verify_installation.py copy scripts\verify_installation.py dist-package\ + rem Copy source code repository (including .git) + echo Adding: Uni-Lab-OS source repository + robocopy . dist-package\Uni-Lab-OS /E /XD dist-package /NFL /NDL /NJH /NJS /NC /NS || if %ERRORLEVEL% LSS 8 exit /b 0 + rem Create README using Python script echo Creating: README.txt python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\README.txt @@ -274,6 +278,10 @@ jobs: echo "Adding: verify_installation.py" cp scripts/verify_installation.py dist-package/ + # Copy source code repository (including .git) + echo "Adding: Uni-Lab-OS source repository" + rsync -a --exclude='dist-package' . dist-package/Uni-Lab-OS + # Create README using Python script echo "Creating: README.txt" python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/README.txt From 1c1f2593fed05d1cf7501708ceba3920c09c93ca Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:12:21 +0800 Subject: [PATCH 24/33] fix to plr type error --- unilabos/ros/nodes/resource_tracker.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index e1b98486..c9aca4a7 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -387,9 +387,6 @@ def to_plr_resources(self) -> List["PLRResource"]: from pylabrobot.utils.object_parsing import find_subclass import inspect - # 类型映射 - TYPE_MAP = {"plate": "plate", "well": "well", "container": "tip_spot", "deck": "deck", "tip_rack": "tip_rack"} - def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict): """一次遍历收集 name_to_uuid 和 all_states""" name_to_uuid[node.res_content.name] = node.res_content.uuid @@ -400,9 +397,6 @@ def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): """转换节点为 PLR 字典格式""" res = node.res_content - plr_type = TYPE_MAP.get(res.type, "tip_spot") - if res.type not in TYPE_MAP: - logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot") d = { "name": res.name, @@ -417,7 +411,7 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): "type": "Coordinate", }, "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"}, - "category": plr_type, + "category": res.type, "children": [node_to_plr_dict(child, has_model) for child in node.children], "parent_name": res.parent_instance_name, **res.config, From 51632e2f982c4674eee36557f0a76496905adcbb Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:19:59 +0800 Subject: [PATCH 25/33] fix to plr type error --- unilabos/ros/nodes/resource_tracker.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index c9aca4a7..e4b05502 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -306,10 +306,7 @@ def replace_plr_type(source: str): replace_info = { "plate": "plate", "well": "well", - "tip_spot": "container", - "trash": "container", "deck": "deck", - "tip_rack": "container", } if source in replace_info: return replace_info[source] @@ -387,6 +384,9 @@ def to_plr_resources(self) -> List["PLRResource"]: from pylabrobot.utils.object_parsing import find_subclass import inspect + # 类型映射 + TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"} + def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict): """一次遍历收集 name_to_uuid 和 all_states""" name_to_uuid[node.res_content.name] = node.res_content.uuid @@ -397,10 +397,13 @@ def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): """转换节点为 PLR 字典格式""" res = node.res_content + plr_type = TYPE_MAP.get(res.type, res.type) + if res.type not in TYPE_MAP: + logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot") d = { "name": res.name, - "type": plr_type, + "type": res.type, "size_x": res.config.get("size_x", 0), "size_y": res.config.get("size_y", 0), "size_z": res.config.get("size_z", 0), @@ -411,7 +414,7 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): "type": "Coordinate", }, "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"}, - "category": res.type, + "category": res.config.get("category", plr_type), "children": [node_to_plr_dict(child, has_model) for child in node.children], "parent_name": res.parent_instance_name, **res.config, From b2ae40b3a9aba2a27f91c9e7bb6ca011b48a6e4a Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:33:28 +0800 Subject: [PATCH 26/33] Update regular container method --- .../comprehensive_station.json | 59 +++++++-- .../registry/resources/organic/container.yaml | 2 +- unilabos/resources/container.py | 122 +++++++++--------- unilabos/resources/graphio.py | 1 + 4 files changed, 115 insertions(+), 69 deletions(-) diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index 1da0d1df..01112b25 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -170,7 +170,10 @@ "z": 0 }, "config": { - "max_volume": 1000.0 + "max_volume": 1000.0, + "size_x": 200, + "size_y": 150, + "size_z": 0 }, "data": { "liquids": [ @@ -194,7 +197,10 @@ "z": 0 }, "config": { - "max_volume": 1000.0 + "max_volume": 1000.0, + "size_x": 200, + "size_y": 150, + "size_z": 0 }, "data": { "liquids": [ @@ -218,7 +224,10 @@ "z": 0 }, "config": { - "max_volume": 1000.0 + "max_volume": 1000.0, + "size_x": 300, + "size_y": 150, + "size_z": 0 }, "data": { "liquids": [ @@ -242,7 +251,10 @@ "z": 0 }, "config": { - "max_volume": 1000.0 + "max_volume": 1000.0, + "size_x": 900, + "size_y": 150, + "size_z": 0 }, "data": { "liquids": [ @@ -266,7 +278,10 @@ "z": 0 }, "config": { - "max_volume": 1000.0 + "max_volume": 1000.0, + "size_x": 950, + "size_y": 150, + "size_z": 0 }, "data": { "liquids": [ @@ -419,7 +434,10 @@ "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 2000.0, + "size_x": 500, + "size_y": 400, + "size_z": 0 }, "data": { "liquids": [ @@ -439,7 +457,10 @@ "z": 0 }, "config": { - "max_volume": 2000.0 + "max_volume": 2000.0, + "size_x": 1100, + "size_y": 500, + "size_z": 0 }, "data": { "liquids": [ @@ -649,7 +670,10 @@ "z": 0 }, "config": { - "max_volume": 250.0 + "max_volume": 250.0, + "size_x": 900, + "size_y": 500, + "size_z": 0 }, "data": { "liquids": [ @@ -669,7 +693,10 @@ "z": 0 }, "config": { - "max_volume": 250.0 + "max_volume": 250.0, + "size_x": 950, + "size_y": 500, + "size_z": 0 }, "data": { "liquids": [ @@ -689,7 +716,10 @@ "z": 0 }, "config": { - "max_volume": 250.0 + "max_volume": 250.0, + "size_x": 1050, + "size_y": 500, + "size_z": 0 }, "data": { "liquids": [ @@ -733,6 +763,9 @@ }, "config": { "max_volume": 500.0, + "size_x": 550, + "size_y": 250, + "size_z": 0, "reagent": "sodium_chloride", "physical_state": "solid" }, @@ -756,6 +789,9 @@ }, "config": { "volume": 500.0, + "size_x": 600, + "size_y": 250, + "size_z": 0, "reagent": "sodium_carbonate", "physical_state": "solid" }, @@ -779,6 +815,9 @@ }, "config": { "volume": 500.0, + "size_x": 650, + "size_y": 250, + "size_z": 0, "reagent": "magnesium_chloride", "physical_state": "solid" }, diff --git a/unilabos/registry/resources/organic/container.yaml b/unilabos/registry/resources/organic/container.yaml index 6a52caf3..7da736c0 100644 --- a/unilabos/registry/resources/organic/container.yaml +++ b/unilabos/registry/resources/organic/container.yaml @@ -3,7 +3,7 @@ container: - container class: module: unilabos.resources.container:RegularContainer - type: unilabos + type: pylabrobot description: regular organic container handles: - data_key: fluid_in diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index 644bfe88..72fba553 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -1,67 +1,73 @@ import json +from pylabrobot.resources import Container from unilabos_msgs.msg import Resource from unilabos.ros.msgs.message_converter import convert_from_ros_msg -class RegularContainer(object): - # 第一个参数必须是id传入 - # noinspection PyShadowingBuiltins - def __init__(self, id: str): - self.id = id - self.ulr_resource = Resource() - self._data = None +class RegularContainer(Container): + pass - @property - def ulr_resource_data(self): - if self._data is None: - self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {} - return self._data - @ulr_resource_data.setter - def ulr_resource_data(self, value: dict): - self._data = value - self.ulr_resource.data = json.dumps(self._data) - - @property - def liquid_type(self): - return self.ulr_resource_data.get("liquid_type", None) - - @liquid_type.setter - def liquid_type(self, value: str): - if value is not None: - self.ulr_resource_data["liquid_type"] = value - else: - self.ulr_resource_data.pop("liquid_type", None) - - @property - def liquid_volume(self): - return self.ulr_resource_data.get("liquid_volume", None) - - @liquid_volume.setter - def liquid_volume(self, value: float): - if value is not None: - self.ulr_resource_data["liquid_volume"] = value - else: - self.ulr_resource_data.pop("liquid_volume", None) - - def get_ulr_resource(self) -> Resource: - """ - 获取UlrResource对象 - :return: UlrResource对象 - """ - self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新 - return self.ulr_resource - - def get_ulr_resource_as_dict(self) -> Resource: - """ - 获取UlrResource对象 - :return: UlrResource对象 - """ - to_dict = convert_from_ros_msg(self.get_ulr_resource()) - to_dict["type"] = "container" - return to_dict - - def __str__(self): - return f"{self.id}" \ No newline at end of file +# +# class RegularContainer(object): +# # 第一个参数必须是id传入 +# # noinspection PyShadowingBuiltins +# def __init__(self, id: str): +# self.id = id +# self.ulr_resource = Resource() +# self._data = None +# +# @property +# def ulr_resource_data(self): +# if self._data is None: +# self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {} +# return self._data +# +# @ulr_resource_data.setter +# def ulr_resource_data(self, value: dict): +# self._data = value +# self.ulr_resource.data = json.dumps(self._data) +# +# @property +# def liquid_type(self): +# return self.ulr_resource_data.get("liquid_type", None) +# +# @liquid_type.setter +# def liquid_type(self, value: str): +# if value is not None: +# self.ulr_resource_data["liquid_type"] = value +# else: +# self.ulr_resource_data.pop("liquid_type", None) +# +# @property +# def liquid_volume(self): +# return self.ulr_resource_data.get("liquid_volume", None) +# +# @liquid_volume.setter +# def liquid_volume(self, value: float): +# if value is not None: +# self.ulr_resource_data["liquid_volume"] = value +# else: +# self.ulr_resource_data.pop("liquid_volume", None) +# +# def get_ulr_resource(self) -> Resource: +# """ +# 获取UlrResource对象 +# :return: UlrResource对象 +# """ +# self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新 +# return self.ulr_resource +# +# def get_ulr_resource_as_dict(self) -> Resource: +# """ +# 获取UlrResource对象 +# :return: UlrResource对象 +# """ +# to_dict = convert_from_ros_msg(self.get_ulr_resource()) +# to_dict["type"] = "container" +# return to_dict +# +# def __str__(self): +# return f"{self.id}" \ No newline at end of file diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index b1b5a611..98f6c74e 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -781,6 +781,7 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni else: r = resource_plr elif resource_class_config["type"] == "unilabos": + raise ValueError(f"No more support for unilabos Resource class {resource_class_config}") res_instance: RegularContainer = RESOURCE(id=resource_config["name"]) res_instance.ulr_resource = convert_to_ros_msg( Resource, {k: v for k, v in resource_config.items() if k != "class"} From 18254a9e72c6c3f60f3ea91f7c144e3f59feaebb Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:35:59 +0800 Subject: [PATCH 27/33] support no size init --- unilabos/resources/container.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index 72fba553..adf9a23c 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -7,7 +7,14 @@ class RegularContainer(Container): - pass + def __init__(self, *args, **kwargs): + if "size_x" not in kwargs: + kwargs["size_x"] = 0 + if "size_y" not in kwargs: + kwargs["size_y"] = 0 + if "size_z" not in kwargs: + kwargs["size_z"] = 0 + super().__init__(*args, **kwargs) # From 95f5592857171ee0b58a06ea371a92bc055574f4 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:46:59 +0800 Subject: [PATCH 28/33] fix comprehensive_station.json --- .../comprehensive_station.json | 14 ++++++++++++++ unilabos/resources/container.py | 1 + 2 files changed, 15 insertions(+) diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index 01112b25..646af5a8 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -171,6 +171,7 @@ }, "config": { "max_volume": 1000.0, + "category": "RegularContainer", "size_x": 200, "size_y": 150, "size_z": 0 @@ -198,6 +199,7 @@ }, "config": { "max_volume": 1000.0, + "category": "RegularContainer", "size_x": 200, "size_y": 150, "size_z": 0 @@ -225,6 +227,7 @@ }, "config": { "max_volume": 1000.0, + "category": "RegularContainer", "size_x": 300, "size_y": 150, "size_z": 0 @@ -252,6 +255,7 @@ }, "config": { "max_volume": 1000.0, + "category": "RegularContainer", "size_x": 900, "size_y": 150, "size_z": 0 @@ -279,6 +283,7 @@ }, "config": { "max_volume": 1000.0, + "category": "RegularContainer", "size_x": 950, "size_y": 150, "size_z": 0 @@ -350,6 +355,7 @@ }, "config": { "max_volume": 500.0, + "category": "RegularContainer", "max_temp": 200.0, "min_temp": -20.0, "has_stirrer": true, @@ -435,6 +441,7 @@ }, "config": { "max_volume": 2000.0, + "category": "RegularContainer", "size_x": 500, "size_y": 400, "size_z": 0 @@ -458,6 +465,7 @@ }, "config": { "max_volume": 2000.0, + "category": "RegularContainer", "size_x": 1100, "size_y": 500, "size_z": 0 @@ -671,6 +679,7 @@ }, "config": { "max_volume": 250.0, + "category": "RegularContainer", "size_x": 900, "size_y": 500, "size_z": 0 @@ -694,6 +703,7 @@ }, "config": { "max_volume": 250.0, + "category": "RegularContainer", "size_x": 950, "size_y": 500, "size_z": 0 @@ -717,6 +727,7 @@ }, "config": { "max_volume": 250.0, + "category": "RegularContainer", "size_x": 1050, "size_y": 500, "size_z": 0 @@ -766,6 +777,7 @@ "size_x": 550, "size_y": 250, "size_z": 0, + "category": "RegularContainer", "reagent": "sodium_chloride", "physical_state": "solid" }, @@ -792,6 +804,7 @@ "size_x": 600, "size_y": 250, "size_z": 0, + "category": "RegularContainer", "reagent": "sodium_carbonate", "physical_state": "solid" }, @@ -818,6 +831,7 @@ "size_x": 650, "size_y": 250, "size_z": 0, + "category": "RegularContainer", "reagent": "magnesium_chloride", "physical_state": "solid" }, diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index adf9a23c..cf6606ea 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -14,6 +14,7 @@ def __init__(self, *args, **kwargs): kwargs["size_y"] = 0 if "size_z" not in kwargs: kwargs["size_z"] = 0 + self.kwargs = kwargs super().__init__(*args, **kwargs) From 6cf7a2b80f09e75d36f87335fe303705c23f5b5d Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:52:07 +0800 Subject: [PATCH 29/33] fix comprehensive_station.json --- .../comprehensive_station.json | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index 646af5a8..0dd4f7bd 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -171,7 +171,8 @@ }, "config": { "max_volume": 1000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 200, "size_y": 150, "size_z": 0 @@ -199,7 +200,8 @@ }, "config": { "max_volume": 1000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 200, "size_y": 150, "size_z": 0 @@ -227,7 +229,8 @@ }, "config": { "max_volume": 1000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 300, "size_y": 150, "size_z": 0 @@ -255,7 +258,8 @@ }, "config": { "max_volume": 1000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 900, "size_y": 150, "size_z": 0 @@ -283,7 +287,8 @@ }, "config": { "max_volume": 1000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 950, "size_y": 150, "size_z": 0 @@ -355,7 +360,8 @@ }, "config": { "max_volume": 500.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "max_temp": 200.0, "min_temp": -20.0, "has_stirrer": true, @@ -441,7 +447,8 @@ }, "config": { "max_volume": 2000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 500, "size_y": 400, "size_z": 0 @@ -465,7 +472,8 @@ }, "config": { "max_volume": 2000.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 1100, "size_y": 500, "size_z": 0 @@ -679,7 +687,8 @@ }, "config": { "max_volume": 250.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 900, "size_y": 500, "size_z": 0 @@ -703,7 +712,8 @@ }, "config": { "max_volume": 250.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 950, "size_y": 500, "size_z": 0 @@ -727,7 +737,8 @@ }, "config": { "max_volume": 250.0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "size_x": 1050, "size_y": 500, "size_z": 0 @@ -777,7 +788,8 @@ "size_x": 550, "size_y": 250, "size_z": 0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "reagent": "sodium_chloride", "physical_state": "solid" }, @@ -804,7 +816,8 @@ "size_x": 600, "size_y": 250, "size_z": 0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "reagent": "sodium_carbonate", "physical_state": "solid" }, @@ -831,7 +844,8 @@ "size_x": 650, "size_y": 250, "size_z": 0, - "category": "RegularContainer", + "type": "RegularContainer", + "category": "container", "reagent": "magnesium_chloride", "physical_state": "solid" }, From dc2fe7e2ce071b81398543c7d9074a4e94247d77 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:55:38 +0800 Subject: [PATCH 30/33] fix type conversion --- unilabos/ros/nodes/resource_tracker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index e4b05502..22919c5c 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -399,11 +399,11 @@ def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): res = node.res_content plr_type = TYPE_MAP.get(res.type, res.type) if res.type not in TYPE_MAP: - logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot") + logger.warning(f"未知类型 {res.type}") d = { "name": res.name, - "type": res.type, + "type": res.config.get("type", plr_type), "size_x": res.config.get("size_x", 0), "size_y": res.config.get("size_y", 0), "size_z": res.config.get("size_z", 0), From 6eff3c09d46da79694d27cdd4afc150f39df9493 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:04:03 +0800 Subject: [PATCH 31/33] fix state loading for regular container --- .../comprehensive_station.json | 64 +++++++------------ unilabos/resources/container.py | 5 +- 2 files changed, 26 insertions(+), 43 deletions(-) diff --git a/test/experiments/comprehensive_protocol/comprehensive_station.json b/test/experiments/comprehensive_protocol/comprehensive_station.json index 0dd4f7bd..9af64af3 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_station.json +++ b/test/experiments/comprehensive_protocol/comprehensive_station.json @@ -178,12 +178,8 @@ "size_z": 0 }, "data": { - "liquids": [ - { - "liquid_type": "DMF", - "liquid_volume": 1000.0 - } - ] + "liquids": [["DMF", 500.0]], + "pending_liquids": [["DMF", 500.0]] } }, { @@ -207,12 +203,8 @@ "size_z": 0 }, "data": { - "liquids": [ - { - "liquid_type": "ethyl_acetate", - "liquid_volume": 1000.0 - } - ] + "liquids": [["ethyl_acetate", 1000.0]], + "pending_liquids": [["ethyl_acetate", 1000.0]] } }, { @@ -236,12 +228,8 @@ "size_z": 0 }, "data": { - "liquids": [ - { - "liquid_type": "hexane", - "liquid_volume": 1000.0 - } - ] + "liquids": [["hexane", 1000.0]], + "pending_liquids": [["hexane", 1000.0]] } }, { @@ -265,12 +253,8 @@ "size_z": 0 }, "data": { - "liquids": [ - { - "liquid_type": "methanol", - "liquid_volume": 1000.0 - } - ] + "liquids": [["methanol", 1000.0]], + "pending_liquids": [["methanol", 1000.0]] } }, { @@ -294,12 +278,8 @@ "size_z": 0 }, "data": { - "liquids": [ - { - "liquid_type": "water", - "liquid_volume": 1000.0 - } - ] + "liquids": [["water", 1000.0]], + "pending_liquids": [["water", 1000.0]] } }, { @@ -368,8 +348,8 @@ "has_heater": true }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { @@ -454,8 +434,8 @@ "size_z": 0 }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { @@ -479,8 +459,8 @@ "size_z": 0 }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { @@ -694,8 +674,8 @@ "size_z": 0 }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { @@ -719,8 +699,8 @@ "size_z": 0 }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { @@ -744,8 +724,8 @@ "size_z": 0 }, "data": { - "liquids": [ - ] + "liquids": [], + "pending_liquids": [] } }, { diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index cf6606ea..23b044c7 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -1,4 +1,5 @@ import json +from typing import Dict, Any from pylabrobot.resources import Container from unilabos_msgs.msg import Resource @@ -15,9 +16,11 @@ def __init__(self, *args, **kwargs): if "size_z" not in kwargs: kwargs["size_z"] = 0 self.kwargs = kwargs + self.state = {} super().__init__(*args, **kwargs) - + def load_state(self, state: Dict[str, Any]): + self.state = state # # class RegularContainer(object): # # 第一个参数必须是id传入 From 7ab2c81e39cf7323432212dbb3bd5c089aa88cda Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:23:22 +0800 Subject: [PATCH 32/33] Update deploy-docs.yml --- .github/workflows/deploy-docs.yml | 53 ++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index d19dbb87..dce45ec9 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -39,24 +39,55 @@ jobs: uses: actions/checkout@v4 with: ref: ${{ github.event.inputs.branch || github.ref }} + fetch-depth: 0 - - name: Setup Python environment - uses: actions/setup-python@v5 + - name: Setup Miniforge (with mamba) + uses: conda-incubator/setup-miniconda@v3 with: - python-version: '3.10' + miniforge-version: latest + use-mamba: true + python-version: '3.11.11' + channels: conda-forge,robostack-staging,uni-lab,defaults + channel-priority: flexible + activate-environment: unilab + auto-update-conda: false + show-channel-urls: true - name: Install system dependencies run: | sudo apt-get update sudo apt-get install -y pandoc - - name: Install Python dependencies + - name: Install unilabos and dependencies run: | - python -m pip install --upgrade pip - # Install package in development mode to get version info - pip install -e . - # Install documentation dependencies - pip install -r docs/requirements.txt + echo "Installing unilabos and dependencies to unilab environment..." + echo "Using mamba for faster and more reliable dependency resolution..." + mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y + + - name: Install latest unilabos from source + run: | + echo "Uninstalling existing unilabos..." + mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip" + echo "Installing unilabos from source..." + mamba run -n unilab pip install . + echo "Verifying installation..." + mamba run -n unilab pip show unilabos + + - name: Install documentation dependencies + run: | + echo "Installing documentation build dependencies..." + mamba run -n unilab pip install -r docs/requirements.txt + + - name: Display environment info + run: | + echo "=== Environment Information ===" + mamba env list + echo "" + echo "=== Installed Packages ===" + mamba list -n unilab | grep -E "(unilabos|ros-humble|control_msgs|nav2_msgs)" || mamba list -n unilab + echo "" + echo "=== Python Packages ===" + mamba run -n unilab pip list | grep unilabos || mamba run -n unilab pip list - name: Setup Pages id: pages @@ -68,8 +99,8 @@ jobs: cd docs # Clean previous builds rm -rf _build - # Build HTML documentation - python -m sphinx -b html . _build/html -v + # Build HTML documentation in conda environment + mamba run -n unilab python -m sphinx -b html . _build/html -v - name: Check build results run: | From 30cb745c6d1f8e3eecd497af060b0de6ce1a0465 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:28:55 +0800 Subject: [PATCH 33/33] Update deploy-docs.yml --- .github/workflows/deploy-docs.yml | 16 ---------------- docs/conf.py | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index dce45ec9..66aef8d6 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -53,11 +53,6 @@ jobs: auto-update-conda: false show-channel-urls: true - - name: Install system dependencies - run: | - sudo apt-get update - sudo apt-get install -y pandoc - - name: Install unilabos and dependencies run: | echo "Installing unilabos and dependencies to unilab environment..." @@ -78,17 +73,6 @@ jobs: echo "Installing documentation build dependencies..." mamba run -n unilab pip install -r docs/requirements.txt - - name: Display environment info - run: | - echo "=== Environment Information ===" - mamba env list - echo "" - echo "=== Installed Packages ===" - mamba list -n unilab | grep -E "(unilabos|ros-humble|control_msgs|nav2_msgs)" || mamba list -n unilab - echo "" - echo "=== Python Packages ===" - mamba run -n unilab pip list | grep unilabos || mamba run -n unilab pip list - - name: Setup Pages id: pages uses: actions/configure-pages@v4 diff --git a/docs/conf.py b/docs/conf.py index a6dc55a9..c6b7d50a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ "myst_parser", "sphinx.ext.autodoc", "sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings - "sphinx_rtd_theme" + "sphinx_rtd_theme", ] source_suffix = {