Merge remote-tracking branch 'upstream/workstation_dev_YB3' into workstation_dev_YB3

This commit is contained in:
lixinyu1011
2025-10-31 14:02:45 +08:00
42 changed files with 6604 additions and 8666 deletions

View File

@@ -17,29 +17,16 @@
{
"id": "BatteryStation",
"name": "扣电组装工作站",
"children": [
"coin_cell_deck"
],
"children": [],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": true,
"_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"
}
},
"debug_mode": false,
"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
},
"data": {}

File diff suppressed because it is too large Load Diff

695
scripts/workflow.py Normal file
View 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 是 compassn/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直接用 compasse.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}

View File

@@ -8,7 +8,7 @@
],
"parent": null,
"type": "device",
"class": "workstation.bioyond_dispensing_station",
"class": "bioyond_dispensing_station",
"config": {
"config": {
"api_key": "DE9BDDA0",

View 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"
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View 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}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

View 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)

View File

@@ -6,6 +6,8 @@ HTTP客户端模块
import json
import os
import time
from threading import Thread
from typing import List, Dict, Any, Optional
import requests
@@ -84,14 +86,14 @@ class HTTPClient:
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
timeout=60,
)
else:
response = requests.put(
f"{self.remote_addr}/edge/material",
json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid},
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:
@@ -126,12 +128,16 @@ class HTTPClient:
Returns:
Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid}
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4))
response = requests.post(
f"{self.remote_addr}/edge/material/query",
json={"uuids": uuid_list, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=100,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
if response.status_code == 200:
res = response.json()
if "code" in res and res["code"] != 0:
@@ -187,12 +193,16 @@ class HTTPClient:
Returns:
Dict: 返回的资源数据
"""
with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f:
f.write(json.dumps({"id": id, "with_children": with_children}, indent=4))
response = requests.get(
f"{self.remote_addr}/lab/material",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"Lab {self.auth}"},
timeout=20,
)
with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f:
f.write(f"{response.status_code}" + "\n" + response.text)
return response.json()
def resource_del(self, id: str) -> requests.Response:

View File

@@ -253,7 +253,7 @@ class BioyondCellWorkstation(BioyondWorkstation):
def auto_feeding4to3(
self,
# ★ 修改点:默认模板路径
xlsx_path: Optional[str] = "unilabos\\devices\\workstation\\bioyond_studio\\bioyond_cell\\样品导入模板.xlsx",
xlsx_path: Optional[str] = "/Users/calvincao/Desktop/work/uni-lab-all/Uni-Lab-OS/unilabos/devices/workstation/bioyond_studio/bioyond_cell/material_template.xlsx",
# ---------------- WH4 - 加样头面 (Z=1, 12个点位) ----------------
WH4_x1_y1_z1_1_materialName: str = "", WH4_x1_y1_z1_1_quantity: float = 0.0,
WH4_x2_y1_z1_2_materialName: str = "", WH4_x2_y1_z1_2_quantity: float = 0.0,

View File

@@ -16,7 +16,7 @@ API_CONFIG = {
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
# 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")),
"debug_mode": False,# 调试模式
}
@@ -151,10 +151,22 @@ WAREHOUSE_MAPPING = {
# 物料类型配置
MATERIAL_TYPE_MAPPINGS = {
"加样头(大)": ("YB_jia_yang_tou_da_1X1_carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"100ml液体": ("YB_1Bottle100mlCarrier", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
"": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
# YB信息
"高粘液": ("YB_1GaoNianYeBottleCarrier", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
"加样头(大)": ("YB_jia_yang_tou_da_1X1_carrier", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"加样头(大)板": ("YB_jia_yang_tou_da_1X1_carrier", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"5ml分液瓶板": ("YB_6x5ml_DispensingVialCarrier", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液瓶": ("YB_6x5ml_DispensingVialCarrier", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
"20ml分液瓶板": ("YB_6x20ml_DispensingVialCarrier", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
"20ml分液瓶": ("YB_6x20ml_DispensingVialCarrier", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
"配液瓶(小)板": ("YB_6x_SmallSolutionBottleCarrier", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_6x_SmallSolutionBottleCarrier", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"配液瓶(大)板": ("YB_4x_LargeSolutionBottleCarrier", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
"配液瓶(大)": ("YB_4x_LargeSolutionBottleCarrier", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
"适配器块": ("YB_AdapterBlock", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
"枪头盒": ("YB_TipBox", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
"枪头": ("YB_TipBox", "b6196971-1050-46da-9927-333e8dea062d"),
}
SOLID_LIQUID_MAPPINGS = {

View File

@@ -338,7 +338,7 @@ class BioyondDispensingStation(BioyondWorkstation):
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
# 4. 查询工作流对应的holdMID
material_info = self.material_id_query(workflow_id)
material_info = self.hardware_interface.material_id_query(workflow_id)
if not material_info:
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
@@ -409,6 +409,265 @@ class BioyondDispensingStation(BioyondWorkstation):
self.hardware_interface._logger.error(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__":
bioyond = BioyondDispensingStation(config={
@@ -416,6 +675,8 @@ if __name__ == "__main__":
"api_host": "http://192.168.1.200:44388"
})
# ============ 原有示例代码 ============
# 示例1使用material_id_query查询工作流对应的holdMID
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID

View File

@@ -58,8 +58,8 @@ class BioyondReactionStation(BioyondWorkstation):
Args:
assign_material_name: 物料名称(不能为空)
cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000"
temperature: 温度上限°C范围-50.00 至 100.00
cutoff: 粘度上限(需为有效数字字符串,默认 "900000"
temperature: 温度设定°C范围-50.00 至 100.00
Returns:
str: JSON 字符串,格式为 {"suc": True}
@@ -113,11 +113,11 @@ class BioyondReactionStation(BioyondWorkstation):
"""固体进料小瓶
Args:
material_id: 粉末类型ID
material_id: 粉末类型ID1=盐21分钟2=面粉27分钟3=BTDA38分钟
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
torque_variation: 是否观察(int类型, 1=否, 2=是)
assign_material_name: 物料名称(用于获取试剂瓶位ID)
temperature: 温度上限(°C)
temperature: 温度设定(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None
@@ -165,9 +165,9 @@ class BioyondReactionStation(BioyondWorkstation):
Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称
titration_type: 是否滴定(1=滴定, 其他=非滴定)
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
@@ -208,7 +208,8 @@ class BioyondReactionStation(BioyondWorkstation):
def liquid_feeding_solvents(
self,
assign_material_name: str,
volume: str,
volume: str = None,
solvents = None,
titration_type: str = "1",
time: str = "360",
torque_variation: int = 2,
@@ -218,12 +219,41 @@ class BioyondReactionStation(BioyondWorkstation):
Args:
assign_material_name: 物料名称
volume: 分液量(μL)
titration_type: 是否滴定
volume: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算)
solvents: 溶剂信息的字典或JSON字符串(可选),格式如下:
{
"additional_solvent": 33.55092503597727, # 溶剂体积(mL)
"total_liquid_volume": 48.00916988195499
}
如果提供solvents,则从中提取additional_solvent并转换为μL
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
temperature: 温度上限(°C)
torque_variation: 是否观察(int类型, 1=否, 2=是)
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"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
@@ -273,9 +303,9 @@ class BioyondReactionStation(BioyondWorkstation):
Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称
titration_type: 是否滴定
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
@@ -328,9 +358,9 @@ class BioyondReactionStation(BioyondWorkstation):
volume: 分液量(μL)
assign_material_name: 物料名称(试剂瓶位)
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
titration_type: 是否滴定
temperature: 温度上限(°C)
torque_variation: 是否观察(int类型, 1=否, 2=是)
titration_type: 是否滴定(1=否, 2=是)
temperature: 温度设定(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
@@ -381,9 +411,9 @@ class BioyondReactionStation(BioyondWorkstation):
Args:
assign_material_name: 物料名称(液体种类)
volume: 分液量(μL)
titration_type: 是否滴定
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
@@ -605,7 +635,8 @@ class BioyondReactionStation(BioyondWorkstation):
total_params += 1
step_parameters[step_id][action_name].append({
"Key": param_key,
"DisplayValue": param_value
"DisplayValue": param_value,
"Value": param_value
})
successful_params += 1
# print(f" ✓ {param_key} = {param_value}")

View File

@@ -4,6 +4,7 @@ Bioyond Workstation Implementation
集成Bioyond物料管理的工作站示例
"""
import time
import traceback
from datetime import datetime
from typing import Dict, Any, List, Optional, Union

View File

@@ -0,0 +1,976 @@
"""
纽扣电池组装工作站物料类定义
Button Battery Assembly Station Resource Classes
"""
from __future__ import annotations
from collections import OrderedDict
from typing import Any, Dict, List, Optional, TypedDict, Union, cast
from pylabrobot.resources.coordinate import Coordinate
from pylabrobot.resources.container import Container
from pylabrobot.resources.deck import Deck
from pylabrobot.resources.itemized_resource import ItemizedResource
from pylabrobot.resources.resource import Resource
from pylabrobot.resources.resource_stack import ResourceStack
from pylabrobot.resources.tip_rack import TipRack, TipSpot
from pylabrobot.resources.trash import Trash
from pylabrobot.resources.utils import create_ordered_items_2d
class ElectrodeSheetState(TypedDict):
diameter: float # 直径 (mm)
thickness: float # 厚度 (mm)
mass: float # 质量 (g)
material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等)
height: float
electrolyte_name: str
data_electrolyte_code: str
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
info: Optional[str] # 附加信息
class ElectrodeSheet(Resource):
"""极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料"""
def __init__(
self,
name: str = "极片",
size_x=10,
size_y=10,
size_z=10,
category: str = "electrode_sheet",
model: Optional[str] = None,
):
"""初始化极片
Args:
name: 极片名称
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState(
diameter=14,
thickness=0.1,
mass=0.5,
material_type="copper",
info=None
)
# TODO: 这个还要不要给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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
# TODO: 这个应该只能放一个极片
class MaterialHoleState(TypedDict):
diameter: int
depth: int
max_sheets: int
info: Optional[str] # 附加信息
class MaterialHole(Resource):
"""料板洞位类"""
children: List[ElectrodeSheet] = []
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "material_hole",
**kwargs
):
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
)
self._unilabos_state: MaterialHoleState = MaterialHoleState(
diameter=20,
depth=10,
max_sheets=1,
info=None
)
def get_all_sheet_info(self):
info_list = []
for sheet in self.children:
info_list.append(sheet._unilabos_state["info"])
return info_list
#这个函数函数好像没用,一般不会集中赋值质量
def set_all_sheet_mass(self):
for sheet in self.children:
sheet._unilabos_state["mass"] = 0.5 # 示例设置质量为0.5g
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
#移动极片前先取出对象
def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]:
for sheet in self.children:
if sheet.name == name:
return sheet
return None
def has_electrode_sheet(self) -> bool:
"""检查洞位是否有极片"""
return len(self.children) > 0
def assign_child_resource(
self,
resource: ElectrodeSheet,
location: Optional[Coordinate],
reassign: bool = True,
):
"""放置极片"""
# TODO: 这里要改diameter找不到加入._unilabos_state后应该没问题
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
#if len(self.children) >= self._unilabos_state["max_sheets"]:
# raise ValueError(f"洞位已满,无法放置更多极片")
super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。
def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet:
return self.children[index]
class MaterialPlateState(TypedDict):
hole_spacing_x: float
hole_spacing_y: float
hole_diameter: float
info: Optional[str] # 附加信息
class MaterialPlate(ItemizedResource[MaterialHole]):
"""料板类 - 4x4个洞位每个洞位放1个极片"""
children: List[MaterialHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
ordered_items: Optional[Dict[str, MaterialHole]] = None,
ordering: Optional[OrderedDict[str, str]] = None,
category: str = "material_plate",
model: Optional[str] = None,
fill: bool = False
):
"""初始化料板
Args:
name: 料板名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing_x: X方向洞位间距 (mm)
hole_spacing_y: Y方向洞位间距 (mm)
number: 编号
category: 类别
model: 型号
"""
self._unilabos_state: MaterialPlateState = MaterialPlateState(
hole_spacing_x=24.0,
hole_spacing_y=24.0,
hole_diameter=20.0,
info="",
)
# 创建4x4的洞位
# TODO: 这里要改,对应不同形状
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
dz=size_z,
item_dx=self._unilabos_state["hole_spacing_x"],
item_dy=self._unilabos_state["hole_spacing_y"],
size_x = 16,
size_y = 16,
size_z = 16,
)
if fill:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
else:
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=ordered_items,
ordering=ordering,
category=category,
model=model,
)
def update_locations(self):
# TODO:调多次相加
holes = create_ordered_items_2d(
klass=MaterialHole,
num_items_x=4,
num_items_y=4,
dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中
dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中
dz=self._size_z,
item_dx=self._unilabos_state["hole_spacing_x"],
item_dy=self._unilabos_state["hole_spacing_y"],
size_x = 1,
size_y = 1,
size_z = 1,
)
for item, original_item in zip(holes.items(), self.children):
original_item.location = item[1].location
class PlateSlot(ResourceStack):
"""板槽位类 - 1个槽上能堆放8个板移板只能操作最上方的板"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
max_plates: int = 8,
category: str = "plate_slot",
model: Optional[str] = None
):
"""初始化板槽位
Args:
name: 槽位名称
max_plates: 最大板数量
category: 类别
"""
super().__init__(
name=name,
direction="z", # Z方向堆叠
resources=[],
)
self.max_plates = max_plates
self.category = category
def can_add_plate(self) -> bool:
"""检查是否可以添加板"""
return len(self.children) < self.max_plates
def add_plate(self, plate: MaterialPlate) -> None:
"""添加料板"""
if not self.can_add_plate():
raise ValueError(f"槽位 {self.name} 已满,无法添加更多板")
self.assign_child_resource(plate)
def get_top_plate(self) -> MaterialPlate:
"""获取最上方的板"""
if len(self.children) == 0:
raise ValueError(f"槽位 {self.name} 为空")
return cast(MaterialPlate, self.get_top_item())
def take_top_plate(self) -> MaterialPlate:
"""取出最上方的板"""
top_plate = self.get_top_plate()
self.unassign_child_resource(top_plate)
return top_plate
def can_access_for_picking(self) -> bool:
"""检查是否可以进行取料操作(只有最上方的板能进行取料操作)"""
return len(self.children) > 0
def serialize(self) -> dict:
return {
**super().serialize(),
"max_plates": self.max_plates,
}
class ClipMagazineHole(Container):
"""子弹夹洞位类"""
def __init__(
self,
name: str,
diameter: float,
depth: float,
max_sheets: int = 100,
category: str = "clip_magazine_hole",
):
"""初始化子弹夹洞位
Args:
name: 洞位名称
diameter: 洞直径 (mm)
depth: 洞深度 (mm)
max_sheets: 最大极片数量
category: 类别
"""
super().__init__(
name=name,
size_x=diameter,
size_y=diameter,
size_z=depth,
category=category,
)
self.diameter = diameter
self.depth = depth
self.max_sheets = max_sheets
self._sheets: List[ElectrodeSheet] = []
def can_add_sheet(self, sheet: ElectrodeSheet) -> bool:
"""检查是否可以添加极片"""
return (len(self._sheets) < self.max_sheets and
sheet.diameter <= self.diameter)
def add_sheet(self, sheet: ElectrodeSheet) -> None:
"""添加极片"""
if not self.can_add_sheet(sheet):
raise ValueError(f"无法向洞位 {self.name} 添加极片")
self._sheets.append(sheet)
def take_sheet(self) -> ElectrodeSheet:
"""取出极片"""
if len(self._sheets) == 0:
raise ValueError(f"洞位 {self.name} 没有极片")
return self._sheets.pop()
def get_sheet_count(self) -> int:
"""获取极片数量"""
return len(self._sheets)
def serialize_state(self) -> Dict[str, Any]:
return {
"sheet_count": len(self._sheets),
"sheets": [sheet.serialize() for sheet in self._sheets],
}
# TODO: 这个要改
class ClipMagazine(Resource):
"""子弹夹类 - 有6个洞位每个洞位放多个极片"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 20.0,
hole_depth: float = 50.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建6个洞位排成2x3布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=3,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
def serialize(self) -> dict:
return {
**super().serialize(),
"hole_diameter": self.hole_diameter,
"hole_depth": self.hole_depth,
"max_sheets_per_hole": self.max_sheets_per_hole,
}
#是一种类型注解不用self
class BatteryState(TypedDict):
"""电池状态字典"""
diameter: float
height: float
assembly_pressure: float
electrolyte_volume: float
electrolyte_name: str
class Battery(Resource):
"""电池类 - 可容纳极片"""
children: List[ElectrodeSheet] = []
def __init__(
self,
name: str,
size_x=1,
size_y=1,
size_z=1,
category: str = "battery",
):
"""初始化电池
Args:
name: 电池名称
diameter: 直径 (mm)
height: 高度 (mm)
max_volume: 最大容量 (μL)
barcode: 二维码编号
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=1,
size_y=1,
size_z=1,
category=category,
)
self._unilabos_state: BatteryState = BatteryState(
diameter = 1.0,
height = 1.0,
assembly_pressure = 1.0,
electrolyte_volume = 1.0,
electrolyte_name = "DP001"
)
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
to_add_name = bottle._unilabos_state["electrolyte_name"]
if bottle.aspirate_electrolyte(10):
if self.add_electrolyte(to_add_name, 10):
pass
else:
bottle._unilabos_state["electrolyte_volume"] += 10
def set_electrolyte(self, name: str, volume: float) -> None:
"""设置电解液信息"""
self._unilabos_state["electrolyte_name"] = name
self._unilabos_state["electrolyte_volume"] = volume
#这个应该没用,不会有加了后再加的事情
def add_electrolyte(self, name: str, volume: float) -> bool:
"""添加电解液信息"""
if name != self._unilabos_state["electrolyte_name"]:
return False
self._unilabos_state["electrolyte_volume"] += volume
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
# 电解液作为属性放进去
class BatteryPressSlotState(TypedDict):
"""电池状态字典"""
diameter: float =20.0
depth: float = 4.0
class BatteryPressSlot(Resource):
"""电池压制槽类 - 设备,可容纳一个电池"""
children: List[Battery] = []
def __init__(
self,
name: str = "BatteryPressSlot",
category: str = "battery_press_slot",
):
"""初始化电池压制槽
Args:
name: 压制槽名称
diameter: 直径 (mm)
depth: 深度 (mm)
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=10,
size_y=12,
size_z=13,
category=category,
)
self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState()
def has_battery(self) -> bool:
"""检查是否有电池"""
return len(self.children) > 0
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
def assign_child_resource(
self,
resource: Battery,
location: Optional[Coordinate],
reassign: bool = True,
):
"""放置极片"""
# TODO: 让高京看下槽位只有一个电池时是否这么写。
if self.has_battery():
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。
def get_battery_info(self, index: int) -> Battery:
return self.children[0]
# TODO:这个移液枪架子看一下从哪继承
class TipBox64State(TypedDict):
"""电池状态字典"""
tip_diameter: float = 5.0
tip_length: float = 50.0
with_tips: bool = True
class TipBox64(TipRack):
"""64孔枪头盒类"""
children: List[TipSpot] = []
def __init__(
self,
name: str,
size_x: float = 127.8,
size_y: float = 85.5,
size_z: float = 60.0,
category: str = "tip_box_64",
model: Optional[str] = None,
):
"""初始化64孔枪头盒
Args:
name: 枪头盒名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
tip_diameter: 枪头直径 (mm)
tip_length: 枪头长度 (mm)
category: 类别
model: 型号
with_tips: 是否带枪头
"""
from pylabrobot.resources.tip import Tip
# 创建8x8=64个枪头位
def make_tip():
return Tip(
has_filter=False,
total_tip_length=20.0,
maximal_volume=1000, # 1mL
fitting_depth=8.0,
)
tip_spots = create_ordered_items_2d(
klass=TipSpot,
num_items_x=8,
num_items_y=8,
dx=8.0,
dy=8.0,
dz=0.0,
item_dx=9.0,
item_dy=9.0,
size_x=10,
size_y=10,
size_z=0.0,
make_tip=make_tip,
)
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=tip_spots,
category=category,
model=model,
with_tips=True,
)
class WasteTipBoxstate(TypedDict):
""""废枪头盒状态字典"""
max_tips: int = 100
tip_count: int = 0
#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断
class WasteTipBox(Trash):
"""废枪头盒类 - 100个枪头容量"""
def __init__(
self,
name: str,
size_x: float = 127.8,
size_y: float = 85.5,
size_z: float = 60.0,
category: str = "waste_tip_box",
model: Optional[str] = None,
):
"""初始化废枪头盒
Args:
name: 废枪头盒名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
max_tips: 最大枪头容量
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
def add_tip(self) -> None:
"""添加废枪头"""
if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]:
raise ValueError(f"废枪头盒 {self.name} 已满")
self._unilabos_state["tip_count"] += 1
def get_tip_count(self) -> int:
"""获取枪头数量"""
return self._unilabos_state["tip_count"]
def empty(self) -> None:
"""清空废枪头盒"""
self._unilabos_state["tip_count"] = 0
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
class BottleRackState(TypedDict):
""" bottle_diameter: 瓶子直径 (mm)
bottle_height: 瓶子高度 (mm)
position_spacing: 位置间距 (mm)"""
bottle_diameter: float
bottle_height: float
name_to_index: dict
class BottleRack(Resource):
"""瓶架类 - 12个待配位置+12个已配位置"""
children: List[Bottle] = []
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str = "bottle_rack",
model: Optional[str] = None,
):
"""初始化瓶架
Args:
name: 瓶架名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
category=category,
model=model,
)
# TODO: 添加瓶位坐标映射
self.index_to_pos = {
0: Coordinate.zero(),
1: Coordinate(x=1, y=2, z=3) # 添加
}
self.name_to_index = {}
self.name_to_pos = {}
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
# TODO: 这里有些问题要重新写一下
def assign_child_resource(self, resource: Bottle, location=Coordinate.zero(), reassign = True):
assert len(self.children) <= 12, "瓶架已满,无法添加更多瓶子"
index = len(self.children)
location = Coordinate(x=20 + (index % 4) * 15, y=20 + (index // 4) * 15, z=0)
self.name_to_pos[resource.name] = location
self.name_to_index[resource.name] = index
return super().assign_child_resource(resource, location, reassign)
def assign_child_resource_by_index(self, resource: Bottle, index: int):
assert 0 <= index < 12, "无效的瓶子索引"
self.name_to_index[resource.name] = index
location = self.index_to_pos[index]
return super().assign_child_resource(resource, location)
def unassign_child_resource(self, resource: Bottle):
super().unassign_child_resource(resource)
self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None)
# def serialize(self):
# self.children.sort(key=lambda x: self.name_to_index.get(x.name, 0))
# return super().serialize()
class BottleState(TypedDict):
diameter: float
height: float
electrolyte_name: str
electrolyte_volume: float
max_volume: float
class Bottle(Resource):
"""瓶子类 - 容纳电解液"""
def __init__(
self,
name: str,
category: str = "bottle",
):
"""初始化瓶子
Args:
name: 瓶子名称
diameter: 直径 (mm)
height: 高度 (mm)
max_volume: 最大体积 (μL)
barcode: 二维码
category: 类别
model: 型号
"""
super().__init__(
name=name,
size_x=1,
size_y=1,
size_z=1,
category=category,
)
self._unilabos_state: BottleState = BottleState()
def aspirate_electrolyte(self, volume: float) -> bool:
current_volume = self._unilabos_state["electrolyte_volume"]
assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available."
self._unilabos_state["electrolyte_volume"] -= volume
return True
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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
class CoincellDeck(Deck):
"""纽扣电池组装工作站台面类"""
def __init__(
self,
name: str = "coin_cell_deck",
size_x: float = 1000.0, # 1m
size_y: float = 1000.0, # 1m
size_z: float = 900.0, # 0.9m
origin: Coordinate = Coordinate(0, 0, 0),
category: str = "coin_cell_deck",
setup: bool = False, # 是否自动执行 setup
):
"""初始化纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm) - 1m
size_y: 宽度 (mm) - 1m
size_z: 高度 (mm) - 0.9m
origin: 原点坐标
category: 类别
setup: 是否自动执行 setup 配置标准布局
"""
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
origin=origin,
category=category,
)
if setup:
self.setup()
def setup(self) -> None:
"""设置工作站的标准布局 - 包含3个料盘"""
# 步骤 1: 创建所有料盘
self.plates = {
"liaopan1": MaterialPlate(
name="liaopan1",
size_x=120.8,
size_y=120.5,
size_z=10.0,
fill=True
),
"liaopan2": MaterialPlate(
name="liaopan2",
size_x=120.8,
size_y=120.5,
size_z=10.0,
fill=True
),
"电池料盘": MaterialPlate(
name="电池料盘",
size_x=120.8,
size_y=160.5,
size_z=10.0,
fill=True
),
}
# 步骤 2: 定义料盘在 deck 上的位置
# Deck 尺寸: 1000×1000mm料盘尺寸: 120.8×120.5mm 或 120.8×160.5mm
self.plate_locations = {
"liaopan1": Coordinate(x=50, y=50, z=0), # 左上角,留 50mm 边距
"liaopan2": Coordinate(x=250, y=50, z=0), # 中间liaopan1 右侧
"电池料盘": Coordinate(x=450, y=50, z=0), # 右侧
}
# 步骤 3: 将料盘分配到 deck 上
for plate_name, plate in self.plates.items():
self.assign_child_resource(
plate,
location=self.plate_locations[plate_name]
)
# 步骤 4: 为 liaopan1 添加初始极片
for i in range(16):
jipian = ElectrodeSheet(
name=f"jipian1_{i}",
size_x=12,
size_y=12,
size_z=0.1
)
self.plates["liaopan1"].children[i].assign_child_resource(
jipian,
location=None
)
def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0, size_y: float = 1000.0, size_z: float = 900.0) -> CoincellDeck:
"""创建并配置标准的纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
Returns:
已配置好的 CoincellDeck 对象
"""
deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z)
deck.setup()
return deck

