From bbcbcde9a47522faf7c3e3d9030d232ee96c0523 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Mon, 13 Oct 2025 09:41:43 +0800 Subject: [PATCH] add plr_to_bioyond, and refactor bioyond stations --- test/resources/test_converter_bioyond.py | 24 +- test/resources/test_itemized_carrier.py | 115 + unilabos/devices/workstation/README.md | 10 +- .../workstation/bioyond_studio/bioyond_rpc.py | 1894 +++----------- .../bioyond_studio/dispensing_station.py | 824 ++++++ .../bioyond_studio/reaction_station.py | 207 ++ .../workstation/bioyond_studio/station.py | 2267 ++--------------- unilabos/registry/devices/bioyond.yaml | 241 ++ .../devices/dispensing_station_bioyond.yaml | 1582 ------------ unilabos/resources/graphio.py | 80 +- unilabos/resources/itemized_carrier.py | 83 + 11 files changed, 2101 insertions(+), 5226 deletions(-) create mode 100644 test/resources/test_itemized_carrier.py create mode 100644 unilabos/devices/workstation/bioyond_studio/dispensing_station.py create mode 100644 unilabos/devices/workstation/bioyond_studio/reaction_station.py create mode 100644 unilabos/registry/devices/bioyond.yaml delete mode 100644 unilabos/registry/devices/dispensing_station_bioyond.yaml diff --git a/test/resources/test_converter_bioyond.py b/test/resources/test_converter_bioyond.py index 84db8ca5..068a0530 100644 --- a/test/resources/test_converter_bioyond.py +++ b/test/resources/test_converter_bioyond.py @@ -12,23 +12,13 @@ lab_registry.setup() type_mapping = { - "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", - "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", - "样品板": "BIOYOND_PolymerStation_6StockCarrier", - "分装板": "BIOYOND_PolymerStation_6VialCarrier", - "样品瓶": "BIOYOND_PolymerStation_Solid_Stock", - "90%分装小瓶": "BIOYOND_PolymerStation_Solid_Vial", - "10%分装小瓶": "BIOYOND_PolymerStation_Liquid_Vial", -} - -type_uuid_mapping = { - "烧杯": "", - "试剂瓶": "", - "样品板": "", - "分装板": "3a14196e-5dfe-6e21-0c79-fe2036d052c4", - "样品瓶": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", - "90%分装小瓶": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", - "10%分装小瓶": "3a14196c-76be-2279-4e22-7310d69aed68", + "烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), + "试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""), + "样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), + "分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), + "样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), + "90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), + "10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), } diff --git a/test/resources/test_itemized_carrier.py b/test/resources/test_itemized_carrier.py new file mode 100644 index 00000000..88b305e7 --- /dev/null +++ b/test/resources/test_itemized_carrier.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +""" +测试修改后的 get_child_identifier 函数 +""" + +from unilabos.resources.itemized_carrier import ItemizedCarrier, Bottle +from pylabrobot.resources.coordinate import Coordinate + +def test_get_child_identifier_with_indices(): + """测试返回x,y,z索引的 get_child_identifier 函数""" + + # 创建一些测试瓶子 + bottle1 = Bottle("bottle1", diameter=25.0, height=50.0, max_volume=15.0) + bottle1.location = Coordinate(10, 20, 5) + + bottle2 = Bottle("bottle2", diameter=25.0, height=50.0, max_volume=15.0) + bottle2.location = Coordinate(50, 20, 5) + + bottle3 = Bottle("bottle3", diameter=25.0, height=50.0, max_volume=15.0) + bottle3.location = Coordinate(90, 20, 5) + + # 创建载架,指定维度 + sites = { + "A1": bottle1, + "A2": bottle2, + "A3": bottle3, + "B1": None, # 空位 + "B2": None, + "B3": None + } + + carrier = ItemizedCarrier( + name="test_carrier", + size_x=150, + size_y=100, + size_z=30, + num_items_x=3, # 3列 + num_items_y=2, # 2行 + num_items_z=1, # 1层 + sites=sites + ) + + print("测试载架维度:") + print(f"num_items_x: {carrier.num_items_x}") + print(f"num_items_y: {carrier.num_items_y}") + print(f"num_items_z: {carrier.num_items_z}") + print() + + # 测试获取bottle1的标识符信息 (A1 = idx:0, x:0, y:0, z:0) + result1 = carrier.get_child_identifier(bottle1) + print("测试bottle1 (A1):") + print(f" identifier: {result1['identifier']}") + print(f" idx: {result1['idx']}") + print(f" x index: {result1['x']}") + print(f" y index: {result1['y']}") + print(f" z index: {result1['z']}") + + # Assert 验证 bottle1 (A1) 的结果 + assert result1['identifier'] == 'A1', f"Expected identifier 'A1', got '{result1['identifier']}'" + assert result1['idx'] == 0, f"Expected idx 0, got {result1['idx']}" + assert result1['x'] == 0, f"Expected x index 0, got {result1['x']}" + assert result1['y'] == 0, f"Expected y index 0, got {result1['y']}" + assert result1['z'] == 0, f"Expected z index 0, got {result1['z']}" + print(" ✓ bottle1 (A1) 测试通过") + print() + + # 测试获取bottle2的标识符信息 (A2 = idx:1, x:1, y:0, z:0) + result2 = carrier.get_child_identifier(bottle2) + print("测试bottle2 (A2):") + print(f" identifier: {result2['identifier']}") + print(f" idx: {result2['idx']}") + print(f" x index: {result2['x']}") + print(f" y index: {result2['y']}") + print(f" z index: {result2['z']}") + + # Assert 验证 bottle2 (A2) 的结果 + assert result2['identifier'] == 'A2', f"Expected identifier 'A2', got '{result2['identifier']}'" + assert result2['idx'] == 1, f"Expected idx 1, got {result2['idx']}" + assert result2['x'] == 1, f"Expected x index 1, got {result2['x']}" + assert result2['y'] == 0, f"Expected y index 0, got {result2['y']}" + assert result2['z'] == 0, f"Expected z index 0, got {result2['z']}" + print(" ✓ bottle2 (A2) 测试通过") + print() + + # 测试获取bottle3的标识符信息 (A3 = idx:2, x:2, y:0, z:0) + result3 = carrier.get_child_identifier(bottle3) + print("测试bottle3 (A3):") + print(f" identifier: {result3['identifier']}") + print(f" idx: {result3['idx']}") + print(f" x index: {result3['x']}") + print(f" y index: {result3['y']}") + print(f" z index: {result3['z']}") + + # Assert 验证 bottle3 (A3) 的结果 + assert result3['identifier'] == 'A3', f"Expected identifier 'A3', got '{result3['identifier']}'" + assert result3['idx'] == 2, f"Expected idx 2, got {result3['idx']}" + assert result3['x'] == 2, f"Expected x index 2, got {result3['x']}" + assert result3['y'] == 0, f"Expected y index 0, got {result3['y']}" + assert result3['z'] == 0, f"Expected z index 0, got {result3['z']}" + print(" ✓ bottle3 (A3) 测试通过") + print() + + # 测试错误情况:查找不存在的资源 + bottle_not_exists = Bottle("bottle_not_exists", diameter=25.0, height=50.0, max_volume=15.0) + try: + carrier.get_child_identifier(bottle_not_exists) + assert False, "应该抛出 ValueError 异常" + except ValueError as e: + print("✓ 正确抛出了 ValueError 异常:", str(e)) + assert "is not assigned to this carrier" in str(e), "异常消息应该包含预期的文本" + + print("\n🎉 所有测试都通过了!") + +if __name__ == "__main__": + test_get_child_identifier_with_indices() \ No newline at end of file diff --git a/unilabos/devices/workstation/README.md b/unilabos/devices/workstation/README.md index 710a2211..f96ed7f0 100644 --- a/unilabos/devices/workstation/README.md +++ b/unilabos/devices/workstation/README.md @@ -37,7 +37,7 @@ def _initialize_material_system(self, deck_config: Dict[str, Any], children_conf **定义在**: `workstation_base.py` **设计目的**: -- 提供外部物料系统(如Bioyong、LIMS等)集成的标准接口 +- 提供外部物料系统(如Bioyond、LIMS等)集成的标准接口 - 双向同步:从外部系统同步到本地deck,以及将本地变更同步到外部系统 - 处理外部系统的变更通知 @@ -59,7 +59,7 @@ async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: **扩展功能**: - HTTP报送接收服务集成 - 具体工作流实现(液体转移、板洗等) -- Bioyong物料系统同步器示例 +- Bioyond物料系统同步器示例 - 外部报送处理方法 ## 技术栈 @@ -142,11 +142,11 @@ success = workstation.execute_workflow("liquid_transfer", { ### 3. 外部系统集成 ```python -class BioyongResourceSynchronizer(ResourceSynchronizer): - """Bioyong系统同步器""" +class BioyondResourceSynchronizer(ResourceSynchronizer): + """Bioyond系统同步器""" async def sync_from_external(self) -> bool: - # 从Bioyong API获取物料 + # 从Bioyond API获取物料 external_materials = await self._fetch_bioyong_materials() # 转换并添加到本地deck diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index 12c03860..e12f14b2 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -1,6 +1,7 @@ # bioyond_rpc.py """ -BioyondV1RPC类定义 - 包含所有RPC接口和业务逻辑 +BioyondV1RPC类定义 - 负责HTTP接口通信和通用函数 +仅包含基础的API调用、通用工具函数,不包含特定站点业务逻辑 """ from enum import Enum @@ -8,7 +9,7 @@ from datetime import datetime, timezone from unilabos.device_comms.rpc import BaseRequest from typing import Optional, List, Dict, Any import json -from unilabos.devices.workstation.bioyond_studio.config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING +from unilabos.devices.workstation.bioyond_studio.config import LOCATION_MAPPING class SimpleLogger: @@ -36,110 +37,55 @@ class MaterialType(Enum): Product = 3 +class BioyondException(Exception): + """Bioyond操作异常""" + pass + + class BioyondV1RPC(BaseRequest): def __init__(self, config): super().__init__() - print("开始初始化") + print("开始初始化 BioyondV1RPC") self.config = config self.api_key = config["api_key"] self.host = config["api_host"] self._logger = SimpleLogger() - self.is_running = False - self.workflow_mappings = {} - self.workflow_sequence = [] - 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}") - return "" - 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) - web_workflow_list = data.get("web_workflow_list", []) - 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: - result[web_name] = workflow_id - 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("清空工作流执行顺序") + # ==================== 基础通用方法 ==================== def get_current_time_iso8601(self) -> str: - current_time = datetime.now(timezone.utc).isoformat(timespec='milliseconds') - return current_time.replace("+00:00", "Z") + """ + 获取当前时间,并格式化为 ISO 8601 格式(包含毫秒部分)。 + + :return: 当前时间的 ISO 8601 格式字符串 + """ + current_time = datetime.now(timezone.utc).isoformat( + timespec='milliseconds' + ) + # 替换时区部分为 'Z' + current_time = current_time.replace("+00:00", "Z") + return current_time + + def get_logger(self): + return self._logger + + # ==================== 物料查询相关接口 ==================== - # 物料查询接口 def stock_material(self, json_str: str) -> list: + """ + 描述:返回所有当前在库的,已启用的物料 + json_str 字段介绍,格式为JSON字符串: + '{"typeMode": 0, "filter": "样品", "includeDetail": true}' + + typeMode: 物料类型, 样品1、试剂2、耗材0 + filter: 过滤字段, 物料名称/物料编码 + includeDetail: 是否包含所在库位。true,false + """ try: params = json.loads(json_str) - except: + except json.JSONDecodeError: return [] response = self.post( @@ -154,11 +100,170 @@ class BioyondV1RPC(BaseRequest): return [] return response.get("data", []) - # 工作流列表查询 + def query_warehouse_by_material_type(self, type_id: str) -> dict: + """ + 描述:查询物料类型可以入库的库位 + type_id: 物料类型ID + """ + params = { + "typeId": type_id + } + + response = self.post( + url=f'{self.host}/api/lims/storage/warehouse-info-by-mat-type-id', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + print( + f"query warehouse by material type error: {response.get('message', '')}" + ) + 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 add_material(self, params: dict) -> dict: + """ + 描述:添加新的物料 + json_str 格式为JSON字符串 + """ + + response = self.post( + url=f'{self.host}/api/lims/storage/material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + print(f"add material error: {response.get('message', '')}") + return {} + + print(f"add material data: {response['data']}") + return response.get("data", {}) + + def query_matial_type_id(self, data) -> list: + """查找物料typeid""" + response = self.post( + url=f'{self.host}/api/lims/storage/material-types', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": data + }) + + if not response or response['code'] != 1: + return [] + return str(response.get("data", {})) + + def material_inbound(self, material_id: str, location_id: str) -> dict: + """ + 描述:指定库位入库一个物料 + material_id: 物料ID + location_name: 库位名称(会自动映射到location_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 or response['code'] != 1: + return {} + return response.get("data", {}) + + def delete_material(self, material_id: str) -> dict: + """ + 描述:删除尚未入库的物料 + material_id: 物料ID + """ + response = self.post( + url=f'{self.host}/api/lims/storage/delete-material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": material_id + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: + """指定库位出库物料""" + location_id = LOCATION_MAPPING.get(location_name, location_name) + + 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 or response['code'] != 1: + return {} + return response + + # ==================== 工作流查询相关接口 ==================== + def query_workflow(self, json_str: str) -> dict: try: params = json.loads(json_str) - except: + except json.JSONDecodeError: + print(f"无效的JSON字符串: {json_str}") + return {} + except Exception as e: + print(f"处理JSON时出错: {str(e)}") return {} response = self.post( @@ -173,15 +278,27 @@ 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]: 验证结果 + def workflow_step_query(self, workflow_id: str) -> dict: """ + 描述:查询某一个子工作流的详细信息,包含所有步骤、参数信息 + json_str 格式为JSON字符串: + '{"workflow_id": "workflow123"}' + """ + + response = self.post( + url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": workflow_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]: + """验证工作流参数格式""" try: validation_errors = [] @@ -247,11 +364,7 @@ class BioyondV1RPC(BaseRequest): } def get_workflow_parameter_template(self) -> Dict[str, Any]: - """获取工作流参数模板 - - Returns: - Dict[str, Any]: 参数模板和说明 - """ + """获取工作流参数模板""" return { "template": { "name": "拼接后的长工作流的名称", @@ -276,83 +389,62 @@ class BioyondV1RPC(BaseRequest): "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: - data = json.loads(json_str) - workflow_id = data.get("workflow_id", "") - except: - return {} + # ==================== 任务订单相关接口 ==================== - response = self.post( - url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": workflow_id, - }) - - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - # 任务推送接口 def create_order(self, json_str: str) -> dict: + """ + 描述:新建并开始任务,返回需要的物料和入库的库位 + json_str 格式为JSON字符串,包含任务参数 + """ try: params = json.loads(json_str) - except Exception as e: - result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) + self._logger.info(f"创建任务参数: {params}") + self._logger.info(f"参数类型: {type(params)}") + + response = self.post( + url=f'{self.host}/api/lims/order/order', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + raise BioyondException("API调用失败:未收到响应") + + if response['code'] != 1: + error_msg = f"创建任务失败: {response.get('message', '未知错误')}" + self._logger.error(error_msg) + raise BioyondException(error_msg) + + self._logger.info(f"创建任务成功,返回数据: {response['data']}") + result = str(response.get("data", {})) return result - print('===============', json.dumps(params)) + except BioyondException: + # 重新抛出BioyondException + raise + except json.JSONDecodeError as e: + error_msg = f"JSON解析失败: {str(e)}" + self._logger.error(error_msg) + raise BioyondException(error_msg) from e + except Exception as e: + # 捕获其他未预期的异常,转换为BioyondException + error_msg = f"创建任务时发生未预期的错误: {str(e)}" + self._logger.error(error_msg) + raise BioyondException(error_msg) from e - 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) - - if response['code'] != 1: - print(f"create order error: {response.get('message')}") - - print(f"create order data: {response['data']}") - result = str(response.get("data", {})) - return result - - # 查询任务列表 def order_query(self, json_str: str) -> dict: + """ + 描述:查询任务列表 + json_str 格式为JSON字符串 + """ try: params = json.loads(json_str) - except: + except json.JSONDecodeError: return {} response = self.post( @@ -367,12 +459,16 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) - # 任务明细查询 def order_report(self, json_str: str) -> dict: + """ + 描述:查询某个任务明细 + json_str 格式为JSON字符串: + '{"order_id": "order123"}' + """ try: data = json.loads(json_str) order_id = data.get("order_id", "") - except: + except json.JSONDecodeError: return {} response = self.post( @@ -387,41 +483,19 @@ 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: + """ + 描述:取出任务产物 + json_str 格式为JSON字符串: + '{"order_id": "order123", "preintake_id": "preintake123"}' + """ try: data = json.loads(json_str) params = { "orderId": data.get("order_id", ""), "preintakeId": data.get("preintake_id", "") } - except: + except json.JSONDecodeError: return 0 response = self.post( @@ -436,14 +510,71 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 设备列表查询 + 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 cancel_order(self, json_str: str) -> bool: + """ + 描述:取消指定任务 + json_str 格式为JSON字符串: + '{"order_id": "order123"}' + """ + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except json.JSONDecodeError: + return False + + response = self.post( + url=f'{self.host}/api/lims/order/cancel-order', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return False + return True + + # ==================== 设备管理相关接口 ==================== + def device_list(self, json_str: str = "") -> list: + """ + 描述:获取所有设备列表 + json_str 格式为JSON字符串,可选 + """ device_no = None if json_str: try: data = json.loads(json_str) device_no = data.get("device_no", None) - except: + except json.JSONDecodeError: pass url = f'{self.host}/api/lims/device/device-list' @@ -461,8 +592,11 @@ class BioyondV1RPC(BaseRequest): return [] return response.get("data", []) - # 设备操作 def device_operation(self, json_str: str) -> int: + """ + 描述:操作设备 + json_str 格式为JSON字符串 + """ try: data = json.loads(json_str) params = { @@ -470,7 +604,7 @@ class BioyondV1RPC(BaseRequest): "operationType": data.get("operation_type", 0), "operationParams": data.get("operation_params", {}) } - except: + except json.JSONDecodeError: return 0 response = self.post( @@ -485,7 +619,8 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 调度器状态查询 + # ==================== 调度器相关接口 ==================== + def scheduler_status(self) -> dict: response = self.post( url=f'{self.host}/api/lims/scheduler/scheduler-status', @@ -498,8 +633,8 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) - # 调度器启动 def scheduler_start(self) -> int: + """描述:启动调度器""" response = self.post( url=f'{self.host}/api/lims/scheduler/start', params={ @@ -511,8 +646,8 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 调度器暂停 def scheduler_pause(self) -> int: + """描述:暂停调度器""" response = self.post( url=f'{self.host}/api/lims/scheduler/scheduler-pause', params={ @@ -524,7 +659,6 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 调度器继续 def scheduler_continue(self) -> int: response = self.post( url=f'{self.host}/api/lims/scheduler/scheduler-continue', @@ -537,8 +671,8 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 调度器停止 def scheduler_stop(self) -> int: + """描述:停止调度器""" response = self.post( url=f'{self.host}/api/lims/scheduler/scheduler-stop', params={ @@ -550,8 +684,8 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 调度器重置 def scheduler_reset(self) -> int: + """描述:重置调度器""" response = self.post( url=f'{self.host}/api/lims/scheduler/scheduler-reset', params={ @@ -563,579 +697,8 @@ class BioyondV1RPC(BaseRequest): return 0 return response.get("code", 0) - # 取消任务 - def cancel_order(self, json_str: str) -> bool: - try: - data = json.loads(json_str) - order_id = data.get("order_id", "") - except: - return False + # ==================== 辅助方法 ==================== - response = self.post( - url=f'{self.host}/api/lims/order/cancel-order', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": order_id, - }) - - if not response or response['code'] != 1: - return False - return True - - # 获取可拼接工作流 - def query_split_workflow(self) -> list: - response = self.post( - url=f'{self.host}/api/lims/workflow/split-workflow-list', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - }) - - if not response or response['code'] != 1: - return [] - return str(response.get("data", {})) - - # 合并工作流 - def merge_workflow(self, json_str: str) -> dict: - try: - data = json.loads(json_str) - params = { - "name": data.get("name", ""), - "workflowIds": data.get("workflow_ids", []) - } - except: - return {} - - response = self.post( - url=f'{self.host}/api/lims/workflow/merge-workflow', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": params, - }) - - 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: - data = json.loads(json_str) - 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={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": params, - }) - - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - # 发布任务 - 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", "") - print(f"使用工作流创建任务: {workflow_name} (ID: {workflow_id})") - - 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('-', '')}", - "workFlowId": workflow_id, - "borderNumber": 1, - "paramValues": param_values, - "extendProperties": "" - }] - - task_json = json.dumps(task_params) - print(f"创建任务参数: {type(task_json)}") - result = self.create_order(task_json) - - if not result: - error_msg = "创建任务失败" - print(error_msg) - result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"}) - return result - - print(f"任务创建成功: {result}") - self.pending_task_params.clear() - print("已清空pending_task_params") - - return { - "success": True, - "workflow": {"name": workflow_name, "id": workflow_id}, - "task": result, - "method": "process_and_execute_workflow" - } - - # 生成任务参数 - def generate_task_param_values(self, workflow_params_structure): - if not workflow_params_structure: - print("workflow_params_structure为空") - return {} - - data = workflow_params_structure - - # 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织 - pending_params_by_section = {} - print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组") - - # 获取工作流执行顺序,用于按顺序匹配参数 - workflow_sequence = self.get_workflow_sequence() - print(f"工作流执行顺序: {workflow_sequence}") - - workflow_index = 0 - - for i, task_param in enumerate(self.pending_task_params): - if 'param_values' in task_param: - print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤") - - if workflow_index < len(workflow_sequence): - current_workflow = workflow_sequence[workflow_index] - section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow) - print(f" 匹配到工作流: {current_workflow} -> {section_name}") - workflow_index += 1 - else: - print(f" 警告: 参数组{i+1}超出了工作流序列范围") - continue - - if not section_name: - print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName") - continue - - if section_name not in pending_params_by_section: - pending_params_by_section[section_name] = {} - - for step_id, param_list in task_param['param_values'].items(): - print(f" 步骤ID: {step_id},参数数量: {len(param_list)}") - - for param_item in param_list: - key = param_item.get('Key', '') - value = param_item.get('Value', '') - m = param_item.get('m', 0) - n = param_item.get('n', 0) - print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}") - - param_key = f"{section_name}.{key}" - if param_key not in pending_params_by_section[section_name]: - pending_params_by_section[section_name][param_key] = [] - - pending_params_by_section[section_name][param_key].append({ - 'value': value, - 'm': m, - 'n': n - }) - - print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组") - - # 收集所有参数,过滤TaskDisplayable为0的项 - filtered_params = [] - - for step_id, step_info in data.items(): - if isinstance(step_info, list): - for step_item in step_info: - param_list = step_item.get("parameterList", []) - for param in param_list: - if param.get("TaskDisplayable") == 0: - continue - - param_with_step = param.copy() - param_with_step['step_id'] = step_id - param_with_step['step_name'] = step_item.get("name", "") - param_with_step['step_m'] = step_item.get("m", 0) - param_with_step['step_n'] = step_item.get("n", 0) - filtered_params.append(param_with_step) - - # 按DisplaySectionIndex排序 - filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0)) - - # 生成参数映射 - param_mapping = {} - step_params = {} - for param in filtered_params: - step_id = param['step_id'] - if step_id not in step_params: - step_params[step_id] = [] - step_params[step_id].append(param) - - # 为每个步骤生成参数 - for step_id, params in step_params.items(): - param_list = [] - for param in params: - key = param.get('Key', '') - display_section_index = param.get('DisplaySectionIndex', 0) - step_m = param.get('step_m', 0) - step_n = param.get('step_n', 0) - - section_name = param.get('DisplaySectionName', '') - param_key = f"{section_name}.{key}" - - if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]: - pending_param_list = pending_params_by_section[section_name][param_key] - if pending_param_list: - pending_param = pending_param_list[0] - value = pending_param['value'] - m = step_m - n = step_n - print(f" 匹配成功: {section_name}.{key} = {value} (m={m}, n={n})") - pending_param_list.pop(0) - else: - value = "1" - m = step_m - n = step_n - print(f" 匹配失败: {section_name}.{key},参数列表为空,使用默认值 = {value}") - else: - value = "1" - m = display_section_index - n = step_n - print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") - - param_item = { - "m": m, - "n": n, - "key": key, - "value": str(value).strip() - } - param_list.append(param_item) - - if param_list: - param_mapping[step_id] = param_list - - print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤") - return param_mapping - - # 工作流方法 - def reactor_taken_out(self): - """反应器取出""" - self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') - reactor_taken_out_params = {"param_values": {}} - self.pending_task_params.append(reactor_taken_out_params) - print(f"成功添加反应器取出工作流") - print(f"当前队列长度: {len(self.pending_task_params)}") - return json.dumps({"suc": True}) - - def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00): - """反应器放入""" - 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) - - step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] - reactor_taken_in_params = { - "param_values": { - step_id: [ - {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, - {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} - ] - } - } - - self.pending_task_params.append(reactor_taken_in_params) - 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", - assign_material_name: str = None, temperature: float = 25.00): - """固体进料小瓶""" - self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') - material_id_m = self._get_material_id_by_name(assign_material_name) - - if isinstance(temperature, str): - temperature = float(temperature) - - feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] - observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] - - solid_feeding_vials_params = { - "param_values": { - feeding_id: [ - {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] - } - } - - self.pending_task_params.append(solid_feeding_vials_params) - print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C") - print(f"当前队列长度: {len(self.pending_task_params)}") - return json.dumps({"suc": True}) - - def liquid_feeding_vials_non_titration(self, volumeFormula: str, 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)"}') - material_id = self._get_material_id_by_name(assign_material_name) - - if isinstance(temperature, str): - temperature = float(temperature) - - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] - - params = { - "param_values": { - liquid_id: [ - {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] - } - } - - self.pending_task_params.append(params) - 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): - """液体进料溶剂""" - self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') - material_id = self._get_material_id_by_name(assign_material_name) - - if isinstance(temperature, str): - temperature = float(temperature) - - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] - - params = { - "param_values": { - liquid_id: [ - {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, - {"m": 0, "n": 1, "Key": "volume", "Value": volume}, - {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] - } - } - - self.pending_task_params.append(params) - print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") - print(f"当前队列长度: {len(self.pending_task_params)}") - return json.dumps({"suc": True}) - - 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): - """液体进料(滴定)""" - self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') - material_id = self._get_material_id_by_name(assign_material_name) - - if isinstance(temperature, str): - temperature = float(temperature) - - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] - - params = { - "param_values": { - liquid_id: [ - {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, - {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, - {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] - } - } - - self.pending_task_params.append(params) - 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", - temperature: float = 25.00): - """液体进料烧杯""" - self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') - material_id = self._get_material_id_by_name(assign_material_name) - - if isinstance(temperature, str): - temperature = float(temperature) - - liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] - observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] - - params = { - "param_values": { - liquid_id: [ - {"m": 0, "n": 2, "Key": "volume", "Value": volume}, - {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, - {"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType} - ], - observe_id: [ - {"m": 1, "n": 0, "Key": "time", "Value": time}, - {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, - {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} - ] - } - } - - self.pending_task_params.append(params) - print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") - print(f"当前队列长度: {len(self.pending_task_params)}") - return json.dumps({"suc": True}) - - # 辅助方法 def _load_material_cache(self): """预加载材料列表到缓存中""" try: @@ -1181,771 +744,4 @@ class BioyondV1RPC(BaseRequest): def get_available_materials(self): """获取所有可用的材料名称列表""" - return list(self.material_cache.keys()) - - # 物料管理接口 - def add_material(self, json_str: str) -> dict: - """添加新的物料""" - try: - params = json.loads(json_str) - except: - return {} - - response = self.post( - url=f'{self.host}/api/lims/storage/material', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": params - }) - - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - def query_matial_type_id(self, data) -> list: - """查找物料typeid""" - response = self.post( - url=f'{self.host}/api/lims/storage/material-types', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": data - }) - - if not response or response['code'] != 1: - return [] - return str(response.get("data", {})) - - def query_warehouse_by_material_type(self, type_id: str) -> dict: - """查询物料类型可以入库的库位""" - params = {"typeId": type_id} - - response = self.post( - url=f'{self.host}/api/lims/storage/warehouse-info-by-mat-type-id', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": params - }) - - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - def material_inbound(self, material_id: str, location_name: str) -> dict: - """指定库位入库一个物料""" - location_id = LOCATION_MAPPING.get(location_name, location_name) - - 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 or response['code'] != 1: - return {} - return response.get("data", {}) - - def delete_material(self, material_id: str) -> dict: - """删除尚未入库的物料""" - response = self.post( - url=f'{self.host}/api/lims/storage/delete-material', - params={ - "apiKey": self.api_key, - "requestTime": self.get_current_time_iso8601(), - "data": material_id - }) - - if not response or response['code'] != 1: - return {} - return response.get("data", {}) - - def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: - """指定库位出库物料""" - location_id = LOCATION_MAPPING.get(location_name, location_name) - - 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 or response['code'] != 1: - return {} - return response - - 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} + return list(self.material_cache.keys()) \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py new file mode 100644 index 00000000..b1820d6c --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py @@ -0,0 +1,824 @@ +from datetime import datetime +import json + +from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException +from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation + + +class BioyondDispensingStation(BioyondWorkstation): + def __init__(self, config): + super().__init__(config) + # self.config = config + # self.api_key = config["api_key"] + # self.host = config["api_host"] + # + # # 使用简单的Logger替代原来的logger + # self._logger = SimpleLogger() + # self.is_running = False + + # 90%10%小瓶投料任务创建方法 + 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 + + 返回: 任务创建结果 + + 异常: + - BioyondException: 各种错误情况下的统一异常 + """ + try: + # 1. 参数验证 + if not hold_m_name: + raise BioyondException("hold_m_name 是必填参数") + + # 检查90%物料参数的完整性 + # 90%_1物料:如果有物料名称或目标重量,就必须有全部参数 + if percent_90_1_assign_material_name or percent_90_1_target_weigh: + if not percent_90_1_assign_material_name: + raise BioyondException("90%_1物料:如果提供了目标重量,必须同时提供物料名称") + if not percent_90_1_target_weigh: + raise BioyondException("90%_1物料:如果提供了物料名称,必须同时提供目标重量") + + # 90%_2物料:如果有物料名称或目标重量,就必须有全部参数 + if percent_90_2_assign_material_name or percent_90_2_target_weigh: + if not percent_90_2_assign_material_name: + raise BioyondException("90%_2物料:如果提供了目标重量,必须同时提供物料名称") + if not percent_90_2_target_weigh: + raise BioyondException("90%_2物料:如果提供了物料名称,必须同时提供目标重量") + + # 90%_3物料:如果有物料名称或目标重量,就必须有全部参数 + if percent_90_3_assign_material_name or percent_90_3_target_weigh: + if not percent_90_3_assign_material_name: + raise BioyondException("90%_3物料:如果提供了目标重量,必须同时提供物料名称") + if not percent_90_3_target_weigh: + raise BioyondException("90%_3物料:如果提供了物料名称,必须同时提供目标重量") + + # 检查10%物料参数的完整性 + # 10%_1物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 + if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]): + if not percent_10_1_assign_material_name: + raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供固体物料名称") + if not percent_10_1_target_weigh: + raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供固体目标重量") + if not percent_10_1_volume: + raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体体积") + if not percent_10_1_liquid_material_name: + raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体物料名称") + + # 10%_2物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 + if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]): + if not percent_10_2_assign_material_name: + raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供固体物料名称") + if not percent_10_2_target_weigh: + raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供固体目标重量") + if not percent_10_2_volume: + raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体体积") + if not percent_10_2_liquid_material_name: + raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体物料名称") + + # 10%_3物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 + if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]): + if not percent_10_3_assign_material_name: + raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供固体物料名称") + if not percent_10_3_target_weigh: + raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供固体目标重量") + if not percent_10_3_volume: + raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体体积") + if not percent_10_3_liquid_material_name: + raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体物料名称") + + # 2. 生成任务编码和设置默认值 + order_code = "task_vial_" + str(int(datetime.now().timestamp())) + if order_name is None: + order_name = "90%10%小瓶投料任务" + if speed is None: + speed = "400" + if temperature is None: + temperature = "40" + if delay_time is None: + delay_time = "600" + + # 3. 工作流ID + workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b" + + # 4. 查询工作流对应的holdMID + material_info = self.hardware_interface.material_id_query(workflow_id) + if not material_info: + raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息") + + # 获取locations列表 + locations = material_info.get("locations", []) if isinstance(material_info, dict) else [] + if not locations: + raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息") + + # 查找指定名称的库位 + hold_mid = None + for location in locations: + if location.get("holdMName") == hold_m_name: + hold_mid = location.get("holdMId") + break + + if not hold_mid: + raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确") + + extend_properties = f"{{\"{ hold_mid }\": {{}}}}" + self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}") + + # 5. 构建任务参数 + order_data = { + "orderCode": order_code, + "orderName": order_name, + "workflowId": workflow_id, + "borderNumber": 1, + "paramValues": {}, + "ExtendProperties": extend_properties + } + + # 添加搅拌参数 + order_data["paramValues"]["e8264e47-c319-d9d9-8676-4dd5cb382b11"] = [ + {"m": 0, "n": 3, "Key": "speed", "Value": speed}, + {"m": 0, "n": 3, "Key": "temperature", "Value": temperature} + ] + + # 添加延迟时间参数 + order_data["paramValues"]["dc5dba79-5e4b-8eae-cbc5-e93482e43b1f"] = [ + {"m": 0, "n": 4, "Key": "DelayTime", "Value": delay_time} + ] + + # 添加90%_1参数 + if percent_90_1_assign_material_name is not None and percent_90_1_target_weigh is not None: + order_data["paramValues"]["e7d3c0a3-25c2-c42d-c84b-860c4a5ef844"] = [ + {"m": 15, "n": 1, "Key": "targetWeigh", "Value": percent_90_1_target_weigh}, + {"m": 15, "n": 1, "Key": "assignMaterialName", "Value": percent_90_1_assign_material_name} + ] + + # 添加90%_2参数 + if percent_90_2_assign_material_name is not None and percent_90_2_target_weigh is not None: + order_data["paramValues"]["50b912c4-6c81-0734-1c8b-532428b2a4a5"] = [ + {"m": 18, "n": 1, "Key": "targetWeigh", "Value": percent_90_2_target_weigh}, + {"m": 18, "n": 1, "Key": "assignMaterialName", "Value": percent_90_2_assign_material_name} + ] + + # 添加90%_3参数 + if percent_90_3_assign_material_name is not None and percent_90_3_target_weigh is not None: + order_data["paramValues"]["9c3674b3-c7cb-946e-fa03-fa2861d8aec4"] = [ + {"m": 21, "n": 1, "Key": "targetWeigh", "Value": percent_90_3_target_weigh}, + {"m": 21, "n": 1, "Key": "assignMaterialName", "Value": percent_90_3_assign_material_name} + ] + + # 添加10%_1固体参数 + if percent_10_1_assign_material_name is not None and percent_10_1_target_weigh is not None: + order_data["paramValues"]["73a0bfd8-1967-45e9-4bab-c07ccd1a2727"] = [ + {"m": 3, "n": 1, "Key": "targetWeigh", "Value": percent_10_1_target_weigh}, + {"m": 3, "n": 1, "Key": "assignMaterialName", "Value": percent_10_1_assign_material_name} + ] + + # 添加10%_1液体参数 + if percent_10_1_liquid_material_name is not None and percent_10_1_volume is not None: + order_data["paramValues"]["39634d40-c623-473a-8e5f-bc301aca2522"] = [ + {"m": 3, "n": 3, "Key": "volume", "Value": percent_10_1_volume}, + {"m": 3, "n": 3, "Key": "assignMaterialName", "Value": percent_10_1_liquid_material_name} + ] + + # 添加10%_2固体参数 + if percent_10_2_assign_material_name is not None and percent_10_2_target_weigh is not None: + order_data["paramValues"]["2d9c16fa-2a19-cd47-a67b-3cadff9e3e3d"] = [ + {"m": 7, "n": 1, "Key": "targetWeigh", "Value": percent_10_2_target_weigh}, + {"m": 7, "n": 1, "Key": "assignMaterialName", "Value": percent_10_2_assign_material_name} + ] + + # 添加10%_2液体参数 + if percent_10_2_liquid_material_name is not None and percent_10_2_volume is not None: + order_data["paramValues"]["e60541bb-ed68-e839-7305-2b4abe38a13d"] = [ + {"m": 7, "n": 3, "Key": "volume", "Value": percent_10_2_volume}, + {"m": 7, "n": 3, "Key": "assignMaterialName", "Value": percent_10_2_liquid_material_name} + ] + + # 添加10%_3固体参数 + if percent_10_3_assign_material_name is not None and percent_10_3_target_weigh is not None: + order_data["paramValues"]["27494733-0f71-a916-7cd2-1929a0125f17"] = [ + {"m": 11, "n": 1, "Key": "targetWeigh", "Value": percent_10_3_target_weigh}, + {"m": 11, "n": 1, "Key": "assignMaterialName", "Value": percent_10_3_assign_material_name} + ] + + # 添加10%_3液体参数 + if percent_10_3_liquid_material_name is not None and percent_10_3_volume is not None: + order_data["paramValues"]["c8798c29-786f-6858-7d7f-5330b890f2a6"] = [ + {"m": 11, "n": 3, "Key": "volume", "Value": percent_10_3_volume}, + {"m": 11, "n": 3, "Key": "assignMaterialName", "Value": percent_10_3_liquid_material_name} + ] + + # 6. 转换为JSON字符串并创建任务 + json_str = json.dumps([order_data], ensure_ascii=False) + self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务参数: {json_str}") + + # 7. 调用create_order方法创建任务 + result = self.hardware_interface.create_order(json_str) + self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}") + return json.dumps({"suc": True}) + + except BioyondException: + # 重新抛出BioyondException + raise + except Exception as e: + # 捕获其他未预期的异常,转换为BioyondException + error_msg = f"创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(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则使用默认值20 + - delay_time: 延迟时间,如果为None则使用默认值600 + - hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId + + 返回: 任务创建结果 + + 异常: + - BioyondException: 各种错误情况下的统一异常 + """ + try: + # 1. 参数验证 + if not material_name: + raise BioyondException("material_name 是必填参数") + if not target_weigh: + raise BioyondException("target_weigh 是必填参数") + if not volume: + raise BioyondException("volume 是必填参数") + if not hold_m_name: + raise BioyondException("hold_m_name 是必填参数") + + + # 2. 生成任务编码和设置默认值 + order_code = "task_oda_" + str(int(datetime.now().timestamp())) + if order_name is None: + order_name = f"二胺溶液配置-{material_name}" + if speed is None: + speed = "400" + if temperature is None: + temperature = "20" + if delay_time is None: + delay_time = "600" + + # 3. 工作流ID - 二胺溶液配置工作流 + workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5" + + # 4. 查询工作流对应的holdMID + material_info = self.material_id_query(workflow_id) + if not material_info: + raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息") + + # 获取locations列表 + locations = material_info.get("locations", []) if isinstance(material_info, dict) else [] + if not locations: + raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息") + + # 查找指定名称的库位 + hold_mid = None + for location in locations: + if location.get("holdMName") == hold_m_name: + hold_mid = location.get("holdMId") + break + + if not hold_mid: + raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确") + + extend_properties = f"{{\"{ hold_mid }\": {{}}}}" + self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}") + + # 5. 构建任务参数 + order_data = { + "orderCode": order_code, + "orderName": order_name, + "workflowId": workflow_id, + "borderNumber": 1, + "paramValues": { + # 固体物料参数 + "3a15d4a1-3bde-f5bc-053f-1ae0bf1f357e": [ + {"m": 3, "n": 2, "Key": "targetWeigh", "Value": target_weigh}, + {"m": 3, "n": 2, "Key": "assignMaterialName", "Value": material_name} + ], + # 液体物料参数 + "3a15d4a1-3bde-d584-b309-e661ae8f1c01": [ + {"m": 3, "n": 3, "Key": "volume", "Value": volume}, + {"m": 3, "n": 3, "Key": "assignMaterialName", "Value": liquid_material_name} + ], + # 搅拌参数 + "3a15d4a1-3bde-8ec4-1ced-92efc97ed73d": [ + {"m": 3, "n": 6, "Key": "speed", "Value": speed}, + {"m": 3, "n": 6, "Key": "temperature", "Value": temperature} + ], + # 延迟时间参数 + "3a15d4a1-3bde-3b92-83ff-8923a0addbbc": [ + {"m": 3, "n": 7, "Key": "DelayTime", "Value": delay_time} + ] + }, + "ExtendProperties": extend_properties + } + + # 6. 转换为JSON字符串并创建任务 + json_str = json.dumps([order_data], ensure_ascii=False) + self.hardware_interface._logger.info(f"创建二胺溶液配置任务参数: {json_str}") + + # 7. 调用create_order方法创建任务 + result = self.hardware_interface.create_order(json_str) + self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}") + + return json.dumps({"suc": True}) + + except BioyondException: + # 重新抛出BioyondException + raise + except Exception as e: + # 捕获其他未预期的异常,转换为BioyondException + error_msg = f"创建二胺溶液配置任务时发生未预期的错误: {str(e)}" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(error_msg) + + +if __name__ == "__main__": + bioyond = BioyondDispensingStation(config={ + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44388" + }) + + # 示例1:使用material_id_query查询工作流对应的holdMID + workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID + workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID + + #示例2:创建二胺溶液配置任务 - ODA,指定库位名称 + # bioyond.create_diamine_solution_task( + # order_code="task_oda_" + str(int(datetime.now().timestamp())), + # order_name="二胺溶液配置-ODA", + # material_name="ODA-1", + # target_weigh="12.000", + # volume="60", + # liquid_material_name= "NMP", + # speed="400", + # temperature="20", + # delay_time="600", + # hold_m_name="烧杯ODA" + # ) + + # bioyond.create_diamine_solution_task( + # order_code="task_pda_" + str(int(datetime.now().timestamp())), + # order_name="二胺溶液配置-PDA", + # material_name="PDA-1", + # target_weigh="4.178", + # volume="60", + # liquid_material_name= "NMP", + # speed="400", + # temperature="20", + # delay_time="600", + # hold_m_name="烧杯PDA-2" + # ) + + # bioyond.create_diamine_solution_task( + # order_code="task_mpda_" + str(int(datetime.now().timestamp())), + # order_name="二胺溶液配置-MPDA", + # material_name="MPDA-1", + # target_weigh="3.298", + # volume="50", + # liquid_material_name= "NMP", + # speed="400", + # temperature="20", + # delay_time="600", + # hold_m_name="烧杯MPDA" + # ) + + bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b") + bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5") + + + #示例4:创建90%10%小瓶投料任务 + # vial_result = bioyond.create_90_10_vial_feeding_task( + # order_code="task_vial_" + str(int(datetime.now().timestamp())), + # order_name="90%10%小瓶投料-1", + # percent_90_1_assign_material_name="BTDA-1", + # percent_90_1_target_weigh="7.392", + # percent_90_2_assign_material_name="BTDA-1", + # percent_90_2_target_weigh="7.392", + # percent_90_3_assign_material_name="BTDA-2", + # percent_90_3_target_weigh="7.392", + # percent_10_1_assign_material_name="BTDA-2", + # percent_10_1_target_weigh="1.500", + # percent_10_1_volume="20", + # percent_10_1_liquid_material_name="NMP", + # # percent_10_2_assign_material_name="BTDA-c", + # # percent_10_2_target_weigh="1.2", + # # percent_10_2_volume="20", + # # percent_10_2_liquid_material_name="NMP", + # speed="400", + # temperature="60", + # delay_time="1200", + # hold_m_name="8.4分装板-1" + # ) + + # vial_result = bioyond.create_90_10_vial_feeding_task( + # order_code="task_vial_" + str(int(datetime.now().timestamp())), + # order_name="90%10%小瓶投料-2", + # percent_90_1_assign_material_name="BPDA-1", + # percent_90_1_target_weigh="5.006", + # percent_90_2_assign_material_name="PMDA-1", + # percent_90_2_target_weigh="3.810", + # percent_90_3_assign_material_name="BPDA-1", + # percent_90_3_target_weigh="8.399", + # percent_10_1_assign_material_name="BPDA-1", + # percent_10_1_target_weigh="1.200", + # percent_10_1_volume="20", + # percent_10_1_liquid_material_name="NMP", + # percent_10_2_assign_material_name="BPDA-1", + # percent_10_2_target_weigh="1.200", + # percent_10_2_volume="20", + # percent_10_2_liquid_material_name="NMP", + # speed="400", + # temperature="60", + # delay_time="1200", + # hold_m_name="8.4分装板-2" + # ) + + #启动调度器 + #bioyond.scheduler_start() + + #继续调度器 + #bioyond.scheduler_continue() + + result0 = bioyond.stock_material('{"typeMode": 0, "includeDetail": true}') + result1 = bioyond.stock_material('{"typeMode": 1, "includeDetail": true}') + result2 = bioyond.stock_material('{"typeMode": 2, "includeDetail": true}') + + matpos1 = bioyond.query_warehouse_by_material_type("3a14196e-b7a0-a5da-1931-35f3000281e9") + matpos2 = bioyond.query_warehouse_by_material_type("3a14196e-5dfe-6e21-0c79-fe2036d052c4") + matpos3 = bioyond.query_warehouse_by_material_type("3a14196b-24f2-ca49-9081-0cab8021bf1a") + + #样品板(里面有样品瓶) + material_data_yp = { + "typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "8.4样品板", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "BTDA-1", + "quantity": 20, + "x": 1, + "y": 1, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "BPDA-1", + "quantity": 20, + "x": 2, + "y": 1, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "BTDA-2", + "quantity": 20, + "x": 1, + "y": 2, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "PMDA-1", + "quantity": 20, + "x": 2, + "y": 2, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + } + ], + "Parameters":"{}" + } + + material_data_yp = { + "typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "8.7样品板", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "mianfen", + "quantity": 13, + "x": 1, + "y": 1, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94", + #"code": "物料编码001", + "name": "mianfen2", + "quantity": 13, + "x": 1, + "y": 2, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + } + ], + "Parameters":"{}" + } + + #分装板 + material_data_fzb_1 = { + "typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "8.7分装板", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶1", + "quantity": 1, + "x": 1, + "y": 1, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶2", + "quantity": 1, + "x": 1, + "y": 2, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶3", + "quantity": 1, + "x": 1, + "y": 3, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶1", + "quantity": 1, + "x": 2, + "y": 1, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶2", + "quantity": 1, + "x": 2, + "y": 2, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶3", + "quantity": 1, + "x": 2, + "y": 3, + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + } + ], + "Parameters":"{}" + } + + material_data_fzb_2 = { + "typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "8.4分装板-2", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶1", + "quantity": 1, + "x": 1, + "y": 1, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶2", + "quantity": 1, + "x": 1, + "y": 2, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-76be-2279-4e22-7310d69aed68", + #"code": "物料编码001", + "name": "10%小瓶3", + "quantity": 1, + "x": 1, + "y": 3, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶1", + "quantity": 1, + "x": 2, + "y": 1, #x1y2是A02 + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶2", + "quantity": 1, + "x": 2, + "y": 2, + #"unit": "单位" + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + }, + { + "typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea", + #"code": "物料编码001", + "name": "90%小瓶3", + "quantity": 1, + "x": 2, + "y": 3, + "molecular": 1, + "Parameters":"{\"molecular\": 1}" + } + ], + "Parameters":"{}" + } + + #烧杯 + material_data_sb_oda = { + "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "mianfen1", + "unit": "个", + "quantity": 1, + "Parameters":"{}" + } + + material_data_sb_pda_2 = { + "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", + #"code": "物料编码001", + #"barCode": "物料条码001", + "name": "mianfen2", + "unit": "个", + "quantity": 1, + "Parameters":"{}" + } + + # material_data_sb_mpda = { + # "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", + # #"code": "物料编码001", + # #"barCode": "物料条码001", + # "name": "烧杯MPDA", + # "unit": "个", + # "quantity": 1, + # "Parameters":"{}" + # } + + + #result_1 = bioyond.add_material(json.dumps(material_data_yp, ensure_ascii=False)) + #result_2 = bioyond.add_material(json.dumps(material_data_fzb_1, ensure_ascii=False)) + # result_3 = bioyond.add_material(json.dumps(material_data_fzb_2, ensure_ascii=False)) + # result_4 = bioyond.add_material(json.dumps(material_data_sb_oda, ensure_ascii=False)) + # result_5 = bioyond.add_material(json.dumps(material_data_sb_pda_2, ensure_ascii=False)) + # #result会返回id + # #样品板1id:3a1b3e7d-339d-0291-dfd3-13e2a78fe521 + + + # #将指定物料入库到指定库位 + #bioyond.material_inbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df") + #bioyond.material_inbound(result_2, "3a14198e-6929-46fe-841e-03dd753f1e4a") + # bioyond.material_inbound(result_3, "3a14198e-6929-72ac-32ce-9b50245682b8") + # bioyond.material_inbound(result_4, "3a14198e-d724-e036-afdc-2ae39a7f3383") + # bioyond.material_inbound(result_5, "3a14198e-d724-d818-6d4f-5725191a24b5") + + #bioyond.material_outbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df") + + # bioyond.stock_material('{"typeMode": 2, "includeDetail": true}') + + query_order = {"status":"100", "pageCount": "10"} + bioyond.order_query(json.dumps(query_order, ensure_ascii=False)) + + # id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc" + # bioyond.sample_waste_removal(id) + diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station.py new file mode 100644 index 00000000..e35c657f --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station.py @@ -0,0 +1,207 @@ +import json + +from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation +from unilabos.devices.workstation.bioyond_studio.config import ( + API_CONFIG, WORKFLOW_MAPPINGS, WORKFLOW_STEP_IDS, MATERIAL_TYPE_MAPPINGS, + STATION_TYPES, DEFAULT_STATION_CONFIG +) + + +class BioyondReactionStation(BioyondWorkstation): + def __init__(self, config: dict = None): + super().__init__(config) + + # 工作流方法 + def reactor_taken_out(self): + """反应器取出""" + self.hardware_interface.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') + reactor_taken_out_params = {"param_values": {}} + self.hardware_interface.pending_task_params.append(reactor_taken_out_params) + print(f"成功添加反应器取出工作流") + print(f"当前队列长度: {len(self.hardware_interface.pending_task_params)}") + return json.dumps({"suc": True}) + + def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00): + """反应器放入""" + 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) + + step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] + reactor_taken_in_params = { + "param_values": { + step_id: [ + {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, + {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ] + } + } + + self.pending_task_params.append(reactor_taken_in_params) + 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", + assign_material_name: str = None, temperature: float = 25.00): + """固体进料小瓶""" + self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') + material_id_m = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] + observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] + + solid_feeding_vials_params = { + "param_values": { + feeding_id: [ + {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(solid_feeding_vials_params) + print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_vials_non_titration(self, volumeFormula: str, 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)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + 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): + """液体进料溶剂""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 1, "Key": "volume", "Value": volume}, + {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + 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): + """液体进料(滴定)""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + 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", + temperature: float = 25.00): + """液体进料烧杯""" + self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 2, "Key": "volume", "Value": volume}, + {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index b2c3ac2d..68abe775 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -5,6 +5,7 @@ Bioyond Workstation Implementation 集成Bioyond物料管理的工作站示例 """ import traceback +from datetime import datetime from typing import Dict, Any, List, Optional, Union import json @@ -13,15 +14,14 @@ from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.resources.warehouse import WareHouse from unilabos.utils.log import logger -from unilabos.resources.graphio import resource_bioyond_to_plr +from unilabos.resources.graphio import resource_bioyond_to_plr, resource_plr_to_bioyond from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from pylabrobot.resources.resource import Resource as ResourcePLR from unilabos.devices.workstation.bioyond_studio.config import ( - API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, - STATION_TYPES, DEFAULT_STATION_CONFIG + API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING ) @@ -68,7 +68,11 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): return False # 转换为UniLab格式 - unilab_resources = resource_bioyond_to_plr(bioyond_data, type_mapping=self.workstation.bioyond_config["material_type_mappings"], deck=self.workstation.deck) + unilab_resources = resource_bioyond_to_plr( + bioyond_data, + type_mapping=self.workstation.bioyond_config["material_type_mappings"], + deck=self.workstation.deck + ) logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源") return True @@ -84,11 +88,22 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): logger.error("Bioyond API客户端未初始化") return False - # 调用入库、出库操作 - # bioyond_format_data = self._convert_resource_to_bioyond_format(resource) - # success = await self.bioyond_api_client.update_material(bioyond_format_data) - # - # if success + bioyond_material = resource_plr_to_bioyond( + [resource], + type_mapping=self.workstation.bioyond_config["material_type_mappings"], + warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"] + )[0] + + location_info = bioyond_material.pop("locations") + + material_id = self.bioyond_api_client.add_material(bioyond_material) + + response = self.bioyond_api_client.material_inbound(material_id, location_info[0]["id"]) + if not response: + return { + "status": "error", + "message": "Failed to inbound material" + } except: pass @@ -146,11 +161,21 @@ class BioyondWorkstation(WorkstationBase): self.resource_synchronizer.sync_from_external() # TODO: self._ros_node里面拿属性 + + # 工作流加载 + self.is_running = False + self.workflow_mappings = {} + self.workflow_sequence = [] + self.pending_task_params = [] + self.material_cache = {} + self._load_material_cache() + + if "workflow_mappings" in bioyond_config: + self._set_workflow_mappings(bioyond_config["workflow_mappings"]) logger.info(f"Bioyond工作站初始化完成") def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node - #self.deck = create_a_coin_cell_deck() ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ "resources": [self.deck] }) @@ -163,47 +188,6 @@ class BioyondWorkstation(WorkstationBase): "sites": sites, }) - 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 { @@ -212,73 +196,15 @@ class BioyondWorkstation(WorkstationBase): "material_type_mappings": MATERIAL_TYPE_MAPPINGS } - # 根据站点配置有条件地初始化接口 - self.hardware_interface = None - self.dispensing_interface = None + self.hardware_interface = BioyondV1RPC(self.bioyond_config) - 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: - """检查指定接口是否可用 + def resource_tree_add(self, resources: List[ResourcePLR]) -> None: + """添加资源到资源树并更新ROS节点 Args: - interface_type (str): 接口类型,'reaction' 或 'dispensing' - - Returns: - bool: 接口是否可用 - - Raises: - RuntimeError: 当接口不可用时抛出异常 + resources (List[ResourcePLR]): 要添加的资源列表 """ - 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()) - } + self.resource_synchronizer.sync_to_external(resources) @property def bioyond_status(self) -> Dict[str, Any]: @@ -290,1786 +216,172 @@ class BioyondWorkstation(WorkstationBase): 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) + # 基础状态信息 + status = { } - # 检查反应站接口状态 - if self.hardware_interface is not None: + # 如果有反应站接口,获取调度器状态 + if self.hardware_interface: 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 + scheduler_status = self.hardware_interface.scheduler_status() + status["scheduler"] = scheduler_status + except Exception as e: + logger.warning(f"获取调度器状态失败: {e}") + status["scheduler"] = {"error": str(e)} - # 检查配液站接口状态 - if self.dispensing_interface is not None: + # 添加物料缓存信息 + if self.hardware_interface: try: - # 配液站接口也使用相同的连接检查方式 - interface_status["dispensing_interface_connected"] = True - except Exception: - interface_status["dispensing_interface_connected"] = False + available_materials = self.hardware_interface.get_available_materials() + status["material_cache_count"] = len(available_materials) + except Exception as e: + logger.warning(f"获取物料缓存失败: {e}") + status["material_cache_count"] = 0 - # 获取资源同步状态 - 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" - } + return status 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", - description="从Bioyond系统同步物料", - parameters={ - "sync_type": {"type": "string", "default": "full", "options": ["full", "incremental"]}, - "force_sync": {"type": "boolean", "default": False} - } - ) - - # Bioyond物料更新工作流 - self.supported_workflows["bioyond_update"] = WorkflowInfo( - name="bioyond_update", - description="将本地物料变更同步到Bioyond", - parameters={ - "material_ids": {"type": "list", "default": []}, - "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", - "message": f"Bioyond同步完成: {sync_type}", - "synced_resources": len(material_manager.plr_resources) - } - else: - result = { - "status": "failed", - "message": "Bioyond同步失败" - } - - logger.info(f"Bioyond同步工作流执行完成: {result['status']}") - return result - - except Exception as e: - logger.error(f"Bioyond同步工作流执行失败: {e}") + logger.error(f"获取Bioyond状态失败: {e}") return { "status": "error", - "message": str(e) + "message": str(e), + "station_type": getattr(self, 'station_type', 'unknown'), + "station_name": getattr(self, 'station_name', 'unknown') } # ==================== 工作流合并与参数设置 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 - ) - """ + def merge_workflow_with_parameters(self, json_str: str) -> dict: + """合并工作流并设置参数""" try: - # 检查反应站接口是否可用 - self._check_interface_availability("reaction") + # 解析输入的 JSON 数据 + data = json.loads(json_str) - 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 参数 + # 构造 API 请求参数 params = { - "order_id": order_id, - "preintake_id": preintake_id + "name": data.get("name", ""), + "workflows": data.get("workflows", []) } - 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) + # 验证必要参数 + if not params["name"]: return { - "success": True, - "code": result_code, - "return_info": success_msg, - "action": "reactor_taken_out" + "code": 0, + "message": "工作流名称不能为空", + "timestamp": int(datetime.now().timestamp() * 1000) } - else: - error_msg = f"反应器取出操作失败,返回代码: {result_code}" - logger.error(error_msg) + + if not params["workflows"]: return { - "success": False, - "code": result_code, - "return_info": error_msg, - "action": "reactor_taken_out" + "code": 0, + "message": "工作流列表不能为空", + "timestamp": int(datetime.now().timestamp() * 1000) } - 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" + "message": f"JSON 解析错误: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) } - - 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" - } - + except Exception as e: return { - "success": True, - "return_info": "流程执行完成", - "result": result, - "action": "reaction_station_process_execute" + "code": 0, + "message": f"参数处理错误: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + # 发送 POST 请求到 Bioyond API + try: + response = self.hardware_interface.post( + url=f'{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters', + params={ + "apiKey": self.hardware_interface.api_key, + "requestTime": self.hardware_interface.get_current_time_iso8601(), + "data": params, + }) + + # 处理响应 + if not response: + return { + "code": 0, + "message": "API 请求失败,未收到响应", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + # 返回完整的响应结果 + return { + "code": response.get("code", 0), + "message": response.get("message", ""), + "timestamp": response.get("timestamp", int(datetime.now().timestamp() * 1000)) } except Exception as e: - logger.error(f"反应站流程执行失败: {e}") return { - "success": False, - "return_info": f"流程执行失败: {str(e)}", - "action": "reaction_station_process_execute" + "code": 0, + "message": f"API 请求异常: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) } - # ==================== 物料管理动作函数 ==================== + def append_to_workflow_sequence(self, web_workflow_name: str) -> bool: + 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 material_inbound(self, material_id: str, location_name: str) -> Dict[str, Any]: - """物料入库操作 - - 将物料添加到指定位置 - - Args: - material_id (str): 物料ID - location_name (str): 位置名称 - - Returns: - Dict[str, Any]: 操作结果,包含状态和消息 - """ + def set_workflow_sequence(self, json_str: str) -> List[str]: try: - logger.info(f"开始执行物料入库操作: 物料ID={material_id}, 位置={location_name}") - result = self.hardware_interface.material_inbound( - material_id=material_id, - location_name=location_name - ) + data = json.loads(json_str) + web_workflow_names = data.get("web_workflow_names", []) + except: + return [] - if result: - logger.info("物料入库操作成功") - return { - "status": "success", - "message": f"物料入库成功,物料ID: {material_id}", - "data": result - } + sequence = [] + for web_name in web_workflow_names: + workflow_id = self._get_workflow(web_name) + if workflow_id: + sequence.append(workflow_id) + + def get_all_workflows(self) -> Dict[str, str]: + return self.workflow_mappings.copy() + + def _get_workflow(self, web_workflow_name: str) -> str: + if web_workflow_name not in self.workflow_mappings: + print(f"未找到工作流映射配置: {web_workflow_name}") + return "" + workflow_id = self.workflow_mappings[web_workflow_name] + print(f"获取工作流: {web_workflow_name} -> {workflow_id}") + return workflow_id + + def _set_workflow_mappings(self, mappings: Dict[str, str]): + self.workflow_mappings = mappings + print(f"设置工作流映射配置: {mappings}") + + def process_web_workflows(self, json_str: str) -> Dict[str, str]: + try: + data = json.loads(json_str) + web_workflow_list = data.get("web_workflow_list", []) + 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: + result[web_name] = workflow_id + self.workflow_sequence.append(workflow_id) else: - logger.error("物料入库操作失败") - return { - "status": "failed", - "message": "物料入库失败" - } + print(f"无法获取工作流ID: {web_name}") + print(f"工作流执行顺序: {self.workflow_sequence}") + return result - except Exception as e: - logger.error(f"物料入库操作异常: {e}") - return { - "status": "error", - "message": f"物料入库操作异常: {str(e)}" - } + def clear_workflows(self): + self.workflow_sequence = [] + print("清空工作流执行顺序") - 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)}" - } + # ==================== 基础物料管理接口 ==================== # ============ 工作站状态管理 ============ @@ -2080,39 +392,34 @@ class BioyondWorkstation(WorkstationBase): Dict[str, Any]: 工作站状态信息 """ try: - # 获取基础状态信息 + # 获取基础工作站状态 base_status = { - "is_busy": self.is_busy, - "workflow_status": self.workflow_status, - "workflow_runtime": self.workflow_runtime + "station_info": self.get_station_info(), + "bioyond_status": self.bioyond_status } - # 获取调度器状态 - scheduler_status = self.get_scheduler_status() - - # 获取设备列表 - device_list = self.get_device_list() + # 如果有接口,获取设备列表 + if self.hardware_interface: + try: + devices = self.hardware_interface.device_list() + base_status["devices"] = devices + except Exception as e: + logger.warning(f"获取设备列表失败: {e}") + base_status["devices"] = [] 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", {}) - } - } + "success": True, + "data": base_status, + "action": "get_workstation_status" } except Exception as e: - logger.error(f"获取工作站状态异常: {e}") + error_msg = f"获取工作站状态失败: {str(e)}" + logger.error(error_msg) return { - "status": "error", - "message": f"获取工作站状态异常: {str(e)}" + "success": False, + "message": error_msg, + "action": "get_workstation_status" } def get_bioyond_status(self) -> Dict[str, Any]: @@ -2125,42 +432,20 @@ class BioyondWorkstation(WorkstationBase): Dict[str, Any]: 格式化的 Bioyond 状态响应 """ try: - # 获取 bioyond_status 属性的数据 - status_data = self.bioyond_status - + status = self.bioyond_status return { - "status": "success", - "message": "Bioyond 状态获取成功", - "data": status_data + "success": True, + "data": status, + "action": "get_bioyond_status" } except Exception as e: - logger.error(f"获取 Bioyond 状态异常: {e}") + error_msg = f"获取 Bioyond 状态失败: {str(e)}" + logger.error(error_msg) 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) - } + "success": False, + "message": error_msg, + "action": "get_bioyond_status" } def reset_workstation(self) -> Dict[str, Any]: @@ -2174,101 +459,50 @@ class BioyondWorkstation(WorkstationBase): try: logger.info("开始重置工作站") - # 停止当前工作流(如果有) - if self.is_busy: - self.stop_workflow() + # 重置调度器 + if self.hardware_interface: + self.hardware_interface.scheduler_reset() - # 停止调度器 - self.stop_scheduler() + # 刷新物料缓存 + if self.hardware_interface: + self.hardware_interface.refresh_material_cache() - # 重新启动调度器 - start_result = self.start_scheduler() + # 重新同步资源 + if self.resource_synchronizer: + self.resource_synchronizer.sync_from_external() - 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}") + logger.info("工作站重置完成") return { - "status": "error", - "message": f"工作站重置异常: {str(e)}" + "success": True, + "message": "工作站重置成功", + "action": "reset_workstation" } - 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 - for resource in material_manager.plr_resources.values(): - success = await material_manager.sync_to_bioyond(resource) - if success: - success_count += 1 - else: - # 同步指定物料 - success_count = 0 - for material_id in material_ids: - resource = material_manager.find_material_by_id(material_id) - if resource: - 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}") + error_msg = f"重置工作站失败: {str(e)}" + logger.error(error_msg) return { - "status": "error", - "message": str(e) + "success": False, + "message": error_msg, + "action": "reset_workstation" } def load_bioyond_data_from_file(self, file_path: str) -> bool: """从文件加载Bioyond数据(用于测试)""" try: - with open(file_path, 'r', encoding='utf-8') as f: + with open(file_path, "r", encoding="utf-8") as f: bioyond_data = json.load(f) - # 获取物料管理模块 - material_manager = self.material_management + logger.info(f"从文件加载Bioyond数据: {file_path}") # 转换为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) + unilab_resources = resource_bioyond_to_plr( + bioyond_data, + type_mapping=self.bioyond_config["material_type_mappings"], + deck=self.deck + ) - # 分配到Deck - import asyncio - asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) - - logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") + logger.info(f"成功加载 {len(unilab_resources)} 个资源") return True except Exception as e: @@ -2325,65 +559,4 @@ def create_bioyond_workstation_example(): 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 * - from typing import List - from pylabrobot.resources import Resource as PLRResource - 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)} 个资源") - ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) - print(f"转换结果类型: {type(ulab_resources)}") - print(f"转换结果长度: {len(ulab_resources) if ulab_resources else 0}") - deck = Deck(size_x=2000, - size_y=653.5, - size_z=900) - - Stack0 = Stack(name="Stack0", location=Coordinate(0, 100, 0)) - Stack1 = Stack(name="Stack1", location=Coordinate(100, 100, 0)) - Stack2 = Stack(name="Stack2", location=Coordinate(200, 100, 0)) - Stack3 = Stack(name="Stack3", location=Coordinate(300, 100, 0)) - Stack4 = Stack(name="Stack4", location=Coordinate(400, 100, 0)) - Stack5 = Stack(name="Stack5", location=Coordinate(500, 100, 0)) - - deck.assign_child_resource(Stack1, Stack1.location) - deck.assign_child_resource(Stack2, Stack2.location) - deck.assign_child_resource(Stack3, Stack3.location) - deck.assign_child_resource(Stack4, Stack4.location) - deck.assign_child_resource(Stack5, Stack5.location) - - Stack0.assign_child_resource(ulab_resources[0], Stack0.location) - Stack1.assign_child_resource(ulab_resources[1], Stack1.location) - Stack2.assign_child_resource(ulab_resources[2], Stack2.location) - Stack3.assign_child_resource(ulab_resources[3], Stack3.location) - Stack4.assign_child_resource(ulab_resources[4], Stack4.location) - Stack5.assign_child_resource(ulab_resources[5], Stack5.location) - - from unilabos.resources.graphio import convert_resources_from_type - from unilabos.app.web.client import http_client - - 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) + pass \ No newline at end of file diff --git a/unilabos/registry/devices/bioyond.yaml b/unilabos/registry/devices/bioyond.yaml new file mode 100644 index 00000000..574d18a7 --- /dev/null +++ b/unilabos/registry/devices/bioyond.yaml @@ -0,0 +1,241 @@ +workstation.bioyond_dispensing_station: + category: + - workstation + - bioyond + class: + action_value_mappings: + create_90_10_vial_feeding_task: + feedback: {} + goal: + delay_time: delay_time + hold_m_name: hold_m_name + 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_10_3_assign_material_name: percent_10_3_assign_material_name + percent_10_3_liquid_material_name: percent_10_3_liquid_material_name + percent_10_3_target_weigh: percent_10_3_target_weigh + percent_10_3_volume: percent_10_3_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: '' + hold_m_name: '' + 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_10_3_assign_material_name: '' + percent_10_3_liquid_material_name: '' + percent_10_3_target_weigh: '' + percent_10_3_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: '' + temperature: '' + handles: [] + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: DispenStationVialFeed_Feedback + type: object + goal: + properties: + delay_time: + type: string + hold_m_name: + type: string + order_name: + type: string + percent_10_1_assign_material_name: + type: string + percent_10_1_liquid_material_name: + type: string + percent_10_1_target_weigh: + type: string + percent_10_1_volume: + type: string + percent_10_2_assign_material_name: + type: string + percent_10_2_liquid_material_name: + type: string + percent_10_2_target_weigh: + type: string + percent_10_2_volume: + type: string + percent_10_3_assign_material_name: + type: string + percent_10_3_liquid_material_name: + type: string + percent_10_3_target_weigh: + type: string + percent_10_3_volume: + type: string + percent_90_1_assign_material_name: + type: string + percent_90_1_target_weigh: + type: string + percent_90_2_assign_material_name: + type: string + percent_90_2_target_weigh: + type: string + percent_90_3_assign_material_name: + type: string + percent_90_3_target_weigh: + type: string + speed: + type: string + temperature: + type: string + required: + - order_name + - 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 + - percent_10_1_assign_material_name + - percent_10_1_target_weigh + - percent_10_1_volume + - percent_10_1_liquid_material_name + - percent_10_2_assign_material_name + - percent_10_2_target_weigh + - percent_10_2_volume + - percent_10_2_liquid_material_name + - percent_10_3_assign_material_name + - percent_10_3_target_weigh + - percent_10_3_volume + - percent_10_3_liquid_material_name + - speed + - temperature + - delay_time + - hold_m_name + title: DispenStationVialFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: DispenStationVialFeed_Result + type: object + required: + - goal + title: DispenStationVialFeed + type: object + type: DispenStationVialFeed + 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: '' + hold_m_name: '' + liquid_material_name: '' + material_name: '' + order_name: '' + speed: '' + target_weigh: '' + temperature: '' + volume: '' + handles: [] + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: DispenStationSolnPrep_Feedback + type: object + goal: + properties: + delay_time: + type: string + hold_m_name: + type: string + liquid_material_name: + type: string + material_name: + type: string + order_name: + type: string + speed: + type: string + target_weigh: + type: string + temperature: + type: string + volume: + type: string + required: + - order_name + - material_name + - target_weigh + - volume + - liquid_material_name + - speed + - temperature + - delay_time + - hold_m_name + title: DispenStationSolnPrep_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: DispenStationSolnPrep_Result + type: object + required: + - goal + title: DispenStationSolnPrep + type: object + type: DispenStationSolnPrep + module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispendsingStation + status_types: {} + type: python + config_info: [] + description: '' + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 diff --git a/unilabos/registry/devices/dispensing_station_bioyond.yaml b/unilabos/registry/devices/dispensing_station_bioyond.yaml deleted file mode 100644 index 11c234ef..00000000 --- a/unilabos/registry/devices/dispensing_station_bioyond.yaml +++ /dev/null @@ -1,1582 +0,0 @@ -dispensing_station.bioyond: - category: - - work_station - - dispensing_station_bioyond - class: - action_value_mappings: - auto-add_material: - feedback: {} - goal: {} - goal_default: - material_data: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_data: - type: object - required: - - material_data - type: object - result: {} - required: - - goal - title: add_material参数 - type: object - type: UniLabJsonCommand - auto-create_order: - feedback: {} - goal: {} - goal_default: - parameters: null - task_name: null - workflow_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - parameters: - type: object - task_name: - type: string - workflow_name: - type: string - required: - - workflow_name - - task_name - type: object - result: {} - required: - - goal - title: create_order参数 - type: object - type: UniLabJsonCommand - auto-delete_material: - feedback: {} - goal: {} - goal_default: - material_data: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_data: - type: string - required: - - material_data - type: object - result: {} - required: - - goal - title: delete_material参数 - type: object - type: UniLabJsonCommand - auto-device_operation: - feedback: {} - goal: {} - goal_default: - device_id: null - operation: null - parameters: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - device_id: - type: string - operation: - type: string - parameters: - type: object - required: - - device_id - - operation - type: object - result: {} - required: - - goal - title: device_operation参数 - type: object - type: UniLabJsonCommand - auto-drip_back: - feedback: {} - goal: {} - goal_default: - assign_material_name: Reactor - temperature: 25.0 - time: '0' - torque_variation: '1' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - assign_material_name: - default: Reactor - type: string - temperature: - default: 25.0 - type: number - time: - default: '0' - type: string - torque_variation: - default: '1' - type: string - required: [] - type: object - result: {} - required: - - goal - title: drip_back参数 - type: object - type: UniLabJsonCommand - auto-execute_bioyond_sync_workflow: - feedback: {} - goal: {} - goal_default: - parameters: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - parameters: - type: object - required: - - parameters - type: object - result: {} - required: - - goal - title: execute_bioyond_sync_workflow参数 - type: object - type: UniLabJsonCommandAsync - auto-execute_bioyond_update_workflow: - feedback: {} - goal: {} - goal_default: - parameters: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - parameters: - type: object - required: - - parameters - type: object - result: {} - required: - - goal - title: execute_bioyond_update_workflow参数 - type: object - type: UniLabJsonCommandAsync - auto-liquid_feeding_beaker: - feedback: {} - goal: {} - goal_default: - material_name: '' - volume: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - default: '' - type: string - volume: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: liquid_feeding_beaker参数 - type: object - type: UniLabJsonCommand - auto-liquid_feeding_solvents: - feedback: {} - goal: {} - goal_default: - material_name: '' - volume: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - default: '' - type: string - volume: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: liquid_feeding_solvents参数 - type: object - type: UniLabJsonCommand - auto-liquid_feeding_titration: - feedback: {} - goal: {} - goal_default: - material_name: '' - time: '120' - titration_type: '1' - torque_variation: '2' - volume: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - default: '' - type: string - time: - default: '120' - type: string - titration_type: - default: '1' - type: string - torque_variation: - default: '2' - type: string - volume: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: liquid_feeding_titration参数 - type: object - type: UniLabJsonCommand - auto-liquid_feeding_vials_non_titration: - feedback: {} - goal: {} - goal_default: - material_name: '' - volume: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - default: '' - type: string - volume: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: liquid_feeding_vials_non_titration参数 - type: object - type: UniLabJsonCommand - auto-load_bioyond_data_from_file: - feedback: {} - goal: {} - goal_default: - file_path: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - file_path: - type: string - required: - - file_path - type: object - result: {} - required: - - goal - title: load_bioyond_data_from_file参数 - type: object - type: UniLabJsonCommand - auto-material_inbound: - feedback: {} - goal: {} - goal_default: - location_name: null - material_id: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - location_name: - type: string - material_id: - type: string - required: - - material_id - - location_name - type: object - result: {} - required: - - goal - title: material_inbound参数 - type: object - type: UniLabJsonCommand - auto-material_outbound: - feedback: {} - goal: {} - goal_default: - location_name: null - material_id: null - quantity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - location_name: - type: string - material_id: - type: string - quantity: - type: integer - required: - - material_id - - location_name - - quantity - type: object - result: {} - required: - - goal - title: material_outbound参数 - type: object - type: UniLabJsonCommand - auto-merge_workflow_with_parameters: - feedback: {} - goal: {} - goal_default: - name: null - workflows: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - name: - type: string - workflows: - items: - type: object - type: array - required: - - name - - workflows - type: object - result: {} - required: - - goal - title: merge_workflow_with_parameters参数 - type: object - type: UniLabJsonCommand - auto-order_query: - feedback: {} - goal: {} - goal_default: - query_data: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - query_data: - type: string - required: - - query_data - type: object - result: {} - required: - - goal - title: order_query参数 - type: object - type: UniLabJsonCommand - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-reaction_station_drip_back: - feedback: {} - goal: {} - goal_default: - assign_material_name: null - time: null - torque_variation: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - assign_material_name: - type: string - time: - type: string - torque_variation: - type: string - volume: - type: string - required: - - volume - - assign_material_name - - time - - torque_variation - type: object - result: {} - required: - - goal - title: reaction_station_drip_back参数 - type: object - type: UniLabJsonCommand - auto-reaction_station_liquid_feed: - feedback: {} - goal: {} - goal_default: - assign_material_name: null - time: null - titration_type: null - torque_variation: null - volume: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - assign_material_name: - type: string - time: - type: string - titration_type: - type: string - torque_variation: - type: string - volume: - 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 - auto-reaction_station_process_execute: - feedback: {} - goal: {} - goal_default: - task_name: null - workflow_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - task_name: - type: string - workflow_name: - type: string - required: - - workflow_name - - task_name - type: object - result: {} - required: - - goal - title: reaction_station_process_execute参数 - type: object - type: UniLabJsonCommand - auto-reaction_station_reactor_taken_out: - feedback: {} - goal: {} - goal_default: - order_id: '' - preintake_id: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - order_id: - default: '' - type: string - preintake_id: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: reaction_station_reactor_taken_out参数 - type: object - type: UniLabJsonCommand - auto-reaction_station_solid_feed_vial: - feedback: {} - goal: {} - goal_default: - assign_material_name: null - material_id: null - time: null - torque_variation: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - assign_material_name: - type: string - material_id: - type: string - time: - type: string - torque_variation: - 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 - auto-reaction_station_take_in: - feedback: {} - goal: {} - goal_default: - assign_material_name: null - cutoff: null - temperature: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - assign_material_name: - type: string - cutoff: - type: string - temperature: - type: string - required: - - cutoff - - temperature - - assign_material_name - type: object - result: {} - required: - - goal - title: reaction_station_take_in参数 - type: object - type: UniLabJsonCommand - auto-reactor_taken_in: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reactor_taken_in参数 - type: object - type: UniLabJsonCommand - auto-reactor_taken_out: - feedback: {} - goal: {} - goal_default: - order_id: '' - preintake_id: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - order_id: - default: '' - type: string - preintake_id: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: reactor_taken_out参数 - type: object - type: UniLabJsonCommand - auto-reset_workstation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset_workstation参数 - type: object - type: UniLabJsonCommand - auto-solid_feeding_vials: - feedback: {} - goal: {} - goal_default: - material_name: '' - volume: '' - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - material_name: - default: '' - type: string - volume: - default: '' - type: string - required: [] - type: object - result: {} - required: - - goal - title: solid_feeding_vials参数 - type: object - type: UniLabJsonCommand - auto-start_scheduler: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: start_scheduler参数 - type: object - type: UniLabJsonCommand - auto-stock_material: - feedback: {} - goal: {} - goal_default: - location: null - material_id: null - quantity: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - location: - type: string - material_id: - type: string - quantity: - type: integer - required: - - material_id - - location - - quantity - type: object - result: {} - required: - - goal - title: stock_material参数 - type: object - type: UniLabJsonCommand - auto-stop_scheduler: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: stop_scheduler参数 - type: object - type: UniLabJsonCommand - auto-transfer_resource_to_another: - feedback: {} - goal: {} - goal_default: - mount_device_id: null - mount_resource: null - resource: null - sites: null - handles: {} - placeholder_keys: - mount_device_id: unilabos_devices - mount_resource: unilabos_resources - resource: unilabos_resources - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mount_device_id: - type: object - mount_resource: - items: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: mount_resource - type: object - type: array - resource: - items: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: resource - type: object - type: array - sites: - items: - type: string - type: array - required: - - resource - - mount_resource - - sites - - mount_device_id - type: object - result: {} - required: - - goal - title: transfer_resource_to_another参数 - type: object - type: UniLabJsonCommand - auto-validate_workflow_parameters: - feedback: {} - goal: {} - goal_default: - workflows: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - workflows: - items: - type: object - type: array - required: - - workflows - type: object - result: {} - required: - - goal - title: validate_workflow_parameters参数 - type: object - type: UniLabJsonCommand - 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 - device_list: dict - scheduler_status: dict - station_info: dict - workflow_parameter_template: dict - workstation_status: dict - type: python - config_info: [] - description: Bioyond配液站 - 专门用于物料配制和管理的工作站 - handles: [] - icon: 配液站.webp - init_param_schema: - config: - properties: - bioyond_config: - type: string - deck: - type: string - station_config: - type: string - required: [] - type: object - data: - properties: - bioyond_status: - type: object - device_list: - type: object - scheduler_status: - type: object - station_info: - type: object - workflow_parameter_template: - type: object - workstation_status: - type: object - required: - - station_info - - bioyond_status - - workflow_parameter_template - - scheduler_status - - device_list - - workstation_status - type: object - version: 1.0.0 diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 889c92e6..28eb77c1 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -2,12 +2,13 @@ import importlib import inspect import json import traceback -from typing import Union, Any, Dict, List +from typing import Union, Any, Dict, List, Tuple import networkx as nx from pylabrobot.resources import ResourceHolder from unilabos_msgs.msg import Resource from unilabos.resources.container import RegularContainer +from unilabos.resources.itemized_carrier import ItemizedCarrier from unilabos.ros.msgs.message_converter import convert_to_ros_msg from unilabos.ros.nodes.resource_tracker import ( ResourceDictInstance, @@ -576,13 +577,13 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w return r -def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]: +def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[str, Tuple[str, str]] = {}, deck: Any = None) -> list[dict]: """ 将 bioyond 物料格式转换为 ulab 物料格式 Args: bioyond_materials: bioyond 系统的物料查询结果列表 - type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name} + type_mapping: 物料类型映射字典,格式 {bioyond_type: [plr_class_name, class_uuid]} location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name} Returns: @@ -592,7 +593,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = for material in bioyond_materials: className = ( - type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer" + type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer" ) plr_material: ResourcePLR = initialize_resource( @@ -614,7 +615,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = # plr_material.unassign_child_resource(bottle) plr_material.sites[number] = None plr_material[number] = initialize_resource( - {"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]]}, resource_type=ResourcePLR + {"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]][0]}, resource_type=ResourcePLR ) else: bottle.tracker.liquids = [ @@ -645,32 +646,59 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = return plr_materials -def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]: +def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]: bioyond_materials = [] - for plr_material in plr_materials: - material = { - "name": plr_material.name, - "typeName": plr_material.__class__.__name__, - "code": plr_material.code, - "quantity": 0, - "detail": [], - "locations": [], - } - if hasattr(plr_material, "capacity") and plr_material.capacity > 1: - for idx in range(plr_material.capacity): - bottle = plr_material[idx] - detail = { - "x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1, - "y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1, - "z": (idx % plr_material.num_items_x) + 1, + for resource in plr_resources: + if hasattr(resource, "capacity") and resource.capacity > 1: + material = { + "typeId": type_mapping.get(resource.model)[1], + "name": resource.name, + "unit": "个", + "quantity": 1, + "details": [], + "Parameters": "{}" + } + for bottle in resource.children: + if isinstance(resource, ItemizedCarrier): + site = resource.get_child_identifier(bottle) + else: + site = {"x": bottle.location.x - 1, "y": bottle.location.y - 1} + detail_item = { + "typeId": type_mapping.get(bottle.model)[1], + "name": bottle.name, "code": bottle.code if hasattr(bottle, "code") else "", "quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0, + "x": site["x"] + 1, + "y": site["y"] + 1, + "molecular": 1, + "Parameters": json.dumps({"molecular": 1}) } - material["detail"].append(detail) - material["quantity"] = 1.0 + material["details"].append(detail_item) else: - bottle = plr_material[0] if plr_material.capacity > 0 else plr_material - material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0 + bottle = resource[0] if resource.capacity > 0 else resource + material = { + "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", + "name": resource.get("name", ""), + "unit": "", + "quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0, + "Parameters": "{}" + } + + if resource.parent is not None and isinstance(resource.parent, ItemizedCarrier): + site_in_parent = resource.parent.get_child_identifier(resource) + material["locations"] = [ + { + "id": warehouse_mapping[resource.parent.name]["site_uuids"][site_in_parent["identifier"]], + "whid": warehouse_mapping[resource.parent.name]["uuid"], + "whName": resource.parent.name, + "x": site_in_parent["z"] + 1, + "y": site_in_parent["y"] + 1, + "z": 1, + "quantity": 0 + } + ], + + print(f"material_data: {material}") bioyond_materials.append(material) return bioyond_materials diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 6d5c2980..17f63cf4 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -163,6 +163,89 @@ class ItemizedCarrier(ResourcePLR): if hasattr(resource, "unassign"): resource.unassign() + def get_child_identifier(self, child: ResourcePLR): + """Get the identifier information for a given child resource. + + Args: + child: The Resource object to find the identifier for + + Returns: + dict: A dictionary containing: + - identifier: The string identifier (e.g. "A1", "B2") + - idx: The integer index in the sites list + - x: The x index (column index, 0-based) + - y: The y index (row index, 0-based) + - z: The z index (layer index, 0-based) + + Raises: + ValueError: If the child resource is not found in this carrier + """ + # Find the child resource in sites + for idx, resource in enumerate(self.sites): + if resource is child: + # Get the identifier from ordering keys + identifier = list(self._ordering.keys())[idx] + + # Parse identifier to get x, y, z indices + x_idx, y_idx, z_idx = self._parse_identifier_to_indices(identifier, idx) + + return { + "identifier": identifier, + "idx": idx, + "x": x_idx, + "y": y_idx, + "z": z_idx + } + + # If not found, raise an error + raise ValueError(f"Resource {child} is not assigned to this carrier") + + def _parse_identifier_to_indices(self, identifier: str, idx: int) -> Tuple[int, int, int]: + """Parse identifier string to get x, y, z indices. + + Args: + identifier: String identifier like "A1", "B2", etc. + idx: Linear index as fallback for calculation + + Returns: + Tuple of (x_idx, y_idx, z_idx) + """ + # If we have explicit dimensions, calculate from idx + if self.num_items_x > 0 and self.num_items_y > 0: + # Calculate 3D indices from linear index + z_idx = idx // (self.num_items_x * self.num_items_y) if self.num_items_z > 0 else 0 + remaining = idx % (self.num_items_x * self.num_items_y) + y_idx = remaining // self.num_items_x + x_idx = remaining % self.num_items_x + return x_idx, y_idx, z_idx + + # Fallback: parse from Excel-style identifier + if isinstance(identifier, str) and len(identifier) >= 2: + # Extract row (letter) and column (number) + row_letters = "" + col_numbers = "" + + for char in identifier: + if char.isalpha(): + row_letters += char + elif char.isdigit(): + col_numbers += char + + if row_letters and col_numbers: + # Convert letter(s) to row index (A=0, B=1, etc.) + y_idx = 0 + for char in row_letters: + y_idx = y_idx * 26 + (ord(char.upper()) - ord('A')) + + # Convert number to column index (1-based to 0-based) + x_idx = int(col_numbers) - 1 + z_idx = 0 # Default layer + + return x_idx, y_idx, z_idx + + # If all else fails, assume linear arrangement + return idx, 0, 0 + def __getitem__( self, identifier: Union[str, int, Sequence[int], Sequence[str], slice, range],