From d4415f5a35f1dc107af2ab125b2d2568ed6094b0 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Fri, 17 Oct 2025 03:08:15 +0800 Subject: [PATCH] Fix/update resource (#112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cancel upload_registry * Refactor Bioyond workstation and experiment workflow -fix (#111) * refactor(bioyond_studio): 优化材料缓存加载和参数验证逻辑 改进材料缓存加载逻辑以支持多种材料类型和详细材料处理 更新工作流参数验证中的字段名从key/value改为Key/DisplayValue 移除未使用的merge_workflow_with_parameters方法 添加get_station_info方法获取工作站基础信息 清理实验文件中的注释代码和更新导入路径 * fix: 修复资源移除时的父资源检查问题 在BaseROS2DeviceNode中,移除资源前添加对父资源是否为None的检查,避免空指针异常 同时更新Bottle和BottleCarrier类以支持**kwargs参数 修正测试文件中Liquid_feeding_beaker的大小写拼写错误 * correct return message --------- Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com> --- .../experiments/reaction_station_bioyond.json | 6 +- unilabos/app/main.py | 2 +- .../workstation/bioyond_studio/bioyond_rpc.py | 43 +- .../workstation/bioyond_studio/experiment.py | 334 +++---- .../bioyond_studio/reaction_station.py | 881 ++++++++++-------- .../workstation/bioyond_studio/station.py | 91 +- unilabos/resources/itemized_carrier.py | 2 + unilabos/ros/nodes/base_device_node.py | 20 +- 8 files changed, 727 insertions(+), 652 deletions(-) diff --git a/test/experiments/reaction_station_bioyond.json b/test/experiments/reaction_station_bioyond.json index f09aeb91..013855ed 100644 --- a/test/experiments/reaction_station_bioyond.json +++ b/test/experiments/reaction_station_bioyond.json @@ -10,7 +10,7 @@ "type": "device", "class": "reaction_station.bioyond", "config": { - "bioyond_config": { + "config": { "api_key": "DE9BDDA0", "api_host": "http://192.168.1.200:44402", "workflow_mappings": { @@ -19,8 +19,8 @@ "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", - "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046", - "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", + "Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6", + "liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" }, "material_type_mappings": { diff --git a/unilabos/app/main.py b/unilabos/app/main.py index b65da8e9..c646518f 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -356,7 +356,7 @@ def main(): if BasicConfig.upload_registry: # 设备注册到服务端 - 需要 ak 和 sk - if args_dict.get("ak") and args_dict.get("sk"): + if BasicConfig.ak and BasicConfig.sk: print_status("开始注册设备到服务端...", "info") try: register_devices_and_resources(lab_registry) diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index e12f14b2..45d0cadb 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -334,8 +334,8 @@ class BioyondV1RPC(BaseRequest): for j, param in enumerate(params): if not isinstance(param, dict): workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 参数 {j} 必须是字典类型") - elif "key" not in param or "value" not in param: - workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 参数 {j} 必须包含 key 和 value") + elif "Key" not in param or "DisplayValue" not in param: + workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 参数 {j} 必须包含 Key 和 DisplayValue") if workflow_errors: validation_errors.append({ @@ -703,20 +703,35 @@ class BioyondV1RPC(BaseRequest): """预加载材料列表到缓存中""" try: print("正在加载材料列表缓存...") - stock_query = '{"typeMode": 2, "includeDetail": true}' - stock_result = self.stock_material(stock_query) + + # 加载所有类型的材料:耗材(0)、样品(1)、试剂(2) + material_types = [1, 2] + + for type_mode in material_types: + print(f"正在加载类型 {type_mode} 的材料...") + stock_query = f'{{"typeMode": {type_mode}, "includeDetail": true}}' + stock_result = self.stock_material(stock_query) - if isinstance(stock_result, str): - stock_data = json.loads(stock_result) - else: - stock_data = stock_result + if isinstance(stock_result, str): + stock_data = json.loads(stock_result) + else: + stock_data = stock_result - materials = stock_data - for material in materials: - material_name = material.get("name") - material_id = material.get("id") - if material_name and material_id: - self.material_cache[material_name] = material_id + materials = stock_data + for material in materials: + material_name = material.get("name") + material_id = material.get("id") + if material_name and material_id: + self.material_cache[material_name] = material_id + + # 处理样品板等容器中的detail材料 + detail_materials = material.get("detail", []) + for detail_material in detail_materials: + detail_name = detail_material.get("name") + detail_id = detail_material.get("detailMaterialId") + if detail_name and detail_id: + self.material_cache[detail_name] = detail_id + print(f"加载detail材料: {detail_name} -> ID: {detail_id}") print(f"材料列表缓存加载完成,共加载 {len(self.material_cache)} 个材料") diff --git a/unilabos/devices/workstation/bioyond_studio/experiment.py b/unilabos/devices/workstation/bioyond_studio/experiment.py index 92e52b45..28ef3323 100644 --- a/unilabos/devices/workstation/bioyond_studio/experiment.py +++ b/unilabos/devices/workstation/bioyond_studio/experiment.py @@ -3,8 +3,8 @@ """ import json -from reaction_station import BioyondReactionStation -from config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS +from unilabos.devices.workstation.bioyond_studio.reaction_station import BioyondReactionStation +from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS def run_experiment(): @@ -45,7 +45,7 @@ def run_experiment(): assign_material_name="ODA", time="0", torque_variation="1", - titrationType="1", + titration_type="1", temperature=-10 ) @@ -56,14 +56,14 @@ def run_experiment(): assign_material_name="MPDA", time="5", torque_variation="2", - titrationType="1", + titration_type="1", temperature=0 ) # 4. 液体投料-小瓶非滴定 print("4. 添加液体投料-小瓶非滴定,带参数...") Bioyond.liquid_feeding_vials_non_titration( - volumeFormula="639.5", + volume_formula="639.5", assign_material_name="SIDA", titration_type="1", time="0", @@ -91,7 +91,7 @@ def run_experiment(): assign_material_name="BTDA1", temperature=-10.00 ) -#二杆,样品版90 + print("7. 添加固体进料小瓶,带参数...") Bioyond.solid_feeding_vials( material_id="3", @@ -100,7 +100,7 @@ def run_experiment(): assign_material_name="BTDA2", temperature=25.00 ) -#二杆,样品版90 + print("8. 添加固体进料小瓶,带参数...") Bioyond.solid_feeding_vials( material_id="3", @@ -196,8 +196,8 @@ def run_experiment(): print("\n4. 执行process_and_execute_workflow...") result = Bioyond.process_and_execute_workflow( - workflow_name="test3_8", - task_name="实验3_8" + workflow_name="test3", + task_name="实验3" ) # 显示执行结果 @@ -207,9 +207,9 @@ def run_experiment(): result_dict = json.loads(result) if result_dict.get("success"): print("任务创建成功!") - print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") - print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") - print(f"- 任务结果: {result_dict.get('task')}") + # print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") + # print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") + # print(f"- 任务结果: {result_dict.get('task')}") else: print(f"任务创建失败: {result_dict.get('error')}") except: @@ -229,166 +229,166 @@ def run_experiment(): return Bioyond -def prepare_materials(bioyond): - """准备实验材料(可选)""" +# def prepare_materials(bioyond): +# """准备实验材料(可选)""" - # 样品板材料数据定义 - material_data_yp_1 = { - "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", - "name": "样品板-1", - "unit": "个", - "quantity": 1, - "details": [ - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BPDA-DD-1", - "quantity": 1, - "x": 1, - "y": 1, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "PEPA", - "quantity": 1, - "x": 1, - "y": 2, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BPDA-DD-2", - "quantity": 1, - "x": 1, - "y": 3, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BPDA-1", - "quantity": 1, - "x": 2, - "y": 1, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "PMDA", - "quantity": 1, - "x": 2, - "y": 2, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BPDA-2", - "quantity": 1, - "x": 2, - "y": 3, - "Parameters": "{\"molecular\": 1}" - } - ], - "Parameters": "{}" - } +# # 样品板材料数据定义 +# material_data_yp_1 = { +# "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", +# "name": "样品板-1", +# "unit": "个", +# "quantity": 1, +# "details": [ +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BPDA-DD-1", +# "quantity": 1, +# "x": 1, +# "y": 1, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "PEPA", +# "quantity": 1, +# "x": 1, +# "y": 2, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BPDA-DD-2", +# "quantity": 1, +# "x": 1, +# "y": 3, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BPDA-1", +# "quantity": 1, +# "x": 2, +# "y": 1, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "PMDA", +# "quantity": 1, +# "x": 2, +# "y": 2, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BPDA-2", +# "quantity": 1, +# "x": 2, +# "y": 3, +# "Parameters": "{\"molecular\": 1}" +# } +# ], +# "Parameters": "{}" +# } - material_data_yp_2 = { - "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", - "name": "样品板-2", - "unit": "个", - "quantity": 1, - "details": [ - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BPDA-DD", - "quantity": 1, - "x": 1, - "y": 1, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "SIDA", - "quantity": 1, - "x": 1, - "y": 2, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BTDA-1", - "quantity": 1, - "x": 2, - "y": 1, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BTDA-2", - "quantity": 1, - "x": 2, - "y": 2, - "Parameters": "{\"molecular\": 1}" - }, - { - "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", - "name": "BTDA-3", - "quantity": 1, - "x": 2, - "y": 3, - "Parameters": "{\"molecular\": 1}" - } - ], - "Parameters": "{}" - } +# material_data_yp_2 = { +# "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", +# "name": "样品板-2", +# "unit": "个", +# "quantity": 1, +# "details": [ +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BPDA-DD", +# "quantity": 1, +# "x": 1, +# "y": 1, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "SIDA", +# "quantity": 1, +# "x": 1, +# "y": 2, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BTDA-1", +# "quantity": 1, +# "x": 2, +# "y": 1, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BTDA-2", +# "quantity": 1, +# "x": 2, +# "y": 2, +# "Parameters": "{\"molecular\": 1}" +# }, +# { +# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", +# "name": "BTDA-3", +# "quantity": 1, +# "x": 2, +# "y": 3, +# "Parameters": "{\"molecular\": 1}" +# } +# ], +# "Parameters": "{}" +# } - # 烧杯材料数据定义 - beaker_materials = [ - { - "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", - "name": "PDA-1", - "unit": "微升", - "quantity": 1, - "parameters": "{\"DeviceMaterialType\":\"NMP\"}" - }, - { - "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", - "name": "TFDB", - "unit": "微升", - "quantity": 1, - "parameters": "{\"DeviceMaterialType\":\"NMP\"}" - }, - { - "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", - "name": "ODA", - "unit": "微升", - "quantity": 1, - "parameters": "{\"DeviceMaterialType\":\"NMP\"}" - }, - { - "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", - "name": "MPDA", - "unit": "微升", - "quantity": 1, - "parameters": "{\"DeviceMaterialType\":\"NMP\"}" - }, - { - "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", - "name": "PDA-2", - "unit": "微升", - "quantity": 1, - "parameters": "{\"DeviceMaterialType\":\"NMP\"}" - } - ] +# # 烧杯材料数据定义 +# beaker_materials = [ +# { +# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", +# "name": "PDA-1", +# "unit": "微升", +# "quantity": 1, +# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" +# }, +# { +# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", +# "name": "TFDB", +# "unit": "微升", +# "quantity": 1, +# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" +# }, +# { +# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", +# "name": "ODA", +# "unit": "微升", +# "quantity": 1, +# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" +# }, +# { +# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", +# "name": "MPDA", +# "unit": "微升", +# "quantity": 1, +# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" +# }, +# { +# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", +# "name": "PDA-2", +# "unit": "微升", +# "quantity": 1, +# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" +# } +# ] - # 如果需要,可以在这里调用add_material方法添加材料 - # 例如: - # result = bioyond.add_material(json.dumps(material_data_yp_1)) - # print(f"添加材料结果: {result}") +# # 如果需要,可以在这里调用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 - } +# return { +# "sample_plates": [material_data_yp_1, material_data_yp_2], +# "beakers": beaker_materials +# } if __name__ == "__main__": diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station.py index 5bb8709c..4bfe92f6 100644 --- a/unilabos/devices/workstation/bioyond_studio/reaction_station.py +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station.py @@ -1,38 +1,39 @@ import json +import requests from typing import List, Dict, Any from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation from unilabos.devices.workstation.bioyond_studio.config import ( WORKFLOW_STEP_IDS, - WORKFLOW_TO_SECTION_MAP + WORKFLOW_TO_SECTION_MAP, + ACTION_NAMES ) +from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG class BioyondReactionStation(BioyondWorkstation): """Bioyond反应站类 - 继承自BioyondWorkstation,提供反应站特定的业务方法 + 继承自BioyondWorkstation,提供反应站特定的业务方法 """ - def __init__(self, config: dict = None, deck=None): + def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): """初始化反应站 Args: - config: 配置字典,应包含workflow_mappings等配置 + config: 配置字典,应包含workflow_mappings等配置 deck: Deck对象 + protocol_type: 协议类型(由ROS系统传递,此处忽略) + **kwargs: 其他可能的参数 """ - # 如果 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)}") @@ -50,18 +51,33 @@ class BioyondReactionStation(BioyondWorkstation): def reactor_taken_in( self, assign_material_name: str, - cutoff: str = "900000", + cutoff: str = "900000", temperature: float = -10.00 ): """反应器放入 Args: - assign_material_name: 物料名称 - cutoff: 截止参数 - temperature: 温度 + assign_material_name: 物料名称(不能为空) + cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000") + temperature: 温度上限(°C,范围:-50.00 至 100.00) + + Returns: + str: JSON 字符串,格式为 {"suc": True} + + Raises: + ValueError: 若物料名称无效或 cutoff 格式错误 """ + if not assign_material_name: + raise ValueError("物料名称不能为空") + try: + float(cutoff) + except ValueError: + raise ValueError("cutoff 必须是有效的数字字符串") + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") if isinstance(temperature, str): temperature = float(temperature) @@ -69,11 +85,15 @@ class BioyondReactionStation(BioyondWorkstation): step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] reactor_taken_in_params = { "param_values": { - step_id: [ - {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, - {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} - ] + step_id: { + ACTION_NAMES["reactor_taken_in"]["config"]: [ + {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ], + ACTION_NAMES["reactor_taken_in"]["stirring"]: [ + {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } @@ -86,92 +106,102 @@ class BioyondReactionStation(BioyondWorkstation): self, material_id: str, time: str = "0", - torque_variation: str = "1", + torque_variation: int = 1, assign_material_name: str = None, temperature: float = 25.00 ): """固体进料小瓶 Args: - material_id: 物料ID - time: 时间 - torque_variation: 扭矩变化 - assign_material_name: 物料名称 - temperature: 温度 + material_id: 粉末类型ID + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + assign_material_name: 物料名称(用于获取试剂瓶位ID) + temperature: 温度上限(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') - material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) + material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None if isinstance(temperature, str): temperature = float(temperature) - feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] - observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] - + feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] + observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] + solid_feeding_vials_params = { "param_values": { - feeding_id: [ - {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] + feeding_step_id: { + ACTION_NAMES["solid_feeding_vials"]["feeding"]: [ + {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} if material_id_m else {} + ] + }, + observe_step_id: { + ACTION_NAMES["solid_feeding_vials"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } self.pending_task_params.append(solid_feeding_vials_params) - print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C") + print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, torque={torque_variation}, temp={temperature:.2f}°C") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) def liquid_feeding_vials_non_titration( self, - volumeFormula: str, + volume_formula: str, assign_material_name: str, - titration_type: str = "1", + titration_type: str = "1", time: str = "0", - torque_variation: str = "1", + torque_variation: int = 1, temperature: float = 25.00 ): """液体进料小瓶(非滴定) Args: - volumeFormula: 体积公式 + volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 滴定类型 - time: 时间 - torque_variation: 扭矩变化 - temperature: 温度 + titration_type: 是否滴定(1=滴定, 其他=非滴定) + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") if isinstance(temperature, str): temperature = float(temperature) - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] - + liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] + observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] + params = { "param_values": { - liquid_id: [ - {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] + liquid_step_id: { + ACTION_NAMES["liquid_feeding_vials_non_titration"]["liquid"]: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} + ] + }, + observe_step_id: { + ACTION_NAMES["liquid_feeding_vials_non_titration"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } self.pending_task_params.append(params) - print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}") + print(f"成功添加液体进料小瓶(非滴定)参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) @@ -181,40 +211,46 @@ class BioyondReactionStation(BioyondWorkstation): volume: str, titration_type: str = "1", time: str = "360", - torque_variation: str = "2", + torque_variation: int = 2, temperature: float = 25.00 ): - """液体进料溶剂 + """液体进料-溶剂 Args: assign_material_name: 物料名称 - volume: 体积 - titration_type: 滴定类型 - time: 时间 - torque_variation: 扭矩变化 - temperature: 温度 + volume: 分液量(μL) + titration_type: 是否滴定 + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + temperature: 温度上限(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") if isinstance(temperature, str): temperature = float(temperature) - - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] - + + liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] + observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] + params = { "param_values": { - liquid_id: [ - {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, - {"m": 0, "n": 1, "Key": "volume", "Value": volume}, - {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] + liquid_step_id: { + ACTION_NAMES["liquid_feeding_solvents"]["liquid"]: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 1, "Key": "volume", "Value": volume}, + {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} + ] + }, + observe_step_id: { + ACTION_NAMES["liquid_feeding_solvents"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } @@ -229,40 +265,46 @@ class BioyondReactionStation(BioyondWorkstation): assign_material_name: str, titration_type: str = "1", time: str = "90", - torque_variation: int = 2, + torque_variation: int = 2, temperature: float = 25.00 ): """液体进料(滴定) Args: - volume_formula: 体积公式 + volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 滴定类型 - time: 时间 - torque_variation: 扭矩变化 - temperature: 温度 + titration_type: 是否滴定 + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") if isinstance(temperature, str): temperature = float(temperature) - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] + liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] + observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] params = { "param_values": { - liquid_id: [ - {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] + liquid_step_id: { + ACTION_NAMES["liquid_feeding_titration"]["liquid"]: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ] + }, + observe_step_id: { + ACTION_NAMES["liquid_feeding_titration"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } @@ -276,41 +318,47 @@ class BioyondReactionStation(BioyondWorkstation): volume: str = "35000", assign_material_name: str = "BAPP", time: str = "0", - torque_variation: str = "1", - titrationType: str = "1", + torque_variation: int = 1, + titration_type: str = "1", temperature: float = 25.00 ): """液体进料烧杯 Args: - volume: 体积 - assign_material_name: 物料名称 - time: 时间 - torque_variation: 扭矩变化 - titrationType: 滴定类型 - temperature: 温度 + volume: 分液量(μL) + assign_material_name: 物料名称(试剂瓶位) + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + titration_type: 是否滴定 + temperature: 温度上限(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") if isinstance(temperature, str): temperature = float(temperature) - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] + liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] + observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] params = { "param_values": { - liquid_id: [ - {"m": 0, "n": 2, "Key": "volume", "Value": volume}, - {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] + liquid_step_id: { + ACTION_NAMES["liquid_feeding_beaker"]["liquid"]: [ + {"m": 0, "n": 2, "Key": "volume", "Value": volume}, + {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 2, "Key": "titrationType", "Value": titration_type} + ] + }, + observe_step_id: { + ACTION_NAMES["liquid_feeding_beaker"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } } } @@ -318,6 +366,60 @@ class BioyondReactionStation(BioyondWorkstation): print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) + + def drip_back( + self, + assign_material_name: str, + volume: str, + titration_type: str = "1", + time: str = "90", + torque_variation: int = 2, + temperature: float = 25.00 + ): + """滴回去 + + Args: + assign_material_name: 物料名称(液体种类) + volume: 分液量(μL) + titration_type: 是否滴定 + time: 观察时间(分钟) + torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + temperature: 温度(°C) + """ + self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}') + material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) + if material_id is None: + raise ValueError(f"无法找到物料 {assign_material_name} 的 ID") + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_step_id = WORKFLOW_STEP_IDS["drip_back"]["liquid"] + observe_step_id = WORKFLOW_STEP_IDS["drip_back"]["observe"] + + params = { + "param_values": { + liquid_step_id: { + ACTION_NAMES["drip_back"]["liquid"]: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 1, "Key": "volume", "Value": volume} + ] + }, + observe_step_id: { + ACTION_NAMES["drip_back"]["observe"]: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + } + + self.pending_task_params.append(params) + print(f"成功添加滴回去参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) # ==================== 工作流管理方法 ==================== @@ -330,7 +432,9 @@ class BioyondReactionStation(BioyondWorkstation): 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)) + workflow_name = id_to_name.get(workflow_id, workflow_id) + workflow_names.append(workflow_name) + print(f"工作流序列: {workflow_names}") return workflow_names def workflow_step_query(self, workflow_id: str) -> dict: @@ -357,283 +461,290 @@ class BioyondReactionStation(BioyondWorkstation): # ==================== 工作流执行核心方法 ==================== - # 发布任务 - 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: - """合并当前工作流序列 + def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]: + """处理网页工作流列表 Args: - json_str: 包含name等参数的JSON字符串 + web_workflow_json: JSON 格式的网页工作流列表 Returns: - 合并结果 + List[Dict[str, str]]: 包含工作流 ID 和名称的字典列表 + """ + try: + web_workflow_data = json.loads(web_workflow_json) + web_workflow_list = web_workflow_data.get("web_workflow_list", []) + workflows_result = [] + for name in web_workflow_list: + workflow_id = self.workflow_mappings.get(name, "") + if not workflow_id: + print(f"警告:未找到工作流名称 {name} 对应的 ID") + continue + workflows_result.append({"id": workflow_id, "name": name}) + print(f"process_web_workflows 输出: {workflows_result}") + return workflows_result + except json.JSONDecodeError as e: + print(f"错误:无法解析 web_workflow_json: {e}") + return [] + except Exception as e: + print(f"错误:处理工作流失败: {e}") + return [] + + def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: + """ + 一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务 + + Args: + workflow_name: 合并后的工作流名称 + task_name: 任务名称 + + Returns: + 任务创建结果 + """ + web_workflow_list = self.get_workflow_sequence() + print(f"\n{'='*60}") + print(f"📋 处理网页工作流列表: {web_workflow_list}") + print(f"{'='*60}") + + web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) + workflows_result = self.process_web_workflows(web_workflow_json) + + if not workflows_result: + return self._create_error_result("处理网页工作流列表失败", "process_web_workflows") + + print(f"workflows_result 类型: {type(workflows_result)}") + print(f"workflows_result 内容: {workflows_result}") + + workflows_with_params = self._build_workflows_with_parameters(workflows_result) + + merge_data = { + "name": workflow_name, + "workflows": workflows_with_params + } + + # print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}") + merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data)) + + if not merged_workflow: + return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters") + + workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") + # print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})") + + order_params = [{ + "orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}", + "orderName": task_name, + "workFlowId": workflow_id, + "borderNumber": 1, + "paramValues": {} + }] + + result = self.create_order(json.dumps(order_params)) + + if not result: + return self._create_error_result("创建任务失败", "create_order") + + self.pending_task_params = [] + + # print(f"\n✅ 任务创建成功: {result}") + # print(f"\n✅ 任务创建成功") + print(f"{'='*60}\n") + return json.dumps({"success": True, "result": result}) + + def _build_workflows_with_parameters(self, workflows_result: list) -> list: + """ + 构建带参数的工作流列表 + + Args: + workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表) + + Returns: + 符合新接口格式的工作流参数结构 + """ + workflows_with_params = [] + total_params = 0 + successful_params = 0 + failed_params = [] + + for idx, workflow_info in enumerate(workflows_result): + if not isinstance(workflow_info, dict): + print(f"错误:workflows_result[{idx}] 不是字典,而是 {type(workflow_info)}: {workflow_info}") + continue + workflow_id = workflow_info.get("id") + if not workflow_id: + print(f"警告:workflows_result[{idx}] 缺少 'id' 键") + continue + workflow_name = workflow_info.get("name", "") + # print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})") + + if idx >= len(self.pending_task_params): + # print(f" ⚠️ 无对应参数,跳过") + workflows_with_params.append({"id": workflow_id}) + continue + + param_data = self.pending_task_params[idx] + param_values = param_data.get("param_values", {}) + if not param_values: + # print(f" ⚠️ 参数为空,跳过") + workflows_with_params.append({"id": workflow_id}) + continue + + step_parameters = {} + for step_id, actions_dict in param_values.items(): + # print(f" 📍 步骤ID: {step_id}") + for action_name, param_list in actions_dict.items(): + # print(f" 🔹 模块: {action_name}, 参数数量: {len(param_list)}") + if step_id not in step_parameters: + step_parameters[step_id] = {} + if action_name not in step_parameters[step_id]: + step_parameters[step_id][action_name] = [] + for param_item in param_list: + param_key = param_item.get("Key", "") + param_value = param_item.get("Value", "") + total_params += 1 + step_parameters[step_id][action_name].append({ + "Key": param_key, + "DisplayValue": param_value + }) + successful_params += 1 + # print(f" ✓ {param_key} = {param_value}") + + workflows_with_params.append({ + "id": workflow_id, + "stepParameters": step_parameters + }) + + self._print_mapping_stats(total_params, successful_params, failed_params) + return workflows_with_params + + def _print_mapping_stats(self, total: int, success: int, failed: list): + """打印参数映射统计""" + print(f"\n{'='*20} 参数映射统计 {'='*20}") + print(f"📊 总参数数量: {total}") + print(f"✅ 成功映射: {success}") + print(f"❌ 映射失败: {len(failed)}") + if not failed: + print("🎉 成功映射所有参数!") + else: + print(f"⚠️ 失败的参数: {', '.join(failed)}") + success_rate = (success/total*100) if total > 0 else 0 + print(f"📈 映射成功率: {success_rate:.1f}%") + print("="*60) + + def _create_error_result(self, error_msg: str, step: str) -> str: + """创建统一的错误返回格式""" + print(f"❌ {error_msg}") + return json.dumps({ + "success": False, + "error": f"process_and_execute_workflow: {error_msg}", + "method": "process_and_execute_workflow", + "step": step + }) + + def merge_workflow_with_parameters(self, json_str: str) -> dict: + """ + 调用新接口:合并工作流并传递参数 + + Args: + json_str: JSON格式的字符串,包含: + - name: 工作流名称 + - workflows: [{"id": "工作流ID", "stepParameters": {...}}] + + 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, + + # 在工作流名称后面添加时间戳,避免重复 + if "name" in data and data["name"]: + timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-") + original_name = data["name"] + data["name"] = f"{original_name}_{timestamp}" + print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}") + + request_data = { + "apiKey": API_CONFIG["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), - "data": params, - }) + "data": data + } + print(f"\n📤 发送合并请求:") + print(f" 工作流名称: {data.get('name')}") + print(f" 子工作流数量: {len(data.get('workflows', []))}") + + # 打印完整的POST请求内容 + print(f"\n🔍 POST请求详细内容:") + print(f" URL: {self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters") + print(f" Headers: {{'Content-Type': 'application/json'}}") + print(f" Request Data:") + print(f" {json.dumps(request_data, indent=4, ensure_ascii=False)}") + # + response = requests.post( + f"{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters", + json=request_data, + headers={"Content-Type": "application/json"}, + timeout=30 + ) + + # # 打印响应详细内容 + # print(f"\n📥 POST响应详细内容:") + # print(f" 状态码: {response.status_code}") + # print(f" 响应头: {dict(response.headers)}") + # print(f" 响应体: {response.text}") + # # + try: + result = response.json() + # # + # print(f"\n📋 解析后的响应JSON:") + # print(f" {json.dumps(result, indent=4, ensure_ascii=False)}") + # # + except json.JSONDecodeError: + print(f"❌ 服务器返回非 JSON 格式响应: {response.text}") + return None + + if result.get("code") == 1: + print(f"✅ 工作流合并成功(带参数)") + return result.get("data", {}) + else: + error_msg = result.get('message', '未知错误') + print(f"❌ 工作流合并失败: {error_msg}") + return None + + except requests.exceptions.Timeout: + print(f"❌ 合并工作流请求超时") + return None + except requests.exceptions.RequestException as e: + print(f"❌ 合并工作流网络异常: {str(e)}") + return None + except json.JSONDecodeError as e: + print(f"❌ 合并工作流响应解析失败: {str(e)}") + return None + except Exception as e: + print(f"❌ 合并工作流异常: {str(e)}") + return None - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - def generate_task_param_values(self, workflow_params_structure: dict) -> dict: - """生成任务参数值 - - 根据工作流参数结构和待处理的任务参数,生成最终的任务参数值 + def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool: + """验证工作流ID是否有效,如果无效则重新合并 Args: - workflow_params_structure: 工作流参数结构 + workflow_name: 工作流名称 Returns: - 任务参数值字典 + bool: 验证或刷新是否成功 """ - 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 + print(f"\n🔍 验证工作流ID有效性...") + if not self.workflow_sequence: + print(f" ⚠️ 工作流序列为空,需要重新合并") + return False + first_workflow_id = self.workflow_sequence[0] + try: + structure = self.workflow_step_query(first_workflow_id) + if structure: + print(f" ✅ 工作流ID有效") + return True + else: + print(f" ⚠️ 工作流ID已过期,需要重新合并") + return False + except Exception as e: + print(f" ❌ 工作流ID验证失败: {e}") + print(f" 💡 将重新合并工作流") + return False \ 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 910fdb3a..33975d8c 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -245,79 +245,7 @@ class BioyondWorkstation(WorkstationBase): } # ==================== 工作流合并与参数设置 API ==================== - - def merge_workflow_with_parameters(self, json_str: str) -> dict: - """合并工作流并设置参数""" - try: - # 解析输入的 JSON 数据 - data = json.loads(json_str) - - # 构造 API 请求参数 - params = { - "name": data.get("name", ""), - "workflows": data.get("workflows", []) - } - - # 验证必要参数 - if not params["name"]: - return { - "code": 0, - "message": "工作流名称不能为空", - "timestamp": int(datetime.now().timestamp() * 1000) - } - - if not params["workflows"]: - return { - "code": 0, - "message": "工作流列表不能为空", - "timestamp": int(datetime.now().timestamp() * 1000) - } - - except json.JSONDecodeError as e: - return { - "code": 0, - "message": f"JSON 解析错误: {str(e)}", - "timestamp": int(datetime.now().timestamp() * 1000) - } - except Exception as e: - return { - "code": 0, - "message": f"参数处理错误: {str(e)}", - "timestamp": int(datetime.now().timestamp() * 1000) - } - - # 发送 POST 请求到 Bioyond API - try: - 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: - return { - "code": 0, - "message": "API 请求失败,未收到响应", - "timestamp": int(datetime.now().timestamp() * 1000) - } - - # 返回完整的响应结果 - return { - "code": response.get("code", 0), - "message": response.get("message", ""), - "timestamp": response.get("timestamp", int(datetime.now().timestamp() * 1000)) - } - - except Exception as e: - return { - "code": 0, - "message": f"API 请求异常: {str(e)}", - "timestamp": int(datetime.now().timestamp() * 1000) - } - + def append_to_workflow_sequence(self, web_workflow_name: str) -> bool: # 检查是否为JSON格式的字符串 actual_workflow_name = web_workflow_name @@ -391,6 +319,23 @@ class BioyondWorkstation(WorkstationBase): # ==================== 基础物料管理接口 ==================== # ============ 工作站状态管理 ============ + def get_station_info(self) -> Dict[str, Any]: + """获取工作站基础信息 + + Returns: + Dict[str, Any]: 工作站基础信息,包括设备ID、状态等 + """ + return { + "device_id": getattr(self._ros_node, 'device_id', 'unknown'), + "station_type": "BioyondWorkstation", + "workflow_status": self.current_workflow_status.value if hasattr(self, 'current_workflow_status') else "unknown", + "is_busy": getattr(self, 'is_busy', False), + "deck_info": { + "name": self.deck.name if self.deck and hasattr(self.deck, 'name') else "unknown", + "children_count": len(self.deck.children) if self.deck and hasattr(self.deck, 'children') else 0 + } if self.deck else None, + "hardware_interface": type(self.hardware_interface).__name__ if self.hardware_interface else None + } def get_workstation_status(self) -> Dict[str, Any]: """获取工作站状态 diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 17f63cf4..44074b53 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -32,6 +32,7 @@ class Bottle(Well): barcode: Optional[str] = "", category: str = "container", model: Optional[str] = None, + **kwargs, ): super().__init__( name=name, @@ -427,6 +428,7 @@ class BottleCarrier(ItemizedCarrier): sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, category: str = "bottle_carrier", model: Optional[str] = None, + **kwargs, ): super().__init__( name=name, diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 5900acac..8523a66f 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -582,6 +582,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): - update: 更新现有资源 - remove: 从资源树中移除资源 """ + from pylabrobot.resources.resource import Resource as ResourcePLR try: data = json.loads(req.command) results = [] @@ -662,22 +663,23 @@ class BaseROS2DeviceNode(Node, Generic[T]): ) 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) + for found_resource in found_resources: + for resource in found_resource: + if issubclass(resource.__class__, ResourcePLR): + found_plr_resources.append(resource) else: - other_plr_resources.append(res) + other_plr_resources.append(resource) func = getattr(self.driver_instance, "resource_tree_remove", None) if callable(func): func(found_plr_resources) for plr_resource in found_plr_resources: - plr_resource.parent.unassign_child_resource(plr_resource) + if plr_resource.parent is not None: + 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} 及其子节点") + for other_plr_resource in other_plr_resources: + self.resource_tracker.remove_resource(other_plr_resource) + self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点") results.append({"success": True, "action": "remove"}) except Exception as e: error_msg = f"Error processing {action} operation: {str(e)}"