diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py new file mode 100644 index 00000000..9f46dfcf --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -0,0 +1,1058 @@ +# bioyond_rpc.py +""" +BioyondV1RPC类定义 - 包含所有RPC接口和业务逻辑 +""" + +from enum import Enum +from datetime import datetime, timezone +from unilabos.device_comms.rpc import BaseRequest +from typing import Optional, List, Dict, Any +import json +from config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING + + +class SimpleLogger: + """简单的日志记录器""" + def info(self, msg): print(f"[INFO] {msg}") + def error(self, msg): print(f"[ERROR] {msg}") + def debug(self, msg): print(f"[DEBUG] {msg}") + def warning(self, msg): print(f"[WARNING] {msg}") + def critical(self, msg): print(f"[CRITICAL] {msg}") + + +class MachineState(Enum): + INITIAL = 0 + STOPPED = 1 + RUNNING = 2 + PAUSED = 3 + ERROR_PAUSED = 4 + ERROR_STOPPED = 5 + + +class MaterialType(Enum): + Consumables = 0 + Sample = 1 + Reagent = 2 + Product = 3 + + +class BioyondV1RPC(BaseRequest): + def __init__(self, config): + super().__init__() + print("开始初始化") + 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") + + # 物料查询接口 + def stock_material(self, json_str: str) -> list: + try: + params = json.loads(json_str) + except: + return [] + + response = self.post( + url=f'{self.host}/api/lims/storage/stock-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_workflow(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/work-flow-list', + 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 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: + try: + params = json.loads(json_str) + except Exception as e: + result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) + return result + + print('===============', json.dumps(params)) + + request_params = { + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + } + + response = self.post( + url=f'{self.host}/api/lims/order/order', + params=request_params) + + 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: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-list', + 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 order_report(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-report', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务取出接口 + def order_takeout(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "orderId": data.get("order_id", ""), + "preintakeId": data.get("preintake_id", "") + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/order/order-takeout', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 设备列表查询 + def device_list(self, json_str: str = "") -> list: + device_no = None + if json_str: + try: + data = json.loads(json_str) + device_no = data.get("device_no", None) + except: + pass + + url = f'{self.host}/api/lims/device/device-list' + if device_no: + url += f'/{device_no}' + + response = self.post( + url=url, + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return [] + return response.get("data", []) + + # 设备操作 + def device_operation(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "deviceNo": data.get("device_no", ""), + "operationType": data.get("operation_type", 0), + "operationParams": data.get("operation_params", {}) + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/device/device-operation', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器状态查询 + def scheduler_status(self) -> dict: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-status', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 调度器启动 + def scheduler_start(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/start', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + 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={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器继续 + def scheduler_continue(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-continue', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + 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={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + 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={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + 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", {}) + + # 合并当前工作流序列 + 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: + print("正在加载材料列表缓存...") + stock_query = '{"typeMode": 2, "includeDetail": true}' + stock_result = self.stock_material(stock_query) + + if isinstance(stock_result, str): + stock_data = json.loads(stock_result) + else: + stock_data = stock_result + + materials = stock_data + for material in materials: + material_name = material.get("name") + material_id = material.get("id") + if material_name and material_id: + self.material_cache[material_name] = material_id + + print(f"材料列表缓存加载完成,共加载 {len(self.material_cache)} 个材料") + + except Exception as e: + print(f"加载材料列表缓存时出错: {e}") + self.material_cache = {} + + def _get_material_id_by_name(self, material_name_or_id: str) -> str: + """根据材料名称获取材料ID""" + if len(material_name_or_id) > 20 and '-' in material_name_or_id: + return material_name_or_id + + if material_name_or_id in self.material_cache: + material_id = self.material_cache[material_name_or_id] + print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}") + return material_id + + print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值") + return material_name_or_id + + def refresh_material_cache(self): + """刷新材料列表缓存""" + print("正在刷新材料列表缓存...") + self._load_material_cache() + + 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 diff --git a/unilabos/devices/workstation/bioyond_studio/experiment.py b/unilabos/devices/workstation/bioyond_studio/experiment.py new file mode 100644 index 00000000..ae3111b8 --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/experiment.py @@ -0,0 +1,398 @@ +# experiment_workflow.py +""" +实验流程主程序 +""" + +import json +from bioyond_rpc import BioyondV1RPC +from config import API_CONFIG, WORKFLOW_MAPPINGS + + +def run_experiment(): + """运行实验流程""" + + # 初始化Bioyond客户端 + config = { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS + } + + Bioyond = BioyondV1RPC(config) + + print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") + + # 显示可用的材料名称(前20个) + available_materials = Bioyond.get_available_materials() + print(f"可用材料名称(前20个): {available_materials[:20]}") + print(f"总共有 {len(available_materials)} 个材料可用\n") + + # 1. 反应器放入 + print("1. 添加反应器放入工作流,带参数...") + Bioyond.reactor_taken_in( + assign_material_name="BTDA-DD", + cutoff="10000", + temperature="-10" + ) + + # 2. 液体投料-烧杯 (第一个) + print("2. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34768.7", + assign_material_name="ODA", + time="0", + torque_variation="1", + titrationType="1", + temperature=-10 + ) + + # 3. 液体投料-烧杯 (第二个) + print("3. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34080.9", + assign_material_name="MPDA", + time="5", + torque_variation="2", + titrationType="1", + temperature=0 + ) + + # 4. 液体投料-小瓶非滴定 + print("4. 添加液体投料-小瓶非滴定,带参数...") + Bioyond.liquid_feeding_vials_non_titration( + volumeFormula="639.5", + assign_material_name="SIDA", + titration_type="1", + time="0", + torque_variation="1", + temperature=-10 + ) + + # 5. 液体投料溶剂 + print("5. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="NMP", + volume="19000", + titration_type="1", + time="5", + torque_variation="2", + temperature=-10 + ) + + # 6-8. 固体进料小瓶 (三个) + print("6. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-1", + temperature=-10.00 + ) + + print("7. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-2", + temperature=25.00 + ) + + print("8. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="480", + torque_variation="2", + assign_material_name="BTDA-3", + temperature=25.00 + ) + + # 液体投料滴定(第一个) + print("9. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="1000", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第二个) + print("10. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第三个) + print("11. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("12. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("13. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("14. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + + + print("15. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="PGME", + volume="16894.6", + titration_type="1", + time="360", + torque_variation="2", + temperature=25.00 + ) + + # 16. 反应器取出 + print("16. 添加反应器取出工作流...") + Bioyond.reactor_taken_out() + + # 显示当前工作流序列 + sequence = Bioyond.get_workflow_sequence() + print("\n当前工作流执行顺序:") + print(sequence) + + # 执行process_and_execute_workflow,合并工作流并创建任务 + print("\n4. 执行process_and_execute_workflow...") + + result = Bioyond.process_and_execute_workflow( + workflow_name="test3_86", + task_name="实验3_86" + ) + + # 显示执行结果 + print("\n5. 执行结果:") + if isinstance(result, str): + try: + result_dict = json.loads(result) + if result_dict.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result_dict.get('task')}") + else: + print(f"任务创建失败: {result_dict.get('error')}") + except: + print(f"结果解析失败: {result}") + else: + if result.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result.get('task')}") + else: + print(f"任务创建失败: {result.get('error')}") + + # 可选:启动调度器 + # Bioyond.scheduler_start() + + return Bioyond + + +def prepare_materials(bioyond): + """准备实验材料(可选)""" + + # 样品板材料数据定义 + material_data_yp_1 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-1", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-1", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PEPA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-2", + "quantity": 1, + "x": 1, + "y": 3, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PMDA", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-2", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + material_data_yp_2 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-2", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "SIDA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-2", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-3", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + # 烧杯材料数据定义 + beaker_materials = [ + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-1", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "TFDB", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "ODA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "MPDA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-2", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + } + ] + + # 如果需要,可以在这里调用add_material方法添加材料 + # 例如: + # result = bioyond.add_material(json.dumps(material_data_yp_1)) + # print(f"添加材料结果: {result}") + + return { + "sample_plates": [material_data_yp_1, material_data_yp_2], + "beakers": beaker_materials + } + + +if __name__ == "__main__": + # 运行主实验流程 + bioyond_client = run_experiment() + + # 可选:准备材料数据 + # materials = prepare_materials(bioyond_client) + # print(f"\n准备的材料数据: {materials}") diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py new file mode 100644 index 00000000..6152a4eb --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -0,0 +1,334 @@ +""" +Bioyond工作站实现 +Bioyond Workstation Implementation + +集成Bioyond物料管理的工作站示例 +""" +from typing import Dict, Any, List, Optional, Union +import json + +from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer +from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker +from unilabos.utils.log import logger +from unilabos.resources.graphio import resource_bioyond_to_plr + + +class BioyondWorkstation(WorkstationBase): + """Bioyond工作站 + + 集成Bioyond物料管理的工作站实现 + """ + + def __init__( + self, + bioyond_config: Optional[Dict[str, Any]] = None, + deck: Optional[str, Any] = None, + *args, + **kwargs, + ): + # 设置Bioyond配置 + self.bioyond_config = bioyond_config or { + "base_url": "http://localhost:8080", + "api_key": "", + "sync_interval": 30, + "timeout": 30 + } + + # 设置默认deck配置 + + # 初始化父类 + super().__init__( + #桌子 + deck=deck, + *args, + **kwargs, + ) + + # TODO: self._ros_node里面拿属性 + logger.info(f"Bioyond工作站初始化完成") + + def _create_communication_module(self): + """创建Bioyond通信模块""" + # 暂时返回None,因为工作站基类没有强制要求通信模块 + return None + + def _create_material_management_module(self) -> BioyondMaterialManagement: + """创建Bioyond物料管理模块""" + # 获取必要的属性,如果不存在则使用默认值 + device_id = getattr(self, 'device_id', 'bioyond_workstation') + resource_tracker = getattr(self, 'resource_tracker', None) + children_config = getattr(self, '_children', {}) + + + + 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}") + return { + "status": "error", + "message": str(e) + } + + async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """执行Bioyond更新工作流""" + try: + material_ids = parameters.get("material_ids", []) + sync_all = parameters.get("sync_all", True) + + logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}") + + # 获取物料管理模块 + material_manager = self.material_management + + if sync_all: + # 同步所有物料 + success_count = 0 + 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}") + return { + "status": "error", + "message": str(e) + } + + def get_bioyond_status(self) -> Dict[str, Any]: + """获取Bioyond系统状态""" + try: + material_manager = self.material_management + + return { + "bioyond_connected": material_manager.bioyond_api_client is not None, + "sync_interval": material_manager.sync_interval, + "total_resources": len(material_manager.plr_resources), + "deck_size": { + "x": material_manager.plr_deck.size_x, + "y": material_manager.plr_deck.size_y, + "z": material_manager.plr_deck.size_z + }, + "bioyond_config": self.bioyond_config + } + except Exception as e: + logger.error(f"获取Bioyond状态失败: {e}") + return { + "error": str(e) + } + + def load_bioyond_data_from_file(self, file_path: str) -> bool: + """从文件加载Bioyond数据(用于测试)""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + bioyond_data = json.load(f) + + # 获取物料管理模块 + material_manager = self.material_management + + # 转换为UniLab格式 + if isinstance(bioyond_data, dict) and "data" in bioyond_data: + unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data) + else: + unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data) + + # 分配到Deck + import asyncio + asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) + + logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") + return True + + except Exception as e: + logger.error(f"从文件加载Bioyond数据失败: {e}") + return False + + +# 使用示例 +def create_bioyond_workstation_example(): + """创建Bioyond工作站示例""" + + # 配置参数 + device_id = "bioyond_workstation_001" + + # 子资源配置 + children = { + "plate_1": { + "name": "plate_1", + "type": "plate", + "position": {"x": 100, "y": 100, "z": 0}, + "config": { + "size_x": 127.76, + "size_y": 85.48, + "size_z": 14.35, + "model": "Generic 96 Well Plate" + } + } + } + + # Bioyond配置 + bioyond_config = { + "base_url": "http://bioyond.example.com/api", + "api_key": "your_api_key_here", + "sync_interval": 60, # 60秒同步一次 + "timeout": 30 + } + + # Deck配置 + deck_config = { + "size_x": 1000.0, + "size_y": 1000.0, + "size_z": 100.0, + "model": "BioyondDeck" + } + + # 创建工作站 + workstation = BioyondWorkstation( + station_resource=deck_config, + bioyond_config=bioyond_config, + deck_config=deck_config, + ) + + return workstation + + +if __name__ == "__main__": + # 创建示例工作站 + #workstation = create_bioyond_workstation_example() + + # 从文件加载测试数据 + #workstation.load_bioyond_data_from_file("bioyond_test_yibin.json") + + # 获取状态 + #status = workstation.get_bioyond_status() + #print("Bioyond工作站状态:", status) + + # 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式 + + # 读取 bioyond_resources_unilab_output3 copy.json 文件 + from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type + from Bioyond_wuliao import * + 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) \ No newline at end of file diff --git a/unilabos/registry/devices/reaction_station_bioyong.yaml b/unilabos/registry/devices/reaction_station_bioyong.yaml new file mode 100644 index 00000000..f14e818e --- /dev/null +++ b/unilabos/registry/devices/reaction_station_bioyong.yaml @@ -0,0 +1,481 @@ +reaction_station_bioyong: + category: + - reaction_station_bioyong + class: + action_value_mappings: + drip_back: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationDripBack_Feedback + type: object + 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 + title: ReactionStationDripBack_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationDripBack_Result + type: object + required: + - goal + title: ReactionStationDripBack + type: object + type: ReactionStationDripBack + liquid_feeding_beaker: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + 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 + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_solvents: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + 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 + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + 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 + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_vials_non_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + 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 + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + process_and_execute_workflow: + feedback: {} + goal: + task_name: task_name + workflow_name: workflow_name + goal_default: + task_name: '' + workflow_name: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationProExecu_Feedback + type: object + goal: + properties: + task_name: + type: string + workflow_name: + type: string + required: + - workflow_name + - task_name + title: ReactionStationProExecu_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationProExecu_Result + type: object + required: + - goal + title: ReactionStationProExecu + type: object + type: ReactionStationProExecu + reactor_taken_in: + feedback: {} + goal: + assign_material_name: assign_material_name + cutoff: cutoff + temperature: temperature + goal_default: + assign_material_name: '' + cutoff: '' + temperature: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationReaTackIn_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + cutoff: + type: string + temperature: + type: string + required: + - cutoff + - temperature + - assign_material_name + title: ReactionStationReaTackIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationReaTackIn_Result + type: object + required: + - goal + title: ReactionStationReaTackIn + type: object + type: ReactionStationReaTackIn + reactor_taken_out: + feedback: {} + goal: {} + goal_default: + command: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd + solid_feeding_vials: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + time: '' + torque_variation: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationSolidFeedVial_Feedback + type: object + 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 + title: ReactionStationSolidFeedVial_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationSolidFeedVial_Result + type: object + required: + - goal + title: ReactionStationSolidFeedVial + type: object + type: ReactionStationSolidFeedVial + module: unilabos.devices.reaction_station.reaction_station_bioyong:BioyongV1RPC + status_types: {} + type: python + config_info: [] + description: reaction_station_bioyong Device + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0