View File

@@ -1,33 +1,133 @@
import csv
import inspect
import json
import os
import threading
import time
import types
from datetime import datetime
from typing import Any, Dict, Optional
from pylabrobot.resources import Resource as PLRResource
from functools import wraps
from pylabrobot.resources import Deck, Resource as PLRResource
from unilabos_msgs.msg import Resource
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import *
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import *
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import CoincellDeck
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
from unilabos.resources.graphio import convert_resources_to_type
from unilabos.utils.log import logger
def _ensure_modbus_slave_kw_alias(modbus_client):
if modbus_client is None:
return
method_names = [
"read_coils",
"write_coils",
"write_coil",
"read_discrete_inputs",
"read_holding_registers",
"write_register",
"write_registers",
]
def _wrap(func):
signature = inspect.signature(func)
has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values())
accepts_unit = has_var_kwargs or "unit" in signature.parameters
accepts_slave = has_var_kwargs or "slave" in signature.parameters
@wraps(func)
def _wrapped(self, *args, **kwargs):
if "slave" in kwargs and not accepts_slave:
slave_value = kwargs.pop("slave")
if accepts_unit and "unit" not in kwargs:
kwargs["unit"] = slave_value
if "unit" in kwargs and not accepts_unit:
unit_value = kwargs.pop("unit")
if accepts_slave and "slave" not in kwargs:
kwargs["slave"] = unit_value
return func(self, *args, **kwargs)
_wrapped._has_slave_alias = True
return _wrapped
for name in method_names:
if not hasattr(modbus_client, name):
continue
bound_method = getattr(modbus_client, name)
func = getattr(bound_method, "__func__", None)
if func is None:
continue
if getattr(func, "_has_slave_alias", False):
continue
wrapped = _wrap(func)
setattr(modbus_client, name, types.MethodType(wrapped, modbus_client))
def _coerce_deck_input(deck: Any) -> Optional[Deck]:
if deck is None:
return None
if isinstance(deck, Deck):
return deck
if isinstance(deck, PLRResource):
return deck if isinstance(deck, Deck) else None
candidates = None
if isinstance(deck, dict):
if "nodes" in deck and isinstance(deck["nodes"], list):
candidates = deck["nodes"]
else:
candidates = [deck]
elif isinstance(deck, list):
candidates = deck
if candidates is None:
return None
try:
converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck)
if isinstance(converted, Deck):
return converted
if isinstance(converted, list):
for item in converted:
if isinstance(item, Deck):
return item
except Exception as exc:
logger.warning(f"deck 转换 Deck 失败: {exc}")
return None
#构建物料系统
class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__(
self,
deck: CoincellDeck,
address: str = "192.168.1.20",
deck: Deck=None,
address: str = "172.21.32.111",
port: str = "502",
debug_mode: bool = True,
debug_mode: bool = False,
*args,
**kwargs,
):
if deck is None and "deck" in kwargs:
deck = kwargs.pop("deck")
else:
kwargs.pop("deck", None)
normalized_deck = _coerce_deck_input(deck)
if deck is None and isinstance(normalized_deck, Deck):
deck = normalized_deck
super().__init__(
#桌子
deck=deck,
@@ -35,10 +135,22 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
**kwargs,
)
self.debug_mode = debug_mode
self.deck = deck
# 如果没有传入 deck则创建标准配置的 deck
if self.deck is None:
self.deck = CoincellDeck(size_x=1000, size_y=1000, size_z=900, setup=True)
else:
# 如果传入了 deck 但还没有 setup可以选择是否 setup
if self.deck is not None and len(self.deck.children) == 0:
# deck 为空,执行 setup
self.deck.setup()
# 否则使用传入的 deck可能已经配置好了
self.deck = self.deck
""" 连接初始化 """
modbus_client = TCPClient(addr=address, port=port)
print("modbus_client", modbus_client)
_ensure_modbus_slave_kw_alias(modbus_client.client)
if not debug_mode:
modbus_client.client.connect()
count = 100
@@ -62,8 +174,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量
#创建一个物料台面,包含两个极片板
#self.deck = create_a_coin_cell_deck()
#self._ros_node.update_resource(self.deck)
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
@@ -708,7 +818,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("data_electrolyte_code", data_electrolyte_code)
print("data_coin_cell_code", data_coin_cell_code)
#接收完信息后读取完毕标志位置True
liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8")
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
#把物料解绑后放到另一盘上
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
battery._unilabos_state = {
@@ -721,7 +831,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
#print(jipian2.parent)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.station_resource]
"resources": [self.deck]
})
@@ -782,7 +892,19 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.success = True
return self.success
def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool:
self.success = False
self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu)
self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei)
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode)
# self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure)
self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore)
self.success = True
return self.success
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="D:\\coin_cell_data") -> bool:
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure)
@@ -892,7 +1014,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘
liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8")
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
for i in range(16):
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2)
battery._unilabos_state = {
@@ -905,9 +1027,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
liaopan3.children[i].assign_child_resource(battery, location=None)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.station_resource]
"resources": [self.deck]
})
time.sleep(4)
# for i in range(40):
# print(f"fun_wuliao_test 运行结束{i}")
# time.sleep(1)
# time.sleep(40)
# 数据读取与输出
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
# 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环
@@ -1104,69 +1229,10 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
'''
if __name__ == "__main__":
from pylabrobot.resources import Resource
Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True)
#Coin_Cell.func_pack_device_init()
#Coin_Cell.func_pack_device_auto()
#Coin_Cell.func_pack_device_start()
#Coin_Cell.func_pack_send_bottle_num(2)
#Coin_Cell.func_pack_send_msg_cmd(2)
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_send_finished_cmd()
#
#Coin_Cell.func_allpack_cmd(3, 2)
#print(Coin_Cell.data_stack_vision_code)
#print("success")
#创建一个物料台面
deck = create_a_coin_cell_deck()
#deck = create_a_full_coin_cell_deck()
##在台面上找到料盘和极片
#liaopan1 = deck.get_resource("liaopan1")
#liaopan2 = deck.get_resource("liaopan2")
#jipian1 = liaopan1.children[1].children[0]
##
#print(jipian1)
##把物料解绑后放到另一盘上
#jipian1.parent.unassign_child_resource(jipian1)
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
##print(jipian2.parent)
liaopan1 = deck.get_resource("liaopan1")
liaopan2 = deck.get_resource("liaopan2")
for i in range(16):
#找到liaopan1上每一个jipian
jipian_linshi = liaopan1.children[i].children[0]
#把物料解绑后放到另一盘上
print("极片:", jipian_linshi)
jipian_linshi.parent.unassign_child_resource(jipian_linshi)
liaopan2.children[i].assign_child_resource(jipian_linshi, location=None)
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
#with open("./button_battery_station_resources_unilab.json", "r", encoding="utf-8") as f:
# bioyond_resources_unilab = json.load(f)
#print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
#ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
#print(f"转换结果类型: {type(ulab_resources)}")
#print(ulab_resources)
from unilabos.resources.graphio import convert_resources_from_type
from unilabos.config.config import BasicConfig
BasicConfig.ak = "beb0c15f-2279-46a1-aba5-00eaf89aef55"
BasicConfig.sk = "15d4f25e-3512-4f9c-9bfb-43ab85e7b561"
from unilabos.app.web.client import http_client
resources = convert_resources_from_type([deck], [Resource])
json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2)
#print(resources)
http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
http_client.resource_add(resources)
# 简单测试
workstation = CoinCellAssemblyWorkstation()
workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False)
print(f"工作站创建成功: {workstation.deck.name}")
print(f"料盘数量: {len(workstation.deck.children)}")

View File

@@ -43,21 +43,22 @@ REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,ʹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,coil,8340,
REG_MSG_NE_PLATE_MATRIX,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>λ,,hold_register,440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,<EFBFBD><EFBFBD>Ĥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>λ,,hold_register,450,
REG_MSG_TIP_BOX_MATRIX,INT16,,<EFBFBD><EFBFBD>Һǹͷ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>λ,,hold_register,480,
REG_MSG_NE_PLATE_NUM,INT16,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,<EFBFBD><EFBFBD>Ĥ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>,,hold_register,453,
REG_MSG_PRESS_MODE,BOOL,,ѹ<EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD>false:ѹ<><D1B9><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ<C4A3><CABD>True:<3A><><EFBFBD><EFBFBD>ģʽ<C4A3><CABD>,,coil,8360,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><EFBFBD>ģʽ
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
REG_MSG_PRESS_MODE,BOOL,,压制模式false:压力检测模式True:距离模式),,coil,8360,电池压制模式
,,,,,,,
,BOOL,,<EFBFBD>Ӿ<EFBFBD><EFBFBD><EFBFBD>λ<EFBFBD><EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8300,<EFBFBD>Ӿ<EFBFBD><EFBFBD><EFBFBD>λ
,BOOL,,<EFBFBD><EFBFBD><EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8310,<EFBFBD>Ӿ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD><EFBFBD><EFBFBD>֣<EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8320,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>_<EFBFBD>Ҳ֣<EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8420,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ҳ<EFBFBD>
,BOOL,,<EFBFBD><EFBFBD><EFBFBD>ռ<EFBFBD>֪<EFBFBD><EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8350,<EFBFBD><EFBFBD><EFBFBD>ռ<EFBFBD>֪
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Һ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ģʽ<EFBFBD><EFBFBD>false:<3A><><EFBFBD>ε<EFBFBD>Һ<EFBFBD><D2BA>true:<3A><><EFBFBD>ε<EFBFBD>Һ<EFBFBD><D2BA>,,coil,8370,<EFBFBD><EFBFBD>Һģʽ
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD>أ<EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8380,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD>װ<EFBFBD><EFBFBD>ʽ<EFBFBD><EFBFBD>false:<3A><>װ<EFBFBD><D7B0>true:<3A><>װ<EFBFBD><D7B0>,,coil,8390,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>װ
,BOOL,,ѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>false:ʹ<EFBFBD>ã<EFBFBD>true:<EFBFBD><EFBFBD><EFBFBD>ԣ<EFBFBD>,,coil,8400,ѹ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
,BOOL,,<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>̰<EFBFBD><EFBFBD>̷<EFBFBD>ʽ<EFBFBD><EFBFBD>false:ˮƽ<CBAE><C6BD><EFBFBD>̣<EFBFBD>true:<3A>ѵ<EFBFBD><D1B5><EFBFBD><EFBFBD>̣<EFBFBD>,,coil,8410,<EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ƭ<EFBFBD><EFBFBD><EFBFBD>̷<EFBFBD>ʽ
,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位
,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检
,BOOL,,手套箱_左仓false:使用,true:忽略),,coil,8320,手套箱左仓
,BOOL,,手套箱_右仓false:使用,true:忽略),,coil,8420,手套箱右仓
,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知
,BOOL,,电解液添加模式false:单次滴液true:二次滴液),,coil,8370,滴液模式
,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重
,BOOL,,正负极片组装方式false:正装true:倒装),,coil,8390,正负极反装
,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁
,BOOL,,物料盘摆盘方式false:水平摆盘true:堆叠摆盘),,coil,8410,负极片摆盘方式
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁false:使用true:忽略),,coil,8460,
1 Name DataType InitValue Comment Attribute DeviceType Address
43 UNILAB_SEND_FINISHED_CMD BOOL coil 8730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 8530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 10018 ASSEMBLY_TYPE7or8
46 COIL_ALUMINUM_FOIL BOOL ʹ 使用铝箔垫 coil 8340
47 REG_MSG_NE_PLATE_MATRIX INT16 Ƭλ 负极片矩阵点位 hold_register 440
48 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 Ĥλ 隔膜矩阵点位 hold_register 450
49 REG_MSG_TIP_BOX_MATRIX INT16 Һǹͷλ 移液枪头矩阵点位 hold_register 480
50 REG_MSG_NE_PLATE_NUM INT16 Ƭ 负极片盘数 hold_register 443
51 REG_MSG_SEPARATOR_PLATE_NUM INT16 Ĥ 隔膜盘数 hold_register 453
52 REG_MSG_PRESS_MODE BOOL ѹģʽfalse:ѹģʽTrue:ģʽ 压制模式(false:压力检测模式,True:距离模式) coil 8360 ѹģʽ 电池压制模式
53
54 BOOL Ӿλfalse:ʹãtrue:ԣ 视觉对位(false:使用,true:忽略) coil 8300 Ӿλ 视觉对位
55 BOOL 죨false:ʹãtrue:ԣ 复检(false:使用,true:忽略) coil 8310 Ӿ 视觉复检
56 BOOL _֣false:ʹãtrue:ԣ 手套箱_左仓(false:使用,true:忽略) coil 8320 手套箱左仓
57 BOOL _Ҳ֣false:ʹãtrue:ԣ 手套箱_右仓(false:使用,true:忽略) coil 8420 Ҳ 手套箱右仓
58 BOOL ռ֪false:ʹãtrue:ԣ 真空检知(false:使用,true:忽略) coil 8350 ռ֪ 真空检知
59 BOOL Һģʽfalse:εҺtrue:εҺ 电解液添加模式(false:单次滴液,true:二次滴液) coil 8370 Һģʽ 滴液模式
60 BOOL Ƭأfalse:ʹãtrue:ԣ 正极片称重(false:使用,true:忽略) coil 8380 Ƭ 正极片称重
61 BOOL Ƭװʽfalse:װtrue:װ 正负极片组装方式(false:正装,true:倒装) coil 8390 װ 正负极反装
62 BOOL ѹࣨfalse:ʹãtrue:ԣ 压制清洁(false:使用,true:忽略) coil 8400 ѹ 压制清洁
63 BOOL ̷̰ʽfalse:ˮƽ̣true:ѵ̣ 物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘) coil 8410 Ƭ̷ʽ 负极片摆盘方式
64 REG_MSG_BATTERY_CLEAN_IGNORE BOOL 忽略电池清洁(false:使用,true:忽略) coil 8460

View File

@@ -1,6 +1,20 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
@@ -8,7 +22,7 @@
],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"class": "coincellassemblyworkstation_device",
"position": {
"x": 600,
"y": 400,
@@ -16,674 +30,7 @@
},
"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
"protocol_type": []
}
}
],

View File

@@ -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

View File

@@ -137,7 +137,7 @@ bioyond_cell:
WH4_x5_y1_z1_5_quantity: 0.0
WH4_x5_y2_z1_10_materialName: ''
WH4_x5_y2_z1_10_quantity: 0.0
xlsx_path: unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
xlsx_path: unilabos\devices\workstation\bioyond_studio\bioyond_cell\样品导入模板.xlsx
handles: {}
placeholder_keys: {}
result: {}
@@ -463,7 +463,7 @@ bioyond_cell:
default: 0.0
type: number
xlsx_path:
default: unilabos/devices/workstation/bioyond_cell/样品导入模板.xlsx
default: unilabos\devices\workstation\bioyond_studio\bioyond_cell\样品导入模板.xlsx
type: string
required: []
type: object
@@ -498,6 +498,63 @@ bioyond_cell:
title: auto_feeding4to3_from_xlsx参数
type: object
type: UniLabJsonCommand
auto-create_and_inbound_materials:
feedback: {}
goal: {}
goal_default:
material_names: null
type_id: 3a190ca0-b2f6-9aeb-8067-547e72c11469
warehouse_name: 粉末加样头堆栈
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_names:
type: string
type_id:
default: 3a190ca0-b2f6-9aeb-8067-547e72c11469
type: string
warehouse_name:
default: 粉末加样头堆栈
type: string
required: []
type: object
result: {}
required:
- goal
title: create_and_inbound_materials参数
type: object
type: UniLabJsonCommand
auto-create_materials:
feedback: {}
goal: {}
goal_default:
mappings: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
mappings:
type: object
required:
- mappings
type: object
result: {}
required:
- goal
title: create_materials参数
type: object
type: UniLabJsonCommand
auto-create_orders:
feedback: {}
goal: {}
@@ -576,6 +633,84 @@ bioyond_cell:
title: order_list_v2参数
type: object
type: UniLabJsonCommand
auto-process_order_finish_report:
feedback: {}
goal: {}
goal_default:
report_request: null
used_materials: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
report_request:
type: string
used_materials:
type: string
required:
- report_request
type: object
result: {}
required:
- goal
title: process_order_finish_report参数
type: object
type: UniLabJsonCommand
auto-process_sample_finish_report:
feedback: {}
goal: {}
goal_default:
report_request: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
report_request:
type: string
required:
- report_request
type: object
result: {}
required:
- goal
title: process_sample_finish_report参数
type: object
type: UniLabJsonCommand
auto-process_step_finish_report:
feedback: {}
goal: {}
goal_default:
report_request: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
report_request:
type: string
required:
- report_request
type: object
result: {}
required:
- goal
title: process_step_finish_report参数
type: object
type: UniLabJsonCommand
auto-report_material_change:
feedback: {}
goal: {}
@@ -622,6 +757,27 @@ bioyond_cell:
title: scheduler_continue参数
type: object
type: UniLabJsonCommand
auto-scheduler_reset:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: scheduler_reset参数
type: object
type: UniLabJsonCommand
auto-scheduler_start:
feedback: {}
goal: {}
@@ -778,6 +934,62 @@ bioyond_cell:
title: transfer_3_to_2_to_1参数
type: object
type: UniLabJsonCommand
auto-update_push_ip:
feedback: {}
goal: {}
goal_default:
ip: null
port: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ip:
type: string
port:
type: string
required: []
type: object
result: {}
required:
- goal
title: update_push_ip参数
type: object
type: UniLabJsonCommand
auto-wait_for_order_finish:
feedback: {}
goal: {}
goal_default:
order_code: null
timeout: 1800
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
order_code:
type: string
timeout:
default: 1800
type: integer
required:
- order_code
type: object
result: {}
required:
- goal
title: wait_for_order_finish参数
type: object
type: UniLabJsonCommand
auto-wait_for_transfer_task:
feedback: {}
goal: {}

View File

@@ -0,0 +1,584 @@
coincellassemblyworkstation_device:
category:
- coin_cell_workstation
class:
action_value_mappings:
auto-change_hole_sheet_to_2:
feedback: {}
goal: {}
goal_default:
hole: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
hole:
type: object
required:
- hole
type: object
result: {}
required:
- goal
title: change_hole_sheet_to_2参数
type: object
type: UniLabJsonCommandAsync
auto-fill_plate:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fill_plate参数
type: object
type: UniLabJsonCommandAsync
auto-fun_wuliao_test:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fun_wuliao_test参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
type: integer
assembly_type:
default: 7
type: integer
elec_num:
type: string
elec_use_num:
type: string
elec_vol:
default: 50
type: integer
file_path:
default: D:\coin_cell_data
type: string
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_allpack_cmd参数
type: object
type: UniLabJsonCommand
auto-func_get_csv_export_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_get_csv_export_status参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_auto:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_auto参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_init:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_init参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_start参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_stop参数
type: object
type: UniLabJsonCommand
auto-func_pack_get_msg_cmd:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_pack_get_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_bottle_num:
feedback: {}
goal: {}
goal_default:
bottle_num: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
bottle_num:
type: string
required:
- bottle_num
type: object
result: {}
required:
- goal
title: func_pack_send_bottle_num参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_finished_cmd:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_send_finished_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_msg_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: null
assembly_type: null
elec_use_num: null
elec_vol: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
type: string
assembly_type:
type: string
elec_use_num:
type: string
elec_vol:
type: string
required:
- elec_use_num
- elec_vol
- assembly_type
- assembly_pressure
type: object
result: {}
required:
- goal
title: func_pack_send_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_read_data_and_output:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_read_data_and_output参数
type: object
type: UniLabJsonCommand
auto-func_stop_read_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_stop_read_data参数
type: object
type: UniLabJsonCommand
auto-modify_deck_name:
feedback: {}
goal: {}
goal_default:
resource_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_name:
type: string
required:
- resource_name
type: object
result: {}
required:
- goal
title: modify_deck_name参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-qiming_coin_cell_code:
feedback: {}
goal: {}
goal_default:
battery_clean_ignore: false
battery_pressure: 4000
battery_pressure_mode: true
fujipian_juzhendianwei: 0
fujipian_panshu: null
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
battery_clean_ignore:
default: false
type: boolean
battery_pressure:
default: 4000
type: integer
battery_pressure_mode:
default: true
type: boolean
fujipian_juzhendianwei:
default: 0
type: integer
fujipian_panshu:
type: integer
gemo_juzhendianwei:
default: 0
type: integer
gemopanshu:
default: 0
type: integer
lvbodian:
default: true
type: boolean
required:
- fujipian_panshu
type: object
result: {}
required:
- goal
title: qiming_coin_cell_code参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
data_axis_x_pos: float
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_open_circuit_voltage: float
data_pole_weight: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
sys_status: str
type: python
config_info: []
description: ''
handles: []
icon: ''
init_param_schema:
config:
properties:
address:
default: 172.21.32.111
type: string
debug_mode:
default: false
type: boolean
deck:
type: object
port:
default: '502'
type: string
required: []
type: object
data:
properties:
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
type: integer
data_assembly_time:
type: number
data_axis_x_pos:
type: number
data_axis_y_pos:
type: number
data_axis_z_pos:
type: number
data_coin_cell_code:
type: string
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
type: boolean
sys_mode:
type: string
sys_status:
type: string
required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num
- data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code
- data_electrolyte_code
- data_glove_box_pressure
- data_glove_box_o2_content
- data_glove_box_water_content
type: object
registry_type: device
version: 1.0.0

View File

@@ -1361,7 +1361,8 @@ laiyu_liquid:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -1491,9 +1492,11 @@ laiyu_liquid:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648

View File

@@ -3994,7 +3994,8 @@ liquid_handler:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -4150,9 +4151,11 @@ liquid_handler:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -5012,7 +5015,8 @@ liquid_handler.biomek:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -5155,9 +5159,11 @@ liquid_handler.biomek:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648
@@ -7801,7 +7807,8 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0
mix_rate: 0
mix_stage: ''
mix_times: 0
mix_times:
- 0
mix_vol: 0
none_keys:
- ''
@@ -7930,9 +7937,11 @@ liquid_handler.prcxi:
mix_stage:
type: string
mix_times:
maximum: 2147483647
minimum: -2147483648
type: integer
items:
maximum: 2147483647
minimum: -2147483648
type: integer
type: array
mix_vol:
maximum: 2147483647
minimum: -2147483648

View File

@@ -1,739 +1 @@
reaction_station.bioyond:
category:
- work_station
- reaction_station_bioyond
class:
action_value_mappings:
auto-append_to_workflow_sequence:
feedback: {}
goal: {}
goal_default:
web_workflow_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
web_workflow_name:
type: string
required:
- web_workflow_name
type: object
result: {}
required:
- goal
title: append_to_workflow_sequence参数
type: object
type: UniLabJsonCommand
auto-clear_workflows:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: clear_workflows参数
type: object
type: UniLabJsonCommand
auto-load_bioyond_data_from_file:
feedback: {}
goal: {}
goal_default:
file_path: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
type: string
required:
- file_path
type: object
result: {}
required:
- goal
title: load_bioyond_data_from_file参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-process_web_workflows:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string
required:
- json_str
type: object
result: {}
required:
- goal
title: process_web_workflows参数
type: object
type: UniLabJsonCommand
auto-reset_workstation:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: reset_workstation参数
type: object
type: UniLabJsonCommand
auto-resource_tree_add:
feedback: {}
goal: {}
goal_default:
resources: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resources:
items:
type: object
type: array
required:
- resources
type: object
result: {}
required:
- goal
title: resource_tree_add参数
type: object
type: UniLabJsonCommand
auto-set_workflow_sequence:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string
required:
- json_str
type: object
result: {}
required:
- goal
title: set_workflow_sequence参数
type: object
type: UniLabJsonCommand
auto-transfer_resource_to_another:
feedback: {}
goal: {}
goal_default:
mount_device_id: null
mount_resource: null
resource: null
sites: null
handles: {}
placeholder_keys:
mount_device_id: unilabos_devices
mount_resource: unilabos_resources
resource: unilabos_resources
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
mount_device_id:
type: object
mount_resource:
items:
properties:
category:
type: string
children:
items:
type: string
type: array
config:
type: string
data:
type: string
id:
type: string
name:
type: string
parent:
type: string
pose:
properties:
orientation:
properties:
w:
type: number
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
- w
title: orientation
type: object
position:
properties:
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
title: position
type: object
required:
- position
- orientation
title: pose
type: object
sample_id:
type: string
type:
type: string
required:
- id
- name
- sample_id
- children
- parent
- type
- category
- pose
- config
- data
title: mount_resource
type: object
type: array
resource:
items:
properties:
category:
type: string
children:
items:
type: string
type: array
config:
type: string
data:
type: string
id:
type: string
name:
type: string
parent:
type: string
pose:
properties:
orientation:
properties:
w:
type: number
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
- w
title: orientation
type: object
position:
properties:
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
title: position
type: object
required:
- position
- orientation
title: pose
type: object
sample_id:
type: string
type:
type: string
required:
- id
- name
- sample_id
- children
- parent
- type
- category
- pose
- config
- data
title: resource
type: object
type: array
sites:
items:
type: string
type: array
required:
- resource
- mount_resource
- sites
- mount_device_id
type: object
result: {}
required:
- goal
title: transfer_resource_to_another参数
type: object
type: UniLabJsonCommand
bioyond_sync:
feedback: {}
goal:
force_sync: force_sync
sync_type: sync_type
goal_default:
force_sync: false
sync_type: full
handles: {}
result: {}
schema:
description: 从Bioyond系统同步物料
properties:
feedback: {}
goal:
properties:
force_sync:
description: 是否强制同步
type: boolean
sync_type:
description: 同步类型
enum:
- full
- incremental
type: string
required:
- sync_type
type: object
result: {}
required:
- goal
title: bioyond_sync参数
type: object
type: UniLabJsonCommand
bioyond_update:
feedback: {}
goal:
material_ids: material_ids
sync_all: sync_all
goal_default:
material_ids: []
sync_all: true
handles: {}
result: {}
schema:
description: 将本地物料变更同步到Bioyond
properties:
feedback: {}
goal:
properties:
material_ids:
description: 要同步的物料ID列表
items:
type: string
type: array
sync_all:
description: 是否同步所有物料
type: boolean
required:
- sync_all
type: object
result: {}
required:
- goal
title: bioyond_update参数
type: object
type: UniLabJsonCommand
reaction_station_drip_back:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 反应站滴回操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 溶剂名称
type: string
time:
description: 观察时间单位min
type: string
torque_variation:
description: 是否观察1否2是
type: string
volume:
description: 投料体积
type: string
required:
- volume
- assign_material_name
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_drip_back参数
type: object
type: UniLabJsonCommand
reaction_station_liquid_feed:
feedback: {}
goal:
assign_material_name: assign_material_name
time: time
titration_type: titration_type
torque_variation: torque_variation
volume: volume
goal_default:
assign_material_name: ''
time: ''
titration_type: ''
torque_variation: ''
volume: ''
handles: {}
result: {}
schema:
description: 反应站液体进料操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 溶剂名称
type: string
time:
description: 观察时间单位min
type: string
titration_type:
description: 滴定类型1否2是
type: string
torque_variation:
description: 是否观察1否2是
type: string
volume:
description: 投料体积
type: string
required:
- titration_type
- volume
- assign_material_name
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_liquid_feed参数
type: object
type: UniLabJsonCommand
reaction_station_process_execute:
feedback: {}
goal:
task_name: task_name
workflow_name: workflow_name
goal_default:
task_name: ''
workflow_name: ''
handles: {}
result: {}
schema:
description: 反应站流程执行
properties:
feedback: {}
goal:
properties:
task_name:
description: 任务名称
type: string
workflow_name:
description: 工作流名称
type: string
required:
- workflow_name
- task_name
type: object
result: {}
required:
- goal
title: reaction_station_process_execute参数
type: object
type: UniLabJsonCommand
reaction_station_reactor_taken_out:
feedback: {}
goal:
order_id: order_id
preintake_id: preintake_id
goal_default:
order_id: ''
preintake_id: ''
handles: {}
result: {}
schema:
description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制
properties:
feedback: {}
goal:
properties:
order_id:
description: 订单ID用于标识要取出的订单
type: string
preintake_id:
description: 预取样ID用于标识具体的取样任务
type: string
required: []
type: object
result:
properties:
code:
description: 操作结果代码1表示成功0表示失败
type: integer
return_info:
description: 操作结果详细信息
type: string
type: object
required:
- goal
title: reaction_station_reactor_taken_out参数
type: object
type: UniLabJsonCommand
reaction_station_solid_feed_vial:
feedback: {}
goal:
assign_material_name: assign_material_name
material_id: material_id
time: time
torque_variation: torque_variation
goal_default:
assign_material_name: ''
material_id: ''
time: ''
torque_variation: ''
handles: {}
result: {}
schema:
description: 反应站固体进料操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 固体名称_粉末加样模块-投料
type: string
material_id:
description: 固体投料类型_粉末加样模块-投料
type: string
time:
description: 观察时间_反应模块-观察搅拌结果
type: string
torque_variation:
description: 是否观察1否2是_反应模块-观察搅拌结果
type: string
required:
- assign_material_name
- material_id
- time
- torque_variation
type: object
result: {}
required:
- goal
title: reaction_station_solid_feed_vial参数
type: object
type: UniLabJsonCommand
reaction_station_take_in:
feedback: {}
goal:
assign_material_name: assign_material_name
cutoff: cutoff
temperature: temperature
goal_default:
assign_material_name: ''
cutoff: ''
temperature: ''
handles: {}
result: {}
schema:
description: 反应站取入操作
properties:
feedback: {}
goal:
properties:
assign_material_name:
description: 物料名称
type: string
cutoff:
description: 截止参数
type: string
temperature:
description: 温度
type: string
required:
- cutoff
- temperature
- assign_material_name
type: object
result: {}
required:
- goal
title: reaction_station_take_in参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
protocol_type: []
status_types:
all_workflows: dict
bioyond_status: dict
station_info: dict
workstation_status: dict
type: python
config_info: []
description: Bioyond反应站 - 专门用于化学反应操作的工作站
handles: []
icon: 反应站.webp
init_param_schema:
config:
properties:
bioyond_config:
type: string
deck:
type: string
required: []
type: object
data:
properties:
all_workflows:
type: object
bioyond_status:
type: object
station_info:
type: object
workstation_status:
type: object
required:
- bioyond_status
- all_workflows
- station_info
- workstation_status
type: object
version: 1.0.0
{}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,65 @@
YB_Pipette_Tip:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_Pipette_Tip
type: pylabrobot
description: YB_Pipette_Tip
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_fen_ye_20ml_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_fen_ye_20ml_Bottle
type: pylabrobot
description: YB_fen_ye_20ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_fen_ye_5ml_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_fen_ye_5ml_Bottle
type: pylabrobot
description: YB_fen_ye_5ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_da_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
type: pylabrobot
description: YB_pei_ye_da_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_xiao_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_xiao_Bottle
type: pylabrobot
description: YB_pei_ye_xiao_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -1,19 +1,20 @@
YB_jia_yang_tou_da_1X1_carrier:
YB_1Bottle100mlCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_1X1_carrier
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_1Bottle100mlCarrier
type: pylabrobot
description: YB_jia_yang_tou_da_1X1_carrier
description: YB_1Bottle100mlCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_1BottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_1BottleCarrier
type: pylabrobot
@@ -23,3 +24,185 @@ YB_1BottleCarrier:
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_1GaoNianYeBottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_1GaoNianYeBottleCarrier
type: pylabrobot
description: YB_1GaoNianYeBottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_4x_LargeSolutionBottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_4x_LargeSolutionBottleCarrier
type: pylabrobot
description: YB_4x_LargeSolutionBottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6StockCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
type: pylabrobot
description: YB_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6VialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
type: pylabrobot
description: YB_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6x20ml_DispensingVialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6x20ml_DispensingVialCarrier
type: pylabrobot
description: YB_6x20ml_DispensingVialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6x5ml_DispensingVialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6x5ml_DispensingVialCarrier
type: pylabrobot
description: YB_6x5ml_DispensingVialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6x_SmallSolutionBottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6x_SmallSolutionBottleCarrier
type: pylabrobot
description: YB_6x_SmallSolutionBottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_AdapterBlock:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_AdapterBlock
type: pylabrobot
description: YB_AdapterBlock
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_TipBox:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_TipBox
type: pylabrobot
description: YB_TipBox
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gao_nian_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
type: pylabrobot
description: YB_gao_nian_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
type: pylabrobot
description: YB_jia_yang_tou_da
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da_1X1_carrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_1X1_carrier
type: pylabrobot
description: YB_jia_yang_tou_da_1X1_carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_100ml_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
type: pylabrobot
description: YB_ye_100ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
type: pylabrobot
description: YB_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -3,7 +3,14 @@ from pylabrobot.resources import create_homogeneous_resources, Coordinate, Resou
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
from unilabos.resources.bioyond.YB_bottles import (
YB_jia_yang_tou_da,
YB_ye_Bottle
YB_ye_Bottle,
YB_ye_100ml_Bottle,
YB_gao_nian_ye_Bottle,
YB_fen_ye_5ml_Bottle,
YB_fen_ye_20ml_Bottle,
YB_pei_ye_xiao_Bottle,
YB_pei_ye_da_Bottle,
YB_Pipette_Tip,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
@@ -53,8 +60,8 @@ def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
for i in range(6):
carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}")
# for i in range(6):
# carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}")
return carrier
@@ -91,7 +98,7 @@ def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
# carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
return carrier
@@ -141,8 +148,8 @@ def YB_6StockCarrier(name: str) -> BottleCarrier:
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(6):
carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
# for i in range(6):
# carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
@@ -192,13 +199,13 @@ def YB_6VialCarrier(name: str) -> BottleCarrier:
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(3):
carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
for i in range(3, 6):
carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
# for i in range(3):
# carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
# for i in range(3, 6):
# carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier
"""1瓶载架 - 单个中央位置"""
# 1瓶载架 - 单个中央位置
def YB_1BottleCarrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
@@ -235,7 +242,80 @@ def YB_1BottleCarrier(name: str) -> BottleCarrier:
return carrier
"""5ml分液瓶板 - 4x2布局8个位置"""
# 高粘液瓶载架 - 单个中央位置
def YB_1GaoNianYeBottleCarrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="1GaoNianYeBottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
return carrier
# 100ml液体瓶载架 - 单个中央位置
def YB_1Bottle100mlCarrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="1Bottle100mlCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
return carrier
# 5ml分液瓶板 - 4x2布局8个位置
def YB_6x5ml_DispensingVialCarrier(name: str) -> BottleCarrier:
@@ -282,10 +362,10 @@ def YB_6x5ml_DispensingVialCarrier(name: str) -> BottleCarrier:
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_5ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}")
carrier[i] = YB_fen_ye_5ml_Bottle(f"{name}_vial_{ordering[i]}")
return carrier
"""20ml分液瓶板 - 4x2布局8个位置"""
# 20ml分液瓶板 - 4x2布局8个位置
def YB_6x20ml_DispensingVialCarrier(name: str) -> BottleCarrier:
@@ -332,10 +412,10 @@ def YB_6x20ml_DispensingVialCarrier(name: str) -> BottleCarrier:
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_20ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}")
carrier[i] = YB_fen_ye_20ml_Bottle(f"{name}_vial_{ordering[i]}")
return carrier
"""配液瓶(小)板 - 4x2布局8个位置"""
# 配液瓶(小)板 - 4x2布局8个位置
def YB_6x_SmallSolutionBottleCarrier(name: str) -> BottleCarrier:
@@ -382,12 +462,12 @@ def YB_6x_SmallSolutionBottleCarrier(name: str) -> BottleCarrier:
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = YB_Small_Solution_Bottle(f"{name}_bottle_{ordering[i]}")
carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 配液瓶(大)板 - 2x2布局4个位置
def YB_4x_LargeSolutionBottleCarrier(name: str) -> BottleCarrier:
"""配液瓶(大)板 - 2x2布局4个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -432,10 +512,10 @@ def YB_4x_LargeSolutionBottleCarrier(name: str) -> BottleCarrier:
carrier.num_items_z = 1
ordering = ["A1", "A2", "B1", "B2"]
for i in range(4):
carrier[i] = YB_Large_Solution_Bottle(f"{name}_bottle_{ordering[i]}")
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
"""加样头(大)板 - 1x1布局1个位置"""
# 加样头(大)板 - 1x1布局1个位置
def YB_jia_yang_tou_da_1X1_carrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)

View File

@@ -1,6 +1,5 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
"""加样头(大)"""
def YB_jia_yang_tou_da(
name: str,
@@ -36,3 +35,129 @@ def YB_ye_Bottle(
barcode=barcode,
model="Liquid_Bottle",
)
"""100ml液体"""
def YB_ye_100ml_Bottle(
name: str,
diameter: float = 50.0,
height: float = 90.0,
max_volume: float = 100000.0, # 100mL
barcode: str = None,
) -> Bottle:
"""创建100ml液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Liquid_Bottle_100ml",
)
"""高粘液"""
def YB_gao_nian_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建高粘液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="High_Viscosity_Liquid",
)
"""5ml分液瓶"""
def YB_fen_ye_5ml_Bottle(
name: str,
diameter: float = 20.0,
height: float = 50.0,
max_volume: float = 5000.0, # 5mL
barcode: str = None,
) -> Bottle:
"""创建5ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Separation_Bottle_5ml",
)
"""20ml分液瓶"""
def YB_fen_ye_20ml_Bottle(
name: str,
diameter: float = 30.0,
height: float = 65.0,
max_volume: float = 20000.0, # 20mL
barcode: str = None,
) -> Bottle:
"""创建20ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Separation_Bottle_20ml",
)
"""配液瓶(小)"""
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(小)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Mixing_Bottle_Small",
)
"""配液瓶(大)"""
def YB_pei_ye_da_Bottle(
name: str,
diameter: float = 55.0,
height: float = 100.0,
max_volume: float = 150000.0, # 150mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(大)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Mixing_Bottle_Large",
)
"""枪头"""
def YB_Pipette_Tip(
name: str,
diameter: float = 10.0,
height: float = 50.0,
max_volume: float = 1000.0, # 1mL
barcode: str = None,
) -> Bottle:
"""创建枪头"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Pipette_Tip",
)

View File

@@ -18,6 +18,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
)
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return warehouse_factory(

View File

@@ -535,6 +535,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
def resource_ulab_to_plr_inner(resource: dict):
all_states[resource["name"]] = resource["data"]
extra = resource.pop("extra", {})
d = {
"name": resource["name"],
"type": resource["type"],
@@ -575,16 +576,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
replace_info = {
"plate": "plate",
"well": "well",
"tip_spot": "container",
"trash": "container",
"tip_spot": "tip_spot",
"trash": "trash",
"deck": "deck",
"tip_rack": "container",
"tip_rack": "tip_rack",
}
if source in replace_info:
return replace_info[source]
else:
print("转换pylabrobot的时候出现未知类型", source)
return "container"
return source
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
r = {

View File

@@ -78,6 +78,7 @@ class ItemizedCarrier(ResourcePLR):
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: Optional[str] = "carrier",
model: Optional[str] = None,
invisible_slots: Optional[str] = None,
):
super().__init__(
name=name,
@@ -89,6 +90,7 @@ class ItemizedCarrier(ResourcePLR):
)
self.num_items = len(sites)
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
self.invisible_slots = [] if invisible_slots is None else invisible_slots
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
if isinstance(sites, dict):
@@ -410,7 +412,7 @@ class ItemizedCarrier(ResourcePLR):
"layout": self.layout,
"sites": [{
"label": str(identifier),
"visible": True if self[identifier] is not None else False,
"visible": False if identifier in self.invisible_slots else True,
"occupied_by": self[identifier].name
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
self[identifier] if isinstance(self[identifier], str) else None,
@@ -433,6 +435,7 @@ class BottleCarrier(ItemizedCarrier):
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
category: str = "bottle_carrier",
model: Optional[str] = None,
invisible_slots: List[str] = None,
**kwargs,
):
super().__init__(
@@ -443,4 +446,5 @@ class BottleCarrier(ItemizedCarrier):
sites=sites,
category=category,
model=model,
invisible_slots=invisible_slots,
)

View File

@@ -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.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher
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.timer import Timer

View File

@@ -49,7 +49,7 @@ from unilabos_msgs.msg import Resource # type: ignore
from unilabos.ros.nodes.resource_tracker import (
DeviceNodeResourceTracker,
ResourceTreeSet,
ResourceTreeSet, ResourceTreeInstance,
)
from unilabos.ros.x.rclpyx import get_event_loop
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] = {
"resource_add": self.create_client(ResourceAdd, "/resources/add"),
"resource_get": self.create_client(SerialCommand, "/resources/get"),
"resource_delete": self.create_client(ResourceDelete, "/resources/delete"),
"resource_update": self.create_client(ResourceUpdate, "/resources/update"),
"resource_list": self.create_client(ResourceList, "/resources/list"),
"c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree"),
"resource_add": self.create_client(ResourceAdd, "/resources/add", callback_group=self.callback_group),
"resource_get": self.create_client(SerialCommand, "/resources/get", callback_group=self.callback_group),
"resource_delete": self.create_client(ResourceDelete, "/resources/delete", callback_group=self.callback_group),
"resource_update": self.create_client(ResourceUpdate, "/resources/update", callback_group=self.callback_group),
"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", callback_group=self.callback_group),
}
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().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):
"""
处理资源树更新请求
@@ -613,28 +659,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
plr_resources = tree_set.to_plr_resources()
for plr_resource, tree in zip(plr_resources, tree_set.trees):
self.resource_tracker.add_resource(plr_resource)
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 = {}
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()}"
)
self.transfer_to_new_resource(plr_resource, tree, additional_add_params)
func = getattr(self.driver_instance, "resource_tree_add", None)
if callable(func):
func(plr_resources)
@@ -647,6 +672,17 @@ class BaseROS2DeviceNode(Node, Generic[T]):
original_instance: ResourcePLR = self.resource_tracker.figure_resource(
{"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)
self.lab_logger().info(
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_name,
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}")
@@ -1500,7 +1536,7 @@ class ROS2DeviceNode:
asyncio.set_event_loop(loop)
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()
logger.info(f"循环线程已启动")

View File

@@ -285,7 +285,7 @@ class HostNode(BaseROS2DeviceNode):
# 创建定时器,定期发现设备
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相关属性
@@ -494,7 +494,7 @@ class HostNode(BaseROS2DeviceNode):
if len(init_new_res) > 1: # 一个物料,多个子节点
init_new_res = [init_new_res]
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_location = [bind_locations]
other_calling_param = [
@@ -618,7 +618,7 @@ class HostNode(BaseROS2DeviceNode):
topic,
lambda msg, d=device_id, p=property_name: self.property_callback(msg, d, p),
1,
callback_group=ReentrantCallbackGroup(),
callback_group=self.callback_group,
)
# 标记为已订阅
self._subscribed_topics.add(topic)
@@ -829,37 +829,37 @@ class HostNode(BaseROS2DeviceNode):
def _init_host_service(self):
self._resource_services: Dict[str, 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(
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(
ResourceDelete,
"/resources/delete",
self._resource_delete_callback,
callback_group=ReentrantCallbackGroup(),
callback_group=self.callback_group,
),
"resource_update": self.create_service(
ResourceUpdate,
"/resources/update",
self._resource_update_callback,
callback_group=ReentrantCallbackGroup(),
callback_group=self.callback_group,
),
"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(
SerialCommand,
"/node_info_update",
self._node_info_update_callback,
callback_group=ReentrantCallbackGroup(),
callback_group=self.callback_group,
),
"c2s_update_resource_tree": self.create_service(
SerialCommand,
"/c2s_update_resource_tree",
self._resource_tree_update_callback,
callback_group=ReentrantCallbackGroup(),
callback_group=self.callback_group,
),
}

View File

@@ -194,7 +194,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
action_type,
action_name,
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}")
return

View File

@@ -42,7 +42,9 @@ class ResourceDictPosition(BaseModel):
rotation: ResourceDictPositionObject = Field(
description="Resource rotation", default_factory=ResourceDictPositionObject
)
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(description="Cross section type", default="rectangle")
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(
description="Cross section type", default="rectangle"
)
# 统一的资源字典模型parent 自动序列化为 parent_uuidchildren 不序列化
@@ -51,7 +53,9 @@ class ResourceDict(BaseModel):
uuid: str = Field(description="Resource UUID")
name: str = Field(description="Resource name")
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)
icon: str = Field(description="Resource icon", default="")
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)
config: Dict[str, Any] = Field(description="Resource configuration")
data: Dict[str, Any] = Field(description="Resource data")
extra: Dict[str, Any] = Field(description="Extra data")
@field_serializer("parent_uuid")
def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]):
@@ -138,6 +143,8 @@ class ResourceDictInstance(object):
content["config"] = {}
if not content.get("data"):
content["data"] = {}
if not content.get("extra"): # MagicCode
content["extra"] = {}
if "pose" not in content:
content["pose"] = content.get("position", {})
return ResourceDictInstance(ResourceDict.model_validate(content))
@@ -311,28 +318,36 @@ class ResourceTreeSet(object):
"plate": "plate",
"well": "well",
"deck": "deck",
"tip_rack": "tip_rack",
"tip_spot": "tip_spot",
"tube": "tube",
"bottle_carrier": "bottle_carrier",
}
if source in replace_info:
return replace_info[source]
else:
print("转换pylabrobot的时候出现未知类型", source)
return "container"
return source
def build_uuid_mapping(res: "PLRResource", uuid_list: list):
"""递归构建uuid映射字典"""
def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None):
"""递归构建uuid和extra映射字典返回(current_uuid, parent_uuid, extra)元组列表"""
uid = getattr(res, "unilabos_uuid", "")
if not uid:
uid = str(uuid.uuid4())
res.unilabos_uuid = uid
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:
build_uuid_mapping(child, uuid_list)
build_uuid_mapping(child, uuid_list, uid)
def resource_plr_inner(
d: dict, parent_resource: Optional[ResourceDict], states: dict, uuids: list
) -> ResourceDictInstance:
current_uuid = uuids.pop(0)
current_uuid, parent_uuid, extra = uuids.pop(0)
raw_pos = (
{"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]}
@@ -355,13 +370,30 @@ class ResourceTreeSet(object):
"uuid": current_uuid,
"name": d["name"],
"parent": parent_resource, # 直接传入 ResourceDict 对象
"parent_uuid": parent_uuid, # 使用 parent_uuid 而不是 parent 对象
"type": replace_plr_type(d.get("category", "")),
"class": d.get("class", ""),
"position": pos,
"pose": pos,
"config": {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"]},
"config": {
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"]],
"extra": extra,
}
# 先转换为 ResourceDictInstance获取其中的 ResourceDict
@@ -379,7 +411,7 @@ class ResourceTreeSet(object):
for resource in resources:
# 构建uuid列表
uuid_list = []
build_uuid_mapping(resource, uuid_list)
build_uuid_mapping(resource, uuid_list, getattr(resource.parent, "unilabos_uuid", None))
serialized_data = resource.serialize()
all_states = resource.serialize_all_state()
@@ -402,14 +434,15 @@ class ResourceTreeSet(object):
import inspect
# 类型映射
TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"}
TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck", "container": "RegularContainer"}
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
"""一次遍历收集 name_to_uuid all_states"""
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict):
"""一次遍历收集 name_to_uuid, all_states 和 name_to_extra"""
name_to_uuid[node.res_content.name] = node.res_content.uuid
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:
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):
"""转换节点为 PLR 字典格式"""
@@ -419,6 +452,7 @@ class ResourceTreeSet(object):
logger.warning(f"未知类型 {res.type}")
d = {
**res.config,
"name": res.name,
"type": res.config.get("type", plr_type),
"size_x": res.config.get("size_x", 0),
@@ -434,33 +468,35 @@ class ResourceTreeSet(object):
"category": res.config.get("category", plr_type),
"children": [node_to_plr_dict(child, has_model) for child in node.children],
"parent_name": res.parent_instance_name,
**res.config,
}
if has_model:
d["model"] = res.config.get("model", None)
return d
plr_resources = []
trees = []
tracker = DeviceNodeResourceTracker()
for tree in self.trees:
name_to_uuid: Dict[str, str] = {}
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"
plr_dict = node_to_plr_dict(tree.root_node, has_model)
try:
sub_cls = find_subclass(plr_dict["type"], PLRResource)
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)
if "category" not in spec.parameters:
plr_dict.pop("category", None)
plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True)
plr_resource.load_all_state(all_states)
# 使用 DeviceNodeResourceTracker 设置 UUID
# 使用 DeviceNodeResourceTracker 设置 UUID 和 Extra
tracker.loop_set_uuid(plr_resource, name_to_uuid)
tracker.loop_set_extra(plr_resource, name_to_extra)
plr_resources.append(plr_resource)
except Exception as e:
@@ -802,6 +838,20 @@ class DeviceNodeResourceTracker(object):
else:
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:
"""
递归遍历资源树,对每个节点执行处理函数
@@ -850,6 +900,29 @@ class DeviceNodeResourceTracker(object):
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:
"""
递归遍历资源树更新所有节点的uuid
@@ -892,7 +965,9 @@ class DeviceNodeResourceTracker(object):
if current_uuid:
old = self.uuid_to_resources.get(current_uuid)
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
self._traverse_and_process(resource, process)