From a2a827d7ac1eee430e5217b7c35fe37d6aa067d3 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:49:36 +0800 Subject: [PATCH] Update workstation & bioyond example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refine descriptions in Bioyond reaction station YAML Updated and clarified field and operation descriptions in the reaction_station_bioyond.yaml file for improved accuracy and consistency. Changes include more precise terminology, clearer parameter explanations, and standardized formatting for operation schemas. refactor(workstation): 更新反应站参数描述并添加分液站配置文件 修正反应站方法参数描述,使其更准确清晰 添加bioyond_dispensing_station.yaml配置文件 add create_workflow script and test add invisible_slots to carriers fix(warehouses): 修正bioyond_warehouse_1x4x4仓库的尺寸参数 调整仓库的num_items_x和num_items_z值以匹配实际布局,并更新物品尺寸参数 save resource get data. allow empty value for layout and cross_section_type More decks&plates support for bioyond (#115) refactor(registry): 重构反应站设备配置,简化并更新操作命令 移除旧的自动操作命令,新增针对具体化学操作的命令配置 更新模块路径和配置结构,优化参数定义和描述 fix(dispensing_station): 修正物料信息查询方法调用 将直接调用material_id_query改为通过hardware_interface调用,以符合接口设计规范 --- scripts/workflow.py | 689 +++++++++++++ .../dispensing_station_bioyond.json | 2 +- test/workflow/example_bio.json | 186 ++++ test/workflow/merge_workflow.py | 14 + unilabos/app/web/client.py | 8 + .../bioyond_studio/dispensing_station.py | 2 +- .../bioyond_studio/reaction_station.py | 202 ++-- .../devices/bioyond_dispensing_station.yaml | 253 +++++ .../devices/reaction_station_bioyond.yaml | 956 ++++++------------ unilabos/registry/resources/bioyond/deck.yaml | 18 + unilabos/resources/bioyond/decks.py | 60 +- unilabos/resources/bioyond/warehouses.py | 107 ++ unilabos/resources/graphio.py | 8 +- unilabos/resources/itemized_carrier.py | 6 +- unilabos/ros/nodes/resource_tracker.py | 12 +- 15 files changed, 1785 insertions(+), 738 deletions(-) create mode 100644 scripts/workflow.py create mode 100644 test/workflow/example_bio.json create mode 100644 test/workflow/merge_workflow.py create mode 100644 unilabos/registry/devices/bioyond_dispensing_station.yaml diff --git a/scripts/workflow.py b/scripts/workflow.py new file mode 100644 index 00000000..6db3c13e --- /dev/null +++ b/scripts/workflow.py @@ -0,0 +1,689 @@ +import json +import logging +import traceback +import uuid +import xml.etree.ElementTree as ET +from typing import Any, Dict, List + +import networkx as nx +import matplotlib.pyplot as plt +import requests + +logger = logging.getLogger(__name__) + + +class SimpleGraph: + """简单的有向图实现,用于构建工作流图""" + + def __init__(self): + self.nodes = {} + self.edges = [] + + def add_node(self, node_id, **attrs): + """添加节点""" + self.nodes[node_id] = attrs + + def add_edge(self, source, target, **attrs): + """添加边""" + edge = {"source": source, "target": target, **attrs} + self.edges.append(edge) + + def to_dict(self): + """转换为工作流图格式""" + nodes_list = [] + for node_id, attrs in self.nodes.items(): + node_attrs = attrs.copy() + params = node_attrs.pop("parameters", {}) or {} + node_attrs.update(params) + nodes_list.append({"id": node_id, **node_attrs}) + + return { + "directed": True, + "multigraph": False, + "graph": {}, + "nodes": nodes_list, + "links": self.edges, + } + + +def extract_json_from_markdown(text: str) -> str: + """从markdown代码块中提取JSON""" + text = text.strip() + if text.startswith("```json\n"): + text = text[8:] + if text.startswith("```\n"): + text = text[4:] + if text.endswith("\n```"): + text = text[:-4] + return text + + +def convert_to_type(val: str) -> Any: + """将字符串值转换为适当的数据类型""" + if val == "True": + return True + if val == "False": + return False + if val == "?": + return None + if val.endswith(" g"): + return float(val.split(" ")[0]) + if val.endswith("mg"): + return float(val.split("mg")[0]) + elif val.endswith("mmol"): + return float(val.split("mmol")[0]) / 1000 + elif val.endswith("mol"): + return float(val.split("mol")[0]) + elif val.endswith("ml"): + return float(val.split("ml")[0]) + elif val.endswith("RPM"): + return float(val.split("RPM")[0]) + elif val.endswith(" °C"): + return float(val.split(" ")[0]) + elif val.endswith(" %"): + return float(val.split(" ")[0]) + return val + + +def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """统一的数据重构函数,根据操作类型自动选择模板""" + refactored_data = [] + + # 定义操作映射,包含生物实验和有机化学的所有操作 + OPERATION_MAPPING = { + # 生物实验操作 + "transfer_liquid": "SynBioFactory-liquid_handler.transfer_liquid", + "transfer": "SynBioFactory-liquid_handler.biomek-transfer", + "incubation": "SynBioFactory-liquid_handler.biomek-incubation", + "move_labware": "SynBioFactory-liquid_handler.biomek-move_labware", + "oscillation": "SynBioFactory-liquid_handler.biomek-oscillation", + # 有机化学操作 + "HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol", + "StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol", + "StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol", + "HeatChill": "SynBioFactory-workstation-HeatChillProtocol", + "Dissolve": "SynBioFactory-workstation-DissolveProtocol", + "Transfer": "SynBioFactory-workstation-TransferProtocol", + "Evaporate": "SynBioFactory-workstation-EvaporateProtocol", + "Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol", + "Filter": "SynBioFactory-workstation-FilterProtocol", + "Dry": "SynBioFactory-workstation-DryProtocol", + "Add": "SynBioFactory-workstation-AddProtocol", + } + + UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"] + + for step in data: + operation = step.get("action") + if not operation or operation in UNSUPPORTED_OPERATIONS: + continue + + # 处理重复操作 + if operation == "Repeat": + times = step.get("times", step.get("parameters", {}).get("times", 1)) + sub_steps = step.get("steps", step.get("parameters", {}).get("steps", [])) + for i in range(int(times)): + sub_data = refactor_data(sub_steps) + refactored_data.extend(sub_data) + continue + + # 获取模板名称 + template = OPERATION_MAPPING.get(operation) + if not template: + # 自动推断模板类型 + if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]: + template = f"SynBioFactory-liquid_handler.biomek-{operation}" + else: + template = f"SynBioFactory-workstation-{operation}Protocol" + + # 创建步骤数据 + step_data = { + "template": template, + "description": step.get("description", step.get("purpose", f"{operation} operation")), + "lab_node_type": "Device", + "parameters": step.get("parameters", step.get("action_args", {})), + } + refactored_data.append(step_data) + + return refactored_data + + +def build_protocol_graph( + labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str +) -> SimpleGraph: + """统一的协议图构建函数,根据设备类型自动选择构建逻辑""" + G = SimpleGraph() + resource_last_writer = {} + LAB_NAME = "SynBioFactory" + + protocol_steps = refactor_data(protocol_steps) + + # 检查协议步骤中的模板来判断协议类型 + has_biomek_template = any("biomek" in step.get("template", "") for step in protocol_steps) + + if has_biomek_template: + # 生物实验协议图构建 + for labware_id, labware in labware_info.items(): + node_id = str(uuid.uuid4()) + + labware_attrs = labware.copy() + labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}")) + labware_attrs["description"] = labware_id + labware_attrs["lab_node_type"] = ( + "Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample" + ) + labware_attrs["device_id"] = workstation_name + + G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs) + resource_last_writer[labware_id] = f"{node_id}:labware" + + # 处理协议步骤 + for i, step in enumerate(protocol_steps): + node_id = str(uuid.uuid4()) + G.add_node(node_id, **step) + + # 添加控制流边 + G.add_edge(prev_node, node_id, source_port="ready", target_port="ready") + prev_node = node_id + + # 处理物料流 + params = step.get("parameters", {}) + if "sources" in params and params["sources"] in resource_last_writer: + source_node, source_port = resource_last_writer[params["sources"]].split(":") + G.add_edge(source_node, node_id, source_port=source_port, target_port="labware") + + if "targets" in params: + resource_last_writer[params["targets"]] = f"{node_id}:labware" + + # 添加协议结束节点 + end_id = str(uuid.uuid4()) + G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol") + G.add_edge(prev_node, end_id, source_port="ready", target_port="ready") + + else: + # 有机化学协议图构建 + WORKSTATION_ID = workstation_name + + # 为所有labware创建资源节点 + for item_id, item in labware_info.items(): + # item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}") + node_id = str(uuid.uuid4()) + + # 判断节点类型 + if item.get("type") == "hardware" or "reactor" in str(item_id).lower(): + if "reactor" not in str(item_id).lower(): + continue + lab_node_type = "Sample" + description = f"Prepare Reactor: {item_id}" + liquid_type = [] + liquid_volume = [] + else: + lab_node_type = "Reagent" + description = f"Add Reagent to Flask: {item_id}" + liquid_type = [item_id] + liquid_volume = [1e5] + + G.add_node( + node_id, + template=f"{LAB_NAME}-host_node-create_resource", + description=description, + lab_node_type=lab_node_type, + res_id=item_id, + device_id=WORKSTATION_ID, + class_name="container", + parent=WORKSTATION_ID, + bind_locations={"x": 0.0, "y": 0.0, "z": 0.0}, + liquid_input_slot=[-1], + liquid_type=liquid_type, + liquid_volume=liquid_volume, + slot_on_deck="", + role=item.get("role", ""), + ) + resource_last_writer[item_id] = f"{node_id}:labware" + + last_control_node_id = None + + # 处理协议步骤 + for step in protocol_steps: + node_id = str(uuid.uuid4()) + G.add_node(node_id, **step) + + # 控制流 + if last_control_node_id is not None: + G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready") + last_control_node_id = node_id + + # 物料流 + params = step.get("parameters", {}) + input_resources = { + "Vessel": params.get("vessel"), + "ToVessel": params.get("to_vessel"), + "FromVessel": params.get("from_vessel"), + "reagent": params.get("reagent"), + "solvent": params.get("solvent"), + "compound": params.get("compound"), + "sources": params.get("sources"), + "targets": params.get("targets"), + } + + for target_port, resource_name in input_resources.items(): + if resource_name and resource_name in resource_last_writer: + source_node, source_port = resource_last_writer[resource_name].split(":") + G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port) + + output_resources = { + "VesselOut": params.get("vessel"), + "FromVesselOut": params.get("from_vessel"), + "ToVesselOut": params.get("to_vessel"), + "FiltrateOut": params.get("filtrate_vessel"), + "reagent": params.get("reagent"), + "solvent": params.get("solvent"), + "compound": params.get("compound"), + "sources_out": params.get("sources"), + "targets_out": params.get("targets"), + } + + for source_port, resource_name in output_resources.items(): + if resource_name: + resource_last_writer[resource_name] = f"{node_id}:{source_port}" + + return G + + +def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str): + """ + (辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。 + """ + if not protocol_graph: + print("Cannot draw graph: Graph object is empty.") + return + + G = nx.DiGraph() + + for node_id, attrs in protocol_graph.nodes.items(): + label = attrs.get("description", attrs.get("template", node_id[:8])) + G.add_node(node_id, label=label, **attrs) + + for edge in protocol_graph.edges: + G.add_edge(edge["source"], edge["target"]) + + plt.figure(figsize=(20, 15)) + try: + pos = nx.nx_agraph.graphviz_layout(G, prog="dot") + except Exception: + pos = nx.shell_layout(G) # Fallback layout + + node_labels = {node: data["label"] for node, data in G.nodes(data=True)} + nx.draw( + G, + pos, + with_labels=False, + node_size=2500, + node_color="skyblue", + node_shape="o", + edge_color="gray", + width=1.5, + arrowsize=15, + ) + nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold") + + plt.title("Chemical Protocol Workflow Graph", size=15) + plt.savefig(output_path, dpi=300, bbox_inches="tight") + plt.close() + print(f" - Visualization saved to '{output_path}'") + + +from networkx.drawing.nx_agraph import to_agraph +import re + +COMPASS = {"n","e","s","w","ne","nw","se","sw","c"} + +def _is_compass(port: str) -> bool: + return isinstance(port, str) and port.lower() in COMPASS + +def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"): + """ + 使用 Graphviz 端口语法绘制协议工作流图。 + - 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。 + - 否则自动为节点创建 record 形状并定义命名端口 。 + 最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。 + """ + if not protocol_graph: + print("Cannot draw graph: Graph object is empty.") + return + + # 1) 先用 networkx 搭建有向图,保留端口属性 + G = nx.DiGraph() + for node_id, attrs in protocol_graph.nodes.items(): + label = attrs.get("description", attrs.get("template", node_id[:8])) + # 保留一个干净的“中心标签”,用于放在 record 的中间槽 + G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)}) + + edges_data = [] + in_ports_by_node = {} # 收集命名输入端口 + out_ports_by_node = {} # 收集命名输出端口 + + for edge in protocol_graph.edges: + u = edge["source"] + v = edge["target"] + sp = edge.get("source_port") + tp = edge.get("target_port") + + # 记录到图里(保留原始端口信息) + G.add_edge(u, v, source_port=sp, target_port=tp) + edges_data.append((u, v, sp, tp)) + + # 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record + if sp and not _is_compass(sp): + out_ports_by_node.setdefault(u, set()).add(str(sp)) + if tp and not _is_compass(tp): + in_ports_by_node.setdefault(v, set()).add(str(tp)) + + # 2) 转为 AGraph,使用 Graphviz 渲染 + A = to_agraph(G) + A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10") + A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica") + A.edge_attr.update(arrowsize="0.8", color="#666666") + + # 3) 为需要命名端口的节点设置 record 形状与 label + # 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口 + for n in A.nodes(): + node = A.get_node(n) + core = G.nodes[n].get("_core_label", n) + + in_ports = sorted(in_ports_by_node.get(n, [])) + out_ports = sorted(out_ports_by_node.get(n, [])) + + # 如果该节点涉及命名端口,则用 record;否则保留原 box + if in_ports or out_ports: + def port_fields(ports): + if not ports: + return " " # 必须留一个空槽占位 + # 每个端口一个小格子,

name + return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports) + + left = port_fields(in_ports) + right = port_fields(out_ports) + + # 三栏:左(入) | 中(节点名) | 右(出) + record_label = f"{{ {left} | {core} | {right} }}" + node.attr.update(shape="record", label=record_label) + else: + # 没有命名端口:普通盒子,显示核心标签 + node.attr.update(label=str(core)) + + # 4) 给边设置 headport / tailport + # - 若端口为 compass:直接用 compass(e.g., headport="e") + # - 若端口为命名端口:使用在 record 中定义的 名(同名即可) + for (u, v, sp, tp) in edges_data: + e = A.get_edge(u, v) + + # Graphviz 属性:tail 是源,head 是目标 + if sp: + if _is_compass(sp): + e.attr["tailport"] = sp.lower() + else: + # 与 record label 中 名一致;特殊字符已在 label 中做了清洗 + e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp)) + + if tp: + if _is_compass(tp): + e.attr["headport"] = tp.lower() + else: + e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp)) + + # 可选:若想让边更贴边缘,可设置 constraint/spline 等 + # e.attr["arrowhead"] = "vee" + + # 5) 输出 + A.draw(output_path, prog="dot") + print(f" - Port-aware workflow rendered to '{output_path}'") + + +def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]: + """展平嵌套的XDL程序结构""" + flattened_operations = [] + TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"] + + def extract_operations(element: ET.Element): + if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]: + if element.tag not in TEMP_UNSUPPORTED_PROTOCOL: + flattened_operations.append(element) + + for child in element: + extract_operations(child) + + for child in procedure_elem: + extract_operations(child) + + return flattened_operations + + +def parse_xdl_content(xdl_content: str) -> tuple: + """解析XDL内容""" + try: + xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable()) + root = ET.fromstring(xdl_content_cleaned) + + synthesis_elem = root.find("Synthesis") + if synthesis_elem is None: + return None, None, None + + # 解析硬件组件 + hardware_elem = synthesis_elem.find("Hardware") + hardware = [] + if hardware_elem is not None: + hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")] + + # 解析试剂 + reagents_elem = synthesis_elem.find("Reagents") + reagents = [] + if reagents_elem is not None: + reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")] + + # 解析程序 + procedure_elem = synthesis_elem.find("Procedure") + if procedure_elem is None: + return None, None, None + + flattened_operations = flatten_xdl_procedure(procedure_elem) + return hardware, reagents, flattened_operations + + except ET.ParseError as e: + raise ValueError(f"Invalid XDL format: {e}") + + +def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]: + """ + 将XDL XML格式转换为标准的字典格式 + + Args: + xdl_content: XDL XML内容 + + Returns: + 转换结果,包含步骤和器材信息 + """ + try: + hardware, reagents, flattened_operations = parse_xdl_content(xdl_content) + if hardware is None: + return {"error": "Failed to parse XDL content", "success": False} + + # 将XDL元素转换为字典格式 + steps_data = [] + for elem in flattened_operations: + # 转换参数类型 + parameters = {} + for key, val in elem.attrib.items(): + converted_val = convert_to_type(val) + if converted_val is not None: + parameters[key] = converted_val + + step_dict = { + "operation": elem.tag, + "parameters": parameters, + "description": elem.get("purpose", f"Operation: {elem.tag}"), + } + steps_data.append(step_dict) + + # 合并硬件和试剂为统一的labware_info格式 + labware_data = [] + labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware) + labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents) + + return { + "success": True, + "steps": steps_data, + "labware": labware_data, + "message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.", + } + + except Exception as e: + error_msg = f"XDL conversion failed: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + + +def create_workflow( + steps_info: str, + labware_info: str, + workflow_name: str = "Generated Workflow", + workstation_name: str = "workstation", + workflow_description: str = "Auto-generated workflow from protocol", +) -> Dict[str, Any]: + """ + 创建工作流,输入数据已经是统一的字典格式 + + Args: + steps_info: 步骤信息 (JSON字符串,已经是list of dict格式) + labware_info: 实验器材和试剂信息 (JSON字符串,已经是list of dict格式) + workflow_name: 工作流名称 + workflow_description: 工作流描述 + + Returns: + 创建结果,包含工作流UUID和详细信息 + """ + try: + # 直接解析JSON数据 + steps_info_clean = extract_json_from_markdown(steps_info) + labware_info_clean = extract_json_from_markdown(labware_info) + + steps_data = json.loads(steps_info_clean) + labware_data = json.loads(labware_info_clean) + + # 统一处理所有数据 + protocol_graph = build_protocol_graph(labware_data, steps_data, workstation_name=workstation_name) + + # 检测协议类型(用于标签) + protocol_type = "bio" if any("biomek" in step.get("template", "") for step in refactored_steps) else "organic" + + # 转换为工作流格式 + data = protocol_graph.to_dict() + + # 转换节点格式 + for i, node in enumerate(data["nodes"]): + description = node.get("description", "") + onode = { + "template": node.pop("template"), + "id": node["id"], + "lab_node_type": node.get("lab_node_type", "Device"), + "name": description or f"Node {i + 1}", + "params": {"default": node}, + "handles": {}, + } + + # 处理边连接 + for edge in data["links"]: + if edge["source"] == node["id"]: + source_port = edge.get("source_port", "output") + if source_port not in onode["handles"]: + onode["handles"][source_port] = {"type": "source"} + + if edge["target"] == node["id"]: + target_port = edge.get("target_port", "input") + if target_port not in onode["handles"]: + onode["handles"][target_port] = {"type": "target"} + + data["nodes"][i] = onode + + # 发送到API创建工作流 + api_secret = configs.Lab.Key + if not api_secret: + return {"error": "API SecretKey is not configured", "success": False} + + # Step 1: 创建工作流 + workflow_url = f"{configs.Lab.Api}/api/v1/workflow/" + headers = { + "Content-Type": "application/json", + } + params = {"secret_key": api_secret} + + graph_data = {"name": workflow_name, **data} + + logger.info(f"Creating workflow: {workflow_name}") + response = requests.post( + workflow_url, params=params, json=graph_data, headers=headers, timeout=configs.Lab.Timeout + ) + response.raise_for_status() + + workflow_info = response.json() + + if workflow_info.get("code") != 0: + error_msg = f"API returned an error: {workflow_info.get('msg', 'Unknown Error')}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + + workflow_uuid = workflow_info.get("data", {}).get("uuid") + if not workflow_uuid: + return {"error": "Failed to get workflow UUID from response", "success": False} + + # Step 2: 添加到模板库(可选) + try: + library_url = f"{configs.Lab.Api}/api/flociety/vs/workflows/library/" + lib_payload = { + "workflow_uuid": workflow_uuid, + "title": workflow_name, + "description": workflow_description, + "labels": [protocol_type.title(), "Auto-generated"], + } + + library_response = requests.post( + library_url, params=params, json=lib_payload, headers=headers, timeout=configs.Lab.Timeout + ) + library_response.raise_for_status() + + library_info = library_response.json() + logger.info(f"Workflow added to library: {library_info}") + + return { + "success": True, + "workflow_uuid": workflow_uuid, + "workflow_info": workflow_info.get("data"), + "library_info": library_info.get("data"), + "protocol_type": protocol_type, + "message": f"Workflow '{workflow_name}' created successfully", + } + + except Exception as e: + # 即使添加到库失败,工作流创建仍然成功 + logger.warning(f"Failed to add workflow to library: {str(e)}") + return { + "success": True, + "workflow_uuid": workflow_uuid, + "workflow_info": workflow_info.get("data"), + "protocol_type": protocol_type, + "message": f"Workflow '{workflow_name}' created successfully (library addition failed)", + } + + except requests.exceptions.RequestException as e: + error_msg = f"Network error when calling API: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + except Exception as e: + error_msg = f"An unexpected error occurred: {str(e)}" + logger.error(error_msg) + logger.error(traceback.format_exc()) + return {"error": error_msg, "success": False} diff --git a/test/experiments/dispensing_station_bioyond.json b/test/experiments/dispensing_station_bioyond.json index 745e1289..751eac09 100644 --- a/test/experiments/dispensing_station_bioyond.json +++ b/test/experiments/dispensing_station_bioyond.json @@ -8,7 +8,7 @@ ], "parent": null, "type": "device", - "class": "workstation.bioyond_dispensing_station", + "class": "bioyond_dispensing_station", "config": { "config": { "api_key": "DE9BDDA0", diff --git a/test/workflow/example_bio.json b/test/workflow/example_bio.json new file mode 100644 index 00000000..d0f0d7a3 --- /dev/null +++ b/test/workflow/example_bio.json @@ -0,0 +1,186 @@ +{ + "workflow": [ + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_1", + "targets": "Liquid_2", + "asp_vol": 66.0, + "dis_vol": 66.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 58.0, + "dis_vol": 96.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 85.0, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 63.333333333333336, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 72.0, + "dis_vol": 150.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 85.0, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 63.333333333333336, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 72.0, + "dis_vol": 150.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 20.0, + "dis_vol": 20.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_5", + "targets": "Liquid_2", + "asp_vol": 6.0, + "dis_vol": 12.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_5", + "targets": "Liquid_2", + "asp_vol": 10.666666666666666, + "dis_vol": 12.0, + "asp_flow_rate": 7.599999999999999, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_6", + "asp_vol": 12.0, + "dis_vol": 10.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + } + ], + "reagent": { + "Liquid_6": { + "slot": 1, + "well": [ + "A2" + ], + "labware": "elution plate" + }, + "Liquid_1": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_4": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_5": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_2": { + "slot": 4, + "well": [ + "A2" + ], + "labware": "TAG1 plate on Magnetic Module GEN2" + }, + "Liquid_3": { + "slot": 12, + "well": [ + "A1" + ], + "labware": "Opentrons Fixed Trash" + } + } +} \ No newline at end of file diff --git a/test/workflow/merge_workflow.py b/test/workflow/merge_workflow.py new file mode 100644 index 00000000..f42783c5 --- /dev/null +++ b/test/workflow/merge_workflow.py @@ -0,0 +1,14 @@ +import pytest +import json +from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports + + +@pytest.mark.parametrize("protocol_name", [ + "example_bio", + # "bioyond_materials_liquidhandling_1", +]) +def test_build_protocol_graph(protocol_name): + d = json.load(open(f"{protocol_name}.json")) + graph = build_protocol_graph(labware_info=d["reagent"], protocol_steps=d["workflow"], workstation_name="PRCXi") + draw_protocol_graph_with_ports(graph, "graph.png") + print(graph) \ No newline at end of file diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b8c8bea3..3c51b349 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -126,12 +126,16 @@ class HTTPClient: Returns: Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid} """ + with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f: + f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4)) response = requests.post( f"{self.remote_addr}/edge/material/query", json={"uuids": uuid_list, "with_children": with_children}, headers={"Authorization": f"Lab {self.auth}"}, timeout=100, ) + with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f: + f.write(f"{response.status_code}" + "\n" + response.text) if response.status_code == 200: res = response.json() if "code" in res and res["code"] != 0: @@ -187,12 +191,16 @@ class HTTPClient: Returns: Dict: 返回的资源数据 """ + with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f: + f.write(json.dumps({"id": id, "with_children": with_children}, indent=4)) response = requests.get( f"{self.remote_addr}/lab/material", params={"id": id, "with_children": with_children}, headers={"Authorization": f"Lab {self.auth}"}, timeout=20, ) + with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f: + f.write(f"{response.status_code}" + "\n" + response.text) return response.json() def resource_del(self, id: str) -> requests.Response: diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py index 11b011cc..b56965a6 100644 --- a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py @@ -338,7 +338,7 @@ class BioyondDispensingStation(BioyondWorkstation): workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 4. 查询工作流对应的holdMID - material_info = self.material_id_query(workflow_id) + material_info = self.hardware_interface.material_id_query(workflow_id) if not material_info: raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息") diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station.py index 3e9a6a6e..f7cb0f8d 100644 --- a/unilabos/devices/workstation/bioyond_studio/reaction_station.py +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station.py @@ -12,13 +12,13 @@ from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG class BioyondReactionStation(BioyondWorkstation): """Bioyond反应站类 - + 继承自BioyondWorkstation,提供反应站特定的业务方法 """ - + def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs): """初始化反应站 - + Args: config: 配置字典,应包含workflow_mappings等配置 deck: Deck对象 @@ -27,13 +27,13 @@ class BioyondReactionStation(BioyondWorkstation): """ if deck is None and config: deck = config.get('deck') - + print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}") if config and 'workflow_mappings' in config: print(f"workflow_mappings内容: {config['workflow_mappings']}") - + super().__init__(bioyond_config=config, deck=deck) - + print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}") print(f"workflow_mappings长度: {len(self.workflow_mappings)}") @@ -49,21 +49,21 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def reactor_taken_in( - self, - assign_material_name: str, + self, + assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00 ): """反应器放入 - + Args: assign_material_name: 物料名称(不能为空) - cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000") - temperature: 温度上限(°C,范围:-50.00 至 100.00) - + cutoff: 粘度上限(需为有效数字字符串,默认 "900000") + temperature: 温度设定(°C,范围:-50.00 至 100.00) + Returns: str: JSON 字符串,格式为 {"suc": True} - + Raises: ValueError: 若物料名称无效或 cutoff 格式错误 """ @@ -73,7 +73,7 @@ class BioyondReactionStation(BioyondWorkstation): float(cutoff) except ValueError: raise ValueError("cutoff 必须是有效的数字字符串") - + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -103,21 +103,21 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def solid_feeding_vials( - self, - material_id: str, - time: str = "0", + self, + material_id: str, + time: str = "0", torque_variation: int = 1, - assign_material_name: str = None, + assign_material_name: str = None, temperature: float = 25.00 ): """固体进料小瓶 - + Args: - material_id: 粉末类型ID + material_id: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) assign_material_name: 物料名称(用于获取试剂瓶位ID) - temperature: 温度上限(°C) + temperature: 温度设定(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None @@ -127,7 +127,7 @@ class BioyondReactionStation(BioyondWorkstation): feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] - + solid_feeding_vials_params = { "param_values": { feeding_step_id: { @@ -152,22 +152,22 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def liquid_feeding_vials_non_titration( - self, + self, volume_formula: str, assign_material_name: str, titration_type: str = "1", time: str = "0", - torque_variation: int = 1, + torque_variation: int = 1, temperature: float = 25.00 ): """液体进料小瓶(非滴定) - + Args: volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 是否滴定(1=滴定, 其他=非滴定) + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') @@ -180,7 +180,7 @@ class BioyondReactionStation(BioyondWorkstation): liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] - + params = { "param_values": { liquid_step_id: { @@ -206,23 +206,23 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def liquid_feeding_solvents( - self, - assign_material_name: str, - volume: str, + self, + assign_material_name: str, + volume: str, titration_type: str = "1", - time: str = "360", - torque_variation: int = 2, + time: str = "360", + torque_variation: int = 2, temperature: float = 25.00 ): """液体进料-溶剂 - + Args: assign_material_name: 物料名称 volume: 分液量(μL) - titration_type: 是否滴定 + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) - temperature: 温度上限(°C) + torque_variation: 是否观察(int类型, 1=否, 2=是) + temperature: 温度设定(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) @@ -231,10 +231,10 @@ class BioyondReactionStation(BioyondWorkstation): if isinstance(temperature, str): temperature = float(temperature) - + liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] - + params = { "param_values": { liquid_step_id: { @@ -260,22 +260,22 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def liquid_feeding_titration( - self, - volume_formula: str, - assign_material_name: str, + self, + volume_formula: str, + assign_material_name: str, titration_type: str = "1", - time: str = "90", + time: str = "90", torque_variation: int = 2, temperature: float = 25.00 ): """液体进料(滴定) - + Args: volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 是否滴定 + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') @@ -314,23 +314,23 @@ class BioyondReactionStation(BioyondWorkstation): return json.dumps({"suc": True}) def liquid_feeding_beaker( - self, - volume: str = "35000", + self, + volume: str = "35000", assign_material_name: str = "BAPP", - time: str = "0", - torque_variation: int = 1, + time: str = "0", + torque_variation: int = 1, titration_type: str = "1", temperature: float = 25.00 ): """液体进料烧杯 - + Args: volume: 分液量(μL) assign_material_name: 物料名称(试剂瓶位) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) - titration_type: 是否滴定 - temperature: 温度上限(°C) + torque_variation: 是否观察(int类型, 1=否, 2=是) + titration_type: 是否滴定(1=否, 2=是) + temperature: 温度设定(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) @@ -366,7 +366,7 @@ class BioyondReactionStation(BioyondWorkstation): print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - + def drip_back( self, assign_material_name: str, @@ -377,13 +377,13 @@ class BioyondReactionStation(BioyondWorkstation): temperature: float = 25.00 ): """滴回去 - + Args: assign_material_name: 物料名称(液体种类) volume: 分液量(μL) - titration_type: 是否滴定 + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}') @@ -425,7 +425,7 @@ class BioyondReactionStation(BioyondWorkstation): def get_workflow_sequence(self) -> List[str]: """获取当前工作流执行顺序 - + Returns: 工作流名称列表 """ @@ -439,10 +439,10 @@ class BioyondReactionStation(BioyondWorkstation): def workflow_step_query(self, workflow_id: str) -> dict: """查询工作流步骤参数 - + Args: workflow_id: 工作流ID - + Returns: 工作流步骤参数字典 """ @@ -450,10 +450,10 @@ class BioyondReactionStation(BioyondWorkstation): def create_order(self, json_str: str) -> dict: """创建订单 - + Args: json_str: 订单参数的JSON字符串 - + Returns: 创建结果 """ @@ -463,10 +463,10 @@ class BioyondReactionStation(BioyondWorkstation): def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]: """处理网页工作流列表 - + Args: web_workflow_json: JSON 格式的网页工作流列表 - + Returns: List[Dict[str, str]]: 包含工作流 ID 和名称的字典列表 """ @@ -492,11 +492,11 @@ class BioyondReactionStation(BioyondWorkstation): def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: """ 一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务 - + Args: workflow_name: 合并后的工作流名称 task_name: 任务名称 - + Returns: 任务创建结果 """ @@ -504,32 +504,32 @@ class BioyondReactionStation(BioyondWorkstation): print(f"\n{'='*60}") print(f"📋 处理网页工作流列表: {web_workflow_list}") print(f"{'='*60}") - + web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) workflows_result = self.process_web_workflows(web_workflow_json) - + if not workflows_result: return self._create_error_result("处理网页工作流列表失败", "process_web_workflows") - + print(f"workflows_result 类型: {type(workflows_result)}") print(f"workflows_result 内容: {workflows_result}") - + workflows_with_params = self._build_workflows_with_parameters(workflows_result) - + merge_data = { "name": workflow_name, "workflows": workflows_with_params } - + # print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}") merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data)) - + if not merged_workflow: return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters") - + workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") # print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})") - + order_params = [{ "orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}", "orderName": task_name, @@ -537,16 +537,16 @@ class BioyondReactionStation(BioyondWorkstation): "borderNumber": 1, "paramValues": {} }] - + result = self.create_order(json.dumps(order_params)) - + if not result: return self._create_error_result("创建任务失败", "create_order") - + # 清空工作流序列和参数,防止下次执行时累积重复 self.pending_task_params = [] - self.clear_workflows() - + self.clear_workflows() # 清空工作流序列,避免重复累积 + # print(f"\n✅ 任务创建成功: {result}") # print(f"\n✅ 任务创建成功") print(f"{'='*60}\n") @@ -555,10 +555,10 @@ class BioyondReactionStation(BioyondWorkstation): def _build_workflows_with_parameters(self, workflows_result: list) -> list: """ 构建带参数的工作流列表 - + Args: workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表) - + Returns: 符合新接口格式的工作流参数结构 """ @@ -577,19 +577,19 @@ class BioyondReactionStation(BioyondWorkstation): continue workflow_name = workflow_info.get("name", "") # print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})") - + if idx >= len(self.pending_task_params): # print(f" ⚠️ 无对应参数,跳过") workflows_with_params.append({"id": workflow_id}) continue - + param_data = self.pending_task_params[idx] param_values = param_data.get("param_values", {}) if not param_values: # print(f" ⚠️ 参数为空,跳过") workflows_with_params.append({"id": workflow_id}) continue - + step_parameters = {} for step_id, actions_dict in param_values.items(): # print(f" 📍 步骤ID: {step_id}") @@ -646,25 +646,25 @@ class BioyondReactionStation(BioyondWorkstation): def merge_workflow_with_parameters(self, json_str: str) -> dict: """ 调用新接口:合并工作流并传递参数 - + Args: json_str: JSON格式的字符串,包含: - name: 工作流名称 - workflows: [{"id": "工作流ID", "stepParameters": {...}}] - + Returns: 合并后的工作流信息 """ try: data = json.loads(json_str) - + # 在工作流名称后面添加时间戳,避免重复 if "name" in data and data["name"]: timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-") original_name = data["name"] data["name"] = f"{original_name}_{timestamp}" print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}") - + request_data = { "apiKey": API_CONFIG["api_key"], "requestTime": self.hardware_interface.get_current_time_iso8601(), @@ -673,37 +673,37 @@ class BioyondReactionStation(BioyondWorkstation): print(f"\n📤 发送合并请求:") print(f" 工作流名称: {data.get('name')}") print(f" 子工作流数量: {len(data.get('workflows', []))}") - + # 打印完整的POST请求内容 print(f"\n🔍 POST请求详细内容:") print(f" URL: {self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters") print(f" Headers: {{'Content-Type': 'application/json'}}") print(f" Request Data:") print(f" {json.dumps(request_data, indent=4, ensure_ascii=False)}") - # + # response = requests.post( f"{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters", json=request_data, headers={"Content-Type": "application/json"}, timeout=30 ) - + # # 打印响应详细内容 # print(f"\n📥 POST响应详细内容:") # print(f" 状态码: {response.status_code}") # print(f" 响应头: {dict(response.headers)}") # print(f" 响应体: {response.text}") - # # + # # try: result = response.json() - # # + # # # print(f"\n📋 解析后的响应JSON:") # print(f" {json.dumps(result, indent=4, ensure_ascii=False)}") - # # + # # except json.JSONDecodeError: print(f"❌ 服务器返回非 JSON 格式响应: {response.text}") return None - + if result.get("code") == 1: print(f"✅ 工作流合并成功(带参数)") return result.get("data", {}) @@ -711,7 +711,7 @@ class BioyondReactionStation(BioyondWorkstation): error_msg = result.get('message', '未知错误') print(f"❌ 工作流合并失败: {error_msg}") return None - + except requests.exceptions.Timeout: print(f"❌ 合并工作流请求超时") return None @@ -727,10 +727,10 @@ class BioyondReactionStation(BioyondWorkstation): def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool: """验证工作流ID是否有效,如果无效则重新合并 - + Args: workflow_name: 工作流名称 - + Returns: bool: 验证或刷新是否成功 """ diff --git a/unilabos/registry/devices/bioyond_dispensing_station.yaml b/unilabos/registry/devices/bioyond_dispensing_station.yaml new file mode 100644 index 00000000..6250db75 --- /dev/null +++ b/unilabos/registry/devices/bioyond_dispensing_station.yaml @@ -0,0 +1,253 @@ +bioyond_dispensing_station: + category: + - workstation + - bioyond + - bioyond_dispensing_station + 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:BioyondDispensingStation + status_types: {} + type: python + config_info: [] + description: '' + handles: [] + icon: preparation_station.webp + init_param_schema: + config: + properties: + config: + type: string + required: + - config + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 0a4090ea..f8719e57 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -4,534 +4,18 @@ reaction_station.bioyond: - reaction_station_bioyond class: action_value_mappings: - auto-append_to_workflow_sequence: - feedback: {} - goal: {} - goal_default: - web_workflow_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - web_workflow_name: - type: string - required: - - web_workflow_name - type: object - result: {} - required: - - goal - title: append_to_workflow_sequence参数 - type: object - type: UniLabJsonCommand - auto-clear_workflows: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: clear_workflows参数 - 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-merge_workflow_with_parameters: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: merge_workflow_with_parameters参数 - 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-process_web_workflows: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: process_web_workflows参数 - 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-resource_tree_add: - feedback: {} - goal: {} - goal_default: - resources: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - resources: - items: - type: object - type: array - required: - - resources - type: object - result: {} - required: - - goal - title: resource_tree_add参数 - type: object - type: UniLabJsonCommand - auto-set_workflow_sequence: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: set_workflow_sequence参数 - 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 - bioyond_sync: - feedback: {} - goal: - force_sync: force_sync - sync_type: sync_type - goal_default: - force_sync: false - sync_type: full - handles: {} - result: {} - schema: - description: 从Bioyond系统同步物料 - properties: - feedback: {} - goal: - properties: - force_sync: - description: 是否强制同步 - type: boolean - sync_type: - description: 同步类型 - enum: - - full - - incremental - type: string - required: - - sync_type - type: object - result: {} - required: - - goal - title: bioyond_sync参数 - type: object - type: UniLabJsonCommand - bioyond_update: - feedback: {} - goal: - material_ids: material_ids - sync_all: sync_all - goal_default: - material_ids: [] - sync_all: true - handles: {} - result: {} - schema: - description: 将本地物料变更同步到Bioyond - properties: - feedback: {} - goal: - properties: - material_ids: - description: 要同步的物料ID列表 - items: - type: string - type: array - sync_all: - description: 是否同步所有物料 - type: boolean - required: - - sync_all - type: object - result: {} - required: - - goal - title: bioyond_update参数 - type: object - type: UniLabJsonCommand - reaction_station_drip_back: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - torque_variation: '' - volume: '' - handles: {} - result: {} - schema: - description: 反应站滴回操作 - properties: - feedback: {} - goal: - properties: - assign_material_name: - description: 溶剂名称 - type: string - time: - description: 观察时间(单位min) - type: string - torque_variation: - description: 是否观察1否2是 - type: string - volume: - description: 投料体积 - type: string - required: - - volume - - assign_material_name - - time - - torque_variation - type: object - result: {} - required: - - goal - title: reaction_station_drip_back参数 - type: object - type: UniLabJsonCommand - reaction_station_liquid_feed: + drip_back: feedback: {} goal: assign_material_name: assign_material_name + temperature: temperature time: time titration_type: titration_type torque_variation: torque_variation volume: volume goal_default: assign_material_name: '' + temperature: '' time: '' titration_type: '' torque_variation: '' @@ -539,40 +23,268 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站液体进料操作 + description: 滴回去 properties: feedback: {} goal: properties: assign_material_name: - description: 溶剂名称 + description: 物料名称(不能为空) + type: string + temperature: + description: 温度设定(°C) type: string time: - description: 观察时间(单位min) + description: 观察时间(分钟) type: string titration_type: - description: 滴定类型1否2是 + description: 是否滴定(1=否, 2=是) type: string torque_variation: - description: 是否观察1否2是 + description: 是否观察 (1=否, 2=是) type: string volume: - description: 投料体积 + description: 分液公式(μL) type: string required: - - titration_type - volume - assign_material_name - time - torque_variation + - titration_type + - temperature type: object result: {} required: - goal - title: reaction_station_liquid_feed参数 + title: drip_back参数 type: object type: UniLabJsonCommand - reaction_station_process_execute: + liquid_feeding_beaker: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 液体进料烧杯 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume: + description: 分液公式(μL) + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_beaker参数 + type: object + type: UniLabJsonCommand + liquid_feeding_solvents: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 液体投料-溶剂 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume: + description: 分液公式(μL) + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_solvents参数 + type: object + type: UniLabJsonCommand + liquid_feeding_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume_formula: volume_formula + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume_formula: '' + handles: {} + result: {} + schema: + description: 液体进料(滴定) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume_formula: + description: 分液公式(μL) + type: string + required: + - volume_formula + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_titration参数 + type: object + type: UniLabJsonCommand + liquid_feeding_vials_non_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume_formula: volume_formula + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume_formula: '' + handles: {} + result: {} + schema: + description: 液体进料小瓶(非滴定) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume_formula: + description: 分液公式(μL) + type: string + required: + - volume_formula + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_vials_non_titration参数 + type: object + type: UniLabJsonCommand + process_and_execute_workflow: feedback: {} goal: task_name: task_name @@ -583,7 +295,7 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站流程执行 + description: 处理并执行工作流 properties: feedback: {} goal: @@ -601,92 +313,10 @@ reaction_station.bioyond: result: {} required: - goal - title: reaction_station_process_execute参数 + title: process_and_execute_workflow参数 type: object type: UniLabJsonCommand - reaction_station_reactor_taken_out: - feedback: {} - goal: - order_id: order_id - preintake_id: preintake_id - goal_default: - order_id: '' - preintake_id: '' - handles: {} - result: {} - schema: - description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制 - properties: - feedback: {} - goal: - properties: - order_id: - description: 订单ID,用于标识要取出的订单 - type: string - preintake_id: - description: 预取样ID,用于标识具体的取样任务 - type: string - required: [] - type: object - result: - properties: - code: - description: 操作结果代码(1表示成功,0表示失败) - type: integer - return_info: - description: 操作结果详细信息 - type: string - type: object - required: - - goal - title: reaction_station_reactor_taken_out参数 - type: object - type: UniLabJsonCommand - reaction_station_solid_feed_vial: - feedback: {} - goal: - assign_material_name: assign_material_name - material_id: material_id - time: time - torque_variation: torque_variation - goal_default: - assign_material_name: '' - material_id: '' - time: '' - torque_variation: '' - handles: {} - result: {} - schema: - description: 反应站固体进料操作 - properties: - feedback: {} - goal: - properties: - assign_material_name: - description: 固体名称_粉末加样模块-投料 - type: string - material_id: - description: 固体投料类型_粉末加样模块-投料 - type: string - time: - description: 观察时间_反应模块-观察搅拌结果 - type: string - torque_variation: - description: 是否观察1否2是_反应模块-观察搅拌结果 - type: string - required: - - assign_material_name - - material_id - - time - - torque_variation - type: object - result: {} - required: - - goal - title: reaction_station_solid_feed_vial参数 - type: object - type: UniLabJsonCommand - reaction_station_take_in: + reactor_taken_in: feedback: {} goal: assign_material_name: assign_material_name @@ -699,7 +329,7 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站取入操作 + description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 properties: feedback: {} goal: @@ -708,10 +338,10 @@ reaction_station.bioyond: description: 物料名称 type: string cutoff: - description: 截止参数 + description: 粘度上限 type: string temperature: - description: 温度 + description: 温度设定(°C) type: string required: - cutoff @@ -721,10 +351,88 @@ reaction_station.bioyond: result: {} required: - goal - title: reaction_station_take_in参数 + title: reactor_taken_in参数 type: object type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + reactor_taken_out: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + code: + description: 操作结果代码(1表示成功,0表示失败) + type: integer + return_info: + description: 操作结果详细信息 + type: string + type: object + required: + - goal + title: reactor_taken_out参数 + type: object + type: UniLabJsonCommand + solid_feeding_vials: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + temperature: temperature + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + temperature: '' + time: '' + torque_variation: '' + handles: {} + result: {} + schema: + description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称(用于获取试剂瓶位ID) + type: string + material_id: + description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + required: + - assign_material_name + - material_id + - time + - torque_variation + - temperature + type: object + result: {} + required: + - goal + title: solid_feeding_vials参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation protocol_type: [] status_types: all_workflows: dict @@ -732,18 +440,16 @@ reaction_station.bioyond: workstation_status: dict type: python config_info: [] - description: Bioyond反应站 - 专门用于化学反应操作的工作站 + description: Bioyond反应站 handles: [] - icon: 反应站.webp + icon: reaction_station.webp init_param_schema: config: properties: - bioyond_config: - type: string + config: + type: object deck: - type: string - station_config: - type: string + type: object required: [] type: object data: diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index d5a49b84..24c6dd48 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -22,3 +22,21 @@ BIOYOND_PolymerReactionStation_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 + +YB_Deck11: + category: + - deck + class: + module: unilabos.resources.bioyond.decks:YB_Deck + type: pylabrobot + description: BIOYOND PolymerReactionStation Deck + handles: [] + icon: 配液站.webp + init_param_schema: {} + registry_type: resource + version: 1.0.0 + + + + + diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index e8c021fd..fa242c3d 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -1,6 +1,7 @@ +from os import name from pylabrobot.resources import Deck, Coordinate, Rotation -from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling +from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1 class BIOYOND_PolymerReactionStation_Deck(Deck): @@ -66,3 +67,60 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck): for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) + +class BIOYOND_YB_Deck(Deck): + def __init__( + self, + name: str = "YB_Deck", + size_x: float = 4150, + size_y: float = 1400.0, + size_z: float = 2670.0, + category: str = "deck", + setup: bool = False + ) -> None: + super().__init__(name=name, size_x=4150.0, size_y=1400.0, size_z=2670.0) + if setup: + self.setup() + + def setup(self) -> None: + # 添加仓库 + self.warehouses = { + "321窗口": bioyond_warehouse_1x2x2("321窗口"), + "43窗口": bioyond_warehouse_1x2x2("43窗口"), + "手动传递窗左": bioyond_warehouse_1x3x3("手动传递窗左"), + "手动传递窗右": bioyond_warehouse_1x3x3("手动传递窗右"), + "加样头堆栈左": bioyond_warehouse_10x1x1("加样头堆栈左"), + "加样头堆栈右": bioyond_warehouse_10x1x1("加样头堆栈右"), + + "15ml配液堆栈左": bioyond_warehouse_3x3x1("15ml配液堆栈左"), + "母液加样右": bioyond_warehouse_3x3x1_2("母液加样右"), + "大瓶母液堆栈左": bioyond_warehouse_5x1x1("大瓶母液堆栈左"), + "大瓶母液堆栈右": bioyond_warehouse_5x1x1("大瓶母液堆栈右"), + } + # warehouse 的位置 + self.warehouse_locations = { + "321窗口": Coordinate(-150.0, 158.0, 0.0), + "43窗口": Coordinate(4160.0, 158.0, 0.0), + "手动传递窗左": Coordinate(-150.0, 877.0, 0.0), + "手动传递窗右": Coordinate(4160.0, 877.0, 0.0), + "加样头堆栈左": Coordinate(385.0, 1300.0, 0.0), + "加样头堆栈右": Coordinate(2187.0, 1300.0, 0.0), + + "15ml配液堆栈左": Coordinate(749.0, 355.0, 0.0), + "母液加样右": Coordinate(2152.0, 333.0, 0.0), + "大瓶母液堆栈左": Coordinate(1164.0, 676.0, 0.0), + "大瓶母液堆栈右": Coordinate(2717.0, 676.0, 0.0), + } + + for warehouse_name, warehouse in self.warehouses.items(): + self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) + +def YB_Deck(name: str) -> Deck: + by=BIOYOND_YB_Deck(name=name) + by.setup() + return by + + + + + diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py index 266a3666..a4bd46c4 100644 --- a/unilabos/resources/bioyond/warehouses.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -18,6 +18,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse: ) + def bioyond_warehouse_1x4x2(name: str) -> WareHouse: """创建BioYond 4x1x2仓库""" return warehouse_factory( @@ -34,7 +35,113 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse: category="warehouse", removed_positions=None ) + # 定义benyond的堆栈 +def bioyond_warehouse_1x2x2(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=2, + num_items_z=2, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_10x1x1(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=10, + num_items_y=1, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_1x3x3(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=1, + num_items_y=3, + num_items_z=3, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_2x1x3(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=2, + num_items_y=1, + num_items_z=3, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_3x3x1(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=3, + num_items_y=3, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_5x1x1(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=5, + num_items_y=1, + num_items_z=1, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) +def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return warehouse_factory( + name=name, + num_items_x=3, + num_items_y=3, + num_items_z=1, + dx=12.0, + dy=12.0, + dz=12.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, + category="warehouse", + ) def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: """创建BioYond开关盖加液模块台面""" diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index ada7e8d2..70e569ba 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -575,16 +575,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w replace_info = { "plate": "plate", "well": "well", - "tip_spot": "container", - "trash": "container", + "tip_spot": "tip_spot", + "trash": "trash", "deck": "deck", - "tip_rack": "container", + "tip_rack": "tip_rack", } if source in replace_info: return replace_info[source] else: print("转换pylabrobot的时候,出现未知类型", source) - return "container" + return source def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict: r = { diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 7607cf4d..fef09e25 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -78,6 +78,7 @@ class ItemizedCarrier(ResourcePLR): sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, category: Optional[str] = "carrier", model: Optional[str] = None, + invisible_slots: Optional[str] = None, ): super().__init__( name=name, @@ -89,6 +90,7 @@ class ItemizedCarrier(ResourcePLR): ) self.num_items = len(sites) self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z + self.invisible_slots = [] if invisible_slots is None else invisible_slots self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y" if isinstance(sites, dict): @@ -410,7 +412,7 @@ class ItemizedCarrier(ResourcePLR): "layout": self.layout, "sites": [{ "label": str(identifier), - "visible": True if self[identifier] is not None else False, + "visible": False if identifier in self.invisible_slots else True, "occupied_by": self[identifier].name if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else self[identifier] if isinstance(self[identifier], str) else None, @@ -433,6 +435,7 @@ class BottleCarrier(ItemizedCarrier): sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, category: str = "bottle_carrier", model: Optional[str] = None, + invisible_slots: List[str] = None, **kwargs, ): super().__init__( @@ -443,4 +446,5 @@ class BottleCarrier(ItemizedCarrier): sites=sites, category=category, model=model, + invisible_slots=invisible_slots, ) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index cd533aab..1506007b 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -32,7 +32,7 @@ class ResourceDictPositionObject(BaseModel): class ResourceDictPosition(BaseModel): size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize) scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale) - layout: Literal["2d", "x-y", "z-y", "x-z"] = Field(description="Resource layout", default="x-y") + layout: Literal["2d", "x-y", "z-y", "x-z", ""] = Field(description="Resource layout", default="x-y") position: ResourceDictPositionObject = Field( description="Resource position", default_factory=ResourceDictPositionObject ) @@ -42,7 +42,7 @@ class ResourceDictPosition(BaseModel): rotation: ResourceDictPositionObject = Field( description="Resource rotation", default_factory=ResourceDictPositionObject ) - cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(description="Cross section type", default="rectangle") + cross_section_type: Literal["rectangle", "circle", "rounded_rectangle", ""] = Field(description="Cross section type", default="rectangle") # 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化 @@ -311,12 +311,16 @@ class ResourceTreeSet(object): "plate": "plate", "well": "well", "deck": "deck", + "tip_rack": "tip_rack", + "tip_spot": "tip_spot", + "tube": "tube", + "bottle_carrier": "bottle_carrier", } if source in replace_info: return replace_info[source] else: print("转换pylabrobot的时候,出现未知类型", source) - return "container" + return source def build_uuid_mapping(res: "PLRResource", uuid_list: list): """递归构建uuid映射字典""" @@ -402,7 +406,7 @@ class ResourceTreeSet(object): import inspect # 类型映射 - TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"} + TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck", "container": "RegularContainer"} def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict): """一次遍历收集 name_to_uuid 和 all_states"""