diff --git a/docs/developer_guide/add_action.md b/docs/developer_guide/add_action.md index 0e39e119..94a9110a 100644 --- a/docs/developer_guide/add_action.md +++ b/docs/developer_guide/add_action.md @@ -127,16 +127,16 @@ add_action_files( ```bash mamba remove --force ros-humble-unilabos-msgs mamba config set safety_checks disabled # 如果没有提升版本号,会触发md5与网络上md5不一致,是正常现象,因此通过本指令关闭md5检查 -mamba install xxx.conda2 --offline +mamba install xxx.conda --offline ``` ## 常见问题 -**Q: 构建失败怎么办?** +**Q: 构建失败怎么办?** A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。 -**Q: 如何测试特定平台?** +**Q: 如何测试特定平台?** A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。 -**Q: 构建包在哪里下载?** +**Q: 构建包在哪里下载?** A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。 diff --git a/test/experiments/dispensing_station_bioyond.json b/test/experiments/dispensing_station_bioyond.json new file mode 100644 index 00000000..b2f79c80 --- /dev/null +++ b/test/experiments/dispensing_station_bioyond.json @@ -0,0 +1,60 @@ +{ + "nodes": [ + { + "id": "dispensing_station_bioyond", + "name": "dispensing_station_bioyond", + "children": [ + "Bioyond_Dispensing_Deck" + ], + "parent": null, + "type": "device", + "class": "dispensing_station.bioyond", + "config": { + "config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44388" + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Dispensing_Deck", + "_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": {} + }, + { + "id": "Bioyond_Dispensing_Deck", + "name": "Bioyond_Dispensing_Deck", + "sample_id": null, + "children": [], + "parent": "dispensing_station_bioyond", + "type": "deck", + "class": "BIOYOND_PolymerPreparationStation_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_PolymerPreparationStation_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} diff --git a/test/experiments/reaction_station_bioyond.json b/test/experiments/reaction_station_bioyond.json new file mode 100644 index 00000000..2a18d90a --- /dev/null +++ b/test/experiments/reaction_station_bioyond.json @@ -0,0 +1,69 @@ +{ + "nodes": [ + { + "id": "reaction_station_bioyond", + "name": "reaction_station_bioyond", + "parent": null, + "children": [ + "Bioyond_Deck" + ], + "type": "device", + "class": "reaction_station.bioyond", + "config": { + "bioyond_config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44402", + "workflow_mappings": { + "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", + "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", + "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", + "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + }, + "material_type_mappings": { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier" + } + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + } + }, + "protocol_type": [] + }, + "data": {} + }, + { + "id": "Bioyond_Deck", + "name": "Bioyond_Deck", + "sample_id": null, + "children": [ + ], + "parent": "reaction_station_bioyond", + "type": "deck", + "class": "BIOYOND_PolymerReactionStation_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_PolymerReactionStation_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index f545a2ec..12c03860 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -50,14 +50,14 @@ class BioyondV1RPC(BaseRequest): self.pending_task_params = [] self.material_cache = {} self._load_material_cache() - + if "workflow_mappings" in config: self._set_workflow_mappings(config["workflow_mappings"]) - + def _set_workflow_mappings(self, mappings: Dict[str, str]): self.workflow_mappings = mappings print(f"设置工作流映射配置: {mappings}") - + def _get_workflow(self, web_workflow_name: str) -> str: if web_workflow_name not in self.workflow_mappings: print(f"未找到工作流映射配置: {web_workflow_name}") @@ -65,7 +65,7 @@ class BioyondV1RPC(BaseRequest): workflow_id = self.workflow_mappings[web_workflow_name] print(f"获取工作流: {web_workflow_name} -> {workflow_id}") return workflow_id - + def process_web_workflows(self, json_str: str) -> Dict[str, str]: try: data = json.loads(json_str) @@ -73,10 +73,10 @@ class BioyondV1RPC(BaseRequest): except json.JSONDecodeError: print(f"无效的JSON字符串: {json_str}") return {} - + result = {} self.workflow_sequence = [] - + for web_name in web_workflow_list: workflow_id = self._get_workflow(web_name) if workflow_id: @@ -84,49 +84,49 @@ class BioyondV1RPC(BaseRequest): self.workflow_sequence.append(workflow_id) else: print(f"无法获取工作流ID: {web_name}") - + print(f"工作流执行顺序: {self.workflow_sequence}") return result - + def get_workflow_sequence(self) -> List[str]: 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 append_to_workflow_sequence(self, json_str: str) -> bool: try: data = json.loads(json_str) web_workflow_name = data.get("web_workflow_name", "") except: return False - + workflow_id = self._get_workflow(web_workflow_name) if workflow_id: self.workflow_sequence.append(workflow_id) print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}") - + def set_workflow_sequence(self, json_str: str) -> List[str]: try: data = json.loads(json_str) web_workflow_names = data.get("web_workflow_names", []) except: return [] - + sequence = [] for web_name in web_workflow_names: workflow_id = self._get_workflow(web_name) if workflow_id: sequence.append(workflow_id) - + self.workflow_sequence = sequence print(f"设置工作流执行顺序: {self.workflow_sequence}") return self.workflow_sequence.copy() - + def get_all_workflows(self) -> Dict[str, str]: return self.workflow_mappings.copy() - + def clear_workflows(self): self.workflow_sequence = [] print("清空工作流执行顺序") @@ -141,7 +141,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return [] - + response = self.post( url=f'{self.host}/api/lims/storage/stock-material', params={ @@ -160,7 +160,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/work-flow-list', params={ @@ -173,6 +173,134 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) + def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]: + """验证工作流参数格式 + + Args: + workflows (List[Dict[str, Any]]): 工作流列表 + + Returns: + Dict[str, Any]: 验证结果 + """ + try: + validation_errors = [] + + for i, workflow in enumerate(workflows): + workflow_errors = [] + + # 检查基本结构 + if not isinstance(workflow, dict): + workflow_errors.append("工作流必须是字典类型") + continue + + if "id" not in workflow: + workflow_errors.append("缺少必要的 'id' 字段") + + # 检查 stepParameters(如果存在) + if "stepParameters" in workflow: + step_params = workflow["stepParameters"] + + if not isinstance(step_params, dict): + workflow_errors.append("stepParameters 必须是字典类型") + else: + # 验证参数结构 + for step_id, modules in step_params.items(): + if not isinstance(modules, dict): + workflow_errors.append(f"步骤 {step_id} 的模块配置必须是字典类型") + continue + + for module_name, params in modules.items(): + if not isinstance(params, list): + workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 的参数必须是列表类型") + continue + + 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") + + if workflow_errors: + validation_errors.append({ + "workflow_index": i, + "workflow_id": workflow.get("id", "unknown"), + "errors": workflow_errors + }) + + if validation_errors: + return { + "valid": False, + "errors": validation_errors, + "message": f"发现 {len(validation_errors)} 个工作流存在验证错误" + } + else: + return { + "valid": True, + "message": f"所有 {len(workflows)} 个工作流验证通过" + } + + except Exception as e: + return { + "valid": False, + "errors": [{"general_error": str(e)}], + "message": f"验证过程中发生异常: {str(e)}" + } + + def get_workflow_parameter_template(self) -> Dict[str, Any]: + """获取工作流参数模板 + + Returns: + Dict[str, Any]: 参数模板和说明 + """ + return { + "template": { + "name": "拼接后的长工作流的名称", + "workflows": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "stepParameters": { + "步骤ID (UUID)": { + "模块名称": [ + { + "key": "参数键名", + "value": "参数值或变量引用 {{index-m-n}}" + } + ] + } + } + } + ] + }, + "parameter_descriptions": { + "name": "拼接后的长工作流名称", + "workflows": "待合并的子工作流列表", + "id": "子工作流 ID,对应工作流列表中 workflows 数组中每个对象的 id 字段", + "stepParameters": "步骤参数配置,如果子工作流没有参数则不需要填写" + }, + "common_modules": { + "反应模块-开始搅拌": { + "description": "反应模块搅拌控制", + "common_parameters": ["temperature"] + }, + "通量-配置": { + "description": "通量配置模块", + "common_parameters": ["cutoff", "assignMaterialName"] + }, + "烧杯溶液放置位-烧杯吸液分液": { + "description": "烧杯液体处理模块", + "common_parameters": ["titrationType", "assignMaterialName", "volume"] + } + }, + "variable_reference_format": { + "format": "{{index-m-n}}", + "description": { + "index": "该步骤所在子工作流的拼接顺序(从 1 开始)", + "m": "拼接前该步骤在子工作流内部的 m 值", + "n": "拼接前该步骤在子工作流内部的 n 值" + } + } + } + # 工作流步骤查询接口 def workflow_step_query(self, json_str: str) -> dict: try: @@ -180,7 +308,7 @@ class BioyondV1RPC(BaseRequest): workflow_id = data.get("workflow_id", "") except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', params={ @@ -200,15 +328,15 @@ class BioyondV1RPC(BaseRequest): except Exception as e: result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) return result - + print('===============', json.dumps(params)) - + request_params = { "apiKey": self.api_key, "requestTime": self.get_current_time_iso8601(), "data": params } - + response = self.post( url=f'{self.host}/api/lims/order/order', params=request_params) @@ -226,7 +354,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return {} - + response = self.post( url=f'{self.host}/api/lims/order/order-list', params={ @@ -246,7 +374,7 @@ class BioyondV1RPC(BaseRequest): order_id = data.get("order_id", "") except: return {} - + response = self.post( url=f'{self.host}/api/lims/order/order-report', params={ @@ -259,6 +387,32 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) + def material_id_query(self, json_str: str) -> dict: + """ + 查询物料id + json_str 格式为JSON字符串: + '{"material123"}' + """ + params = json_str + + response = self.post( + url=f'{self.host}/api/lims/storage/workflow-sample-locations', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + print(f"material_id_query error: {response.get('message')}") + return {} + + print(f"material_id_query data: {response['data']}") + return response.get("data", {}) + # 任务取出接口 def order_takeout(self, json_str: str) -> int: try: @@ -269,7 +423,7 @@ class BioyondV1RPC(BaseRequest): } except: return 0 - + response = self.post( url=f'{self.host}/api/lims/order/order-takeout', params={ @@ -281,7 +435,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 设备列表查询 def device_list(self, json_str: str = "") -> list: device_no = None @@ -291,11 +445,11 @@ class BioyondV1RPC(BaseRequest): device_no = data.get("device_no", None) except: pass - + url = f'{self.host}/api/lims/device/device-list' if device_no: url += f'/{device_no}' - + response = self.post( url=url, params={ @@ -306,7 +460,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return [] return response.get("data", []) - + # 设备操作 def device_operation(self, json_str: str) -> int: try: @@ -318,7 +472,7 @@ class BioyondV1RPC(BaseRequest): } except: return 0 - + response = self.post( url=f'{self.host}/api/lims/device/device-operation', params={ @@ -330,7 +484,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器状态查询 def scheduler_status(self) -> dict: response = self.post( @@ -343,7 +497,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return {} return response.get("data", {}) - + # 调度器启动 def scheduler_start(self) -> int: response = self.post( @@ -356,7 +510,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器暂停 def scheduler_pause(self) -> int: response = self.post( @@ -369,7 +523,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器继续 def scheduler_continue(self) -> int: response = self.post( @@ -382,7 +536,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器停止 def scheduler_stop(self) -> int: response = self.post( @@ -395,7 +549,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器重置 def scheduler_reset(self) -> int: response = self.post( @@ -408,7 +562,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 取消任务 def cancel_order(self, json_str: str) -> bool: try: @@ -416,7 +570,7 @@ class BioyondV1RPC(BaseRequest): order_id = data.get("order_id", "") except: return False - + response = self.post( url=f'{self.host}/api/lims/order/cancel-order', params={ @@ -428,7 +582,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return False return True - + # 获取可拼接工作流 def query_split_workflow(self) -> list: response = self.post( @@ -441,7 +595,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return [] return str(response.get("data", {})) - + # 合并工作流 def merge_workflow(self, json_str: str) -> dict: try: @@ -452,7 +606,7 @@ class BioyondV1RPC(BaseRequest): } except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/merge-workflow', params={ @@ -464,7 +618,89 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return {} return response.get("data", {}) - + + # 合并工作流并设置参数 API + def merge_workflow_with_parameters(self, json_str: str) -> dict: + """合并工作流并设置参数 + + 调用 Bioyond API: /api/lims/workflow/merge-workflow-with-parameters + + Args: + json_str (str): JSON 字符串,包含工作流合并配置数据 + + Returns: + dict: API 响应结果,包含 code、message 和 timestamp + """ + 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.post( + url=f'{self.host}/api/lims/workflow/merge-workflow-with-parameters', + params={ + "apiKey": self.api_key, + "requestTime": self.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 merge_sequence_workflow(self, json_str: str) -> dict: try: @@ -472,16 +708,16 @@ class BioyondV1RPC(BaseRequest): name = data.get("name", "合并工作流") except: return {} - + if not self.workflow_sequence: print("工作流序列为空,无法合并") return {} - + params = { "name": name, "workflowIds": self.workflow_sequence } - + response = self.post( url=f'{self.host}/api/lims/workflow/merge-workflow', params={ @@ -498,33 +734,33 @@ class BioyondV1RPC(BaseRequest): 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. 合并所有参数并创建任务 workflow_name = merged_workflow.get("name", "") workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") @@ -532,12 +768,12 @@ class BioyondV1RPC(BaseRequest): 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.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}", "orderName": f"实验-{self.get_current_time_iso8601()[:10].replace('-', '')}", @@ -546,7 +782,7 @@ class BioyondV1RPC(BaseRequest): "paramValues": param_values, "extendProperties": "" }] - + task_json = json.dumps(task_params) print(f"创建任务参数: {type(task_json)}") result = self.create_order(task_json) @@ -556,11 +792,11 @@ class BioyondV1RPC(BaseRequest): 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": workflow_name, "id": workflow_id}, @@ -573,23 +809,23 @@ class BioyondV1RPC(BaseRequest): 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) @@ -598,39 +834,39 @@ class BioyondV1RPC(BaseRequest): 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: @@ -638,17 +874,17 @@ class BioyondV1RPC(BaseRequest): 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 = {} @@ -657,7 +893,7 @@ class BioyondV1RPC(BaseRequest): 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 = [] @@ -666,10 +902,10 @@ class BioyondV1RPC(BaseRequest): 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: @@ -689,7 +925,7 @@ class BioyondV1RPC(BaseRequest): m = display_section_index n = step_n print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") - + param_item = { "m": m, "n": n, @@ -697,10 +933,10 @@ class BioyondV1RPC(BaseRequest): "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 @@ -718,7 +954,7 @@ class BioyondV1RPC(BaseRequest): """反应器放入""" self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') material_id = self._get_material_id_by_name(assign_material_name) - + if isinstance(temperature, str): temperature = float(temperature) @@ -737,8 +973,8 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}") 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", + + def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", assign_material_name: str = None, temperature: float = 25.00): """固体进料小瓶""" self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') @@ -749,7 +985,7 @@ class BioyondV1RPC(BaseRequest): feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] - + solid_feeding_vials_params = { "param_values": { feeding_id: [ @@ -769,8 +1005,8 @@ class BioyondV1RPC(BaseRequest): 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", + 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): """液体进料小瓶(非滴定)""" self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') @@ -781,7 +1017,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -801,7 +1037,7 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}") 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): """液体进料溶剂""" @@ -813,7 +1049,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -833,7 +1069,7 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") 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): """液体进料(滴定)""" @@ -845,7 +1081,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -865,9 +1101,9 @@ class BioyondV1RPC(BaseRequest): 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}) - + def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP", - time: str = "0", torque_variation: str = "1", titrationType: str = "1", + time: str = "0", torque_variation: str = "1", titrationType: str = "1", temperature: float = 25.00): """液体进料烧杯""" self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') @@ -878,7 +1114,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -1056,3 +1292,660 @@ class BioyondV1RPC(BaseRequest): def get_logger(self): return self._logger + + # ==================== 配液站特有方法 ==================== + + def sample_waste_removal(self, order_id: str) -> dict: + """ + 样品/废料取出接口 + + 参数: + - order_id: 订单ID + + 返回: 取出结果 + """ + params = {"orderId": order_id} + + response = self.post( + url=f'{self.host}/api/lims/order/take-out', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"样品废料取出错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def dispensing_material_inbound(self, material_id: str, location_id: str) -> dict: + """ + 配液站物料入库接口 + + 参数: + - material_id: 物料ID + - location_id: 库位ID + + 返回: 入库结果 + """ + params = { + "materialId": material_id, + "locationId": location_id + } + + response = self.post( + url=f'{self.host}/api/lims/storage/inbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"配液站物料入库错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def dispensing_material_outbound(self, material_id: str, location_id: str, quantity: int) -> dict: + """ + 配液站物料出库接口 + + 参数: + - material_id: 物料ID + - location_id: 库位ID + - quantity: 出库数量 + + 返回: 出库结果 + """ + params = { + "materialId": material_id, + "locationId": location_id, + "quantity": quantity + } + + response = self.post( + url=f'{self.host}/api/lims/storage/outbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"配液站物料出库错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def create_90_10_vial_feeding_task(self, + order_name: str = None, + speed: str = None, + temperature: str = None, + delay_time: str = None, + percent_90_1_assign_material_name: str = None, + percent_90_1_target_weigh: str = None, + percent_90_2_assign_material_name: str = None, + percent_90_2_target_weigh: str = None, + percent_90_3_assign_material_name: str = None, + percent_90_3_target_weigh: str = None, + percent_10_1_assign_material_name: str = None, + percent_10_1_target_weigh: str = None, + percent_10_1_volume: str = None, + percent_10_1_liquid_material_name: str = None, + percent_10_2_assign_material_name: str = None, + percent_10_2_target_weigh: str = None, + percent_10_2_volume: str = None, + percent_10_2_liquid_material_name: str = None, + percent_10_3_assign_material_name: str = None, + percent_10_3_target_weigh: str = None, + percent_10_3_volume: str = None, + percent_10_3_liquid_material_name: str = None, + hold_m_name: str = None) -> dict: + """ + 创建90%10%小瓶投料任务 + + 参数说明: + - order_name: 任务名称,如果为None则使用默认名称 + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值40 + - delay_time: 延迟时间,如果为None则使用默认值600 + - percent_90_1_assign_material_name: 90%_1物料名称 + - percent_90_1_target_weigh: 90%_1目标重量 + - percent_90_2_assign_material_name: 90%_2物料名称 + - percent_90_2_target_weigh: 90%_2目标重量 + - percent_90_3_assign_material_name: 90%_3物料名称 + - percent_90_3_target_weigh: 90%_3目标重量 + - percent_10_1_assign_material_name: 10%_1固体物料名称 + - percent_10_1_target_weigh: 10%_1固体目标重量 + - percent_10_1_volume: 10%_1液体体积 + - percent_10_1_liquid_material_name: 10%_1液体物料名称 + - percent_10_2_assign_material_name: 10%_2固体物料名称 + - percent_10_2_target_weigh: 10%_2固体目标重量 + - percent_10_2_volume: 10%_2液体体积 + - percent_10_2_liquid_material_name: 10%_2液体物料名称 + - percent_10_3_assign_material_name: 10%_3固体物料名称 + - percent_10_3_target_weigh: 10%_3固体目标重量 + - percent_10_3_volume: 10%_3液体体积 + - percent_10_3_liquid_material_name: 10%_3液体物料名称 + - hold_m_name: 库位名称,如"C01",用于查找对应的holdMId + + 返回: 任务创建结果 + """ + # 设置默认值 + if order_name is None: + order_name = f"90%10%小瓶投料任务_{self.get_current_time_iso8601()}" + if speed is None: + speed = "400" + if temperature is None: + temperature = "20" + if delay_time is None: + delay_time = "600" + + # 获取工作流ID + workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID + + # 查询holdMId + holdMId = None + if hold_m_name: + holdMId_response = self.material_id_query(hold_m_name) + if holdMId_response: + holdMId = holdMId_response + + # 构建订单数据 + order_data = [{ + "code": order_name, + "Name": "90%10%小瓶投料任务", + "workflowName": "90%10%小瓶投料", + "borderNumber": 1, + "paramValues": { + workflow_id: [ + # 搅拌速度 + {"m": 3, "n": 2, "key": "speed", "value": speed}, + # 温度 + {"m": 3, "n": 2, "key": "temperature", "value": temperature}, + # 延迟时间 + {"m": 3, "n": 2, "key": "delayTime", "value": delay_time}, + # 90%_1固体物料 + {"m": 3, "n": 2, "key": "90%_1_assignMaterialName", "value": percent_90_1_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_1_targetWeigh", "value": percent_90_1_target_weigh}, + # 90%_2固体物料 + {"m": 3, "n": 2, "key": "90%_2_assignMaterialName", "value": percent_90_2_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_2_targetWeigh", "value": percent_90_2_target_weigh}, + # 90%_3固体物料 + {"m": 3, "n": 2, "key": "90%_3_assignMaterialName", "value": percent_90_3_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_3_targetWeigh", "value": percent_90_3_target_weigh}, + # 10%_1液体物料 + {"m": 3, "n": 2, "key": "10%_1_assignMaterialName", "value": percent_10_1_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_1_targetWeigh", "value": percent_10_1_target_weigh}, + {"m": 3, "n": 2, "key": "10%_1_volume", "value": percent_10_1_volume}, + {"m": 3, "n": 2, "key": "10%_1_liquidMaterialName", "value": percent_10_1_liquid_material_name}, + # 10%_2液体物料 + {"m": 3, "n": 2, "key": "10%_2_assignMaterialName", "value": percent_10_2_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_2_targetWeigh", "value": percent_10_2_target_weigh}, + {"m": 3, "n": 2, "key": "10%_2_volume", "value": percent_10_2_volume}, + {"m": 3, "n": 2, "key": "10%_2_liquidMaterialName", "value": percent_10_2_liquid_material_name}, + # 10%_3液体物料 + {"m": 3, "n": 2, "key": "10%_3_assignMaterialName", "value": percent_10_3_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_3_targetWeigh", "value": percent_10_3_target_weigh}, + {"m": 3, "n": 2, "key": "10%_3_volume", "value": percent_10_3_volume}, + {"m": 3, "n": 2, "key": "10%_3_liquidMaterialName", "value": percent_10_3_liquid_material_name} + ] + }, + "ExtendProperties": f"{{{holdMId}:null}}" if holdMId else "{}" + }] + + try: + # 调用create_order方法创建任务 + result = self.create_order(json.dumps(order_data, ensure_ascii=False)) + self._logger.info(f"90%10%小瓶投料任务创建成功: {result}") + return result + + except Exception as e: + error_msg = f"90%10%小瓶投料任务创建异常: {str(e)}" + self._logger.error(error_msg) + return {"error": error_msg} + + def create_diamine_solution_task(self, + order_name: str = None, + material_name: str = None, + target_weigh: str = None, + volume: str = None, + liquid_material_name: str = "NMP", + speed: str = None, + temperature: str = None, + delay_time: str = None, + hold_m_name: str = None) -> dict: + """ + 创建二胺溶液配置任务 + + 参数说明: + - order_name: 任务名称,如果为None则使用默认名称 + - material_name: 固体物料名称,必填 + - target_weigh: 固体目标重量,必填 + - volume: 液体体积,必填 + - liquid_material_name: 液体物料名称,默认为NMP + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值25 + - delay_time: 延迟时间,如果为None则使用默认值600 + - hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId + + 返回: 任务创建结果 + """ + # 验证必填参数 + if not material_name or not target_weigh or not volume: + return { + "status": "error", + "message": "material_name、target_weigh和volume为必填参数" + } + + # 设置默认值 + if order_name is None: + order_name = f"二胺溶液配置任务_{self.get_current_time_iso8601()}" + if speed is None: + speed = "400" + if temperature is None: + temperature = "25" + if delay_time is None: + delay_time = "600" + + # 获取工作流ID + workflow_id = "1" + + # 查询holdMId + holdMId = None + if hold_m_name: + try: + material_query_params = json.dumps({"materialName": hold_m_name}) + material_response = self.material_id_query(material_query_params) + if material_response and material_response.get("code") == 1: + data = material_response.get("data", []) + if data: + holdMId = data[0].get("id") + self._logger.info(f"查询到holdMId: {holdMId} for {hold_m_name}") + else: + self._logger.warning(f"未找到物料: {hold_m_name}") + else: + self._logger.error(f"查询物料ID失败: {material_response}") + except Exception as e: + self._logger.error(f"查询holdMId时发生错误: {e}") + + # 构建order_data + order_data = { + "workflowId": workflow_id, + "orderName": order_name, + "params": { + "1": speed, # 搅拌速度 + "2": temperature, # 温度 + "3": delay_time, # 延迟时间 + "4": material_name, # 固体物料名称 + "5": target_weigh, # 固体目标重量 + "6": volume, # 液体体积 + "7": liquid_material_name # 液体物料名称 + } + } + + if holdMId: + order_data["holdMId"] = holdMId + + try: + # 使用create_order方法创建任务 + order_params = json.dumps(order_data) + response = self.create_order(order_params) + return response + except Exception as e: + self._logger.error(f"创建二胺溶液配置任务时发生错误: {e}") + return {"status": "error", "message": f"创建任务失败: {str(e)}"} + + def create_batch_90_10_vial_feeding_task(self, json_str: str) -> dict: + """ + 创建批量90%10%小瓶投料任务 + + 接受JSON输入,支持多个90%10%小瓶投料任务的批量创建 + + JSON格式示例: + { + "batch_name": "批量90%10%小瓶投料任务_20240101", + "tasks": [ + { + "order_name": "小瓶投料任务1", + "hold_m_name": "C01", + "percent_90_1_assign_material_name": "物料A", + "percent_90_1_target_weigh": "10.5", + "percent_10_1_assign_material_name": "物料B", + "percent_10_1_target_weigh": "5.2", + "percent_10_1_volume": "50.0", + "percent_10_1_liquid_material_name": "NMP", + "speed": "400", + "temperature": "40", + "delay_time": "600" + } + ], + "global_settings": { + "speed": "400", + "temperature": "40", + "delay_time": "600" + } + } + + 参数说明: + - batch_name: 批量任务名称,可选 + - tasks: 任务列表,每个任务包含90%10%小瓶投料参数 + - global_settings: 全局默认设置,当单个任务未指定参数时使用 + + 返回: 批量任务创建结果 + """ + try: + # 解析JSON输入 + data = json.loads(json_str) + + # 获取批量任务参数 + batch_name = data.get("batch_name", f"批量90%10%小瓶投料任务_{self.get_current_time_iso8601()}") + tasks = data.get("tasks", []) + global_settings = data.get("global_settings", {}) + + if not tasks: + return { + "status": "error", + "message": "任务列表不能为空" + } + + # 批量创建结果 + batch_results = { + "batch_name": batch_name, + "total_tasks": len(tasks), + "successful_tasks": 0, + "failed_tasks": 0, + "task_results": [] + } + + self._logger.info(f"开始创建批量90%10%小瓶投料任务: {batch_name}, 包含 {len(tasks)} 个子任务") + + # 逐个创建任务 + for i, task in enumerate(tasks): + try: + # 合并全局设置和任务特定设置 + task_params = {**global_settings, **task} + + # 验证必填参数 - hold_m_name是必须的 + if not task_params.get("hold_m_name"): + error_msg = f"任务 {i+1} 缺少必填参数: hold_m_name" + self._logger.error(error_msg) + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + batch_results["failed_tasks"] += 1 + continue + + # 设置任务名称 + if not task_params.get("order_name"): + task_params["order_name"] = f"{batch_name}_任务{i+1}" + + # 调用单个90%10%小瓶投料任务创建方法 + task_result = self.create_90_10_vial_feeding_task( + order_name=task_params.get("order_name"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + percent_90_1_assign_material_name=task_params.get("percent_90_1_assign_material_name"), + percent_90_1_target_weigh=task_params.get("percent_90_1_target_weigh"), + percent_90_2_assign_material_name=task_params.get("percent_90_2_assign_material_name"), + percent_90_2_target_weigh=task_params.get("percent_90_2_target_weigh"), + percent_90_3_assign_material_name=task_params.get("percent_90_3_assign_material_name"), + percent_90_3_target_weigh=task_params.get("percent_90_3_target_weigh"), + percent_10_1_assign_material_name=task_params.get("percent_10_1_assign_material_name"), + percent_10_1_target_weigh=task_params.get("percent_10_1_target_weigh"), + percent_10_1_volume=task_params.get("percent_10_1_volume"), + percent_10_1_liquid_material_name=task_params.get("percent_10_1_liquid_material_name"), + percent_10_2_assign_material_name=task_params.get("percent_10_2_assign_material_name"), + percent_10_2_target_weigh=task_params.get("percent_10_2_target_weigh"), + percent_10_2_volume=task_params.get("percent_10_2_volume"), + percent_10_2_liquid_material_name=task_params.get("percent_10_2_liquid_material_name"), + percent_10_3_assign_material_name=task_params.get("percent_10_3_assign_material_name"), + percent_10_3_target_weigh=task_params.get("percent_10_3_target_weigh"), + percent_10_3_volume=task_params.get("percent_10_3_volume"), + percent_10_3_liquid_material_name=task_params.get("percent_10_3_liquid_material_name"), + hold_m_name=task_params.get("hold_m_name") + ) + + # 记录任务结果 + if isinstance(task_result, dict) and task_result.get("status") != "error": + batch_results["successful_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "success", + "result": task_result + }) + self._logger.info(f"任务 {i+1} 创建成功: {task_params.get('order_name')}") + else: + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "error", + "message": str(task_result) + }) + self._logger.error(f"任务 {i+1} 创建失败: {task_result}") + + except Exception as e: + error_msg = f"任务 {i+1} 处理时发生异常: {str(e)}" + self._logger.error(error_msg) + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + + # 设置批量任务整体状态 + if batch_results["failed_tasks"] == 0: + batch_results["status"] = "success" + batch_results["message"] = f"批量90%10%小瓶投料任务全部创建成功,共 {batch_results['successful_tasks']} 个任务" + elif batch_results["successful_tasks"] == 0: + batch_results["status"] = "error" + batch_results["message"] = f"批量90%10%小瓶投料任务全部创建失败,共 {batch_results['failed_tasks']} 个任务" + else: + batch_results["status"] = "partial_success" + batch_results["message"] = f"批量90%10%小瓶投料任务部分成功,成功 {batch_results['successful_tasks']} 个,失败 {batch_results['failed_tasks']} 个" + + self._logger.info(f"批量90%10%小瓶投料任务完成: {batch_results['message']}") + return batch_results + + except json.JSONDecodeError as e: + error_msg = f"JSON解析失败: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + except Exception as e: + error_msg = f"创建批量90%10%小瓶投料任务时发生错误: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + + def create_batch_diamine_solution_task(self, json_str: str) -> dict: + """ + 创建批量二胺溶液配制任务 + + 接受JSON输入,支持多个二胺溶液配制任务的批量创建 + + JSON格式示例: + { + "batch_name": "批量二胺溶液配制任务_20240101", + "tasks": [ + { + "order_name": "二胺溶液配制任务1", + "material_name": "物料A", + "target_weigh": "10.5", + "volume": "50.0", + "liquid_material_name": "NMP", + "speed": "400", + "temperature": "25", + "delay_time": "600", + "hold_m_name": "A01" + }, + { + "order_name": "二胺溶液配制任务2", + "material_name": "物料B", + "target_weigh": "15.2", + "volume": "75.0", + "liquid_material_name": "DMF", + "speed": "350", + "temperature": "30", + "delay_time": "800", + "hold_m_name": "B02" + } + ], + "global_settings": { + "speed": "400", + "temperature": "25", + "delay_time": "600", + "liquid_material_name": "NMP" + } + } + + 参数说明: + - batch_name: 批量任务名称,可选 + - tasks: 任务列表,每个任务包含二胺溶液配制参数 + - global_settings: 全局默认设置,当单个任务未指定参数时使用 + + 每个任务参数: + - order_name: 任务名称 + - material_name: 物料名称,必填 + - target_weigh: 目标重量,必填 + - volume: 体积,必填 + - liquid_material_name: 液体物料名称,可选 + - speed: 搅拌速度,可选 + - temperature: 温度,可选 + - delay_time: 延迟时间,可选 + - hold_m_name: 库位名称,可选 + + 返回: 批量任务创建结果 + """ + try: + # 解析JSON输入 + data = json.loads(json_str) + + # 获取批量任务参数 + batch_name = data.get("batch_name", f"批量二胺溶液配制任务_{self.get_current_time_iso8601()}") + tasks = data.get("tasks", []) + global_settings = data.get("global_settings", {}) + + if not tasks: + return { + "status": "error", + "message": "任务列表不能为空" + } + + # 批量创建结果 + batch_results = { + "batch_name": batch_name, + "total_tasks": len(tasks), + "successful_tasks": 0, + "failed_tasks": 0, + "task_results": [] + } + + self._logger.info(f"开始创建批量二胺溶液配制任务: {batch_name}, 包含 {len(tasks)} 个子任务") + + # 逐个创建任务 + for i, task in enumerate(tasks): + try: + # 合并全局设置和任务特定设置 + task_params = {**global_settings, **task} + + # 验证必填参数 + required_params = ["material_name", "target_weigh", "volume"] + missing_params = [param for param in required_params if not task_params.get(param)] + + if missing_params: + error_msg = f"任务 {i+1} 缺少必填参数: {', '.join(missing_params)}" + self._logger.error(error_msg) + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + batch_results["failed_tasks"] += 1 + continue + + # 设置任务名称 + if not task_params.get("order_name"): + task_params["order_name"] = f"{batch_name}_任务{i+1}" + + # 调用单个二胺溶液配制任务创建方法 + task_result = self.create_diamine_solution_task( + order_name=task_params.get("order_name"), + material_name=task_params.get("material_name"), + target_weigh=task_params.get("target_weigh"), + volume=task_params.get("volume"), + liquid_material_name=task_params.get("liquid_material_name", "NMP"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + hold_m_name=task_params.get("hold_m_name") + ) + + # 记录任务结果 + if isinstance(task_result, dict) and task_result.get("status") != "error": + batch_results["successful_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "success", + "result": task_result + }) + self._logger.info(f"任务 {i+1} 创建成功: {task_params.get('order_name')}") + else: + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "error", + "message": str(task_result) + }) + self._logger.error(f"任务 {i+1} 创建失败: {task_result}") + + except Exception as e: + error_msg = f"滴定液任务 {i+1} 处理时发生异常: {str(e)}" + self._logger.error(error_msg) + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + + # 设置批量任务整体状态 + if batch_results["failed_tasks"] == 0: + batch_results["status"] = "success" + batch_results["message"] = f"批量滴定液任务全部创建成功,共 {batch_results['successful_tasks']} 个任务" + elif batch_results["successful_tasks"] == 0: + batch_results["status"] = "error" + batch_results["message"] = f"批量滴定液任务全部创建失败,共 {batch_results['failed_tasks']} 个任务" + else: + batch_results["status"] = "partial_success" + batch_results["message"] = f"批量滴定液任务部分成功,成功 {batch_results['successful_tasks']} 个,失败 {batch_results['failed_tasks']} 个" + + self._logger.info(f"批量滴定液任务完成: {batch_results['message']}") + return batch_results + + except json.JSONDecodeError as e: + error_msg = f"JSON解析失败: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + except Exception as e: + error_msg = f"创建批量滴定液任务时发生错误: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 3685910a..d09a8a3e 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -17,7 +17,10 @@ from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode -from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS +from unilabos.devices.workstation.bioyond_studio.config import ( + API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, + STATION_TYPES, DEFAULT_STATION_CONFIG +) class BioyondResourceSynchronizer(ResourceSynchronizer): @@ -101,14 +104,15 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): class BioyondWorkstation(WorkstationBase): """Bioyond工作站 - + 集成Bioyond物料管理的工作站实现 """ - + def __init__( self, bioyond_config: Optional[Dict[str, Any]] = None, deck: Optional[Any] = None, + station_config: Optional[Dict[str, Any]] = None, *args, **kwargs, ): @@ -119,18 +123,29 @@ class BioyondWorkstation(WorkstationBase): *args, **kwargs, ) + + # 检查 deck 是否为 None,防止 AttributeError + if self.deck is None: + logger.error("❌ Deck 配置为空,请检查配置文件中的 deck 参数") + raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置") + + # 初始化 warehouses 属性 self.deck.warehouses = {} for resource in self.deck.children: 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) self.resource_synchronizer.sync_from_external() - + # TODO: self._ros_node里面拿属性 logger.info(f"Bioyond工作站初始化完成") - + def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node #self.deck = create_a_coin_cell_deck() @@ -138,6 +153,47 @@ class BioyondWorkstation(WorkstationBase): "resources": [self.deck] }) + def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None: + """配置站点类型和功能模块 + + Args: + station_config (Optional[Dict[str, Any]]): 站点配置,如果为None则使用默认配置 + """ + # 合并默认配置和用户配置 + self.station_config = {**DEFAULT_STATION_CONFIG} + if station_config: + self.station_config.update(station_config) + + # 设置站点属性 + self.station_type = self.station_config["station_type"] + self.enable_reaction_station = self.station_config["enable_reaction_station"] + self.enable_dispensing_station = self.station_config["enable_dispensing_station"] + self.station_name = self.station_config["station_name"] + self.station_description = self.station_config["description"] + + # 根据站点类型调整功能启用状态 + if self.station_type == STATION_TYPES["REACTION"]: + self.enable_reaction_station = True + self.enable_dispensing_station = False + self.station_description = "Bioyond反应站" + logger.info("🧪 配置为反应站模式") + + elif self.station_type == STATION_TYPES["DISPENSING"]: + self.enable_reaction_station = False + self.enable_dispensing_station = True + self.station_description = "Bioyond配液站" + logger.info("🧫 配置为配液站模式") + + elif self.station_type == STATION_TYPES["HYBRID"]: + self.enable_reaction_station = True + self.enable_dispensing_station = True + self.station_description = "Bioyond混合工作站" + logger.info("🔬 配置为混合工作站模式") + + logger.info(f"站点配置: {self.station_name} - {self.station_description}") + logger.info(f"反应站功能: {'✅ 启用' if self.enable_reaction_station else '❌ 禁用'}") + logger.info(f"配液站功能: {'✅ 启用' if self.enable_dispensing_station else '❌ 禁用'}") + def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: """创建Bioyond通信模块""" self.bioyond_config = config or { @@ -145,14 +201,159 @@ class BioyondWorkstation(WorkstationBase): "workflow_mappings": WORKFLOW_MAPPINGS, "material_type_mappings": MATERIAL_TYPE_MAPPINGS } - self.hardware_interface = BioyondV1RPC(self.bioyond_config) - + + # 根据站点配置有条件地初始化接口 + self.hardware_interface = None + self.dispensing_interface = None + + if self.enable_reaction_station: + # 反应站接口 + self.hardware_interface = BioyondV1RPC(self.bioyond_config) + logger.info("✅ 反应站接口已初始化") + else: + logger.info("⏭️ 反应站接口已跳过") + + if self.enable_dispensing_station: + # 配液站接口 - 使用统一的BioyondV1RPC类 + self.dispensing_interface = BioyondV1RPC(self.bioyond_config) + logger.info("✅ 配液站接口已初始化") + else: + logger.info("⏭️ 配液站接口已跳过") + return None - + + def _check_interface_availability(self, interface_type: str) -> bool: + """检查指定接口是否可用 + + Args: + interface_type (str): 接口类型,'reaction' 或 'dispensing' + + Returns: + bool: 接口是否可用 + + Raises: + RuntimeError: 当接口不可用时抛出异常 + """ + if interface_type == "reaction": + if not self.enable_reaction_station or self.hardware_interface is None: + raise RuntimeError( + f"❌ 反应站接口不可用!当前站点类型: {self.station_type}, " + f"反应站功能: {'启用' if self.enable_reaction_station else '禁用'}" + ) + return True + + elif interface_type == "dispensing": + if not self.enable_dispensing_station or self.dispensing_interface is None: + raise RuntimeError( + f"❌ 配液站接口不可用!当前站点类型: {self.station_type}, " + f"配液站功能: {'启用' if self.enable_dispensing_station else '禁用'}" + ) + return True + + else: + raise ValueError(f"未知的接口类型: {interface_type}") + + def get_station_info(self) -> Dict[str, Any]: + """获取站点信息 + + Returns: + Dict[str, Any]: 站点配置和状态信息 + """ + return { + "station_name": self.station_name, + "station_type": self.station_type, + "station_description": self.station_description, + "enable_reaction_station": self.enable_reaction_station, + "enable_dispensing_station": self.enable_dispensing_station, + "reaction_interface_available": self.hardware_interface is not None, + "dispensing_interface_available": self.dispensing_interface is not None, + "supported_station_types": list(STATION_TYPES.values()) + } + + @property + def bioyond_status(self) -> Dict[str, Any]: + """获取 Bioyond 系统状态信息 + + 这个属性被 ROS 节点用来发布设备状态 + + Returns: + Dict[str, Any]: Bioyond 系统的状态信息 + """ + try: + # 获取基础站点信息 + station_info = self.get_station_info() + + # 获取接口状态 + interface_status = { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": getattr(self.resource_synchronizer, 'last_sync_time', 0), + "sync_interval": getattr(self.resource_synchronizer, 'sync_interval', 60) + } + + # 检查反应站接口状态 + if self.hardware_interface is not None: + try: + # 尝试获取调度器状态来验证连接 + scheduler_status = self.get_scheduler_status() + interface_status["reaction_interface_connected"] = scheduler_status.get("status") == "success" + except Exception: + interface_status["reaction_interface_connected"] = False + + # 检查配液站接口状态 + if self.dispensing_interface is not None: + try: + # 配液站接口也使用相同的连接检查方式 + interface_status["dispensing_interface_connected"] = True + except Exception: + interface_status["dispensing_interface_connected"] = False + + # 获取资源同步状态 + sync_status = { + "last_sync_success": True, # 默认值,可以根据实际同步结果更新 + "total_resources": len(getattr(self.deck, 'children', [])), + "warehouse_count": len(getattr(self.deck, 'warehouses', {})) + } + + return { + "station_info": station_info, + "interface_status": interface_status, + "sync_status": sync_status, + "timestamp": __import__('time').time(), + "status": "online" if (interface_status["reaction_interface_connected"] or + interface_status["dispensing_interface_connected"]) else "offline" + } + + except Exception as e: + logger.error(f"获取 Bioyond 状态失败: {e}") + # 返回基础状态信息,避免完全失败 + return { + "station_info": { + "station_name": getattr(self, 'station_name', 'BioyondWorkstation'), + "station_type": getattr(self, 'station_type', 'unknown'), + "enable_reaction_station": getattr(self, 'enable_reaction_station', False), + "enable_dispensing_station": getattr(self, 'enable_dispensing_station', False) + }, + "interface_status": { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": 0, + "sync_interval": 60 + }, + "sync_status": { + "last_sync_success": False, + "total_resources": 0, + "warehouse_count": 0 + }, + "timestamp": __import__('time').time(), + "status": "error", + "error_message": str(e) + } + def _register_supported_workflows(self): """注册Bioyond支持的工作流""" from unilabos.devices.workstation.workstation_base import WorkflowInfo - + # Bioyond物料同步工作流 self.supported_workflows["bioyond_sync"] = WorkflowInfo( name="bioyond_sync", @@ -162,7 +363,7 @@ class BioyondWorkstation(WorkstationBase): "force_sync": {"type": "boolean", "default": False} } ) - + # Bioyond物料更新工作流 self.supported_workflows["bioyond_update"] = WorkflowInfo( name="bioyond_update", @@ -172,27 +373,27 @@ class BioyondWorkstation(WorkstationBase): "sync_all": {"type": "boolean", "default": True} } ) - + logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流") - + async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: """执行Bioyond同步工作流""" try: sync_type = parameters.get("sync_type", "full") force_sync = parameters.get("force_sync", False) - + logger.info(f"开始执行Bioyond同步工作流: {sync_type}") - + # 获取物料管理模块 material_manager = self.material_management - + if sync_type == "full": # 全量同步 success = await material_manager.sync_from_bioyond() else: # 增量同步(这里可以实现增量同步逻辑) success = await material_manager.sync_from_bioyond() - + if success: result = { "status": "success", @@ -204,28 +405,1806 @@ class BioyondWorkstation(WorkstationBase): "status": "failed", "message": "Bioyond同步失败" } - + logger.info(f"Bioyond同步工作流执行完成: {result['status']}") return result - + except Exception as e: logger.error(f"Bioyond同步工作流执行失败: {e}") return { "status": "error", "message": str(e) } - + + # ==================== 工作流合并与参数设置 API ==================== + + def merge_workflow_with_parameters( + self, + name: str, + workflows: List[Dict[str, Any]], + **kwargs + ) -> Dict[str, Any]: + """合并工作流并设置参数 API + + 合并子工作流时传入实验参数,新建实验时如果没有传参数,则使用此处传入的参数作为默认值 + + Args: + name (str): 拼接后的长工作流名称 + workflows (List[Dict[str, Any]]): 待合并的子工作流列表,每个元素包含: + - id (str): 子工作流 ID (UUID) + - stepParameters (Dict, 可选): 步骤参数配置 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果,包含 code、message 和 timestamp + + Example: + workflows = [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "stepParameters": { + "5a30bee1-7de2-45de-a89f-a25c78e4404b": { + "反应模块-开始搅拌": [ + { + "key": "temperature", + "value": "25" + } + ], + "通量-配置": [ + { + "key": "cutoff", + "value": "9999" + }, + { + "key": "assignMaterialName", + "value": "3a1bf167-e862-f269-3749-a1c70cbbe6a6" + } + ] + } + } + } + ] + + result = workstation.merge_workflow_with_parameters( + name="拼接后的长工作流的名称", + workflows=workflows + ) + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"开始合并工作流: {name}, 包含 {len(workflows)} 个子工作流") + + # 基本参数验证 + if not name: + raise ValueError("工作流名称不能为空") + + if not workflows or len(workflows) == 0: + raise ValueError("工作流列表不能为空") + + # 使用 RPC 层进行详细的参数验证 + validation_result = self.hardware_interface.validate_workflow_parameters(workflows) + if not validation_result.get("valid", False): + raise ValueError(f"工作流参数验证失败: {validation_result.get('message', '未知错误')}") + + # 构造请求数据 + request_data = { + "name": name, + "workflows": workflows + } + + # 转换为 JSON 字符串 + json_str = json.dumps(request_data, ensure_ascii=False) + + logger.info(f"发送工作流合并请求: {json_str}") + + # 调用底层 API(需要在 bioyond_rpc.py 中实现) + result = self.hardware_interface.merge_workflow_with_parameters(json_str) + + if result.get("code") == 1: + success_msg = f"工作流合并成功: {name}" + logger.info(success_msg) + return { + "success": True, + "code": result.get("code"), + "message": result.get("message", ""), + "timestamp": result.get("timestamp", 0), + "action": "merge_workflow_with_parameters", + "workflow_name": name, + "workflow_count": len(workflows) + } + else: + error_msg = f"工作流合并失败: {result.get('message', '未知错误')}" + logger.error(error_msg) + return { + "success": False, + "code": result.get("code", 0), + "message": result.get("message", error_msg), + "timestamp": result.get("timestamp", 0), + "action": "merge_workflow_with_parameters" + } + + except Exception as e: + error_msg = f"工作流合并操作异常: {str(e)}" + logger.error(error_msg) + traceback.print_exc() + return { + "success": False, + "code": 0, + "message": error_msg, + "action": "merge_workflow_with_parameters" + } + + def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]: + """验证工作流参数格式 + + Args: + workflows (List[Dict[str, Any]]): 工作流列表 + + Returns: + Dict[str, Any]: 验证结果 + """ + # 委托给 RPC 层进行参数验证 + return self.hardware_interface.validate_workflow_parameters(workflows) + + def get_workflow_parameter_template(self) -> Dict[str, Any]: + """获取工作流参数模板 + + Returns: + Dict[str, Any]: 参数模板和说明 + """ + # 委托给 RPC 层获取参数模板 + return self.hardware_interface.get_workflow_parameter_template() + + # ==================== 反应站动作函数 ==================== + # 基于 bioyond_rpc.py 中的反应站方法实现 + + def reactor_taken_out(self, order_id: str = "", preintake_id: str = "", **kwargs) -> Dict[str, Any]: + """反应器取出操作 - 调用底层 order_takeout API + + 从反应站中取出反应器,通过订单ID和预取样ID进行精确控制 + + Args: + order_id (str): 订单ID,用于标识要取出的订单 + preintake_id (str): 预取样ID,用于标识具体的取样任务 + + Returns: + Dict[str, Any]: 操作结果,包含 code 和 return_info + """ + try: + logger.info(f"执行反应器取出操作: 订单ID={order_id}, 预取样ID={preintake_id}") + + # 构造 JSON 参数 + params = { + "order_id": order_id, + "preintake_id": preintake_id + } + json_str = json.dumps(params) + + # 调用底层 order_takeout API + result_code = self.hardware_interface.order_takeout(json_str) + + if result_code == 1: + success_msg = f"反应器取出操作成功完成,订单ID: {order_id}" + logger.info(success_msg) + return { + "success": True, + "code": result_code, + "return_info": success_msg, + "action": "reactor_taken_out" + } + else: + error_msg = f"反应器取出操作失败,返回代码: {result_code}" + logger.error(error_msg) + return { + "success": False, + "code": result_code, + "return_info": error_msg, + "action": "reactor_taken_out" + } + + except Exception as e: + error_msg = f"反应器取出操作异常: {str(e)}" + logger.error(error_msg) + return { + "success": False, + "code": 0, + "return_info": error_msg, + "action": "reactor_taken_out" + } + + def reactor_taken_in(self, **kwargs) -> Dict[str, Any]: + """反应器放入操作 + + 将反应器放入反应站 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("执行反应器放入操作") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.reactor_taken_in() + + return { + "success": True, + "message": "反应器放入操作完成", + "result": result, + "action": "reactor_taken_in" + } + + except Exception as e: + logger.error(f"反应器放入操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "reactor_taken_in" + } + + def solid_feeding_vials(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """固体进料到小瓶 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行固体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.solid_feeding_vials( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"固体进料操作完成: {material_name}", + "result": result, + "action": "solid_feeding_vials", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"固体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "solid_feeding_vials", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_vials_non_titration(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """非滴定液体进料到小瓶 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行非滴定液体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_vials_non_titration( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"非滴定液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_vials_non_titration", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"非滴定液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_vials_non_titration", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_solvents(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """溶剂进料操作 + + Args: + material_name (str): 溶剂名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行溶剂进料操作: 溶剂={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("溶剂名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_solvents( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"溶剂进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_solvents", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"溶剂进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_solvents", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_titration(self, material_name: str = "", volume: str = "", + titration_type: str = "1", time: str = "120", + torque_variation: str = "2", **kwargs) -> Dict[str, Any]: + """滴定液体进料操作 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + titration_type (str): 滴定类型,默认为"1" + time (str): 滴定时间,默认为"120"秒 + torque_variation (str): 扭矩变化,默认为"2" + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行滴定液体进料操作: 物料={material_name}, 体积={volume}, 类型={titration_type}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_titration( + assign_material_name=material_name, + volume=volume, + titration_type=titration_type, + time=time, + torque_variation=torque_variation, + **kwargs + ) + + return { + "success": True, + "message": f"滴定液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_titration", + "parameters": { + "material_name": material_name, + "volume": volume, + "titration_type": titration_type, + "time": time, + "torque_variation": torque_variation + } + } + + except Exception as e: + logger.error(f"滴定液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_titration", + "parameters": { + "material_name": material_name, + "volume": volume, + "titration_type": titration_type, + "time": time, + "torque_variation": torque_variation + } + } + + def liquid_feeding_beaker(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """烧杯液体进料操作 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行烧杯液体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_beaker( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"烧杯液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_beaker", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"烧杯液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_beaker", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + # ==================== 配液站动作函数 ==================== + # 基于 dispensing_station_bioyong.py 中的配液站方法实现 + + def create_order(self, order_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建配液任务订单 + + Args: + order_data (Union[str, Dict[str, Any]]): 订单数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("创建配液任务订单") + + # 处理输入数据 + if isinstance(order_data, str): + order_json = order_data + else: + order_json = json.dumps(order_data) + + # 调用配液站接口 + result = self.dispensing_interface.create_order(order_json) + + return { + "success": True, + "message": "配液任务订单创建完成", + "result": result, + "action": "create_order" + } + + except Exception as e: + logger.error(f"创建配液任务订单失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_order" + } + + def order_query(self, query_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """查询配液任务状态 + + Args: + query_data (Union[str, Dict[str, Any]]): 查询数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 查询结果 + """ + try: + logger.info("查询配液任务状态") + + # 处理输入数据 + if isinstance(query_data, str): + query_json = query_data + else: + query_json = json.dumps(query_data) + + # 调用配液站接口 + result = self.dispensing_interface.order_query(query_json) + + return { + "success": True, + "message": "配液任务状态查询完成", + "result": result, + "action": "order_query" + } + + except Exception as e: + logger.error(f"查询配液任务状态失败: {e}") + return { + "success": False, + "error": str(e), + "action": "order_query" + } + + def dispensing_material_inbound(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """配液站物料入库 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行配液站物料入库操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.material_inbound(material_json) + + return { + "success": True, + "message": "配液站物料入库完成", + "result": result, + "action": "dispensing_material_inbound" + } + + except Exception as e: + logger.error(f"配液站物料入库失败: {e}") + return { + "success": False, + "error": str(e), + "action": "dispensing_material_inbound" + } + + def dispensing_material_outbound(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """配液站物料出库 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行配液站物料出库操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.material_outbound(material_json) + + return { + "success": True, + "message": "配液站物料出库完成", + "result": result, + "action": "dispensing_material_outbound" + } + + except Exception as e: + logger.error(f"配液站物料出库失败: {e}") + return { + "success": False, + "error": str(e), + "action": "dispensing_material_outbound" + } + + def delete_material(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """删除物料 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("执行删除物料操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.delete_material(material_json) + + return { + "success": True, + "message": "删除物料操作完成", + "result": result, + "action": "delete_material" + } + + except Exception as e: + logger.error(f"删除物料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "delete_material" + } + + def sample_waste_removal(self, waste_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """样品废料移除 + + Args: + waste_data (Union[str, Dict[str, Any]]): 废料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行样品废料移除操作") + + # 处理输入数据 + if isinstance(waste_data, str): + waste_json = waste_data + else: + waste_json = json.dumps(waste_data) + + # 调用配液站接口 + result = self.dispensing_interface.sample_waste_removal(waste_json) + + return { + "success": True, + "message": "样品废料移除操作完成", + "result": result, + "action": "sample_waste_removal" + } + + except Exception as e: + logger.error(f"样品废料移除操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "sample_waste_removal" + } + + def create_resource(self, resource_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建资源(样品板等) + + Args: + resource_data (Union[str, Dict[str, Any]]): 资源数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行创建资源操作") + + # 处理输入数据 + if isinstance(resource_data, str): + resource_json = resource_data + else: + resource_json = json.dumps(resource_data) + + # 调用配液站接口 + result = self.dispensing_interface.create_resource(resource_json) + + return { + "success": True, + "message": "创建资源操作完成", + "result": result, + "action": "create_resource" + } + + except Exception as e: + logger.error(f"创建资源操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_resource" + } + + def create_90_10_vial_feeding_task(self, task_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建90/10比例进料任务 + + Args: + task_data (Union[str, Dict[str, Any]]): 任务数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("创建90/10比例进料任务") + + # 处理输入数据 + if isinstance(task_data, str): + task_params = json.loads(task_data) + else: + task_params = task_data + + # 调用配液站接口,传递具体参数而不是JSON字符串 + result = self.dispensing_interface.create_90_10_vial_feeding_task( + order_name=task_params.get("order_name"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + percent_90_1_assign_material_name=task_params.get("percent_90_1_assign_material_name"), + percent_90_1_target_weigh=task_params.get("percent_90_1_target_weigh"), + percent_90_2_assign_material_name=task_params.get("percent_90_2_assign_material_name"), + percent_90_2_target_weigh=task_params.get("percent_90_2_target_weigh"), + percent_90_3_assign_material_name=task_params.get("percent_90_3_assign_material_name"), + percent_90_3_target_weigh=task_params.get("percent_90_3_target_weigh"), + percent_10_1_assign_material_name=task_params.get("percent_10_1_assign_material_name"), + percent_10_1_target_weigh=task_params.get("percent_10_1_target_weigh"), + percent_10_1_volume=task_params.get("percent_10_1_volume"), + percent_10_1_liquid_material_name=task_params.get("percent_10_1_liquid_material_name"), + percent_10_2_assign_material_name=task_params.get("percent_10_2_assign_material_name"), + percent_10_2_target_weigh=task_params.get("percent_10_2_target_weigh"), + percent_10_2_volume=task_params.get("percent_10_2_volume"), + percent_10_2_liquid_material_name=task_params.get("percent_10_2_liquid_material_name"), + percent_10_3_assign_material_name=task_params.get("percent_10_3_assign_material_name"), + percent_10_3_target_weigh=task_params.get("percent_10_3_target_weigh"), + percent_10_3_volume=task_params.get("percent_10_3_volume"), + percent_10_3_liquid_material_name=task_params.get("percent_10_3_liquid_material_name"), + hold_m_name=task_params.get("hold_m_name") + ) + + return { + "success": True, + "message": "90/10比例进料任务创建完成", + "result": result, + "action": "create_90_10_vial_feeding_task" + } + + except Exception as e: + logger.error(f"创建90/10比例进料任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_90_10_vial_feeding_task" + } + + def create_diamine_solution_task(self, solution_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建二胺溶液配制任务 + + Args: + solution_data (Union[str, Dict[str, Any]]): 溶液数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("创建二胺溶液配制任务") + + # 处理输入数据 + if isinstance(solution_data, str): + solution_params = json.loads(solution_data) + else: + solution_params = solution_data + + # 调用配液站接口,传递具体参数而不是JSON字符串 + result = self.dispensing_interface.create_diamine_solution_task( + order_name=solution_params.get("order_name"), + material_name=solution_params.get("material_name"), + target_weigh=solution_params.get("target_weigh"), + volume=solution_params.get("volume"), + liquid_material_name=solution_params.get("liquid_material_name", "NMP"), + speed=solution_params.get("speed"), + temperature=solution_params.get("temperature"), + delay_time=solution_params.get("delay_time"), + hold_m_name=solution_params.get("hold_m_name") + ) + + return { + "success": True, + "message": "二胺溶液配制任务创建完成", + "result": result, + "action": "create_diamine_solution_task" + } + + except Exception as e: + logger.error(f"创建二胺溶液配制任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_diamine_solution_task" + } + + def create_batch_90_10_vial_feeding_task(self, batch_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """ + 创建批量90%10%小瓶投料任务 + + Args: + batch_data: 批量90%10%小瓶投料任务数据,可以是JSON字符串或字典 + 包含batch_name、tasks列表和global_settings + + Returns: + Dict[str, Any]: 批量任务创建结果 + """ + try: + # 检查配液站接口是否可用 + if not self._check_interface_availability("dispensing"): + return { + "success": False, + "error": "配液站接口不可用", + "action": "create_batch_90_10_vial_feeding_task" + } + + # 解析输入数据 + if isinstance(batch_data, str): + batch_params = json.loads(batch_data) + else: + batch_params = batch_data + + logger.info(f"创建批量90%10%小瓶投料任务: {batch_params.get('batch_name', '未命名批量90%10%小瓶投料任务')}") + + # 调用配液站接口的批量90%10%小瓶投料方法 + result = self.dispensing_interface.create_batch_90_10_vial_feeding_task( + json.dumps(batch_params) if isinstance(batch_params, dict) else batch_data + ) + + return { + "success": True, + "result": result, + "action": "create_batch_90_10_vial_feeding_task" + } + + except json.JSONDecodeError as e: + logger.error(f"批量90%10%小瓶投料任务数据解析失败: {e}") + return { + "success": False, + "error": f"JSON解析失败: {str(e)}", + "action": "create_batch_90_10_vial_feeding_task" + } + + except Exception as e: + logger.error(f"创建批量90%10%小瓶投料任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_batch_90_10_vial_feeding_task" + } + + def create_batch_diamine_solution_task(self, batch_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """ + 创建批量二胺溶液配制任务 + + Args: + batch_data: 批量二胺溶液配制任务数据,可以是JSON字符串或字典 + 包含batch_name、tasks列表和global_settings + + Returns: + Dict[str, Any]: 批量任务创建结果 + """ + try: + # 检查配液站接口是否可用 + if not self._check_interface_availability("dispensing"): + return { + "success": False, + "error": "配液站接口不可用", + "action": "create_batch_diamine_solution_task" + } + + # 解析输入数据 + if isinstance(batch_data, str): + batch_params = json.loads(batch_data) + else: + batch_params = batch_data + + logger.info(f"创建批量二胺溶液配制任务: {batch_params.get('batch_name', '未命名批量二胺溶液配制任务')}") + + # 调用配液站接口的批量二胺溶液配制方法 + result = self.dispensing_interface.create_batch_diamine_solution_task( + json.dumps(batch_params) if isinstance(batch_params, dict) else batch_data + ) + + return { + "success": True, + "result": result, + "action": "create_batch_diamine_solution_task" + } + + except json.JSONDecodeError as e: + logger.error(f"批量二胺溶液配制任务数据解析失败: {e}") + return { + "success": False, + "error": f"JSON解析失败: {str(e)}", + "action": "create_batch_diamine_solution_task" + } + + except Exception as e: + logger.error(f"创建批量二胺溶液配制任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_batch_diamine_solution_task" + } + + # ==================== 反应站动作接口 ==================== + + def reaction_station_drip_back(self, volume: str, assign_material_name: str, + time: str, torque_variation: str) -> Dict[str, Any]: + """反应站滴回操作 + + Args: + volume (str): 投料体积 + assign_material_name (str): 溶剂名称 + time (str): 观察时间(单位min) + torque_variation (str): 是否观察1否2是 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站滴回操作: 体积={volume}, 溶剂={assign_material_name}") + + # 调用硬件接口的滴回方法 + result = self.hardware_interface.reactor_taken_out( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "滴回操作完成", + "result": result, + "action": "reaction_station_drip_back" + } + + except Exception as e: + logger.error(f"反应站滴回操作失败: {e}") + return { + "success": False, + "return_info": f"滴回操作失败: {str(e)}", + "action": "reaction_station_drip_back" + } + + def reaction_station_liquid_feed(self, titration_type: str, volume: str, + assign_material_name: str, time: str, + torque_variation: str) -> Dict[str, Any]: + """反应站液体投料操作 + + Args: + titration_type (str): 滴定类型1否2是 + volume (str): 投料体积 + assign_material_name (str): 溶剂名称 + time (str): 观察时间(单位min) + torque_variation (str): 是否观察1否2是 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站液体投料: 类型={titration_type}, 体积={volume}, 溶剂={assign_material_name}") + + # 根据滴定类型选择相应的方法 + if titration_type == "2": # 滴定 + result = self.hardware_interface.liquid_feeding_titration( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + else: # 非滴定 + result = self.hardware_interface.liquid_feeding_vials_non_titration( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "液体投料操作完成", + "result": result, + "action": "reaction_station_liquid_feed" + } + + except Exception as e: + logger.error(f"反应站液体投料操作失败: {e}") + return { + "success": False, + "return_info": f"液体投料操作失败: {str(e)}", + "action": "reaction_station_liquid_feed" + } + + def reaction_station_solid_feed_vial(self, assign_material_name: str, material_id: str, + time: str, torque_variation: str) -> Dict[str, Any]: + """反应站固体投料-小瓶操作 + + Args: + assign_material_name (str): 固体名称_粉末加样模块-投料 + material_id (str): 固体投料类型_粉末加样模块-投料 + time (str): 观察时间_反应模块-观察搅拌结果 + torque_variation (str): 是否观察1否2是_反应模块-观察搅拌结果 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站固体投料: 固体={assign_material_name}, ID={material_id}") + + # 调用硬件接口的固体投料方法 + result = self.hardware_interface.solid_feeding_vials( + assign_material_name=assign_material_name, + material_id=material_id, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "固体投料操作完成", + "result": result, + "action": "reaction_station_solid_feed_vial" + } + + except Exception as e: + logger.error(f"反应站固体投料操作失败: {e}") + return { + "success": False, + "return_info": f"固体投料操作失败: {str(e)}", + "action": "reaction_station_solid_feed_vial" + } + + def reaction_station_take_in(self, cutoff: str, temperature: str, + assign_material_name: str) -> Dict[str, Any]: + """反应站取入操作 + + Args: + cutoff (str): 截止参数 + temperature (str): 温度 + assign_material_name (str): 物料名称 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站取入操作: 温度={temperature}, 物料={assign_material_name}") + + # 调用硬件接口的取入方法 + result = self.hardware_interface.reactor_taken_in( + cutoff=cutoff, + temperature=temperature, + assign_material_name=assign_material_name + ) + + return { + "success": True, + "return_info": "取入操作完成", + "result": result, + "action": "reaction_station_take_in" + } + + except Exception as e: + logger.error(f"反应站取入操作失败: {e}") + return { + "success": False, + "return_info": f"取入操作失败: {str(e)}", + "action": "reaction_station_take_in" + } + + def reaction_station_reactor_taken_out(self, order_id: str = "", preintake_id: str = "") -> Dict[str, Any]: + """反应站反应器取出操作 + + Args: + order_id (str): 订单ID,用于标识要取出的订单 + preintake_id (str): 预取样ID,用于标识具体的取样任务 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站反应器取出操作: 订单ID={order_id}, 预取样ID={preintake_id}") + + # 调用更新后的反应器取出方法 + result = self.reactor_taken_out(order_id=order_id, preintake_id=preintake_id) + + # 更新 action 字段以区分调用来源 + result["action"] = "reaction_station_reactor_taken_out" + + return result + + except Exception as e: + logger.error(f"反应站反应器取出操作失败: {e}") + return { + "success": False, + "code": 0, + "return_info": f"反应器取出操作失败: {str(e)}", + "action": "reaction_station_reactor_taken_out" + } + + def reaction_station_process_execute(self, workflow_name: str, task_name: str) -> Dict[str, Any]: + """反应站流程执行操作 + + Args: + workflow_name (str): 工作流名称 + task_name (str): 任务名称 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站流程: 工作流={workflow_name}, 任务={task_name}") + + # 这里可以根据具体的工作流和任务名称调用相应的方法 + # 暂时使用通用的执行方法 + result = { + "workflow_name": workflow_name, + "task_name": task_name, + "status": "executed" + } + + return { + "success": True, + "return_info": "流程执行完成", + "result": result, + "action": "reaction_station_process_execute" + } + + except Exception as e: + logger.error(f"反应站流程执行失败: {e}") + return { + "success": False, + "return_info": f"流程执行失败: {str(e)}", + "action": "reaction_station_process_execute" + } + + # ==================== 物料管理动作函数 ==================== + + def material_inbound(self, material_id: str, location_name: str) -> Dict[str, Any]: + """物料入库操作 + + 将物料添加到指定位置 + + Args: + material_id (str): 物料ID + location_name (str): 位置名称 + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行物料入库操作: 物料ID={material_id}, 位置={location_name}") + result = self.hardware_interface.material_inbound( + material_id=material_id, + location_name=location_name + ) + + if result: + logger.info("物料入库操作成功") + return { + "status": "success", + "message": f"物料入库成功,物料ID: {material_id}", + "data": result + } + else: + logger.error("物料入库操作失败") + return { + "status": "failed", + "message": "物料入库失败" + } + + except Exception as e: + logger.error(f"物料入库操作异常: {e}") + return { + "status": "error", + "message": f"物料入库操作异常: {str(e)}" + } + + def material_outbound(self, material_id: str, location_name: str, + quantity: int) -> Dict[str, Any]: + """物料出库操作 + + 从指定位置取出物料 + + Args: + material_id (str): 物料ID + location_name (str): 位置名称 + quantity (int): 数量 + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行物料出库操作: 物料ID={material_id}, 位置={location_name}, 数量={quantity}") + result = self.hardware_interface.material_outbound( + material_id=material_id, + location_name=location_name, + quantity=quantity + ) + + if result: + logger.info("物料出库操作成功") + return { + "status": "success", + "message": f"物料出库成功,物料ID: {material_id}", + "data": result + } + else: + logger.error("物料出库操作失败") + return { + "status": "failed", + "message": "物料出库失败" + } + + except Exception as e: + logger.error(f"物料出库操作异常: {e}") + return { + "status": "error", + "message": f"物料出库操作异常: {str(e)}" + } + + # ============ 工作流控制函数 ============ + + def create_order(self, workflow_name: str, task_name: str, + parameters: Dict[str, Any] = None) -> Dict[str, Any]: + """创建工作流订单 + + 创建并提交工作流执行订单 + + Args: + workflow_name (str): 工作流名称 + task_name (str): 任务名称 + parameters (Dict[str, Any]): 工作流参数 + + Returns: + Dict[str, Any]: 操作结果,包含状态和订单信息 + """ + try: + logger.info(f"开始创建工作流订单: 工作流={workflow_name}, 任务={task_name}") + + # 使用 BioyondV1RPC 的工作流处理方法 + result = self.hardware_interface.process_and_execute_workflow( + workflow_name=workflow_name, + task_name=task_name + ) + + if result and result.get("status") == "success": + logger.info("工作流订单创建成功") + return { + "status": "success", + "message": f"工作流订单创建成功: {workflow_name}", + "data": result + } + else: + logger.error("工作流订单创建失败") + return { + "status": "failed", + "message": "工作流订单创建失败", + "data": result + } + + except Exception as e: + logger.error(f"创建工作流订单异常: {e}") + return { + "status": "error", + "message": f"创建工作流订单异常: {str(e)}" + } + + def get_scheduler_status(self) -> Dict[str, Any]: + """获取调度器状态 + + Returns: + Dict[str, Any]: 调度器状态信息 + """ + try: + logger.info("获取调度器状态") + result = self.hardware_interface.scheduler_status() + + return { + "status": "success", + "message": "调度器状态获取成功", + "data": result + } + + except Exception as e: + logger.error(f"获取调度器状态异常: {e}") + return { + "status": "error", + "message": f"获取调度器状态异常: {str(e)}" + } + + def start_scheduler(self) -> Dict[str, Any]: + """启动调度器 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("启动调度器") + result = self.hardware_interface.scheduler_start() + + if result == 1: # 成功返回1 + logger.info("调度器启动成功") + return { + "status": "success", + "message": "调度器启动成功" + } + else: + logger.error("调度器启动失败") + return { + "status": "failed", + "message": "调度器启动失败" + } + + except Exception as e: + logger.error(f"启动调度器异常: {e}") + return { + "status": "error", + "message": f"启动调度器异常: {str(e)}" + } + + def stop_scheduler(self) -> Dict[str, Any]: + """停止调度器 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("停止调度器") + result = self.hardware_interface.scheduler_stop() + + if result == 1: # 成功返回1 + logger.info("调度器停止成功") + return { + "status": "success", + "message": "调度器停止成功" + } + else: + logger.error("调度器停止失败") + return { + "status": "failed", + "message": "调度器停止失败" + } + + except Exception as e: + logger.error(f"停止调度器异常: {e}") + return { + "status": "error", + "message": f"停止调度器异常: {str(e)}" + } + + # ============ 其他操作函数 ============ + + def drip_back(self, assign_material_name: str = "Reactor", time: str = "0", + torque_variation: str = "1", temperature: float = 25.00) -> Dict[str, Any]: + """滴回操作 + + 执行滴回操作,通常用于反应后的物料回收 + + Args: + assign_material_name (str): 指定的物料名称,默认为 "Reactor" + time (str): 操作时间,默认为 "0" + torque_variation (str): 扭矩变化,默认为 "1" + temperature (float): 温度设置,默认为 25.00°C + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行滴回操作: 物料={assign_material_name}, 温度={temperature}°C") + + # 根据配置文件中的映射,滴回操作可能对应特定的工作流 + workflow_name = self.config.get("workflow_mappings", {}).get("Drip_back") + + if workflow_name: + result = self.hardware_interface.process_and_execute_workflow( + workflow_name=workflow_name, + task_name="drip_back_task" + ) + else: + # 如果没有特定的工作流映射,使用通用的液体操作 + logger.warning("未找到滴回操作的工作流映射,使用默认处理") + result = {"status": "success", "message": "滴回操作完成"} + + if result and result.get("status") == "success": + logger.info("滴回操作成功") + return { + "status": "success", + "message": f"滴回操作成功,物料: {assign_material_name}", + "data": result + } + else: + logger.error("滴回操作失败") + return { + "status": "failed", + "message": "滴回操作失败" + } + + except Exception as e: + logger.error(f"滴回操作异常: {e}") + return { + "status": "error", + "message": f"滴回操作异常: {str(e)}" + } + + def get_device_list(self) -> Dict[str, Any]: + """获取设备列表 + + Returns: + Dict[str, Any]: 设备列表信息 + """ + try: + logger.info("获取设备列表") + result = self.hardware_interface.device_list() + + return { + "status": "success", + "message": "设备列表获取成功", + "data": result + } + + except Exception as e: + logger.error(f"获取设备列表异常: {e}") + return { + "status": "error", + "message": f"获取设备列表异常: {str(e)}" + } + + def device_operation(self, device_id: str, operation: str, + parameters: Dict[str, Any] = None) -> Dict[str, Any]: + """设备操作 + + 对指定设备执行操作 + + Args: + device_id (str): 设备ID + operation (str): 操作类型 + parameters (Dict[str, Any]): 操作参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行设备操作: 设备ID={device_id}, 操作={operation}") + result = self.hardware_interface.device_operation( + device_id=device_id, + operation=operation, + parameters=parameters or {} + ) + + if result: + logger.info("设备操作成功") + return { + "status": "success", + "message": f"设备操作成功: {operation}", + "data": result + } + else: + logger.error("设备操作失败") + return { + "status": "failed", + "message": "设备操作失败" + } + + except Exception as e: + logger.error(f"设备操作异常: {e}") + return { + "status": "error", + "message": f"设备操作异常: {str(e)}" + } + + def add_material(self, material_data: Dict[str, Any]) -> Dict[str, Any]: + """添加物料 + + 向系统中添加新的物料信息 + + Args: + material_data (Dict[str, Any]): 物料数据 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"添加物料: {material_data.get('name', 'Unknown')}") + result = self.hardware_interface.add_material(material_data) + + if result: + logger.info("物料添加成功") + return { + "status": "success", + "message": "物料添加成功", + "data": result + } + else: + logger.error("物料添加失败") + return { + "status": "failed", + "message": "物料添加失败" + } + + except Exception as e: + logger.error(f"添加物料异常: {e}") + return { + "status": "error", + "message": f"添加物料异常: {str(e)}" + } + + def stock_material(self, material_id: str, location: str, + quantity: int) -> Dict[str, Any]: + """库存物料 + + 更新物料库存信息 + + Args: + material_id (str): 物料ID + location (str): 位置 + quantity (int): 数量 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"更新物料库存: 物料ID={material_id}, 位置={location}, 数量={quantity}") + result = self.hardware_interface.stock_material( + material_id=material_id, + location=location, + quantity=quantity + ) + + if result: + logger.info("物料库存更新成功") + return { + "status": "success", + "message": "物料库存更新成功", + "data": result + } + else: + logger.error("物料库存更新失败") + return { + "status": "failed", + "message": "物料库存更新失败" + } + + except Exception as e: + logger.error(f"物料库存更新异常: {e}") + return { + "status": "error", + "message": f"物料库存更新异常: {str(e)}" + } + + # ============ 工作站状态管理 ============ + + def get_workstation_status(self) -> Dict[str, Any]: + """获取工作站状态 + + Returns: + Dict[str, Any]: 工作站状态信息 + """ + try: + # 获取基础状态信息 + base_status = { + "is_busy": self.is_busy, + "workflow_status": self.workflow_status, + "workflow_runtime": self.workflow_runtime + } + + # 获取调度器状态 + scheduler_status = self.get_scheduler_status() + + # 获取设备列表 + device_list = self.get_device_list() + + return { + "status": "success", + "message": "工作站状态获取成功", + "data": { + "base_status": base_status, + "scheduler_status": scheduler_status.get("data"), + "device_list": device_list.get("data"), + "config": { + "api_host": self.config.get("api_host"), + "workflow_mappings": self.config.get("workflow_mappings", {}), + "material_type_mappings": self.config.get("material_type_mappings", {}) + } + } + } + + except Exception as e: + logger.error(f"获取工作站状态异常: {e}") + return { + "status": "error", + "message": f"获取工作站状态异常: {str(e)}" + } + + def get_bioyond_status(self) -> Dict[str, Any]: + """获取完整的 Bioyond 状态信息 + + 这个方法提供了比 bioyond_status 属性更详细的状态信息, + 包括错误处理和格式化的响应结构 + + Returns: + Dict[str, Any]: 格式化的 Bioyond 状态响应 + """ + try: + # 获取 bioyond_status 属性的数据 + status_data = self.bioyond_status + + return { + "status": "success", + "message": "Bioyond 状态获取成功", + "data": status_data + } + + except Exception as e: + logger.error(f"获取 Bioyond 状态异常: {e}") + return { + "status": "error", + "message": f"获取 Bioyond 状态异常: {str(e)}", + "data": { + "station_info": { + "station_name": getattr(self, 'station_name', 'BioyondWorkstation'), + "station_type": getattr(self, 'station_type', 'unknown'), + "enable_reaction_station": getattr(self, 'enable_reaction_station', False), + "enable_dispensing_station": getattr(self, 'enable_dispensing_station', False) + }, + "interface_status": { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": 0, + "sync_interval": 60 + }, + "sync_status": { + "last_sync_success": False, + "total_resources": 0, + "warehouse_count": 0 + }, + "timestamp": __import__('time').time(), + "status": "error", + "error_message": str(e) + } + } + + def reset_workstation(self) -> Dict[str, Any]: + """重置工作站 + + 重置工作站到初始状态 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("开始重置工作站") + + # 停止当前工作流(如果有) + if self.is_busy: + self.stop_workflow() + + # 停止调度器 + self.stop_scheduler() + + # 重新启动调度器 + start_result = self.start_scheduler() + + if start_result.get("status") == "success": + logger.info("工作站重置成功") + return { + "status": "success", + "message": "工作站重置成功" + } + else: + logger.error("工作站重置失败") + return { + "status": "failed", + "message": "工作站重置失败" + } + + except Exception as e: + logger.error(f"工作站重置异常: {e}") + return { + "status": "error", + "message": f"工作站重置异常: {str(e)}" + } + async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: """执行Bioyond更新工作流""" try: material_ids = parameters.get("material_ids", []) sync_all = parameters.get("sync_all", True) - + logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}") - + # 获取物料管理模块 material_manager = self.material_management - + if sync_all: # 同步所有物料 success_count = 0 @@ -242,46 +2221,46 @@ class BioyondWorkstation(WorkstationBase): success = await material_manager.sync_to_bioyond(resource) if success: success_count += 1 - + result = { "status": "success", "message": f"Bioyond更新完成", "updated_resources": success_count, "total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources) } - + logger.info(f"Bioyond更新工作流执行完成: {result['status']}") return result - + except Exception as e: logger.error(f"Bioyond更新工作流执行失败: {e}") return { "status": "error", "message": str(e) } - + def load_bioyond_data_from_file(self, file_path: str) -> bool: """从文件加载Bioyond数据(用于测试)""" try: with open(file_path, 'r', encoding='utf-8') as f: bioyond_data = json.load(f) - + # 获取物料管理模块 material_manager = self.material_management - + # 转换为UniLab格式 if isinstance(bioyond_data, dict) and "data" in bioyond_data: unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data) else: unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data) - + # 分配到Deck import asyncio asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) - + logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") return True - + except Exception as e: logger.error(f"从文件加载Bioyond数据失败: {e}") return False @@ -290,10 +2269,10 @@ class BioyondWorkstation(WorkstationBase): # 使用示例 def create_bioyond_workstation_example(): """创建Bioyond工作站示例""" - + # 配置参数 device_id = "bioyond_workstation_001" - + # 子资源配置 children = { "plate_1": { @@ -308,7 +2287,7 @@ def create_bioyond_workstation_example(): } } } - + # Bioyond配置 bioyond_config = { "base_url": "http://bioyond.example.com/api", @@ -316,7 +2295,7 @@ def create_bioyond_workstation_example(): "sync_interval": 60, # 60秒同步一次 "timeout": 30 } - + # Deck配置 deck_config = { "size_x": 1000.0, @@ -324,30 +2303,30 @@ def create_bioyond_workstation_example(): "size_z": 100.0, "model": "BioyondDeck" } - + # 创建工作站 workstation = BioyondWorkstation( station_resource=deck_config, bioyond_config=bioyond_config, deck_config=deck_config, ) - + return workstation if __name__ == "__main__": # 创建示例工作站 #workstation = create_bioyond_workstation_example() - + # 从文件加载测试数据 #workstation.load_bioyond_data_from_file("bioyond_test_yibin.json") - + # 获取状态 #status = workstation.get_bioyond_status() #print("Bioyond工作站状态:", status) # 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式 - + # 读取 bioyond_resources_unilab_output3 copy.json 文件 from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type from Bioyond_wuliao import * @@ -356,7 +2335,7 @@ if __name__ == "__main__": import json from pylabrobot.resources.deck import Deck from pylabrobot.resources.coordinate import Coordinate - + with open("./bioyond_test_yibin3_unilab_result_corr.json", "r", encoding="utf-8") as f: bioyond_resources_unilab = json.load(f) print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") @@ -392,9 +2371,9 @@ if __name__ == "__main__": resources = convert_resources_from_type([deck], [PLRResource]) - + print(resources) http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1" #http_client.auth = "9F05593C" http_client.auth = "ED634D1C" - http_client.resource_add(resources, database_process_later=False) \ No newline at end of file + http_client.resource_add(resources, database_process_later=False) diff --git a/unilabos/registry/devices/dispensing_station_bioyond.yaml b/unilabos/registry/devices/dispensing_station_bioyond.yaml new file mode 100644 index 00000000..4c87e962 --- /dev/null +++ b/unilabos/registry/devices/dispensing_station_bioyond.yaml @@ -0,0 +1,506 @@ +dispensing_station.bioyond: + category: + - work_station + - dispensing_station_bioyond + class: + action_value_mappings: + bioyond_sync: + feedback: {} + goal: + force_sync: force_sync + sync_type: sync_type + goal_default: + force_sync: false + sync_type: full + handles: {} + result: {} + schema: + description: 从Bioyond系统同步物料 + properties: + feedback: {} + goal: + properties: + force_sync: + description: 是否强制同步 + type: boolean + sync_type: + description: 同步类型 + enum: + - full + - incremental + type: string + required: + - sync_type + type: object + result: {} + required: + - goal + title: bioyond_sync参数 + type: object + type: UniLabJsonCommand + bioyond_update: + feedback: {} + goal: + material_ids: material_ids + sync_all: sync_all + goal_default: + material_ids: [] + sync_all: true + handles: {} + result: {} + schema: + description: 将本地物料变更同步到Bioyond + properties: + feedback: {} + goal: + properties: + material_ids: + description: 要同步的物料ID列表 + items: + type: string + type: array + sync_all: + description: 是否同步所有物料 + type: boolean + required: + - sync_all + type: object + result: {} + required: + - goal + title: bioyond_update参数 + type: object + type: UniLabJsonCommand + create_90_10_vial_feeding_task: + feedback: {} + goal: + delay_time: delay_time + order_name: order_name + percent_10_1_assign_material_name: percent_10_1_assign_material_name + percent_10_1_liquid_material_name: percent_10_1_liquid_material_name + percent_10_1_target_weigh: percent_10_1_target_weigh + percent_10_1_volume: percent_10_1_volume + percent_10_2_assign_material_name: percent_10_2_assign_material_name + percent_10_2_liquid_material_name: percent_10_2_liquid_material_name + percent_10_2_target_weigh: percent_10_2_target_weigh + percent_10_2_volume: percent_10_2_volume + percent_90_1_assign_material_name: percent_90_1_assign_material_name + percent_90_1_target_weigh: percent_90_1_target_weigh + percent_90_2_assign_material_name: percent_90_2_assign_material_name + percent_90_2_target_weigh: percent_90_2_target_weigh + percent_90_3_assign_material_name: percent_90_3_assign_material_name + percent_90_3_target_weigh: percent_90_3_target_weigh + speed: speed + temperature: temperature + goal_default: + delay_time: '600' + order_name: '' + percent_10_1_assign_material_name: '' + percent_10_1_liquid_material_name: '' + percent_10_1_target_weigh: '' + percent_10_1_volume: '' + percent_10_2_assign_material_name: '' + percent_10_2_liquid_material_name: '' + percent_10_2_target_weigh: '' + percent_10_2_volume: '' + percent_90_1_assign_material_name: '' + percent_90_1_target_weigh: '' + percent_90_2_assign_material_name: '' + percent_90_2_target_weigh: '' + percent_90_3_assign_material_name: '' + percent_90_3_target_weigh: '' + speed: '400' + temperature: '20' + handles: {} + result: {} + schema: + description: 创建90%/10%小瓶投料任务 + properties: + feedback: {} + goal: + properties: + delay_time: + default: '600' + description: 延迟时间(s) + type: string + order_name: + description: 任务名称 + type: string + percent_10_1_assign_material_name: + description: 10%组分1物料名称 + type: string + percent_10_1_liquid_material_name: + description: 10%组分1液体物料名称 + type: string + percent_10_1_target_weigh: + description: 10%组分1目标重量(g) + type: string + percent_10_1_volume: + description: 10%组分1液体体积(mL) + type: string + percent_10_2_assign_material_name: + description: 10%组分2物料名称 + type: string + percent_10_2_liquid_material_name: + description: 10%组分2液体物料名称 + type: string + percent_10_2_target_weigh: + description: 10%组分2目标重量(g) + type: string + percent_10_2_volume: + description: 10%组分2液体体积(mL) + type: string + percent_90_1_assign_material_name: + description: 90%组分1物料名称 + type: string + percent_90_1_target_weigh: + description: 90%组分1目标重量(g) + type: string + percent_90_2_assign_material_name: + description: 90%组分2物料名称 + type: string + percent_90_2_target_weigh: + description: 90%组分2目标重量(g) + type: string + percent_90_3_assign_material_name: + description: 90%组分3物料名称 + type: string + percent_90_3_target_weigh: + description: 90%组分3目标重量(g) + type: string + speed: + default: '400' + description: 搅拌速度(rpm) + type: string + temperature: + default: '20' + description: 温度(°C) + type: string + type: object + result: {} + required: + - goal + title: create_90_10_vial_feeding_task参数 + type: object + type: UniLabJsonCommand + create_batch_90_10_vial_feeding_task: + feedback: {} + goal: + batch_data: batch_data + goal_default: + batch_data: '{}' + handles: {} + result: {} + schema: + description: 创建批量90%10%小瓶投料任务 + properties: + feedback: {} + goal: + properties: + batch_data: + description: 批量90%10%小瓶投料任务数据(JSON格式),包含batch_name、tasks列表和global_settings + type: string + required: + - batch_data + type: object + result: {} + required: + - goal + title: create_batch_90_10_vial_feeding_task参数 + type: object + type: UniLabJsonCommand + create_batch_diamine_solution_task: + feedback: {} + goal: + batch_data: batch_data + goal_default: + batch_data: '{}' + handles: {} + result: {} + schema: + description: 创建批量二胺溶液配制任务 + properties: + feedback: {} + goal: + properties: + batch_data: + description: 批量二胺溶液配制任务数据(JSON格式),包含batch_name、tasks列表和global_settings + type: string + required: + - batch_data + type: object + result: {} + required: + - goal + title: create_batch_diamine_solution_task参数 + type: object + type: UniLabJsonCommand + create_diamine_solution_task: + feedback: {} + goal: + delay_time: delay_time + hold_m_name: hold_m_name + liquid_material_name: liquid_material_name + material_name: material_name + order_name: order_name + speed: speed + target_weigh: target_weigh + temperature: temperature + volume: volume + goal_default: + delay_time: '600' + hold_m_name: '' + liquid_material_name: NMP + material_name: '' + order_name: '' + speed: '400' + target_weigh: '' + temperature: '20' + volume: '' + handles: {} + result: {} + schema: + description: 创建二胺溶液配制任务 + properties: + feedback: {} + goal: + properties: + delay_time: + default: '600' + description: 延迟时间(s) + type: string + hold_m_name: + description: 库位名称(如ODA-1) + type: string + liquid_material_name: + default: NMP + description: 液体物料名称 + type: string + material_name: + description: 固体物料名称 + type: string + order_name: + description: 任务名称 + type: string + speed: + default: '400' + description: 搅拌速度(rpm) + type: string + target_weigh: + description: 固体目标重量(g) + type: string + temperature: + default: '20' + description: 温度(°C) + type: string + volume: + description: 液体体积(mL) + type: string + required: + - material_name + - target_weigh + - volume + type: object + result: {} + required: + - goal + title: create_diamine_solution_task参数 + type: object + type: UniLabJsonCommand + create_resource: + feedback: {} + goal: + resource_config: resource_config + resource_type: resource_type + goal_default: + resource_config: {} + resource_type: '' + handles: {} + result: {} + schema: + description: 创建资源操作 + properties: + feedback: {} + goal: + properties: + resource_config: + description: 资源配置 + type: object + resource_type: + description: 资源类型 + type: string + required: + - resource_type + - resource_config + type: object + result: {} + required: + - goal + title: create_resource参数 + type: object + type: UniLabJsonCommand + dispensing_material_inbound: + feedback: {} + goal: + location: location + material_id: material_id + goal_default: + location: '' + material_id: '' + handles: {} + result: {} + schema: + description: 配液站物料入库操作 + properties: + feedback: {} + goal: + properties: + location: + description: 存储位置 + type: string + material_id: + description: 物料ID + type: string + required: + - material_id + - location + type: object + result: {} + required: + - goal + title: dispensing_material_inbound参数 + type: object + type: UniLabJsonCommand + dispensing_material_outbound: + feedback: {} + goal: + material_id: material_id + quantity: quantity + goal_default: + material_id: '' + quantity: 0.0 + handles: {} + result: {} + schema: + description: 配液站物料出库操作 + properties: + feedback: {} + goal: + properties: + material_id: + description: 物料ID + type: string + quantity: + description: 出库数量 + type: number + required: + - material_id + - quantity + type: object + result: {} + required: + - goal + title: dispensing_material_outbound参数 + type: object + type: UniLabJsonCommand + sample_waste_removal: + feedback: {} + goal: + sample_id: sample_id + waste_type: waste_type + goal_default: + sample_id: '' + waste_type: general + handles: {} + result: {} + schema: + description: 样品废料移除操作 + properties: + feedback: {} + goal: + properties: + sample_id: + description: 样品ID + type: string + waste_type: + description: 废料类型 + enum: + - general + - hazardous + - organic + - inorganic + type: string + required: + - sample_id + type: object + result: {} + required: + - goal + title: sample_waste_removal参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + protocol_type: [] + status_types: + bioyond_status: dict + enable_dispensing_station: bool + enable_reaction_station: bool + station_type: str + type: python + config_info: [] + description: Bioyond配液站 - 专门用于物料配制和管理的工作站 + handles: [] + icon: 配液站.webp + init_param_schema: + config: + properties: + bioyond_config: + description: Bioyond API配置 + properties: + api_host: + description: Bioyond API主机地址 + type: string + api_key: + description: Bioyond API密钥 + type: string + material_type_mappings: + description: 物料类型映射配置 + type: object + workflow_mappings: + description: 工作流映射配置 + type: object + type: object + deck: + description: Deck配置 + type: object + station_config: + description: 配液站配置 + properties: + description: + description: 配液站描述 + type: string + enable_dispensing_station: + default: true + description: 启用配液站功能 + type: boolean + enable_reaction_station: + default: false + description: 禁用反应站功能 + type: boolean + station_name: + description: 配液站名称 + type: string + station_type: + default: dispensing_station + description: 站点类型 - 配液站 + enum: + - dispensing_station + type: string + type: object + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml new file mode 100644 index 00000000..2dc341a6 --- /dev/null +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -0,0 +1,384 @@ +reaction_station.bioyond: + category: + - work_station + - reaction_station_bioyond + class: + action_value_mappings: + bioyond_sync: + feedback: {} + goal: + force_sync: force_sync + sync_type: sync_type + goal_default: + force_sync: false + sync_type: full + handles: {} + result: {} + schema: + description: 从Bioyond系统同步物料 + properties: + feedback: {} + goal: + properties: + force_sync: + description: 是否强制同步 + type: boolean + sync_type: + description: 同步类型 + enum: + - full + - incremental + type: string + required: + - sync_type + type: object + result: {} + required: + - goal + title: bioyond_sync参数 + type: object + type: UniLabJsonCommand + bioyond_update: + feedback: {} + goal: + material_ids: material_ids + sync_all: sync_all + goal_default: + material_ids: [] + sync_all: true + handles: {} + result: {} + schema: + description: 将本地物料变更同步到Bioyond + properties: + feedback: {} + goal: + properties: + material_ids: + description: 要同步的物料ID列表 + items: + type: string + type: array + sync_all: + description: 是否同步所有物料 + type: boolean + required: + - sync_all + type: object + result: {} + required: + - goal + title: bioyond_update参数 + type: object + type: UniLabJsonCommand + reaction_station_drip_back: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 反应站滴回操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 溶剂名称 + type: string + time: + description: 观察时间(单位min) + type: string + torque_variation: + description: 是否观察1否2是 + type: string + volume: + description: 投料体积 + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_drip_back参数 + type: object + type: UniLabJsonCommand + reaction_station_liquid_feed: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 反应站液体进料操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 溶剂名称 + type: string + time: + description: 观察时间(单位min) + type: string + titration_type: + description: 滴定类型1否2是 + type: string + torque_variation: + description: 是否观察1否2是 + type: string + volume: + description: 投料体积 + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_liquid_feed参数 + type: object + type: UniLabJsonCommand + reaction_station_process_execute: + feedback: {} + goal: + task_name: task_name + workflow_name: workflow_name + goal_default: + task_name: '' + workflow_name: '' + handles: {} + result: {} + schema: + description: 反应站流程执行 + properties: + feedback: {} + goal: + properties: + task_name: + description: 任务名称 + type: string + workflow_name: + description: 工作流名称 + type: string + required: + - workflow_name + - task_name + type: object + result: {} + required: + - goal + title: reaction_station_process_execute参数 + type: object + type: UniLabJsonCommand + reaction_station_reactor_taken_out: + feedback: {} + goal: + order_id: order_id + preintake_id: preintake_id + goal_default: + order_id: '' + preintake_id: '' + handles: {} + result: {} + schema: + description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制 + properties: + feedback: {} + goal: + properties: + order_id: + description: 订单ID,用于标识要取出的订单 + type: string + preintake_id: + description: 预取样ID,用于标识具体的取样任务 + type: string + required: [] + type: object + result: + properties: + code: + description: 操作结果代码(1表示成功,0表示失败) + type: integer + return_info: + description: 操作结果详细信息 + type: string + type: object + required: + - goal + title: reaction_station_reactor_taken_out参数 + type: object + type: UniLabJsonCommand + reaction_station_solid_feed_vial: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + time: '' + torque_variation: '' + handles: {} + result: {} + schema: + description: 反应站固体进料操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 固体名称_粉末加样模块-投料 + type: string + material_id: + description: 固体投料类型_粉末加样模块-投料 + type: string + time: + description: 观察时间_反应模块-观察搅拌结果 + type: string + torque_variation: + description: 是否观察1否2是_反应模块-观察搅拌结果 + type: string + required: + - assign_material_name + - material_id + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_solid_feed_vial参数 + type: object + type: UniLabJsonCommand + reaction_station_take_in: + feedback: {} + goal: + assign_material_name: assign_material_name + cutoff: cutoff + temperature: temperature + goal_default: + assign_material_name: '' + cutoff: '' + temperature: '' + handles: {} + result: {} + schema: + description: 反应站取入操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + cutoff: + description: 截止参数 + type: string + temperature: + description: 温度 + type: string + required: + - cutoff + - temperature + - assign_material_name + type: object + result: {} + required: + - goal + title: reaction_station_take_in参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + protocol_type: [] + status_types: + bioyond_status: dict + enable_dispensing_station: bool + enable_reaction_station: bool + station_type: str + type: python + config_info: [] + description: Bioyond反应站 - 专门用于化学反应操作的工作站 + handles: [] + icon: 反应站.webp + init_param_schema: + config: + properties: + bioyond_config: + description: Bioyond API配置 + properties: + api_host: + description: Bioyond API主机地址 + type: string + api_key: + description: Bioyond API密钥 + type: string + material_type_mappings: + description: 物料类型映射配置 + type: object + workflow_mappings: + description: 工作流映射配置 + type: object + type: object + deck: + description: Deck配置 + type: object + station_config: + description: 反应站配置 + properties: + description: + description: 反应站描述 + type: string + enable_dispensing_station: + default: false + description: 禁用配液站功能 + type: boolean + enable_reaction_station: + default: true + description: 启用反应站功能 + type: boolean + station_name: + description: 反应站名称 + type: string + station_type: + default: reaction_station + description: 站点类型 - 反应站 + enum: + - reaction_station + type: string + type: object + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 0725514f..46028fd9 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -103,10 +103,14 @@ set(action_files "action/PostProcessGrab.action" "action/PostProcessTriggerClean.action" "action/PostProcessTriggerPostPro.action" - + "action/ReactionStationDripBack.action" - "action/ReactionStationLiquidFeed.action" + "action/ReactionStationLiquidFeedBeaker.action" + "action/ReactionStationLiquidFeedSolvents.action" + "action/ReactionStationLiquidFeedTitration.action" + "action/ReactionStationLiquidFeedVialsNonTitration.action" "action/ReactionStationProExecu.action" + "action/ReactionStationReactorTakenOut.action" "action/ReactionStationReaTackIn.action" "action/ReactionStationSolidFeedVial.action" ) diff --git a/unilabos_msgs/action/ReactionStationDripBack.action b/unilabos_msgs/action/ReactionStationDripBack.action index df690b3b..4d35e963 100644 --- a/unilabos_msgs/action/ReactionStationDripBack.action +++ b/unilabos_msgs/action/ReactionStationDripBack.action @@ -1,11 +1,13 @@ -# Goal - 滴回去 -string volume # 投料体积 -string assign_material_name # 溶剂名称 -string time # 观察时间(单位min) -string torque_variation #是否观察1否2是 +# Goal - 滴回去操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 --- -# Result - 操作结果 -string return_info # 结果消息 - +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationLiquidFeed.action b/unilabos_msgs/action/ReactionStationLiquidFeed.action deleted file mode 100644 index 8be9dbba..00000000 --- a/unilabos_msgs/action/ReactionStationLiquidFeed.action +++ /dev/null @@ -1,11 +0,0 @@ -# Goal - 液体投料 -string titration_type # 滴定类型1否2是 -string volume # 投料体积 -string assign_material_name # 溶剂名称 -string time # 观察时间(单位min) -string torque_variation #是否观察1否2是 ---- -# Result - 操作结果 -string return_info # 结果消息 ---- -# Feedback - 实时反馈 diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action b/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action new file mode 100644 index 00000000..ac8d8759 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-烧杯操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action b/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action new file mode 100644 index 00000000..c89050b7 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-溶剂操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action b/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action new file mode 100644 index 00000000..f2885ac9 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action @@ -0,0 +1,15 @@ +# Goal - 液体投料滴定操作参数 +string volume_formula # 投料体积公式 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action b/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action new file mode 100644 index 00000000..b5e879b2 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-小瓶非滴定操作参数 +string volume_formula # 投料体积公式 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationProExecu.action b/unilabos_msgs/action/ReactionStationProExecu.action index 0c4649a8..ceefc3d4 100644 --- a/unilabos_msgs/action/ReactionStationProExecu.action +++ b/unilabos_msgs/action/ReactionStationProExecu.action @@ -1,8 +1,11 @@ -# Goal - 合并工作流+执行 -string workflow_name # 工作流名称 -string task_name # 任务名称 +# Goal - 合并工作流+执行参数 +string workflow_name # 工作流名称 +string task_name # 任务名称 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationReaTackIn.action b/unilabos_msgs/action/ReactionStationReaTackIn.action index 78d873ac..51362571 100644 --- a/unilabos_msgs/action/ReactionStationReaTackIn.action +++ b/unilabos_msgs/action/ReactionStationReaTackIn.action @@ -1,9 +1,12 @@ -# Goal - 通量-配置 -string cutoff # 黏度_通量-配置 -string temperature # 温度_通量-配置 -string assign_material_name # 分液类型_通量-配置 +# Goal - 反应器放入操作参数 +string cutoff # 黏度设置 +string temperature # 温度设置 +string assign_material_name # 分液类型 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationReactorTakenOut.action b/unilabos_msgs/action/ReactionStationReactorTakenOut.action new file mode 100644 index 00000000..29b841b5 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationReactorTakenOut.action @@ -0,0 +1,12 @@ +# Goal - 反应器取出操作参数 +# 反应器取出操作不需要任何参数 +--- +# Result - 操作结果 +# 反应器取出操作的结果 +bool success # 要求必须包含success,以便回传执行结果 +string return_info # 要求必须包含return_info,以便回传执行结果 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +# 反应器取出操作的反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationSolidFeedVial.action b/unilabos_msgs/action/ReactionStationSolidFeedVial.action index b51096d1..e7e677d5 100644 --- a/unilabos_msgs/action/ReactionStationSolidFeedVial.action +++ b/unilabos_msgs/action/ReactionStationSolidFeedVial.action @@ -1,10 +1,13 @@ -# Goal - 固体投料-小瓶 -string assign_material_name # 固体名称_粉末加样模块-投料 -string material_id # 固体投料类型_粉末加样模块-投料 -string time # 观察时间_反应模块-观察搅拌结果 -string torque_variation #是否观察1否2是_反应模块-观察搅拌结果 +# Goal - 固体投料-小瓶操作参数 +string assign_material_name # 固体名称 +string material_id # 固体投料类型 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息