mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-18 21:41:16 +00:00
Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model
This commit is contained in:
@@ -17,29 +17,16 @@
|
|||||||
{
|
{
|
||||||
"id": "BatteryStation",
|
"id": "BatteryStation",
|
||||||
"name": "扣电组装工作站",
|
"name": "扣电组装工作站",
|
||||||
"children": [
|
"children": [],
|
||||||
"coin_cell_deck"
|
|
||||||
],
|
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "bettery_station_registry",
|
"class": "bettery_station_registry",
|
||||||
"position": {
|
|
||||||
"x": 600,
|
|
||||||
"y": 400,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
"config": {
|
||||||
"debug_mode": true,
|
"debug_mode": false,
|
||||||
"_comment": "protocol_type接外部工站固定写法字段,一般为空,deck写法也固定",
|
|
||||||
"protocol_type": [],
|
|
||||||
"deck": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "coin_cell_deck",
|
|
||||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"address": "192.168.1.20",
|
"protocol_type": [],
|
||||||
|
"deck": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck",
|
||||||
|
"address": "172.21.32.20",
|
||||||
"port": 502
|
"port": 502
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|||||||
2521
button_battery_station_resources_unilab.json
Normal file
2521
button_battery_station_resources_unilab.json
Normal file
File diff suppressed because it is too large
Load Diff
695
scripts/workflow.py
Normal file
695
scripts/workflow.py
Normal file
@@ -0,0 +1,695 @@
|
|||||||
|
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.prcxi-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", "")) or ("prcxi" 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"
|
||||||
|
|
||||||
|
# 处理协议步骤
|
||||||
|
prev_node = None
|
||||||
|
for i, step in enumerate(protocol_steps):
|
||||||
|
node_id = str(uuid.uuid4())
|
||||||
|
G.add_node(node_id, **step)
|
||||||
|
|
||||||
|
# 添加控制流边
|
||||||
|
if prev_node is not None:
|
||||||
|
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")
|
||||||
|
if prev_node is not None:
|
||||||
|
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 形状并定义命名端口 <portname>。
|
||||||
|
最终由 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 " " # 必须留一个空槽占位
|
||||||
|
# 每个端口一个小格子,<p> 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 中定义的 <port> 名(同名即可)
|
||||||
|
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 中 <port> 名一致;特殊字符已在 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}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
],
|
],
|
||||||
"parent": null,
|
"parent": null,
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "workstation.bioyond_dispensing_station",
|
"class": "bioyond_dispensing_station",
|
||||||
"config": {
|
"config": {
|
||||||
"config": {
|
"config": {
|
||||||
"api_key": "DE9BDDA0",
|
"api_key": "DE9BDDA0",
|
||||||
|
|||||||
186
test/workflow/example_bio.json
Normal file
186
test/workflow/example_bio.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
test/workflow/example_bio_graph.png
Normal file
BIN
test/workflow/example_bio_graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
63
test/workflow/example_prcxi.json
Normal file
63
test/workflow/example_prcxi.json
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"steps_info": [
|
||||||
|
{
|
||||||
|
"step_number": 1,
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"parameters": {
|
||||||
|
"source": "sample supernatant",
|
||||||
|
"target": "antibody-coated well",
|
||||||
|
"volume": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_number": 2,
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"parameters": {
|
||||||
|
"source": "washing buffer",
|
||||||
|
"target": "antibody-coated well",
|
||||||
|
"volume": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_number": 3,
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"parameters": {
|
||||||
|
"source": "washing buffer",
|
||||||
|
"target": "antibody-coated well",
|
||||||
|
"volume": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_number": 4,
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"parameters": {
|
||||||
|
"source": "washing buffer",
|
||||||
|
"target": "antibody-coated well",
|
||||||
|
"volume": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"step_number": 5,
|
||||||
|
"action": "transfer_liquid",
|
||||||
|
"parameters": {
|
||||||
|
"source": "TMB substrate",
|
||||||
|
"target": "antibody-coated well",
|
||||||
|
"volume": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"labware_info": [
|
||||||
|
{"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1},
|
||||||
|
{"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2},
|
||||||
|
{"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3},
|
||||||
|
{"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 5},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 6},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 7},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 8},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 9},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 10},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 11},
|
||||||
|
{"reagent_name": "", "material_name": "300μL Tip头", "positions": 13}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
test/workflow/example_prcxi_graph.png
Normal file
BIN
test/workflow/example_prcxi_graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
BIN
test/workflow/example_prcxi_graph_20251022_1359.png
Normal file
BIN
test/workflow/example_prcxi_graph_20251022_1359.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
94
test/workflow/merge_workflow.py
Normal file
94
test/workflow/merge_workflow.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
if str(ROOT_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT_DIR))
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports
|
||||||
|
|
||||||
|
|
||||||
|
ROOT_DIR = Path(__file__).resolve().parents[2]
|
||||||
|
if str(ROOT_DIR) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_steps(data):
|
||||||
|
normalized = []
|
||||||
|
for step in data:
|
||||||
|
action = step.get("action") or step.get("operation")
|
||||||
|
if not action:
|
||||||
|
continue
|
||||||
|
raw_params = step.get("parameters") or step.get("action_args") or {}
|
||||||
|
params = dict(raw_params)
|
||||||
|
|
||||||
|
if "source" in raw_params and "sources" not in raw_params:
|
||||||
|
params["sources"] = raw_params["source"]
|
||||||
|
if "target" in raw_params and "targets" not in raw_params:
|
||||||
|
params["targets"] = raw_params["target"]
|
||||||
|
|
||||||
|
description = step.get("description") or step.get("purpose")
|
||||||
|
step_dict = {"action": action, "parameters": params}
|
||||||
|
if description:
|
||||||
|
step_dict["description"] = description
|
||||||
|
normalized.append(step_dict)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_labware(data):
|
||||||
|
labware = {}
|
||||||
|
for item in data:
|
||||||
|
reagent_name = item.get("reagent_name")
|
||||||
|
key = reagent_name or item.get("material_name") or item.get("name")
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
key = str(key)
|
||||||
|
idx = 1
|
||||||
|
original_key = key
|
||||||
|
while key in labware:
|
||||||
|
idx += 1
|
||||||
|
key = f"{original_key}_{idx}"
|
||||||
|
|
||||||
|
labware[key] = {
|
||||||
|
"slot": item.get("positions") or item.get("slot"),
|
||||||
|
"labware": item.get("material_name") or item.get("labware"),
|
||||||
|
"well": item.get("well", []),
|
||||||
|
"type": item.get("type", "reagent"),
|
||||||
|
"role": item.get("role", ""),
|
||||||
|
"name": key,
|
||||||
|
}
|
||||||
|
return labware
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("protocol_name", [
|
||||||
|
"example_bio",
|
||||||
|
# "bioyond_materials_liquidhandling_1",
|
||||||
|
"example_prcxi",
|
||||||
|
])
|
||||||
|
def test_build_protocol_graph(protocol_name):
|
||||||
|
data_path = Path(__file__).with_name(f"{protocol_name}.json")
|
||||||
|
with data_path.open("r", encoding="utf-8") as fp:
|
||||||
|
d = json.load(fp)
|
||||||
|
|
||||||
|
if "workflow" in d and "reagent" in d:
|
||||||
|
protocol_steps = d["workflow"]
|
||||||
|
labware_info = d["reagent"]
|
||||||
|
elif "steps_info" in d and "labware_info" in d:
|
||||||
|
protocol_steps = _normalize_steps(d["steps_info"])
|
||||||
|
labware_info = _normalize_labware(d["labware_info"])
|
||||||
|
else:
|
||||||
|
raise ValueError("Unsupported protocol format")
|
||||||
|
|
||||||
|
graph = build_protocol_graph(
|
||||||
|
labware_info=labware_info,
|
||||||
|
protocol_steps=protocol_steps,
|
||||||
|
workstation_name="PRCXi",
|
||||||
|
)
|
||||||
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M")
|
||||||
|
output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png")
|
||||||
|
draw_protocol_graph_with_ports(graph, str(output_path))
|
||||||
|
print(graph)
|
||||||
@@ -6,6 +6,8 @@ HTTP客户端模块
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
from threading import Thread
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@@ -84,14 +86,14 @@ class HTTPClient:
|
|||||||
f"{self.remote_addr}/edge/material",
|
f"{self.remote_addr}/edge/material",
|
||||||
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=100,
|
timeout=60,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
response = requests.put(
|
response = requests.put(
|
||||||
f"{self.remote_addr}/edge/material",
|
f"{self.remote_addr}/edge/material",
|
||||||
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=100,
|
timeout=10,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f:
|
||||||
@@ -126,12 +128,16 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
|
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(
|
response = requests.post(
|
||||||
f"{self.remote_addr}/edge/material/query",
|
f"{self.remote_addr}/edge/material/query",
|
||||||
json={"uuids": uuid_list, "with_children": with_children},
|
json={"uuids": uuid_list, "with_children": with_children},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=100,
|
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:
|
if response.status_code == 200:
|
||||||
res = response.json()
|
res = response.json()
|
||||||
if "code" in res and res["code"] != 0:
|
if "code" in res and res["code"] != 0:
|
||||||
@@ -187,12 +193,16 @@ class HTTPClient:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict: 返回的资源数据
|
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(
|
response = requests.get(
|
||||||
f"{self.remote_addr}/lab/material",
|
f"{self.remote_addr}/lab/material",
|
||||||
params={"id": id, "with_children": with_children},
|
params={"id": id, "with_children": with_children},
|
||||||
headers={"Authorization": f"Lab {self.auth}"},
|
headers={"Authorization": f"Lab {self.auth}"},
|
||||||
timeout=20,
|
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()
|
return response.json()
|
||||||
|
|
||||||
def resource_del(self, id: str) -> requests.Response:
|
def resource_del(self, id: str) -> requests.Response:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ API_CONFIG = {
|
|||||||
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
|
||||||
|
|
||||||
# HTTP 服务配置
|
# HTTP 服务配置
|
||||||
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.91"), # HTTP服务监听地址,监听计算机飞连ip地址
|
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址,监听计算机飞连ip地址
|
||||||
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
|
||||||
"debug_mode": False,# 调试模式
|
"debug_mode": False,# 调试模式
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,7 +338,7 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
|
||||||
|
|
||||||
# 4. 查询工作流对应的holdMID
|
# 4. 查询工作流对应的holdMID
|
||||||
material_info = self.material_id_query(workflow_id)
|
material_info = self.hardware_interface.material_id_query(workflow_id)
|
||||||
if not material_info:
|
if not material_info:
|
||||||
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
|
||||||
|
|
||||||
@@ -409,6 +409,265 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.error(error_msg)
|
self.hardware_interface._logger.error(error_msg)
|
||||||
raise BioyondException(error_msg)
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
# 批量创建二胺溶液配置任务
|
||||||
|
def batch_create_diamine_solution_tasks(self,
|
||||||
|
solutions,
|
||||||
|
liquid_material_name: str = "NMP",
|
||||||
|
speed: str = None,
|
||||||
|
temperature: str = None,
|
||||||
|
delay_time: str = None) -> str:
|
||||||
|
"""
|
||||||
|
批量创建二胺溶液配置任务
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
- solutions: 溶液列表(数组)或JSON字符串,格式如下:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "MDA",
|
||||||
|
"order": 0,
|
||||||
|
"solid_mass": 5.0,
|
||||||
|
"solvent_volume": 20,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
- liquid_material_name: 液体物料名称,默认为"NMP"
|
||||||
|
- speed: 搅拌速度,如果为None则使用默认值400
|
||||||
|
- temperature: 温度,如果为None则使用默认值20
|
||||||
|
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||||
|
|
||||||
|
返回: JSON字符串格式的任务创建结果
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- BioyondException: 各种错误情况下的统一异常
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 参数类型转换:如果是字符串则解析为列表
|
||||||
|
if isinstance(solutions, str):
|
||||||
|
try:
|
||||||
|
solutions = json.loads(solutions)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise BioyondException(f"solutions JSON解析失败: {str(e)}")
|
||||||
|
|
||||||
|
# 参数验证
|
||||||
|
if not isinstance(solutions, list):
|
||||||
|
raise BioyondException("solutions 必须是列表类型或有效的JSON数组字符串")
|
||||||
|
|
||||||
|
if not solutions:
|
||||||
|
raise BioyondException("solutions 列表不能为空")
|
||||||
|
|
||||||
|
# 批量创建任务
|
||||||
|
results = []
|
||||||
|
success_count = 0
|
||||||
|
failed_count = 0
|
||||||
|
|
||||||
|
for idx, solution in enumerate(solutions):
|
||||||
|
try:
|
||||||
|
# 提取参数
|
||||||
|
name = solution.get("name")
|
||||||
|
solid_mass = solution.get("solid_mass")
|
||||||
|
solvent_volume = solution.get("solvent_volume")
|
||||||
|
order = solution.get("order")
|
||||||
|
|
||||||
|
if not all([name, solid_mass is not None, solvent_volume is not None]):
|
||||||
|
self.hardware_interface._logger.warning(
|
||||||
|
f"跳过第 {idx + 1} 个溶液:缺少必要参数"
|
||||||
|
)
|
||||||
|
results.append({
|
||||||
|
"index": idx + 1,
|
||||||
|
"name": name,
|
||||||
|
"success": False,
|
||||||
|
"error": "缺少必要参数"
|
||||||
|
})
|
||||||
|
failed_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 生成库位名称(直接使用物料名称)
|
||||||
|
# 如果需要其他命名规则,可以在这里调整
|
||||||
|
hold_m_name = name
|
||||||
|
|
||||||
|
# 调用单个任务创建方法
|
||||||
|
result = self.create_diamine_solution_task(
|
||||||
|
order_name=f"二胺溶液配置-{name}",
|
||||||
|
material_name=name,
|
||||||
|
target_weigh=str(solid_mass),
|
||||||
|
volume=str(solvent_volume),
|
||||||
|
liquid_material_name=liquid_material_name,
|
||||||
|
speed=speed,
|
||||||
|
temperature=temperature,
|
||||||
|
delay_time=delay_time,
|
||||||
|
hold_m_name=hold_m_name
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"index": idx + 1,
|
||||||
|
"name": name,
|
||||||
|
"success": True,
|
||||||
|
"hold_m_name": hold_m_name
|
||||||
|
})
|
||||||
|
success_count += 1
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"成功创建二胺溶液配置任务: {name}"
|
||||||
|
)
|
||||||
|
|
||||||
|
except BioyondException as e:
|
||||||
|
results.append({
|
||||||
|
"index": idx + 1,
|
||||||
|
"name": solution.get("name", "unknown"),
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
})
|
||||||
|
failed_count += 1
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
f"创建第 {idx + 1} 个任务失败: {str(e)}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
results.append({
|
||||||
|
"index": idx + 1,
|
||||||
|
"name": solution.get("name", "unknown"),
|
||||||
|
"success": False,
|
||||||
|
"error": f"未知错误: {str(e)}"
|
||||||
|
})
|
||||||
|
failed_count += 1
|
||||||
|
self.hardware_interface._logger.error(
|
||||||
|
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回汇总结果
|
||||||
|
summary = {
|
||||||
|
"total": len(solutions),
|
||||||
|
"success": success_count,
|
||||||
|
"failed": failed_count,
|
||||||
|
"details": results
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"批量创建二胺溶液配置任务完成: 总数={len(solutions)}, "
|
||||||
|
f"成功={success_count}, 失败={failed_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回JSON字符串格式
|
||||||
|
return json.dumps(summary, ensure_ascii=False)
|
||||||
|
|
||||||
|
except BioyondException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"批量创建二胺溶液配置任务时发生未预期的错误: {str(e)}"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
# 批量创建90%10%小瓶投料任务
|
||||||
|
def batch_create_90_10_vial_feeding_tasks(self,
|
||||||
|
titration,
|
||||||
|
hold_m_name: str = None,
|
||||||
|
speed: str = None,
|
||||||
|
temperature: str = None,
|
||||||
|
delay_time: str = None,
|
||||||
|
liquid_material_name: str = "NMP") -> str:
|
||||||
|
"""
|
||||||
|
批量创建90%10%小瓶投料任务(仅创建1个任务,但包含所有90%和10%物料)
|
||||||
|
|
||||||
|
参数说明:
|
||||||
|
- titration: 滴定信息的字典或JSON字符串,格式如下:
|
||||||
|
{
|
||||||
|
"name": "BTDA",
|
||||||
|
"main_portion": 1.9152351915461294, # 主称固体质量(g) -> 90%物料
|
||||||
|
"titration_portion": 0.05923407808905555, # 滴定固体质量(g) -> 10%物料固体
|
||||||
|
"titration_solvent": 3.050555021586361 # 滴定溶液体积(mL) -> 10%物料液体
|
||||||
|
}
|
||||||
|
- hold_m_name: 库位名称,如"C01"。必填参数
|
||||||
|
- speed: 搅拌速度,如果为None则使用默认值400
|
||||||
|
- temperature: 温度,如果为None则使用默认值40
|
||||||
|
- delay_time: 延迟时间,如果为None则使用默认值600
|
||||||
|
- liquid_material_name: 10%物料的液体物料名称,默认为"NMP"
|
||||||
|
|
||||||
|
返回: JSON字符串格式的任务创建结果
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- BioyondException: 各种错误情况下的统一异常
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 参数类型转换:如果是字符串则解析为字典
|
||||||
|
if isinstance(titration, str):
|
||||||
|
try:
|
||||||
|
titration = json.loads(titration)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise BioyondException(f"titration参数JSON解析失败: {str(e)}")
|
||||||
|
|
||||||
|
# 参数验证
|
||||||
|
if not isinstance(titration, dict):
|
||||||
|
raise BioyondException("titration 必须是字典类型或有效的JSON字符串")
|
||||||
|
|
||||||
|
if not hold_m_name:
|
||||||
|
raise BioyondException("hold_m_name 是必填参数")
|
||||||
|
|
||||||
|
if not titration:
|
||||||
|
raise BioyondException("titration 参数不能为空")
|
||||||
|
|
||||||
|
# 提取滴定数据
|
||||||
|
name = titration.get("name")
|
||||||
|
main_portion = titration.get("main_portion") # 主称固体质量
|
||||||
|
titration_portion = titration.get("titration_portion") # 滴定固体质量
|
||||||
|
titration_solvent = titration.get("titration_solvent") # 滴定溶液体积
|
||||||
|
|
||||||
|
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
|
||||||
|
raise BioyondException("titration 数据缺少必要参数")
|
||||||
|
|
||||||
|
# 将main_portion平均分成3份作为90%物料(3个小瓶)
|
||||||
|
portion_90 = main_portion / 3
|
||||||
|
|
||||||
|
# 调用单个任务创建方法
|
||||||
|
result = self.create_90_10_vial_feeding_task(
|
||||||
|
order_name=f"90%10%小瓶投料-{name}",
|
||||||
|
speed=speed,
|
||||||
|
temperature=temperature,
|
||||||
|
delay_time=delay_time,
|
||||||
|
# 90%物料 - 主称固体平均分成3份
|
||||||
|
percent_90_1_assign_material_name=name,
|
||||||
|
percent_90_1_target_weigh=str(round(portion_90, 6)),
|
||||||
|
percent_90_2_assign_material_name=name,
|
||||||
|
percent_90_2_target_weigh=str(round(portion_90, 6)),
|
||||||
|
percent_90_3_assign_material_name=name,
|
||||||
|
percent_90_3_target_weigh=str(round(portion_90, 6)),
|
||||||
|
# 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶)
|
||||||
|
percent_10_1_assign_material_name=name,
|
||||||
|
percent_10_1_target_weigh=str(round(titration_portion, 6)),
|
||||||
|
percent_10_1_volume=str(round(titration_solvent, 6)),
|
||||||
|
percent_10_1_liquid_material_name=liquid_material_name,
|
||||||
|
hold_m_name=hold_m_name
|
||||||
|
)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"success": True,
|
||||||
|
"hold_m_name": hold_m_name,
|
||||||
|
"material_name": name,
|
||||||
|
"90_vials": {
|
||||||
|
"count": 3,
|
||||||
|
"weight_per_vial": round(portion_90, 6),
|
||||||
|
"total_weight": round(main_portion, 6)
|
||||||
|
},
|
||||||
|
"10_vials": {
|
||||||
|
"count": 1,
|
||||||
|
"solid_weight": round(titration_portion, 6),
|
||||||
|
"liquid_volume": round(titration_solvent, 6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.hardware_interface._logger.info(
|
||||||
|
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
|
||||||
|
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 返回JSON字符串格式
|
||||||
|
return json.dumps(summary, ensure_ascii=False)
|
||||||
|
|
||||||
|
except BioyondException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"批量创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
bioyond = BioyondDispensingStation(config={
|
bioyond = BioyondDispensingStation(config={
|
||||||
@@ -416,6 +675,8 @@ if __name__ == "__main__":
|
|||||||
"api_host": "http://192.168.1.200:44388"
|
"api_host": "http://192.168.1.200:44388"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# ============ 原有示例代码 ============
|
||||||
|
|
||||||
# 示例1:使用material_id_query查询工作流对应的holdMID
|
# 示例1:使用material_id_query查询工作流对应的holdMID
|
||||||
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
|
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
|
||||||
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
|
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称(不能为空)
|
assign_material_name: 物料名称(不能为空)
|
||||||
cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000")
|
cutoff: 粘度上限(需为有效数字字符串,默认 "900000")
|
||||||
temperature: 温度上限(°C,范围:-50.00 至 100.00)
|
temperature: 温度设定(°C,范围:-50.00 至 100.00)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: JSON 字符串,格式为 {"suc": True}
|
str: JSON 字符串,格式为 {"suc": True}
|
||||||
@@ -113,11 +113,11 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
"""固体进料小瓶
|
"""固体进料小瓶
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
material_id: 粉末类型ID
|
material_id: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
assign_material_name: 物料名称(用于获取试剂瓶位ID)
|
assign_material_name: 物料名称(用于获取试剂瓶位ID)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
|
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
|
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None
|
||||||
@@ -165,9 +165,9 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
volume_formula: 分液公式(μL)
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定(1=滴定, 其他=非滴定)
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
|
||||||
@@ -208,7 +208,8 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
def liquid_feeding_solvents(
|
def liquid_feeding_solvents(
|
||||||
self,
|
self,
|
||||||
assign_material_name: str,
|
assign_material_name: str,
|
||||||
volume: str,
|
volume: str = None,
|
||||||
|
solvents = None,
|
||||||
titration_type: str = "1",
|
titration_type: str = "1",
|
||||||
time: str = "360",
|
time: str = "360",
|
||||||
torque_variation: int = 2,
|
torque_variation: int = 2,
|
||||||
@@ -218,12 +219,41 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
volume: 分液量(μL)
|
volume: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算)
|
||||||
titration_type: 是否滴定
|
solvents: 溶剂信息的字典或JSON字符串(可选),格式如下:
|
||||||
|
{
|
||||||
|
"additional_solvent": 33.55092503597727, # 溶剂体积(mL)
|
||||||
|
"total_liquid_volume": 48.00916988195499
|
||||||
|
}
|
||||||
|
如果提供solvents,则从中提取additional_solvent并转换为μL
|
||||||
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
|
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
|
||||||
|
if volume is None and solvents is not None:
|
||||||
|
# 参数类型转换:如果是字符串则解析为字典
|
||||||
|
if isinstance(solvents, str):
|
||||||
|
try:
|
||||||
|
solvents = json.loads(solvents)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
raise ValueError(f"solvents参数JSON解析失败: {str(e)}")
|
||||||
|
|
||||||
|
# 参数验证
|
||||||
|
if not isinstance(solvents, dict):
|
||||||
|
raise ValueError("solvents 必须是字典类型或有效的JSON字符串")
|
||||||
|
|
||||||
|
# 提取 additional_solvent 值
|
||||||
|
additional_solvent = solvents.get("additional_solvent")
|
||||||
|
if additional_solvent is None:
|
||||||
|
raise ValueError("solvents 中没有找到 additional_solvent 字段")
|
||||||
|
|
||||||
|
# 转换为微升(μL) - 从毫升(mL)转换
|
||||||
|
volume = str(float(additional_solvent) * 1000)
|
||||||
|
elif volume is None:
|
||||||
|
raise ValueError("必须提供 volume 或 solvents 参数之一")
|
||||||
|
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
if material_id is None:
|
if material_id is None:
|
||||||
@@ -273,9 +303,9 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
Args:
|
Args:
|
||||||
volume_formula: 分液公式(μL)
|
volume_formula: 分液公式(μL)
|
||||||
assign_material_name: 物料名称
|
assign_material_name: 物料名称
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
|
||||||
@@ -328,9 +358,9 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
volume: 分液量(μL)
|
volume: 分液量(μL)
|
||||||
assign_material_name: 物料名称(试剂瓶位)
|
assign_material_name: 物料名称(试剂瓶位)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
temperature: 温度上限(°C)
|
temperature: 温度设定(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
|
||||||
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
|
||||||
@@ -381,9 +411,9 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
Args:
|
Args:
|
||||||
assign_material_name: 物料名称(液体种类)
|
assign_material_name: 物料名称(液体种类)
|
||||||
volume: 分液量(μL)
|
volume: 分液量(μL)
|
||||||
titration_type: 是否滴定
|
titration_type: 是否滴定(1=否, 2=是)
|
||||||
time: 观察时间(分钟)
|
time: 观察时间(分钟)
|
||||||
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
|
torque_variation: 是否观察(int类型, 1=否, 2=是)
|
||||||
temperature: 温度(°C)
|
temperature: 温度(°C)
|
||||||
"""
|
"""
|
||||||
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
|
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
|
||||||
@@ -605,7 +635,8 @@ class BioyondReactionStation(BioyondWorkstation):
|
|||||||
total_params += 1
|
total_params += 1
|
||||||
step_parameters[step_id][action_name].append({
|
step_parameters[step_id][action_name].append({
|
||||||
"Key": param_key,
|
"Key": param_key,
|
||||||
"DisplayValue": param_value
|
"DisplayValue": param_value,
|
||||||
|
"Value": param_value
|
||||||
})
|
})
|
||||||
successful_params += 1
|
successful_params += 1
|
||||||
# print(f" ✓ {param_key} = {param_value}")
|
# print(f" ✓ {param_key} = {param_value}")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Bioyond Workstation Implementation
|
|||||||
|
|
||||||
集成Bioyond物料管理的工作站示例
|
集成Bioyond物料管理的工作站示例
|
||||||
"""
|
"""
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,691 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": [
|
|
||||||
{
|
|
||||||
"id": "BatteryStation",
|
|
||||||
"name": "扣电工作站",
|
|
||||||
"children": [
|
|
||||||
"coin_cell_deck"
|
|
||||||
],
|
|
||||||
"parent": null,
|
|
||||||
"type": "device",
|
|
||||||
"class": "bettery_station_registry",
|
|
||||||
"position": {
|
|
||||||
"x": 600,
|
|
||||||
"y": 400,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"debug_mode": false,
|
|
||||||
"_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定",
|
|
||||||
"protocol_type": [],
|
|
||||||
"station_resource": {
|
|
||||||
"data": {
|
|
||||||
"_resource_child_name": "coin_cell_deck",
|
|
||||||
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"address": "192.168.1.20",
|
|
||||||
"port": 502
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "coin_cell_deck",
|
|
||||||
"name": "coin_cell_deck",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [
|
|
||||||
"\u7535\u6c60\u6599\u76d8"
|
|
||||||
],
|
|
||||||
"parent": null,
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "CoincellDeck",
|
|
||||||
"size_x": 1000,
|
|
||||||
"size_y": 1000,
|
|
||||||
"size_z": 900,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "coin_cell_deck",
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
|
||||||
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
|
||||||
],
|
|
||||||
"parent": "coin_cell_deck",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 100,
|
|
||||||
"y": 100,
|
|
||||||
"z": 0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialPlate",
|
|
||||||
"size_x": 120.8,
|
|
||||||
"size_y": 160.5,
|
|
||||||
"size_z": 10.0,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_plate",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null,
|
|
||||||
"ordering": {
|
|
||||||
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
|
||||||
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
|
||||||
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
|
||||||
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
|
||||||
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
|
||||||
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
|
||||||
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
|
||||||
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
|
||||||
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
|
||||||
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
|
||||||
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
|
||||||
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
|
||||||
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
|
||||||
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
|
||||||
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
|
||||||
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"data": {}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 12.4,
|
|
||||||
"y": 104.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 12.4,
|
|
||||||
"y": 80.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 12.4,
|
|
||||||
"y": 56.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 12.4,
|
|
||||||
"y": 32.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 36.4,
|
|
||||||
"y": 104.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 36.4,
|
|
||||||
"y": 80.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 36.4,
|
|
||||||
"y": 56.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 36.4,
|
|
||||||
"y": 32.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 60.4,
|
|
||||||
"y": 104.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 60.4,
|
|
||||||
"y": 80.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 60.4,
|
|
||||||
"y": 56.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 60.4,
|
|
||||||
"y": 32.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 84.4,
|
|
||||||
"y": 104.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 84.4,
|
|
||||||
"y": 80.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 84.4,
|
|
||||||
"y": 56.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
|
||||||
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
|
|
||||||
"sample_id": null,
|
|
||||||
"children": [],
|
|
||||||
"parent": "\u7535\u6c60\u6599\u76d8",
|
|
||||||
"type": "container",
|
|
||||||
"class": "",
|
|
||||||
"position": {
|
|
||||||
"x": 84.4,
|
|
||||||
"y": 32.25,
|
|
||||||
"z": 10.0
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"type": "MaterialHole",
|
|
||||||
"size_x": 16,
|
|
||||||
"size_y": 16,
|
|
||||||
"size_z": 16,
|
|
||||||
"rotation": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0,
|
|
||||||
"z": 0,
|
|
||||||
"type": "Rotation"
|
|
||||||
},
|
|
||||||
"category": "material_hole",
|
|
||||||
"model": null,
|
|
||||||
"barcode": null
|
|
||||||
},
|
|
||||||
"data": {
|
|
||||||
"diameter": 20,
|
|
||||||
"depth": 10,
|
|
||||||
"max_sheets": 1,
|
|
||||||
"info": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"links": []
|
|
||||||
}
|
|
||||||
@@ -43,21 +43,21 @@ REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
|
|||||||
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
|
||||||
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
|
||||||
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
|
||||||
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
|
COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340,
|
||||||
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
|
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
|
||||||
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
|
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
|
||||||
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
|
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
|
||||||
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
|
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
|
||||||
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
|
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
|
||||||
REG_MSG_PRESS_MODE,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,8360,电池压制模式
|
|
||||||
,,,,,,,
|
,,,,,,,
|
||||||
,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位
|
,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位
|
||||||
,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检
|
,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检
|
||||||
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,8320,手套箱左仓
|
,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,8320,手套箱左仓
|
||||||
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,8420,手套箱右仓
|
,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,8420,手套箱右仓
|
||||||
,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知
|
,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知
|
||||||
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,8370,滴液模式
|
,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,8360,电池压制模式
|
||||||
,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重
|
,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,8370,滴液模式
|
||||||
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,8390,正负极反装
|
,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重
|
||||||
,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁
|
,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,8390,正负极反装
|
||||||
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,8410,负极片摆盘方式
|
,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁
|
||||||
|
,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,8410,负极片摆盘方式
|
||||||
|
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "bioyond_cell_workstation",
|
||||||
|
"name": "配液分液工站",
|
||||||
|
"children": [
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "bioyond_cell",
|
||||||
|
"config": {
|
||||||
|
"protocol_type": [],
|
||||||
|
"station_resource": {}
|
||||||
|
},
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "BatteryStation",
|
||||||
|
"name": "扣电工作站",
|
||||||
|
"children": [
|
||||||
|
"coin_cell_deck"
|
||||||
|
],
|
||||||
|
"parent": null,
|
||||||
|
"type": "device",
|
||||||
|
"class": "bettery_station_registry",
|
||||||
|
"position": {
|
||||||
|
"x": 600,
|
||||||
|
"y": 400,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"debug_mode": false,
|
||||||
|
"protocol_type": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
"""
|
|
||||||
工作站基类
|
|
||||||
Workstation Base Class - 简化版
|
|
||||||
|
|
||||||
基于PLR Deck的简化工作站架构
|
|
||||||
专注于核心物料系统和工作流管理
|
|
||||||
"""
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import time
|
|
||||||
from typing import Dict, Any, List, Optional, Union
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
from pylabrobot.resources import Deck, Plate, Resource as PLRResource
|
|
||||||
|
|
||||||
from pylabrobot.resources.coordinate import Coordinate
|
|
||||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
|
||||||
|
|
||||||
from unilabos.utils.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowStatus(Enum):
|
|
||||||
"""工作流状态"""
|
|
||||||
|
|
||||||
IDLE = "idle"
|
|
||||||
INITIALIZING = "initializing"
|
|
||||||
RUNNING = "running"
|
|
||||||
PAUSED = "paused"
|
|
||||||
STOPPING = "stopping"
|
|
||||||
STOPPED = "stopped"
|
|
||||||
ERROR = "error"
|
|
||||||
COMPLETED = "completed"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class WorkflowInfo:
|
|
||||||
"""工作流信息"""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
estimated_duration: float # 预估持续时间(秒)
|
|
||||||
required_materials: List[str] # 所需物料类型
|
|
||||||
output_product: str # 输出产品类型
|
|
||||||
parameters_schema: Dict[str, Any] # 参数架构
|
|
||||||
|
|
||||||
|
|
||||||
class WorkStationContainer(Plate):
|
|
||||||
"""
|
|
||||||
WorkStation 专用 Container 类,继承自 Plate和TipRack
|
|
||||||
注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
size_x: float,
|
|
||||||
size_y: float,
|
|
||||||
size_z: float,
|
|
||||||
category: str,
|
|
||||||
ordering: collections.OrderedDict,
|
|
||||||
model: Optional[str] = None,
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
这里的初始化入参要和plr的保持一致
|
|
||||||
"""
|
|
||||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
|
||||||
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
|
|
||||||
|
|
||||||
def load_state(self, state: Dict[str, Any]) -> None:
|
|
||||||
"""从给定的状态加载工作台信息。"""
|
|
||||||
super().load_state(state)
|
|
||||||
self._unilabos_state = state
|
|
||||||
|
|
||||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
|
||||||
data = super().serialize_state()
|
|
||||||
data.update(
|
|
||||||
self._unilabos_state
|
|
||||||
) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
|
|
||||||
"""
|
|
||||||
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml
|
|
||||||
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
|
|
||||||
:param name: 资源名称
|
|
||||||
:return: Resource对象
|
|
||||||
"""
|
|
||||||
plate = WorkStationContainer(
|
|
||||||
name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
|
|
||||||
)
|
|
||||||
tip_rack = WorkStationContainer(
|
|
||||||
"tip_rack_inside_plate",
|
|
||||||
size_x=50,
|
|
||||||
size_y=50,
|
|
||||||
size_z=10,
|
|
||||||
category="tip_rack",
|
|
||||||
ordering=collections.OrderedDict(),
|
|
||||||
)
|
|
||||||
plate.assign_child_resource(tip_rack, Coordinate.zero())
|
|
||||||
return plate
|
|
||||||
|
|
||||||
|
|
||||||
class ResourceSynchronizer(ABC):
|
|
||||||
"""资源同步器基类
|
|
||||||
|
|
||||||
负责与外部物料系统的同步,并对 self.deck 做修改
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, workstation: "WorkstationBase"):
|
|
||||||
self.workstation = workstation
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def sync_from_external(self) -> bool:
|
|
||||||
"""从外部系统同步物料到本地deck"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
|
||||||
"""将本地物料同步到外部系统"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
|
||||||
"""处理外部系统的变更通知"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class WorkstationBase(ABC):
|
|
||||||
"""工作站基类 - 简化版
|
|
||||||
|
|
||||||
核心功能:
|
|
||||||
1. 基于 PLR Deck 的物料系统,支持格式转换
|
|
||||||
2. 可选的资源同步器支持外部物料系统
|
|
||||||
3. 简化的工作流管理
|
|
||||||
"""
|
|
||||||
|
|
||||||
_ros_node: ROS2WorkstationNode
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别
|
|
||||||
return self._ros_node.children
|
|
||||||
|
|
||||||
async def update_resource_example(self):
|
|
||||||
return await self._ros_node.update_resource([get_workstation_plate_resource("test")])
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
station_resource: PLRResource,
|
|
||||||
*args,
|
|
||||||
**kwargs, # 必须有kwargs
|
|
||||||
):
|
|
||||||
# 基本配置
|
|
||||||
print(station_resource)
|
|
||||||
self.deck_config = station_resource
|
|
||||||
|
|
||||||
# PLR 物料系统
|
|
||||||
self.deck: Optional[Deck] = None
|
|
||||||
self.plr_resources: Dict[str, PLRResource] = {}
|
|
||||||
|
|
||||||
# 资源同步器(可选)
|
|
||||||
# self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用
|
|
||||||
|
|
||||||
# 硬件接口
|
|
||||||
self.hardware_interface: Union[Any, str] = None
|
|
||||||
|
|
||||||
# 工作流状态
|
|
||||||
self.current_workflow_status = WorkflowStatus.IDLE
|
|
||||||
self.current_workflow_info = None
|
|
||||||
self.workflow_start_time = None
|
|
||||||
self.workflow_parameters = {}
|
|
||||||
|
|
||||||
# 支持的工作流(静态预定义)
|
|
||||||
self.supported_workflows: Dict[str, WorkflowInfo] = {}
|
|
||||||
|
|
||||||
# 初始化物料系统
|
|
||||||
self._initialize_material_system()
|
|
||||||
|
|
||||||
# 注册支持的工作流
|
|
||||||
# self._register_supported_workflows()
|
|
||||||
|
|
||||||
# logger.info(f"工作站 {device_id} 初始化完成(简化版)")
|
|
||||||
|
|
||||||
def _initialize_material_system(self):
|
|
||||||
"""初始化物料系统 - 使用 graphio 转换"""
|
|
||||||
try:
|
|
||||||
from unilabos.resources.graphio import resource_ulab_to_plr
|
|
||||||
|
|
||||||
# # 1. 合并 deck_config 和 children 创建完整的资源树
|
|
||||||
# complete_resource_config = self._create_complete_resource_config()
|
|
||||||
|
|
||||||
# # 2. 使用 graphio 转换为 PLR 资源
|
|
||||||
# self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
|
|
||||||
|
|
||||||
# # 3. 建立资源映射
|
|
||||||
# self._build_resource_mappings(self.deck)
|
|
||||||
|
|
||||||
# # 4. 如果有资源同步器,执行初始同步
|
|
||||||
# if self.resource_synchronizer:
|
|
||||||
# # 这里可以异步执行,暂时跳过
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
|
|
||||||
pass
|
|
||||||
except Exception as e:
|
|
||||||
# logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
|
||||||
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
|
||||||
# 创建主 deck 配置
|
|
||||||
deck_resource = {
|
|
||||||
"id": f"{self.device_id}_deck",
|
|
||||||
"name": f"{self.device_id}_deck",
|
|
||||||
"type": "deck",
|
|
||||||
"position": {"x": 0, "y": 0, "z": 0},
|
|
||||||
"config": {
|
|
||||||
"size_x": self.deck_config.get("size_x", 1000.0),
|
|
||||||
"size_y": self.deck_config.get("size_y", 1000.0),
|
|
||||||
"size_z": self.deck_config.get("size_z", 100.0),
|
|
||||||
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
|
|
||||||
},
|
|
||||||
"data": {},
|
|
||||||
"children": [],
|
|
||||||
"parent": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 添加子资源
|
|
||||||
if self._children:
|
|
||||||
children_list = []
|
|
||||||
for child_id, child_config in self._children.items():
|
|
||||||
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
|
|
||||||
children_list.append(child_resource)
|
|
||||||
deck_resource["children"] = children_list
|
|
||||||
|
|
||||||
return deck_resource
|
|
||||||
|
|
||||||
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
|
||||||
"""标准化子资源配置"""
|
|
||||||
return {
|
|
||||||
"id": resource_id,
|
|
||||||
"name": config.get("name", resource_id),
|
|
||||||
"type": config.get("type", "container"),
|
|
||||||
"position": self._normalize_position(config.get("position", {})),
|
|
||||||
"config": config.get("config", {}),
|
|
||||||
"data": config.get("data", {}),
|
|
||||||
"children": [], # 简化版本:只支持一层子资源
|
|
||||||
"parent": parent_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
def _normalize_position(self, position: Any) -> Dict[str, float]:
|
|
||||||
"""标准化位置信息"""
|
|
||||||
if isinstance(position, dict):
|
|
||||||
return {
|
|
||||||
"x": float(position.get("x", 0)),
|
|
||||||
"y": float(position.get("y", 0)),
|
|
||||||
"z": float(position.get("z", 0)),
|
|
||||||
}
|
|
||||||
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
|
||||||
return {
|
|
||||||
"x": float(position[0]),
|
|
||||||
"y": float(position[1]),
|
|
||||||
"z": float(position[2]) if len(position) > 2 else 0.0,
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {"x": 0.0, "y": 0.0, "z": 0.0}
|
|
||||||
|
|
||||||
def _build_resource_mappings(self, deck: Deck):
|
|
||||||
"""递归构建资源映射"""
|
|
||||||
|
|
||||||
def add_resource_recursive(resource: PLRResource):
|
|
||||||
if hasattr(resource, "name"):
|
|
||||||
self.plr_resources[resource.name] = resource
|
|
||||||
|
|
||||||
if hasattr(resource, "children"):
|
|
||||||
for child in resource.children:
|
|
||||||
add_resource_recursive(child)
|
|
||||||
|
|
||||||
add_resource_recursive(deck)
|
|
||||||
|
|
||||||
# ============ 硬件接口管理 ============
|
|
||||||
|
|
||||||
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
|
|
||||||
"""设置硬件接口"""
|
|
||||||
self.hardware_interface = hardware_interface
|
|
||||||
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
|
||||||
|
|
||||||
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
|
|
||||||
"""设置协议节点引用(用于代理模式)"""
|
|
||||||
self._ros_node = workstation_node
|
|
||||||
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
|
||||||
|
|
||||||
# ============ 设备操作接口 ============
|
|
||||||
|
|
||||||
def call_device_method(self, method: str, *args, **kwargs) -> Any:
|
|
||||||
"""调用设备方法的统一接口"""
|
|
||||||
# 1. 代理模式:通过协议节点转发
|
|
||||||
if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"):
|
|
||||||
if not self._ros_node:
|
|
||||||
raise RuntimeError("代理模式需要设置workstation_node")
|
|
||||||
|
|
||||||
device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
|
|
||||||
return self._ros_node.call_device_method(device_id, method, *args, **kwargs)
|
|
||||||
|
|
||||||
# 2. 直接模式:直接调用硬件接口方法
|
|
||||||
elif self.hardware_interface and hasattr(self.hardware_interface, method):
|
|
||||||
return getattr(self.hardware_interface, method)(*args, **kwargs)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise AttributeError(f"硬件接口不支持方法: {method}")
|
|
||||||
|
|
||||||
def get_device_status(self) -> Dict[str, Any]:
|
|
||||||
"""获取设备状态"""
|
|
||||||
try:
|
|
||||||
return self.call_device_method("get_status")
|
|
||||||
except AttributeError:
|
|
||||||
# 如果设备不支持get_status方法,返回基础状态
|
|
||||||
return {
|
|
||||||
"status": "unknown",
|
|
||||||
"interface_type": type(self.hardware_interface).__name__,
|
|
||||||
"timestamp": time.time(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def is_device_available(self) -> bool:
|
|
||||||
"""检查设备是否可用"""
|
|
||||||
try:
|
|
||||||
self.get_device_status()
|
|
||||||
return True
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ============ 物料系统接口 ============
|
|
||||||
|
|
||||||
def get_deck(self) -> Deck:
|
|
||||||
"""获取主 Deck"""
|
|
||||||
return self.deck
|
|
||||||
|
|
||||||
def get_all_resources(self) -> Dict[str, PLRResource]:
|
|
||||||
"""获取所有 PLR 资源"""
|
|
||||||
return self.plr_resources.copy()
|
|
||||||
|
|
||||||
def find_resource_by_name(self, name: str) -> Optional[PLRResource]:
|
|
||||||
"""按名称查找资源"""
|
|
||||||
return self.plr_resources.get(name)
|
|
||||||
|
|
||||||
def find_resources_by_type(self, resource_type: type) -> List[PLRResource]:
|
|
||||||
"""按类型查找资源"""
|
|
||||||
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
|
||||||
|
|
||||||
async def sync_with_external_system(self) -> bool:
|
|
||||||
"""与外部物料系统同步"""
|
|
||||||
if not self.resource_synchronizer:
|
|
||||||
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
|
|
||||||
return True
|
|
||||||
|
|
||||||
try:
|
|
||||||
success = await self.resource_synchronizer.sync_from_external()
|
|
||||||
if success:
|
|
||||||
logger.info(f"工作站 {self.device_id} 外部同步成功")
|
|
||||||
else:
|
|
||||||
logger.warning(f"工作站 {self.device_id} 外部同步失败")
|
|
||||||
return success
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ============ 简化的工作流控制 ============
|
|
||||||
|
|
||||||
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
|
||||||
"""执行工作流"""
|
|
||||||
try:
|
|
||||||
# 设置工作流状态
|
|
||||||
self.current_workflow_status = WorkflowStatus.INITIALIZING
|
|
||||||
self.workflow_parameters = parameters
|
|
||||||
self.workflow_start_time = time.time()
|
|
||||||
|
|
||||||
# 委托给子类实现
|
|
||||||
success = self._execute_workflow_impl(workflow_name, parameters)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
self.current_workflow_status = WorkflowStatus.RUNNING
|
|
||||||
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
|
|
||||||
else:
|
|
||||||
self.current_workflow_status = WorkflowStatus.ERROR
|
|
||||||
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.current_workflow_status = WorkflowStatus.ERROR
|
|
||||||
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stop_workflow(self, emergency: bool = False) -> bool:
|
|
||||||
"""停止工作流"""
|
|
||||||
try:
|
|
||||||
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
|
|
||||||
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
|
|
||||||
return True
|
|
||||||
|
|
||||||
self.current_workflow_status = WorkflowStatus.STOPPING
|
|
||||||
|
|
||||||
# 委托给子类实现
|
|
||||||
success = self._stop_workflow_impl(emergency)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
self.current_workflow_status = WorkflowStatus.STOPPED
|
|
||||||
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
|
|
||||||
else:
|
|
||||||
self.current_workflow_status = WorkflowStatus.ERROR
|
|
||||||
logger.error(f"工作站 {self.device_id} 工作流停止失败")
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.current_workflow_status = WorkflowStatus.ERROR
|
|
||||||
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# ============ 状态属性 ============
|
|
||||||
|
|
||||||
@property
|
|
||||||
def workflow_status(self) -> WorkflowStatus:
|
|
||||||
"""获取当前工作流状态"""
|
|
||||||
return self.current_workflow_status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_busy(self) -> bool:
|
|
||||||
"""检查工作站是否忙碌"""
|
|
||||||
return self.current_workflow_status in [
|
|
||||||
WorkflowStatus.INITIALIZING,
|
|
||||||
WorkflowStatus.RUNNING,
|
|
||||||
WorkflowStatus.STOPPING,
|
|
||||||
]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def workflow_runtime(self) -> float:
|
|
||||||
"""获取工作流运行时间(秒)"""
|
|
||||||
if self.workflow_start_time is None:
|
|
||||||
return 0.0
|
|
||||||
return time.time() - self.workflow_start_time
|
|
||||||
|
|
||||||
# ============ 抽象方法 - 子类必须实现 ============
|
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
# def _register_supported_workflows(self):
|
|
||||||
# """注册支持的工作流 - 子类必须实现"""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
# def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
|
||||||
# """执行工作流的具体实现 - 子类必须实现"""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# @abstractmethod
|
|
||||||
# def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
|
||||||
# """停止工作流的具体实现 - 子类必须实现"""
|
|
||||||
# pass
|
|
||||||
|
|
||||||
class WorkstationExample(WorkstationBase):
|
|
||||||
"""工作站示例实现"""
|
|
||||||
|
|
||||||
def _register_supported_workflows(self):
|
|
||||||
"""注册支持的工作流"""
|
|
||||||
self.supported_workflows["example_workflow"] = WorkflowInfo(
|
|
||||||
name="example_workflow",
|
|
||||||
description="这是一个示例工作流",
|
|
||||||
estimated_duration=300.0,
|
|
||||||
required_materials=["sample_plate"],
|
|
||||||
output_product="processed_plate",
|
|
||||||
parameters_schema={"param1": "string", "param2": "integer"},
|
|
||||||
)
|
|
||||||
|
|
||||||
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
|
||||||
"""执行工作流的具体实现"""
|
|
||||||
if workflow_name not in self.supported_workflows:
|
|
||||||
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 这里添加实际的工作流逻辑
|
|
||||||
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
|
||||||
"""停止工作流的具体实现"""
|
|
||||||
# 这里添加实际的停止逻辑
|
|
||||||
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
|
|
||||||
return True
|
|
||||||
@@ -1,255 +0,0 @@
|
|||||||
workstation.bioyond_dispensing_station:
|
|
||||||
category:
|
|
||||||
- workstation
|
|
||||||
- bioyond
|
|
||||||
class:
|
|
||||||
action_value_mappings:
|
|
||||||
create_90_10_vial_feeding_task:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
order_name: order_name
|
|
||||||
percent_10_1_assign_material_name: percent_10_1_assign_material_name
|
|
||||||
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
|
|
||||||
percent_10_1_target_weigh: percent_10_1_target_weigh
|
|
||||||
percent_10_1_volume: percent_10_1_volume
|
|
||||||
percent_10_2_assign_material_name: percent_10_2_assign_material_name
|
|
||||||
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
|
|
||||||
percent_10_2_target_weigh: percent_10_2_target_weigh
|
|
||||||
percent_10_2_volume: percent_10_2_volume
|
|
||||||
percent_10_3_assign_material_name: percent_10_3_assign_material_name
|
|
||||||
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
|
|
||||||
percent_10_3_target_weigh: percent_10_3_target_weigh
|
|
||||||
percent_10_3_volume: percent_10_3_volume
|
|
||||||
percent_90_1_assign_material_name: percent_90_1_assign_material_name
|
|
||||||
percent_90_1_target_weigh: percent_90_1_target_weigh
|
|
||||||
percent_90_2_assign_material_name: percent_90_2_assign_material_name
|
|
||||||
percent_90_2_target_weigh: percent_90_2_target_weigh
|
|
||||||
percent_90_3_assign_material_name: percent_90_3_assign_material_name
|
|
||||||
percent_90_3_target_weigh: percent_90_3_target_weigh
|
|
||||||
speed: speed
|
|
||||||
temperature: temperature
|
|
||||||
goal_default:
|
|
||||||
delay_time: ''
|
|
||||||
hold_m_name: ''
|
|
||||||
order_name: ''
|
|
||||||
percent_10_1_assign_material_name: ''
|
|
||||||
percent_10_1_liquid_material_name: ''
|
|
||||||
percent_10_1_target_weigh: ''
|
|
||||||
percent_10_1_volume: ''
|
|
||||||
percent_10_2_assign_material_name: ''
|
|
||||||
percent_10_2_liquid_material_name: ''
|
|
||||||
percent_10_2_target_weigh: ''
|
|
||||||
percent_10_2_volume: ''
|
|
||||||
percent_10_3_assign_material_name: ''
|
|
||||||
percent_10_3_liquid_material_name: ''
|
|
||||||
percent_10_3_target_weigh: ''
|
|
||||||
percent_10_3_volume: ''
|
|
||||||
percent_90_1_assign_material_name: ''
|
|
||||||
percent_90_1_target_weigh: ''
|
|
||||||
percent_90_2_assign_material_name: ''
|
|
||||||
percent_90_2_target_weigh: ''
|
|
||||||
percent_90_3_assign_material_name: ''
|
|
||||||
percent_90_3_target_weigh: ''
|
|
||||||
speed: ''
|
|
||||||
temperature: ''
|
|
||||||
handles: {}
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: DispenStationVialFeed_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
type: string
|
|
||||||
order_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_1_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_1_volume:
|
|
||||||
type: string
|
|
||||||
percent_10_2_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_2_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_2_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_2_volume:
|
|
||||||
type: string
|
|
||||||
percent_10_3_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_3_liquid_material_name:
|
|
||||||
type: string
|
|
||||||
percent_10_3_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_10_3_volume:
|
|
||||||
type: string
|
|
||||||
percent_90_1_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_1_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_90_2_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_2_target_weigh:
|
|
||||||
type: string
|
|
||||||
percent_90_3_assign_material_name:
|
|
||||||
type: string
|
|
||||||
percent_90_3_target_weigh:
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_name
|
|
||||||
- percent_90_1_assign_material_name
|
|
||||||
- percent_90_1_target_weigh
|
|
||||||
- percent_90_2_assign_material_name
|
|
||||||
- percent_90_2_target_weigh
|
|
||||||
- percent_90_3_assign_material_name
|
|
||||||
- percent_90_3_target_weigh
|
|
||||||
- percent_10_1_assign_material_name
|
|
||||||
- percent_10_1_target_weigh
|
|
||||||
- percent_10_1_volume
|
|
||||||
- percent_10_1_liquid_material_name
|
|
||||||
- percent_10_2_assign_material_name
|
|
||||||
- percent_10_2_target_weigh
|
|
||||||
- percent_10_2_volume
|
|
||||||
- percent_10_2_liquid_material_name
|
|
||||||
- percent_10_3_assign_material_name
|
|
||||||
- percent_10_3_target_weigh
|
|
||||||
- percent_10_3_volume
|
|
||||||
- percent_10_3_liquid_material_name
|
|
||||||
- speed
|
|
||||||
- temperature
|
|
||||||
- delay_time
|
|
||||||
- hold_m_name
|
|
||||||
title: DispenStationVialFeed_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationVialFeed_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: DispenStationVialFeed
|
|
||||||
type: object
|
|
||||||
type: DispenStationVialFeed
|
|
||||||
create_diamine_solution_task:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
delay_time: delay_time
|
|
||||||
hold_m_name: hold_m_name
|
|
||||||
liquid_material_name: liquid_material_name
|
|
||||||
material_name: material_name
|
|
||||||
order_name: order_name
|
|
||||||
speed: speed
|
|
||||||
target_weigh: target_weigh
|
|
||||||
temperature: temperature
|
|
||||||
volume: volume
|
|
||||||
goal_default:
|
|
||||||
delay_time: ''
|
|
||||||
hold_m_name: ''
|
|
||||||
liquid_material_name: ''
|
|
||||||
material_name: ''
|
|
||||||
order_name: ''
|
|
||||||
speed: ''
|
|
||||||
target_weigh: ''
|
|
||||||
temperature: ''
|
|
||||||
volume: ''
|
|
||||||
handles: {}
|
|
||||||
result:
|
|
||||||
return_info: return_info
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
title: DispenStationSolnPrep_Feedback
|
|
||||||
type: object
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
delay_time:
|
|
||||||
type: string
|
|
||||||
hold_m_name:
|
|
||||||
type: string
|
|
||||||
liquid_material_name:
|
|
||||||
type: string
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
order_name:
|
|
||||||
type: string
|
|
||||||
speed:
|
|
||||||
type: string
|
|
||||||
target_weigh:
|
|
||||||
type: string
|
|
||||||
temperature:
|
|
||||||
type: string
|
|
||||||
volume:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_name
|
|
||||||
- material_name
|
|
||||||
- target_weigh
|
|
||||||
- volume
|
|
||||||
- liquid_material_name
|
|
||||||
- speed
|
|
||||||
- temperature
|
|
||||||
- delay_time
|
|
||||||
- hold_m_name
|
|
||||||
title: DispenStationSolnPrep_Goal
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
return_info:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- return_info
|
|
||||||
title: DispenStationSolnPrep_Result
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: DispenStationSolnPrep
|
|
||||||
type: object
|
|
||||||
type: DispenStationSolnPrep
|
|
||||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
|
||||||
status_types: {}
|
|
||||||
type: python
|
|
||||||
config_info: []
|
|
||||||
description: ''
|
|
||||||
handles: []
|
|
||||||
icon: ''
|
|
||||||
init_param_schema:
|
|
||||||
config:
|
|
||||||
properties:
|
|
||||||
config:
|
|
||||||
type: string
|
|
||||||
deck:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- config
|
|
||||||
- deck
|
|
||||||
type: object
|
|
||||||
data:
|
|
||||||
properties: {}
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
version: 1.0.0
|
|
||||||
@@ -1361,7 +1361,8 @@ laiyu_liquid:
|
|||||||
mix_liquid_height: 0.0
|
mix_liquid_height: 0.0
|
||||||
mix_rate: 0
|
mix_rate: 0
|
||||||
mix_stage: ''
|
mix_stage: ''
|
||||||
mix_times: 0
|
mix_times:
|
||||||
|
- 0
|
||||||
mix_vol: 0
|
mix_vol: 0
|
||||||
none_keys:
|
none_keys:
|
||||||
- ''
|
- ''
|
||||||
@@ -1491,9 +1492,11 @@ laiyu_liquid:
|
|||||||
mix_stage:
|
mix_stage:
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
items:
|
||||||
minimum: -2147483648
|
maximum: 2147483647
|
||||||
type: integer
|
minimum: -2147483648
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
mix_vol:
|
mix_vol:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
|
|||||||
@@ -3994,7 +3994,8 @@ liquid_handler:
|
|||||||
mix_liquid_height: 0.0
|
mix_liquid_height: 0.0
|
||||||
mix_rate: 0
|
mix_rate: 0
|
||||||
mix_stage: ''
|
mix_stage: ''
|
||||||
mix_times: 0
|
mix_times:
|
||||||
|
- 0
|
||||||
mix_vol: 0
|
mix_vol: 0
|
||||||
none_keys:
|
none_keys:
|
||||||
- ''
|
- ''
|
||||||
@@ -4150,9 +4151,11 @@ liquid_handler:
|
|||||||
mix_stage:
|
mix_stage:
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
items:
|
||||||
minimum: -2147483648
|
maximum: 2147483647
|
||||||
type: integer
|
minimum: -2147483648
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
mix_vol:
|
mix_vol:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
@@ -5012,7 +5015,8 @@ liquid_handler.biomek:
|
|||||||
mix_liquid_height: 0.0
|
mix_liquid_height: 0.0
|
||||||
mix_rate: 0
|
mix_rate: 0
|
||||||
mix_stage: ''
|
mix_stage: ''
|
||||||
mix_times: 0
|
mix_times:
|
||||||
|
- 0
|
||||||
mix_vol: 0
|
mix_vol: 0
|
||||||
none_keys:
|
none_keys:
|
||||||
- ''
|
- ''
|
||||||
@@ -5155,9 +5159,11 @@ liquid_handler.biomek:
|
|||||||
mix_stage:
|
mix_stage:
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
items:
|
||||||
minimum: -2147483648
|
maximum: 2147483647
|
||||||
type: integer
|
minimum: -2147483648
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
mix_vol:
|
mix_vol:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
@@ -7801,7 +7807,8 @@ liquid_handler.prcxi:
|
|||||||
mix_liquid_height: 0.0
|
mix_liquid_height: 0.0
|
||||||
mix_rate: 0
|
mix_rate: 0
|
||||||
mix_stage: ''
|
mix_stage: ''
|
||||||
mix_times: 0
|
mix_times:
|
||||||
|
- 0
|
||||||
mix_vol: 0
|
mix_vol: 0
|
||||||
none_keys:
|
none_keys:
|
||||||
- ''
|
- ''
|
||||||
@@ -7930,9 +7937,11 @@ liquid_handler.prcxi:
|
|||||||
mix_stage:
|
mix_stage:
|
||||||
type: string
|
type: string
|
||||||
mix_times:
|
mix_times:
|
||||||
maximum: 2147483647
|
items:
|
||||||
minimum: -2147483648
|
maximum: 2147483647
|
||||||
type: integer
|
minimum: -2147483648
|
||||||
|
type: integer
|
||||||
|
type: array
|
||||||
mix_vol:
|
mix_vol:
|
||||||
maximum: 2147483647
|
maximum: 2147483647
|
||||||
minimum: -2147483648
|
minimum: -2147483648
|
||||||
|
|||||||
@@ -4,77 +4,6 @@ reaction_station.bioyond:
|
|||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
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-post_init:
|
auto-post_init:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal: {}
|
||||||
@@ -116,397 +45,35 @@ reaction_station.bioyond:
|
|||||||
properties:
|
properties:
|
||||||
json_str:
|
json_str:
|
||||||
type: string
|
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:
|
volume:
|
||||||
description: 投料体积
|
description: 分液公式(μL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume
|
- volume
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
- time
|
- time
|
||||||
- torque_variation
|
- torque_variation
|
||||||
|
- titration_type
|
||||||
|
- temperature
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reaction_station_drip_back参数
|
title: drip_back参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
reaction_station_liquid_feed:
|
drip_back:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
assign_material_name: assign_material_name
|
assign_material_name: assign_material_name
|
||||||
|
temperature: temperature
|
||||||
time: time
|
time: time
|
||||||
titration_type: titration_type
|
titration_type: titration_type
|
||||||
torque_variation: torque_variation
|
torque_variation: torque_variation
|
||||||
volume: volume
|
volume: volume
|
||||||
goal_default:
|
goal_default:
|
||||||
assign_material_name: ''
|
assign_material_name: ''
|
||||||
|
temperature: ''
|
||||||
time: ''
|
time: ''
|
||||||
titration_type: ''
|
titration_type: ''
|
||||||
torque_variation: ''
|
torque_variation: ''
|
||||||
@@ -514,40 +81,265 @@ reaction_station.bioyond:
|
|||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应站液体进料操作
|
description: 滴回去
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
assign_material_name:
|
assign_material_name:
|
||||||
description: 溶剂名称
|
description: 物料名称(不能为空)
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
description: 温度设定(°C)
|
||||||
type: string
|
type: string
|
||||||
time:
|
time:
|
||||||
description: 观察时间(单位min)
|
description: 观察时间(分钟)
|
||||||
type: string
|
|
||||||
titration_type:
|
|
||||||
description: 滴定类型1否2是
|
|
||||||
type: string
|
|
||||||
torque_variation:
|
|
||||||
description: 是否观察1否2是
|
|
||||||
type: string
|
|
||||||
volume:
|
|
||||||
description: 投料体积
|
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- titration_type
|
- file_path
|
||||||
- volume
|
|
||||||
- assign_material_name
|
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reaction_station_liquid_feed参数
|
title: load_bioyond_data_from_file参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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
|
||||||
|
solvents: solvents
|
||||||
|
temperature: temperature
|
||||||
|
time: time
|
||||||
|
titration_type: titration_type
|
||||||
|
torque_variation: torque_variation
|
||||||
|
volume: volume
|
||||||
|
goal_default:
|
||||||
|
assign_material_name: ''
|
||||||
|
solvents: ''
|
||||||
|
temperature: '25.00'
|
||||||
|
time: '360'
|
||||||
|
titration_type: '1'
|
||||||
|
torque_variation: '2'
|
||||||
|
volume: ''
|
||||||
|
handles:
|
||||||
|
input:
|
||||||
|
- data_key: solvents
|
||||||
|
data_source: handle
|
||||||
|
data_type: object
|
||||||
|
handler_key: solvents
|
||||||
|
io_type: source
|
||||||
|
label: Solvents Data From Calculation Node
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
assign_material_name:
|
||||||
|
description: 物料名称
|
||||||
|
type: string
|
||||||
|
solvents:
|
||||||
|
description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume'
|
||||||
|
type: string
|
||||||
|
temperature:
|
||||||
|
default: '25.00'
|
||||||
|
description: 温度设定(°C),默认25.00
|
||||||
|
type: string
|
||||||
|
time:
|
||||||
|
default: '360'
|
||||||
|
description: 观察时间(分钟),默认360
|
||||||
|
type: string
|
||||||
|
titration_type:
|
||||||
|
default: '1'
|
||||||
|
description: 是否滴定(1=否, 2=是),默认1
|
||||||
|
type: string
|
||||||
|
torque_variation:
|
||||||
|
default: '2'
|
||||||
|
description: 是否观察 (1=否, 2=是),默认2
|
||||||
|
type: string
|
||||||
|
volume:
|
||||||
|
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- assign_material_name
|
||||||
|
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: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
task_name: task_name
|
task_name: task_name
|
||||||
@@ -558,7 +350,7 @@ reaction_station.bioyond:
|
|||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应站流程执行
|
description: 处理并执行工作流
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -576,92 +368,10 @@ reaction_station.bioyond:
|
|||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reaction_station_process_execute参数
|
title: process_and_execute_workflow参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
reaction_station_reactor_taken_out:
|
reactor_taken_in:
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
order_id: order_id
|
|
||||||
preintake_id: preintake_id
|
|
||||||
goal_default:
|
|
||||||
order_id: ''
|
|
||||||
preintake_id: ''
|
|
||||||
handles: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
description: 订单ID,用于标识要取出的订单
|
|
||||||
type: string
|
|
||||||
preintake_id:
|
|
||||||
description: 预取样ID,用于标识具体的取样任务
|
|
||||||
type: string
|
|
||||||
required: []
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
code:
|
|
||||||
description: 操作结果代码(1表示成功,0表示失败)
|
|
||||||
type: integer
|
|
||||||
return_info:
|
|
||||||
description: 操作结果详细信息
|
|
||||||
type: string
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: reaction_station_reactor_taken_out参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
reaction_station_solid_feed_vial:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
assign_material_name: assign_material_name
|
|
||||||
material_id: material_id
|
|
||||||
time: time
|
|
||||||
torque_variation: torque_variation
|
|
||||||
goal_default:
|
|
||||||
assign_material_name: ''
|
|
||||||
material_id: ''
|
|
||||||
time: ''
|
|
||||||
torque_variation: ''
|
|
||||||
handles: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: 反应站固体进料操作
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
assign_material_name:
|
|
||||||
description: 固体名称_粉末加样模块-投料
|
|
||||||
type: string
|
|
||||||
material_id:
|
|
||||||
description: 固体投料类型_粉末加样模块-投料
|
|
||||||
type: string
|
|
||||||
time:
|
|
||||||
description: 观察时间_反应模块-观察搅拌结果
|
|
||||||
type: string
|
|
||||||
torque_variation:
|
|
||||||
description: 是否观察1否2是_反应模块-观察搅拌结果
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- assign_material_name
|
|
||||||
- material_id
|
|
||||||
- time
|
|
||||||
- torque_variation
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: reaction_station_solid_feed_vial参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
reaction_station_take_in:
|
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
assign_material_name: assign_material_name
|
assign_material_name: assign_material_name
|
||||||
@@ -674,7 +384,7 @@ reaction_station.bioyond:
|
|||||||
handles: {}
|
handles: {}
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 反应站取入操作
|
description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -683,10 +393,10 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称
|
description: 物料名称
|
||||||
type: string
|
type: string
|
||||||
cutoff:
|
cutoff:
|
||||||
description: 截止参数
|
description: 粘度上限
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
description: 温度
|
description: 温度设定(°C)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- cutoff
|
- cutoff
|
||||||
@@ -696,10 +406,88 @@ reaction_station.bioyond:
|
|||||||
result: {}
|
result: {}
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: reaction_station_take_in参数
|
title: reactor_taken_in参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
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: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
all_workflows: dict
|
all_workflows: dict
|
||||||
@@ -708,14 +496,14 @@ reaction_station.bioyond:
|
|||||||
workstation_status: dict
|
workstation_status: dict
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: Bioyond反应站 - 专门用于化学反应操作的工作站
|
description: Bioyond反应站
|
||||||
handles: []
|
handles: []
|
||||||
icon: 反应站.webp
|
icon: reaction_station.webp
|
||||||
init_param_schema:
|
init_param_schema:
|
||||||
config:
|
config:
|
||||||
properties:
|
properties:
|
||||||
bioyond_config:
|
config:
|
||||||
type: string
|
type: object
|
||||||
deck:
|
deck:
|
||||||
type: string
|
type: string
|
||||||
required: []
|
required: []
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
|
||||||
"""创建BioYond 4x1x2仓库"""
|
"""创建BioYond 4x1x2仓库"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
|
|||||||
@@ -535,6 +535,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
|
|||||||
|
|
||||||
def resource_ulab_to_plr_inner(resource: dict):
|
def resource_ulab_to_plr_inner(resource: dict):
|
||||||
all_states[resource["name"]] = resource["data"]
|
all_states[resource["name"]] = resource["data"]
|
||||||
|
extra = resource.pop("extra", {})
|
||||||
d = {
|
d = {
|
||||||
"name": resource["name"],
|
"name": resource["name"],
|
||||||
"type": resource["type"],
|
"type": resource["type"],
|
||||||
@@ -575,16 +576,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
|
|||||||
replace_info = {
|
replace_info = {
|
||||||
"plate": "plate",
|
"plate": "plate",
|
||||||
"well": "well",
|
"well": "well",
|
||||||
"tip_spot": "container",
|
"tip_spot": "tip_spot",
|
||||||
"trash": "container",
|
"trash": "trash",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
"tip_rack": "container",
|
"tip_rack": "tip_rack",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
else:
|
else:
|
||||||
print("转换pylabrobot的时候,出现未知类型", source)
|
print("转换pylabrobot的时候,出现未知类型", source)
|
||||||
return "container"
|
return source
|
||||||
|
|
||||||
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
|
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
|
||||||
r = {
|
r = {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
|
||||||
category: Optional[str] = "carrier",
|
category: Optional[str] = "carrier",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
invisible_slots: Optional[str] = None,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=name,
|
name=name,
|
||||||
@@ -89,6 +90,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
)
|
)
|
||||||
self.num_items = len(sites)
|
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.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"
|
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):
|
if isinstance(sites, dict):
|
||||||
@@ -410,7 +412,7 @@ class ItemizedCarrier(ResourcePLR):
|
|||||||
"layout": self.layout,
|
"layout": self.layout,
|
||||||
"sites": [{
|
"sites": [{
|
||||||
"label": str(identifier),
|
"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
|
"occupied_by": self[identifier].name
|
||||||
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
|
||||||
self[identifier] if isinstance(self[identifier], str) else None,
|
self[identifier] if isinstance(self[identifier], str) else None,
|
||||||
@@ -433,6 +435,7 @@ class BottleCarrier(ItemizedCarrier):
|
|||||||
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
|
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
|
||||||
category: str = "bottle_carrier",
|
category: str = "bottle_carrier",
|
||||||
model: Optional[str] = None,
|
model: Optional[str] = None,
|
||||||
|
invisible_slots: List[str] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -443,4 +446,5 @@ class BottleCarrier(ItemizedCarrier):
|
|||||||
sites=sites,
|
sites=sites,
|
||||||
category=category,
|
category=category,
|
||||||
model=model,
|
model=model,
|
||||||
|
invisible_slots=invisible_slots,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
|
|||||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet
|
||||||
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
|
from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
|
||||||
from unilabos_msgs.srv import SerialCommand # type: ignore
|
from unilabos_msgs.srv import SerialCommand # type: ignore
|
||||||
from rclpy.executors import MultiThreadedExecutor
|
from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor
|
||||||
from rclpy.node import Node
|
from rclpy.node import Node
|
||||||
from rclpy.timer import Timer
|
from rclpy.timer import Timer
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from unilabos_msgs.msg import Resource # type: ignore
|
|||||||
|
|
||||||
from unilabos.ros.nodes.resource_tracker import (
|
from unilabos.ros.nodes.resource_tracker import (
|
||||||
DeviceNodeResourceTracker,
|
DeviceNodeResourceTracker,
|
||||||
ResourceTreeSet,
|
ResourceTreeSet, ResourceTreeInstance,
|
||||||
)
|
)
|
||||||
from unilabos.ros.x.rclpyx import get_event_loop
|
from unilabos.ros.x.rclpyx import get_event_loop
|
||||||
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator
|
||||||
@@ -338,12 +338,12 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
|
|
||||||
# 创建资源管理客户端
|
# 创建资源管理客户端
|
||||||
self._resource_clients: Dict[str, Client] = {
|
self._resource_clients: Dict[str, Client] = {
|
||||||
"resource_add": self.create_client(ResourceAdd, "/resources/add"),
|
"resource_add": self.create_client(ResourceAdd, "/resources/add", callback_group=self.callback_group),
|
||||||
"resource_get": self.create_client(SerialCommand, "/resources/get"),
|
"resource_get": self.create_client(SerialCommand, "/resources/get", callback_group=self.callback_group),
|
||||||
"resource_delete": self.create_client(ResourceDelete, "/resources/delete"),
|
"resource_delete": self.create_client(ResourceDelete, "/resources/delete", callback_group=self.callback_group),
|
||||||
"resource_update": self.create_client(ResourceUpdate, "/resources/update"),
|
"resource_update": self.create_client(ResourceUpdate, "/resources/update", callback_group=self.callback_group),
|
||||||
"resource_list": self.create_client(ResourceList, "/resources/list"),
|
"resource_list": self.create_client(ResourceList, "/resources/list", callback_group=self.callback_group),
|
||||||
"c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree"),
|
"c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree", callback_group=self.callback_group),
|
||||||
}
|
}
|
||||||
|
|
||||||
def re_register_device(req, res):
|
def re_register_device(req, res):
|
||||||
@@ -573,6 +573,52 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
self.lab_logger().error(traceback.format_exc())
|
self.lab_logger().error(traceback.format_exc())
|
||||||
self.lab_logger().debug(f"资源更新结果: {response}")
|
self.lab_logger().debug(f"资源更新结果: {response}")
|
||||||
|
|
||||||
|
def transfer_to_new_resource(self, plr_resource: "ResourcePLR", tree: ResourceTreeInstance, additional_add_params: Dict[str, Any]):
|
||||||
|
parent_uuid = tree.root_node.res_content.parent_uuid
|
||||||
|
if parent_uuid:
|
||||||
|
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
||||||
|
if parent_resource is None:
|
||||||
|
self.lab_logger().warning(
|
||||||
|
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
||||||
|
additional_params = {}
|
||||||
|
extra = getattr(plr_resource, "unilabos_extra", {})
|
||||||
|
if len(extra):
|
||||||
|
self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra))
|
||||||
|
if "update_resource_site" in extra:
|
||||||
|
additional_add_params["site"] = extra["update_resource_site"]
|
||||||
|
site = additional_add_params.get("site", None)
|
||||||
|
spec = inspect.signature(parent_resource.assign_child_resource)
|
||||||
|
if "spot" in spec.parameters:
|
||||||
|
ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering")
|
||||||
|
if ordering_dict:
|
||||||
|
site = list(ordering_dict.keys()).index(site)
|
||||||
|
additional_params["spot"] = site
|
||||||
|
old_parent = plr_resource.parent
|
||||||
|
if old_parent is not None:
|
||||||
|
# plr并不支持同一个deck的加载和卸载
|
||||||
|
self.lab_logger().warning(
|
||||||
|
f"物料{plr_resource}请求从{old_parent}卸载"
|
||||||
|
)
|
||||||
|
old_parent.unassign_child_resource(plr_resource)
|
||||||
|
self.lab_logger().warning(
|
||||||
|
f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}"
|
||||||
|
)
|
||||||
|
parent_resource.assign_child_resource(
|
||||||
|
plr_resource, location=None, **additional_params
|
||||||
|
)
|
||||||
|
func = getattr(self.driver_instance, "resource_tree_transfer", None)
|
||||||
|
if callable(func):
|
||||||
|
# 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了)
|
||||||
|
func(old_parent, plr_resource, parent_resource)
|
||||||
|
except Exception as e:
|
||||||
|
self.lab_logger().warning(
|
||||||
|
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
||||||
|
)
|
||||||
|
|
||||||
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response):
|
||||||
"""
|
"""
|
||||||
处理资源树更新请求
|
处理资源树更新请求
|
||||||
@@ -613,28 +659,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
plr_resources = tree_set.to_plr_resources()
|
plr_resources = tree_set.to_plr_resources()
|
||||||
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
for plr_resource, tree in zip(plr_resources, tree_set.trees):
|
||||||
self.resource_tracker.add_resource(plr_resource)
|
self.resource_tracker.add_resource(plr_resource)
|
||||||
parent_uuid = tree.root_node.res_content.parent_uuid
|
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
|
||||||
if parent_uuid:
|
|
||||||
parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid)
|
|
||||||
if parent_resource is None:
|
|
||||||
self.lab_logger().warning(
|
|
||||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
# 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步
|
|
||||||
additional_params = {}
|
|
||||||
site = additional_add_params.get("site", None)
|
|
||||||
spec = inspect.signature(parent_resource.assign_child_resource)
|
|
||||||
if "spot" in spec.parameters:
|
|
||||||
additional_params["spot"] = site
|
|
||||||
parent_resource.assign_child_resource(
|
|
||||||
plr_resource, location=None, **additional_params
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
self.lab_logger().warning(
|
|
||||||
f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}"
|
|
||||||
)
|
|
||||||
func = getattr(self.driver_instance, "resource_tree_add", None)
|
func = getattr(self.driver_instance, "resource_tree_add", None)
|
||||||
if callable(func):
|
if callable(func):
|
||||||
func(plr_resources)
|
func(plr_resources)
|
||||||
@@ -647,6 +672,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
|
||||||
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
{"uuid": tree.root_node.res_content.uuid}, try_mode=False
|
||||||
)
|
)
|
||||||
|
original_parent_resource = original_instance.parent
|
||||||
|
original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None)
|
||||||
|
target_parent_resource_uuid = tree.root_node.res_content.uuid_parent
|
||||||
|
self.lab_logger().info(
|
||||||
|
f"物料{original_instance} 原始父节点{original_parent_resource_uuid} 目标父节点{target_parent_resource_uuid} 更新"
|
||||||
|
)
|
||||||
|
# todo: 对extra进行update
|
||||||
|
if getattr(plr_resource, "unilabos_extra", None) is not None:
|
||||||
|
original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra")
|
||||||
|
if original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None:
|
||||||
|
self.transfer_to_new_resource(original_instance, tree, additional_add_params)
|
||||||
original_instance.load_all_state(states)
|
original_instance.load_all_state(states)
|
||||||
self.lab_logger().info(
|
self.lab_logger().info(
|
||||||
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个"
|
f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个"
|
||||||
@@ -879,7 +915,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
action_type,
|
action_type,
|
||||||
action_name,
|
action_name,
|
||||||
execute_callback=self._create_execute_callback(action_name, action_value_mapping),
|
execute_callback=self._create_execute_callback(action_name, action_value_mapping),
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||||
@@ -1500,7 +1536,7 @@ class ROS2DeviceNode:
|
|||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
|
|
||||||
ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNode")
|
ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNodeLoop")
|
||||||
ROS2DeviceNode._loop_thread.start()
|
ROS2DeviceNode._loop_thread.start()
|
||||||
logger.info(f"循环线程已启动")
|
logger.info(f"循环线程已启动")
|
||||||
|
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
|
|
||||||
# 创建定时器,定期发现设备
|
# 创建定时器,定期发现设备
|
||||||
self._discovery_timer = self.create_timer(
|
self._discovery_timer = self.create_timer(
|
||||||
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
|
discovery_interval, self._discovery_devices_callback, callback_group=self.callback_group
|
||||||
)
|
)
|
||||||
|
|
||||||
# 添加ping-pong相关属性
|
# 添加ping-pong相关属性
|
||||||
@@ -494,7 +494,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
if len(init_new_res) > 1: # 一个物料,多个子节点
|
if len(init_new_res) > 1: # 一个物料,多个子节点
|
||||||
init_new_res = [init_new_res]
|
init_new_res = [init_new_res]
|
||||||
resources: List[Resource] | List[List[Resource]] = init_new_res # initialize_resource已经返回list[dict]
|
resources: List[Resource] | List[List[Resource]] = init_new_res # initialize_resource已经返回list[dict]
|
||||||
device_ids = [device_id]
|
device_ids = [device_id.split("/")[-1]]
|
||||||
bind_parent_id = [res_creation_input["parent"]]
|
bind_parent_id = [res_creation_input["parent"]]
|
||||||
bind_location = [bind_locations]
|
bind_location = [bind_locations]
|
||||||
other_calling_param = [
|
other_calling_param = [
|
||||||
@@ -618,7 +618,7 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
topic,
|
topic,
|
||||||
lambda msg, d=device_id, p=property_name: self.property_callback(msg, d, p),
|
lambda msg, d=device_id, p=property_name: self.property_callback(msg, d, p),
|
||||||
1,
|
1,
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
)
|
)
|
||||||
# 标记为已订阅
|
# 标记为已订阅
|
||||||
self._subscribed_topics.add(topic)
|
self._subscribed_topics.add(topic)
|
||||||
@@ -829,37 +829,37 @@ class HostNode(BaseROS2DeviceNode):
|
|||||||
def _init_host_service(self):
|
def _init_host_service(self):
|
||||||
self._resource_services: Dict[str, Service] = {
|
self._resource_services: Dict[str, Service] = {
|
||||||
"resource_add": self.create_service(
|
"resource_add": self.create_service(
|
||||||
ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=ReentrantCallbackGroup()
|
ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=self.callback_group
|
||||||
),
|
),
|
||||||
"resource_get": self.create_service(
|
"resource_get": self.create_service(
|
||||||
SerialCommand, "/resources/get", self._resource_get_callback, callback_group=ReentrantCallbackGroup()
|
SerialCommand, "/resources/get", self._resource_get_callback, callback_group=self.callback_group
|
||||||
),
|
),
|
||||||
"resource_delete": self.create_service(
|
"resource_delete": self.create_service(
|
||||||
ResourceDelete,
|
ResourceDelete,
|
||||||
"/resources/delete",
|
"/resources/delete",
|
||||||
self._resource_delete_callback,
|
self._resource_delete_callback,
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"resource_update": self.create_service(
|
"resource_update": self.create_service(
|
||||||
ResourceUpdate,
|
ResourceUpdate,
|
||||||
"/resources/update",
|
"/resources/update",
|
||||||
self._resource_update_callback,
|
self._resource_update_callback,
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"resource_list": self.create_service(
|
"resource_list": self.create_service(
|
||||||
ResourceList, "/resources/list", self._resource_list_callback, callback_group=ReentrantCallbackGroup()
|
ResourceList, "/resources/list", self._resource_list_callback, callback_group=self.callback_group
|
||||||
),
|
),
|
||||||
"node_info_update": self.create_service(
|
"node_info_update": self.create_service(
|
||||||
SerialCommand,
|
SerialCommand,
|
||||||
"/node_info_update",
|
"/node_info_update",
|
||||||
self._node_info_update_callback,
|
self._node_info_update_callback,
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
"c2s_update_resource_tree": self.create_service(
|
"c2s_update_resource_tree": self.create_service(
|
||||||
SerialCommand,
|
SerialCommand,
|
||||||
"/c2s_update_resource_tree",
|
"/c2s_update_resource_tree",
|
||||||
self._resource_tree_update_callback,
|
self._resource_tree_update_callback,
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
|
|||||||
action_type,
|
action_type,
|
||||||
action_name,
|
action_name,
|
||||||
execute_callback=self._create_protocol_execute_callback(action_name, protocol_steps_generator),
|
execute_callback=self._create_protocol_execute_callback(action_name, protocol_steps_generator),
|
||||||
callback_group=ReentrantCallbackGroup(),
|
callback_group=self.callback_group,
|
||||||
)
|
)
|
||||||
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ class ResourceDictPosition(BaseModel):
|
|||||||
rotation: ResourceDictPositionObject = Field(
|
rotation: ResourceDictPositionObject = Field(
|
||||||
description="Resource rotation", default_factory=ResourceDictPositionObject
|
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 不序列化
|
# 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化
|
||||||
@@ -51,7 +53,9 @@ class ResourceDict(BaseModel):
|
|||||||
uuid: str = Field(description="Resource UUID")
|
uuid: str = Field(description="Resource UUID")
|
||||||
name: str = Field(description="Resource name")
|
name: str = Field(description="Resource name")
|
||||||
description: str = Field(description="Resource description", default="")
|
description: str = Field(description="Resource description", default="")
|
||||||
resource_schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema")
|
resource_schema: Dict[str, Any] = Field(
|
||||||
|
description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema"
|
||||||
|
)
|
||||||
model: Dict[str, Any] = Field(description="Resource model", default_factory=dict)
|
model: Dict[str, Any] = Field(description="Resource model", default_factory=dict)
|
||||||
icon: str = Field(description="Resource icon", default="")
|
icon: str = Field(description="Resource icon", default="")
|
||||||
parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid
|
parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid
|
||||||
@@ -62,6 +66,7 @@ class ResourceDict(BaseModel):
|
|||||||
pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
|
pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
|
||||||
config: Dict[str, Any] = Field(description="Resource configuration")
|
config: Dict[str, Any] = Field(description="Resource configuration")
|
||||||
data: Dict[str, Any] = Field(description="Resource data")
|
data: Dict[str, Any] = Field(description="Resource data")
|
||||||
|
extra: Dict[str, Any] = Field(description="Extra data")
|
||||||
|
|
||||||
@field_serializer("parent_uuid")
|
@field_serializer("parent_uuid")
|
||||||
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
|
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
|
||||||
@@ -138,6 +143,8 @@ class ResourceDictInstance(object):
|
|||||||
content["config"] = {}
|
content["config"] = {}
|
||||||
if not content.get("data"):
|
if not content.get("data"):
|
||||||
content["data"] = {}
|
content["data"] = {}
|
||||||
|
if not content.get("extra"): # MagicCode
|
||||||
|
content["extra"] = {}
|
||||||
if "pose" not in content:
|
if "pose" not in content:
|
||||||
content["pose"] = content.get("position", {})
|
content["pose"] = content.get("position", {})
|
||||||
return ResourceDictInstance(ResourceDict.model_validate(content))
|
return ResourceDictInstance(ResourceDict.model_validate(content))
|
||||||
@@ -311,28 +318,36 @@ class ResourceTreeSet(object):
|
|||||||
"plate": "plate",
|
"plate": "plate",
|
||||||
"well": "well",
|
"well": "well",
|
||||||
"deck": "deck",
|
"deck": "deck",
|
||||||
|
"tip_rack": "tip_rack",
|
||||||
|
"tip_spot": "tip_spot",
|
||||||
|
"tube": "tube",
|
||||||
|
"bottle_carrier": "bottle_carrier",
|
||||||
}
|
}
|
||||||
if source in replace_info:
|
if source in replace_info:
|
||||||
return replace_info[source]
|
return replace_info[source]
|
||||||
else:
|
else:
|
||||||
print("转换pylabrobot的时候,出现未知类型", source)
|
print("转换pylabrobot的时候,出现未知类型", source)
|
||||||
return "container"
|
return source
|
||||||
|
|
||||||
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
|
def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None):
|
||||||
"""递归构建uuid映射字典"""
|
"""递归构建uuid和extra映射字典,返回(current_uuid, parent_uuid, extra)元组列表"""
|
||||||
uid = getattr(res, "unilabos_uuid", "")
|
uid = getattr(res, "unilabos_uuid", "")
|
||||||
if not uid:
|
if not uid:
|
||||||
uid = str(uuid.uuid4())
|
uid = str(uuid.uuid4())
|
||||||
res.unilabos_uuid = uid
|
res.unilabos_uuid = uid
|
||||||
logger.warning(f"{res}没有uuid,请设置后再传入,默认填充{uid}!\n{traceback.format_exc()}")
|
logger.warning(f"{res}没有uuid,请设置后再传入,默认填充{uid}!\n{traceback.format_exc()}")
|
||||||
uuid_list.append(uid)
|
|
||||||
|
# 获取unilabos_extra,默认为空字典
|
||||||
|
extra = getattr(res, "unilabos_extra", {})
|
||||||
|
|
||||||
|
uuid_list.append((uid, parent_uuid, extra))
|
||||||
for child in res.children:
|
for child in res.children:
|
||||||
build_uuid_mapping(child, uuid_list)
|
build_uuid_mapping(child, uuid_list, uid)
|
||||||
|
|
||||||
def resource_plr_inner(
|
def resource_plr_inner(
|
||||||
d: dict, parent_resource: Optional[ResourceDict], states: dict, uuids: list
|
d: dict, parent_resource: Optional[ResourceDict], states: dict, uuids: list
|
||||||
) -> ResourceDictInstance:
|
) -> ResourceDictInstance:
|
||||||
current_uuid = uuids.pop(0)
|
current_uuid, parent_uuid, extra = uuids.pop(0)
|
||||||
|
|
||||||
raw_pos = (
|
raw_pos = (
|
||||||
{"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]}
|
{"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]}
|
||||||
@@ -355,13 +370,30 @@ class ResourceTreeSet(object):
|
|||||||
"uuid": current_uuid,
|
"uuid": current_uuid,
|
||||||
"name": d["name"],
|
"name": d["name"],
|
||||||
"parent": parent_resource, # 直接传入 ResourceDict 对象
|
"parent": parent_resource, # 直接传入 ResourceDict 对象
|
||||||
|
"parent_uuid": parent_uuid, # 使用 parent_uuid 而不是 parent 对象
|
||||||
"type": replace_plr_type(d.get("category", "")),
|
"type": replace_plr_type(d.get("category", "")),
|
||||||
"class": d.get("class", ""),
|
"class": d.get("class", ""),
|
||||||
"position": pos,
|
"position": pos,
|
||||||
"pose": pos,
|
"pose": pos,
|
||||||
"config": {k: v for k, v in d.items() if k not in
|
"config": {
|
||||||
["name", "children", "parent_name", "location", "rotation", "size_x", "size_y", "size_z", "cross_section_type", "bottom_type"]},
|
k: v
|
||||||
|
for k, v in d.items()
|
||||||
|
if k
|
||||||
|
not in [
|
||||||
|
"name",
|
||||||
|
"children",
|
||||||
|
"parent_name",
|
||||||
|
"location",
|
||||||
|
"rotation",
|
||||||
|
"size_x",
|
||||||
|
"size_y",
|
||||||
|
"size_z",
|
||||||
|
"cross_section_type",
|
||||||
|
"bottom_type",
|
||||||
|
]
|
||||||
|
},
|
||||||
"data": states[d["name"]],
|
"data": states[d["name"]],
|
||||||
|
"extra": extra,
|
||||||
}
|
}
|
||||||
|
|
||||||
# 先转换为 ResourceDictInstance,获取其中的 ResourceDict
|
# 先转换为 ResourceDictInstance,获取其中的 ResourceDict
|
||||||
@@ -379,7 +411,7 @@ class ResourceTreeSet(object):
|
|||||||
for resource in resources:
|
for resource in resources:
|
||||||
# 构建uuid列表
|
# 构建uuid列表
|
||||||
uuid_list = []
|
uuid_list = []
|
||||||
build_uuid_mapping(resource, uuid_list)
|
build_uuid_mapping(resource, uuid_list, getattr(resource.parent, "unilabos_uuid", None))
|
||||||
|
|
||||||
serialized_data = resource.serialize()
|
serialized_data = resource.serialize()
|
||||||
all_states = resource.serialize_all_state()
|
all_states = resource.serialize_all_state()
|
||||||
@@ -402,14 +434,15 @@ class ResourceTreeSet(object):
|
|||||||
import inspect
|
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):
|
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict):
|
||||||
"""一次遍历收集 name_to_uuid 和 all_states"""
|
"""一次遍历收集 name_to_uuid, all_states 和 name_to_extra"""
|
||||||
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
name_to_uuid[node.res_content.name] = node.res_content.uuid
|
||||||
all_states[node.res_content.name] = node.res_content.data
|
all_states[node.res_content.name] = node.res_content.data
|
||||||
|
name_to_extra[node.res_content.name] = node.res_content.extra
|
||||||
for child in node.children:
|
for child in node.children:
|
||||||
collect_node_data(child, name_to_uuid, all_states)
|
collect_node_data(child, name_to_uuid, all_states, name_to_extra)
|
||||||
|
|
||||||
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
|
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
|
||||||
"""转换节点为 PLR 字典格式"""
|
"""转换节点为 PLR 字典格式"""
|
||||||
@@ -419,6 +452,7 @@ class ResourceTreeSet(object):
|
|||||||
logger.warning(f"未知类型 {res.type}")
|
logger.warning(f"未知类型 {res.type}")
|
||||||
|
|
||||||
d = {
|
d = {
|
||||||
|
**res.config,
|
||||||
"name": res.name,
|
"name": res.name,
|
||||||
"type": res.config.get("type", plr_type),
|
"type": res.config.get("type", plr_type),
|
||||||
"size_x": res.config.get("size_x", 0),
|
"size_x": res.config.get("size_x", 0),
|
||||||
@@ -434,33 +468,35 @@ class ResourceTreeSet(object):
|
|||||||
"category": res.config.get("category", plr_type),
|
"category": res.config.get("category", plr_type),
|
||||||
"children": [node_to_plr_dict(child, has_model) for child in node.children],
|
"children": [node_to_plr_dict(child, has_model) for child in node.children],
|
||||||
"parent_name": res.parent_instance_name,
|
"parent_name": res.parent_instance_name,
|
||||||
**res.config,
|
|
||||||
}
|
}
|
||||||
if has_model:
|
if has_model:
|
||||||
d["model"] = res.config.get("model", None)
|
d["model"] = res.config.get("model", None)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
plr_resources = []
|
plr_resources = []
|
||||||
trees = []
|
|
||||||
tracker = DeviceNodeResourceTracker()
|
tracker = DeviceNodeResourceTracker()
|
||||||
|
|
||||||
for tree in self.trees:
|
for tree in self.trees:
|
||||||
name_to_uuid: Dict[str, str] = {}
|
name_to_uuid: Dict[str, str] = {}
|
||||||
all_states: Dict[str, Any] = {}
|
all_states: Dict[str, Any] = {}
|
||||||
collect_node_data(tree.root_node, name_to_uuid, all_states)
|
name_to_extra: Dict[str, dict] = {}
|
||||||
|
collect_node_data(tree.root_node, name_to_uuid, all_states, name_to_extra)
|
||||||
has_model = tree.root_node.res_content.type != "deck"
|
has_model = tree.root_node.res_content.type != "deck"
|
||||||
plr_dict = node_to_plr_dict(tree.root_node, has_model)
|
plr_dict = node_to_plr_dict(tree.root_node, has_model)
|
||||||
try:
|
try:
|
||||||
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
sub_cls = find_subclass(plr_dict["type"], PLRResource)
|
||||||
if sub_cls is None:
|
if sub_cls is None:
|
||||||
raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}")
|
raise ValueError(
|
||||||
|
f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}"
|
||||||
|
)
|
||||||
spec = inspect.signature(sub_cls)
|
spec = inspect.signature(sub_cls)
|
||||||
if "category" not in spec.parameters:
|
if "category" not in spec.parameters:
|
||||||
plr_dict.pop("category", None)
|
plr_dict.pop("category", None)
|
||||||
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
|
||||||
plr_resource.load_all_state(all_states)
|
plr_resource.load_all_state(all_states)
|
||||||
# 使用 DeviceNodeResourceTracker 设置 UUID
|
# 使用 DeviceNodeResourceTracker 设置 UUID 和 Extra
|
||||||
tracker.loop_set_uuid(plr_resource, name_to_uuid)
|
tracker.loop_set_uuid(plr_resource, name_to_uuid)
|
||||||
|
tracker.loop_set_extra(plr_resource, name_to_extra)
|
||||||
plr_resources.append(plr_resource)
|
plr_resources.append(plr_resource)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -802,6 +838,20 @@ class DeviceNodeResourceTracker(object):
|
|||||||
else:
|
else:
|
||||||
setattr(resource, "unilabos_uuid", new_uuid)
|
setattr(resource, "unilabos_uuid", new_uuid)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def set_resource_extra(resource, extra: dict):
|
||||||
|
"""
|
||||||
|
设置资源的 extra,统一处理 dict 和 instance 两种类型
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 资源对象(dict或实例)
|
||||||
|
extra: extra字典值
|
||||||
|
"""
|
||||||
|
if isinstance(resource, dict):
|
||||||
|
resource["extra"] = extra
|
||||||
|
else:
|
||||||
|
setattr(resource, "unilabos_extra", extra)
|
||||||
|
|
||||||
def _traverse_and_process(self, resource, process_func) -> int:
|
def _traverse_and_process(self, resource, process_func) -> int:
|
||||||
"""
|
"""
|
||||||
递归遍历资源树,对每个节点执行处理函数
|
递归遍历资源树,对每个节点执行处理函数
|
||||||
@@ -850,6 +900,29 @@ class DeviceNodeResourceTracker(object):
|
|||||||
|
|
||||||
return self._traverse_and_process(resource, process)
|
return self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
|
def loop_set_extra(self, resource, name_to_extra_map: Dict[str, dict]) -> int:
|
||||||
|
"""
|
||||||
|
递归遍历资源树,根据 name 设置所有节点的 extra
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource: 资源对象(可以是dict或实例)
|
||||||
|
name_to_extra_map: name到extra的映射字典,{name: extra}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
更新的资源数量
|
||||||
|
"""
|
||||||
|
|
||||||
|
def process(res):
|
||||||
|
resource_name = self._get_resource_attr(res, "name")
|
||||||
|
if resource_name and resource_name in name_to_extra_map:
|
||||||
|
extra = name_to_extra_map[resource_name]
|
||||||
|
self.set_resource_extra(res, extra)
|
||||||
|
logger.debug(f"设置资源Extra: {resource_name} -> {extra}")
|
||||||
|
return 1
|
||||||
|
return 0
|
||||||
|
|
||||||
|
return self._traverse_and_process(resource, process)
|
||||||
|
|
||||||
def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int:
|
def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int:
|
||||||
"""
|
"""
|
||||||
递归遍历资源树,更新所有节点的uuid
|
递归遍历资源树,更新所有节点的uuid
|
||||||
@@ -892,7 +965,9 @@ class DeviceNodeResourceTracker(object):
|
|||||||
if current_uuid:
|
if current_uuid:
|
||||||
old = self.uuid_to_resources.get(current_uuid)
|
old = self.uuid_to_resources.get(current_uuid)
|
||||||
self.uuid_to_resources[current_uuid] = res
|
self.uuid_to_resources[current_uuid] = res
|
||||||
logger.debug(f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}")
|
logger.debug(
|
||||||
|
f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}"
|
||||||
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
self._traverse_and_process(resource, process)
|
self._traverse_and_process(resource, process)
|
||||||
|
|||||||
Reference in New Issue
Block a user