mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-05 05:45:10 +00:00
Compare commits
41 Commits
dev
...
9a06ef3836
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a06ef3836 | ||
|
|
ce3f2b33c5 | ||
|
|
6f143b068b | ||
|
|
03423e4791 | ||
|
|
9fc6781406 | ||
|
|
9753ef02c3 | ||
|
|
6a0614c0c9 | ||
|
|
a25e8f6853 | ||
|
|
5c249e66a2 | ||
|
|
93ac095e0a | ||
|
|
288d9fea91 | ||
|
|
81b28cef71 | ||
|
|
d57e5ffdae | ||
|
|
d5e0d76311 | ||
|
|
beaa1d7213 | ||
|
|
1e5f6b0c04 | ||
|
|
5ae89d8607 | ||
|
|
74d0ea3379 | ||
|
|
440c9965fd | ||
|
|
9cac852bc3 | ||
|
|
de662a42aa | ||
|
|
632f9b90d1 | ||
|
|
d7c970d244 | ||
|
|
2c69e663a7 | ||
|
|
f03ff96ae4 | ||
|
|
c68903ed83 | ||
|
|
8efbbbe72a | ||
|
|
4a23b05abc | ||
|
|
6d8884a2c7 | ||
|
|
c0e7a69553 | ||
|
|
fb6ee79577 | ||
|
|
dbe129caab | ||
|
|
7250995891 | ||
|
|
68eddbdffd | ||
|
|
32bd234176 | ||
|
|
3d62e8bf6c | ||
|
|
efec1dd501 | ||
|
|
c16756ddb3 | ||
|
|
daf41871a1 | ||
|
|
6b0b28becf | ||
|
|
0f7366f3ee |
@@ -501,6 +501,8 @@ class BaseClient(UniversalDriver):
|
|||||||
val = result_dict.get("value")
|
val = result_dict.get("value")
|
||||||
err = result_dict.get("error")
|
err = result_dict.get("error")
|
||||||
|
|
||||||
|
print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}), 错误 = {err}")
|
||||||
|
|
||||||
print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}, 错误 = {err}")
|
print(f"读取 {node_name} 返回值 = {val} (类型: {type(val).__name__}, 错误 = {err}")
|
||||||
return val, err
|
return val, err
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1193,6 +1195,7 @@ class OpcUaClient(BaseClient):
|
|||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
# ===== 关键修改:参照 BioyondWorkstation 处理 deck =====
|
# ===== 关键修改:参照 BioyondWorkstation 处理 deck =====
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -1373,6 +1376,23 @@ class OpcUaClient(BaseClient):
|
|||||||
# 处理名称映射
|
# 处理名称映射
|
||||||
if name in self._name_mapping:
|
if name in self._name_mapping:
|
||||||
chinese_name = self._name_mapping[name]
|
chinese_name = self._name_mapping[name]
|
||||||
|
# 优先从缓存获取值
|
||||||
|
if chinese_name in self._node_values:
|
||||||
|
return self._node_values[chinese_name]
|
||||||
|
# 缓存中没有则直接读取
|
||||||
|
value, _ = self.use_node(chinese_name).read()
|
||||||
|
return value
|
||||||
|
# 如果提供的是中文名,直接使用
|
||||||
|
elif name in self._node_registry:
|
||||||
|
# 优先从缓存获取值
|
||||||
|
if name in self._node_values:
|
||||||
|
return self._node_values[name]
|
||||||
|
# 缓存中没有则直接读取
|
||||||
|
value, _ = self.use_node(name).read()
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise ValueError(f"未找到名称为 '{name}' 的节点")
|
||||||
|
|
||||||
elif name in self._node_registry:
|
elif name in self._node_registry:
|
||||||
chinese_name = name
|
chinese_name = name
|
||||||
else:
|
else:
|
||||||
@@ -1433,6 +1453,22 @@ class OpcUaClient(BaseClient):
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"未找到名称为 '{name}' 的节点")
|
raise ValueError(f"未找到名称为 '{name}' 的节点")
|
||||||
|
|
||||||
|
# 写入值
|
||||||
|
error = node.write(value)
|
||||||
|
if not error:
|
||||||
|
# 更新缓存
|
||||||
|
if hasattr(node, 'name'):
|
||||||
|
self._node_values[node.name] = value
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _refresh_worker(self):
|
||||||
|
"""节点值刷新线程的工作函数"""
|
||||||
|
self._refresh_running = True
|
||||||
|
logger.info(f"节点值刷新线程已启动,刷新间隔: {self._refresh_interval}秒")
|
||||||
|
|
||||||
|
while self._refresh_running:
|
||||||
|
|
||||||
with self._client_lock:
|
with self._client_lock:
|
||||||
try:
|
try:
|
||||||
node = self.use_node(chinese_name)
|
node = self.use_node(chinese_name)
|
||||||
@@ -1477,6 +1513,17 @@ class OpcUaClient(BaseClient):
|
|||||||
|
|
||||||
while self._connection_monitor_running:
|
while self._connection_monitor_running:
|
||||||
try:
|
try:
|
||||||
|
self.refresh_node_values()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"节点值刷新过程出错: {e}")
|
||||||
|
|
||||||
|
# 等待下一次刷新
|
||||||
|
time.sleep(self._refresh_interval)
|
||||||
|
|
||||||
|
def start_node_refresh(self):
|
||||||
|
"""启动节点值刷新线程"""
|
||||||
|
if self._refresh_thread is not None and self._refresh_thread.is_alive():
|
||||||
|
logger.warning("节点值刷新线程已在运行")
|
||||||
# 检查连接状态
|
# 检查连接状态
|
||||||
if not self._check_connection():
|
if not self._check_connection():
|
||||||
logger.warning("检测到连接断开,尝试重新连接...")
|
logger.warning("检测到连接断开,尝试重新连接...")
|
||||||
@@ -1523,6 +1570,16 @@ class OpcUaClient(BaseClient):
|
|||||||
return
|
return
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
self._refresh_thread = threading.Thread(target=self._refresh_worker, daemon=True)
|
||||||
|
self._refresh_thread.start()
|
||||||
|
|
||||||
|
def stop_node_refresh(self):
|
||||||
|
"""停止节点值刷新线程"""
|
||||||
|
self._refresh_running = False
|
||||||
|
if self._refresh_thread and self._refresh_thread.is_alive():
|
||||||
|
self._refresh_thread.join(timeout=2.0)
|
||||||
|
logger.info("节点值刷新线程已停止")
|
||||||
|
|
||||||
self._connection_monitor_thread = threading.Thread(
|
self._connection_monitor_thread = threading.Thread(
|
||||||
target=self._connection_monitor_worker,
|
target=self._connection_monitor_worker,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
@@ -1621,12 +1678,16 @@ class OpcUaClient(BaseClient):
|
|||||||
if "register_node_list_from_csv_path" in config_data:
|
if "register_node_list_from_csv_path" in config_data:
|
||||||
config_dir = os.path.dirname(os.path.abspath(config_path))
|
config_dir = os.path.dirname(os.path.abspath(config_path))
|
||||||
|
|
||||||
|
# 处理CSV路径,如果是相对路径,则相对于配置文件所在目录
|
||||||
|
|
||||||
if "path" in config_data["register_node_list_from_csv_path"]:
|
if "path" in config_data["register_node_list_from_csv_path"]:
|
||||||
csv_path = config_data["register_node_list_from_csv_path"]["path"]
|
csv_path = config_data["register_node_list_from_csv_path"]["path"]
|
||||||
if not os.path.isabs(csv_path):
|
if not os.path.isabs(csv_path):
|
||||||
csv_path = os.path.join(config_dir, csv_path)
|
csv_path = os.path.join(config_dir, csv_path)
|
||||||
config_data["register_node_list_from_csv_path"]["path"] = csv_path
|
config_data["register_node_list_from_csv_path"]["path"] = csv_path
|
||||||
|
|
||||||
|
# 直接使用字典
|
||||||
|
|
||||||
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
|
self.register_node_list_from_csv_path(**config_data["register_node_list_from_csv_path"])
|
||||||
|
|
||||||
if self.client and self._variables_to_find:
|
if self.client and self._variables_to_find:
|
||||||
@@ -1638,7 +1699,7 @@ class OpcUaClient(BaseClient):
|
|||||||
self.create_workflow_from_json(config_data["create_flow"])
|
self.create_workflow_from_json(config_data["create_flow"])
|
||||||
self.register_workflows_as_methods()
|
self.register_workflows_as_methods()
|
||||||
|
|
||||||
# 将所有节点注册为属性
|
# 将所有节点注册为属性(只注册已找到的节点)
|
||||||
self._register_nodes_as_attributes()
|
self._register_nodes_as_attributes()
|
||||||
|
|
||||||
# 打印统计信息
|
# 打印统计信息
|
||||||
@@ -1658,7 +1719,98 @@ class OpcUaClient(BaseClient):
|
|||||||
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
logger.error(f"加载配置文件 {config_path} 失败: {e}")
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def print_node_registry_status(self):
|
||||||
|
"""打印节点注册状态,用于调试"""
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("节点注册状态诊断报告")
|
||||||
|
print("="*80)
|
||||||
|
print(f"\n待查找节点总数: {len(self._variables_to_find)}")
|
||||||
|
print(f"已找到节点总数: {len(self._node_registry)}")
|
||||||
|
print(f"未找到节点总数: {len(self._variables_to_find) - len(self._node_registry)}")
|
||||||
|
|
||||||
|
# 显示已找到的节点(前10个)
|
||||||
|
if self._node_registry:
|
||||||
|
print(f"\n✓ 已找到的节点 (显示前10个):")
|
||||||
|
for i, (name, node) in enumerate(list(self._node_registry.items())[:10]):
|
||||||
|
eng_name = self._reverse_mapping.get(name, "")
|
||||||
|
eng_info = f" ({eng_name})" if eng_name else ""
|
||||||
|
print(f" {i+1}. '{name}'{eng_info}")
|
||||||
|
print(f" NodeId: {node.node_id}")
|
||||||
|
print(f" Type: {node.type}")
|
||||||
|
|
||||||
|
# 显示未找到的节点
|
||||||
|
not_found = [name for name in self._variables_to_find if name not in self._node_registry]
|
||||||
|
if not_found:
|
||||||
|
print(f"\n✗ 未找到的节点 (显示前20个):")
|
||||||
|
for i, name in enumerate(not_found[:20]):
|
||||||
|
eng_name = self._reverse_mapping.get(name, "")
|
||||||
|
eng_info = f" ({eng_name})" if eng_name else ""
|
||||||
|
node_info = self._variables_to_find[name]
|
||||||
|
print(f" {i+1}. '{name}'{eng_info} - {node_info['node_type']}")
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print("提示:")
|
||||||
|
print("1. 如果大量节点未找到,请检查CSV中的节点名称是否与服务器BrowseName完全匹配")
|
||||||
|
print("2. 可以使用 client.browse_server_nodes() 查看服务器的实际节点结构")
|
||||||
|
print("3. 节点名称区分大小写,且包括所有空格和特殊字符")
|
||||||
|
print("="*80 + "\n")
|
||||||
|
|
||||||
|
def browse_server_nodes(self, max_depth=3, start_path=["0:Objects"]):
|
||||||
|
"""浏览服务器节点树,用于调试和对比"""
|
||||||
|
if not self.client:
|
||||||
|
print("客户端未连接")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("\n" + "="*80)
|
||||||
|
print(f"服务器节点浏览 (最大深度: {max_depth})")
|
||||||
|
print("="*80 + "\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
root = self.client.get_root_node()
|
||||||
|
start_node = root.get_child(start_path)
|
||||||
|
self._browse_node_recursive(start_node, depth=0, max_depth=max_depth)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"浏览失败: {e}")
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
def _browse_node_recursive(self, node, depth=0, max_depth=3):
|
||||||
|
"""递归浏览节点"""
|
||||||
|
if depth > max_depth:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
browse_name = node.get_browse_name()
|
||||||
|
node_class = node.get_node_class()
|
||||||
|
indent = " " * depth
|
||||||
|
|
||||||
|
# 显示节点信息
|
||||||
|
print(f"{indent}├─ {browse_name.Name}")
|
||||||
|
print(f"{indent}│ NodeId: {str(node.nodeid)}")
|
||||||
|
print(f"{indent}│ NodeClass: {node_class}")
|
||||||
|
|
||||||
|
# 如果是变量,显示数据类型
|
||||||
|
if node_class == NodeClass.Variable:
|
||||||
|
try:
|
||||||
|
data_type = node.get_data_type()
|
||||||
|
print(f"{indent}│ DataType: {data_type}")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 递归处理子节点(限制数量避免输出过多)
|
||||||
|
if depth < max_depth:
|
||||||
|
children = node.get_children()
|
||||||
|
for i, child in enumerate(children[:20]): # 每层最多显示20个子节点
|
||||||
|
self._browse_node_recursive(child, depth + 1, max_depth)
|
||||||
|
if len(children) > 20:
|
||||||
|
print(f"{indent} ... ({len(children) - 20} more children)")
|
||||||
|
except Exception as e:
|
||||||
|
# 忽略单个节点的错误
|
||||||
|
pass
|
||||||
|
|
||||||
def disconnect(self):
|
def disconnect(self):
|
||||||
|
# 停止刷新线程
|
||||||
|
self.stop_node_refresh()
|
||||||
|
|
||||||
"""断开连接并清理资源"""
|
"""断开连接并清理资源"""
|
||||||
logger.info("正在断开连接...")
|
logger.info("正在断开连接...")
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,40 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
print(f"add material data: {response['data']}")
|
print(f"add material data: {response['data']}")
|
||||||
return response.get("data", {})
|
|
||||||
|
# 自动更新缓存
|
||||||
|
data = response.get("data", {})
|
||||||
|
if data:
|
||||||
|
if isinstance(data, str):
|
||||||
|
# 如果返回的是字符串,通常是ID
|
||||||
|
mat_id = data
|
||||||
|
name = params.get("name")
|
||||||
|
else:
|
||||||
|
# 如果返回的是字典,尝试获取name和id
|
||||||
|
name = data.get("name") or params.get("name")
|
||||||
|
mat_id = data.get("id")
|
||||||
|
|
||||||
|
if name and mat_id:
|
||||||
|
self.material_cache[name] = mat_id
|
||||||
|
print(f"已自动更新缓存: {name} -> {mat_id}")
|
||||||
|
|
||||||
|
# 处理返回数据中的 details (如果有)
|
||||||
|
# 有些 API 返回结构可能直接包含 details,或者在 data 字段中
|
||||||
|
details = data.get("details", []) if isinstance(data, dict) else []
|
||||||
|
if not details and isinstance(data, dict):
|
||||||
|
details = data.get("detail", [])
|
||||||
|
|
||||||
|
if details:
|
||||||
|
for detail in details:
|
||||||
|
d_name = detail.get("name")
|
||||||
|
# 尝试从不同字段获取 ID
|
||||||
|
d_id = detail.get("id") or detail.get("detailMaterialId")
|
||||||
|
|
||||||
|
if d_name and d_id:
|
||||||
|
self.material_cache[d_name] = d_id
|
||||||
|
print(f"已自动更新 detail 缓存: {d_name} -> {d_id}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
def query_matial_type_id(self, data) -> list:
|
def query_matial_type_id(self, data) -> list:
|
||||||
"""查找物料typeid"""
|
"""查找物料typeid"""
|
||||||
@@ -203,7 +236,7 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
params={
|
params={
|
||||||
"apiKey": self.api_key,
|
"apiKey": self.api_key,
|
||||||
"requestTime": self.get_current_time_iso8601(),
|
"requestTime": self.get_current_time_iso8601(),
|
||||||
"data": {},
|
"data": 0,
|
||||||
})
|
})
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return []
|
return []
|
||||||
@@ -273,6 +306,14 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
|
|
||||||
if not response or response['code'] != 1:
|
if not response or response['code'] != 1:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
# 自动更新缓存 - 移除被删除的物料
|
||||||
|
for name, mid in list(self.material_cache.items()):
|
||||||
|
if mid == material_id:
|
||||||
|
del self.material_cache[name]
|
||||||
|
print(f"已从缓存移除物料: {name}")
|
||||||
|
break
|
||||||
|
|
||||||
return response.get("data", {})
|
return response.get("data", {})
|
||||||
|
|
||||||
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
|
||||||
@@ -1103,6 +1144,10 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
for detail_material in detail_materials:
|
for detail_material in detail_materials:
|
||||||
detail_name = detail_material.get("name")
|
detail_name = detail_material.get("name")
|
||||||
detail_id = detail_material.get("detailMaterialId")
|
detail_id = detail_material.get("detailMaterialId")
|
||||||
|
if not detail_id:
|
||||||
|
# 尝试其他可能的字段
|
||||||
|
detail_id = detail_material.get("id")
|
||||||
|
|
||||||
if detail_name and detail_id:
|
if detail_name and detail_id:
|
||||||
self.material_cache[detail_name] = detail_id
|
self.material_cache[detail_name] = detail_id
|
||||||
print(f"加载detail材料: {detail_name} -> ID: {detail_id}")
|
print(f"加载detail材料: {detail_name} -> ID: {detail_id}")
|
||||||
@@ -1123,6 +1168,14 @@ class BioyondV1RPC(BaseRequest):
|
|||||||
print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}")
|
print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}")
|
||||||
return material_id
|
return material_id
|
||||||
|
|
||||||
|
# 如果缓存中没有,尝试刷新缓存
|
||||||
|
print(f"缓存中未找到材料 '{material_name_or_id}',尝试刷新缓存...")
|
||||||
|
self.refresh_material_cache()
|
||||||
|
if material_name_or_id in self.material_cache:
|
||||||
|
material_id = self.material_cache[material_name_or_id]
|
||||||
|
print(f"刷新缓存后找到材料: {material_name_or_id} -> ID: {material_id}")
|
||||||
|
return material_id
|
||||||
|
|
||||||
print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值")
|
print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值")
|
||||||
return material_name_or_id
|
return material_name_or_id
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import time
|
|||||||
from typing import Optional, Dict, Any, List
|
from typing import Optional, Dict, Any, List
|
||||||
from typing_extensions import TypedDict
|
from typing_extensions import TypedDict
|
||||||
import requests
|
import requests
|
||||||
|
import pint
|
||||||
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
|
||||||
|
|
||||||
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
|
||||||
@@ -43,6 +44,41 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
|
||||||
self.order_completion_status = {}
|
self.order_completion_status = {}
|
||||||
|
|
||||||
|
# 初始化 pint 单位注册表
|
||||||
|
self.ureg = pint.UnitRegistry()
|
||||||
|
|
||||||
|
# 化合物信息
|
||||||
|
self.compound_info = {
|
||||||
|
"MolWt": {
|
||||||
|
"MDA": 108.14 * self.ureg.g / self.ureg.mol,
|
||||||
|
"TDA": 122.16 * self.ureg.g / self.ureg.mol,
|
||||||
|
"PAPP": 521.62 * self.ureg.g / self.ureg.mol,
|
||||||
|
"BTDA": 322.23 * self.ureg.g / self.ureg.mol,
|
||||||
|
"BPDA": 294.22 * self.ureg.g / self.ureg.mol,
|
||||||
|
"6FAP": 366.26 * self.ureg.g / self.ureg.mol,
|
||||||
|
"PMDA": 218.12 * self.ureg.g / self.ureg.mol,
|
||||||
|
"MPDA": 108.14 * self.ureg.g / self.ureg.mol,
|
||||||
|
"SIDA": 248.51 * self.ureg.g / self.ureg.mol,
|
||||||
|
"ODA": 200.236 * self.ureg.g / self.ureg.mol,
|
||||||
|
"4,4'-ODA": 200.236 * self.ureg.g / self.ureg.mol,
|
||||||
|
"134": 292.34 * self.ureg.g / self.ureg.mol,
|
||||||
|
},
|
||||||
|
"FuncGroup": {
|
||||||
|
"MDA": "Amine",
|
||||||
|
"TDA": "Amine",
|
||||||
|
"PAPP": "Amine",
|
||||||
|
"BTDA": "Anhydride",
|
||||||
|
"BPDA": "Anhydride",
|
||||||
|
"6FAP": "Amine",
|
||||||
|
"MPDA": "Amine",
|
||||||
|
"SIDA": "Amine",
|
||||||
|
"PMDA": "Anhydride",
|
||||||
|
"ODA": "Amine",
|
||||||
|
"4,4'-ODA": "Amine",
|
||||||
|
"134": "Amine",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
def _post_project_api(self, endpoint: str, data: Any) -> Dict[str, Any]:
|
||||||
"""项目接口通用POST调用
|
"""项目接口通用POST调用
|
||||||
|
|
||||||
@@ -118,20 +154,22 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
ratio = json.loads(ratio)
|
ratio = json.loads(ratio)
|
||||||
except Exception:
|
except Exception:
|
||||||
ratio = {}
|
ratio = {}
|
||||||
root = str(Path(__file__).resolve().parents[3])
|
|
||||||
if root not in sys.path:
|
|
||||||
sys.path.append(root)
|
|
||||||
try:
|
|
||||||
mod = importlib.import_module("tem.compute")
|
|
||||||
except Exception as e:
|
|
||||||
raise BioyondException(f"无法导入计算模块: {e}")
|
|
||||||
try:
|
try:
|
||||||
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
wp = float(wt_percent) if isinstance(wt_percent, str) else wt_percent
|
||||||
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
mt = float(m_tot) if isinstance(m_tot, str) else m_tot
|
||||||
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
tp = float(titration_percent) if isinstance(titration_percent, str) else titration_percent
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BioyondException(f"参数解析失败: {e}")
|
raise BioyondException(f"参数解析失败: {e}")
|
||||||
res = mod.generate_experiment_design(ratio=ratio, wt_percent=wp, m_tot=mt, titration_percent=tp)
|
|
||||||
|
# 2. 调用内部计算方法
|
||||||
|
res = self._generate_experiment_design(
|
||||||
|
ratio=ratio,
|
||||||
|
wt_percent=wp,
|
||||||
|
m_tot=mt,
|
||||||
|
titration_percent=tp
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 构造返回结果
|
||||||
out = {
|
out = {
|
||||||
"solutions": res.get("solutions", []),
|
"solutions": res.get("solutions", []),
|
||||||
"titration": res.get("titration", {}),
|
"titration": res.get("titration", {}),
|
||||||
@@ -140,11 +178,248 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
"return_info": json.dumps(res, ensure_ascii=False)
|
"return_info": json.dumps(res, ensure_ascii=False)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
|
|
||||||
except BioyondException:
|
except BioyondException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BioyondException(str(e))
|
raise BioyondException(str(e))
|
||||||
|
|
||||||
|
def _generate_experiment_design(
|
||||||
|
self,
|
||||||
|
ratio: dict,
|
||||||
|
wt_percent: float = 0.25,
|
||||||
|
m_tot: float = 70,
|
||||||
|
titration_percent: float = 0.03,
|
||||||
|
) -> dict:
|
||||||
|
"""内部方法:生成实验设计
|
||||||
|
|
||||||
|
根据FuncGroup自动区分二胺和二酐,每种二胺单独配溶液,严格按照ratio顺序投料。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
ratio: 化合物配比字典,格式: {"compound_name": ratio_value}
|
||||||
|
wt_percent: 固体重量百分比
|
||||||
|
m_tot: 反应混合物总质量(g)
|
||||||
|
titration_percent: 滴定溶液百分比
|
||||||
|
|
||||||
|
返回:
|
||||||
|
包含实验设计详细参数的字典
|
||||||
|
"""
|
||||||
|
# 溶剂密度
|
||||||
|
ρ_solvent = 1.03 * self.ureg.g / self.ureg.ml
|
||||||
|
# 二酐溶解度
|
||||||
|
solubility = 0.02 * self.ureg.g / self.ureg.ml
|
||||||
|
# 投入固体时最小溶剂体积
|
||||||
|
V_min = 30 * self.ureg.ml
|
||||||
|
m_tot = m_tot * self.ureg.g
|
||||||
|
|
||||||
|
# 保持ratio中的顺序
|
||||||
|
compound_names = list(ratio.keys())
|
||||||
|
compound_ratios = list(ratio.values())
|
||||||
|
|
||||||
|
# 验证所有化合物是否在 compound_info 中定义
|
||||||
|
undefined_compounds = [name for name in compound_names if name not in self.compound_info["MolWt"]]
|
||||||
|
if undefined_compounds:
|
||||||
|
available = list(self.compound_info["MolWt"].keys())
|
||||||
|
raise ValueError(
|
||||||
|
f"以下化合物未在 compound_info 中定义: {undefined_compounds}。"
|
||||||
|
f"可用的化合物: {available}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取各化合物的分子量和官能团类型
|
||||||
|
molecular_weights = [self.compound_info["MolWt"][name] for name in compound_names]
|
||||||
|
func_groups = [self.compound_info["FuncGroup"][name] for name in compound_names]
|
||||||
|
|
||||||
|
# 记录化合物信息用于调试
|
||||||
|
self.hardware_interface._logger.info(f"化合物名称: {compound_names}")
|
||||||
|
self.hardware_interface._logger.info(f"官能团类型: {func_groups}")
|
||||||
|
|
||||||
|
# 按原始顺序分离二胺和二酐
|
||||||
|
ordered_compounds = list(zip(compound_names, compound_ratios, molecular_weights, func_groups))
|
||||||
|
diamine_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Amine"]
|
||||||
|
anhydride_compounds = [(name, ratio_val, mw, i) for i, (name, ratio_val, mw, fg) in enumerate(ordered_compounds) if fg == "Anhydride"]
|
||||||
|
|
||||||
|
if not diamine_compounds or not anhydride_compounds:
|
||||||
|
raise ValueError(
|
||||||
|
f"需要同时包含二胺(Amine)和二酐(Anhydride)化合物。"
|
||||||
|
f"当前二胺: {[c[0] for c in diamine_compounds]}, "
|
||||||
|
f"当前二酐: {[c[0] for c in anhydride_compounds]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算加权平均分子量 (基于摩尔比)
|
||||||
|
total_molar_ratio = sum(compound_ratios)
|
||||||
|
weighted_molecular_weight = sum(ratio_val * mw for ratio_val, mw in zip(compound_ratios, molecular_weights))
|
||||||
|
|
||||||
|
# 取最后一个二酐用于滴定
|
||||||
|
titration_anhydride = anhydride_compounds[-1]
|
||||||
|
solid_anhydrides = anhydride_compounds[:-1] if len(anhydride_compounds) > 1 else []
|
||||||
|
|
||||||
|
# 二胺溶液配制参数 - 每种二胺单独配制
|
||||||
|
diamine_solutions = []
|
||||||
|
total_diamine_volume = 0 * self.ureg.ml
|
||||||
|
|
||||||
|
# 计算反应物的总摩尔量
|
||||||
|
n_reactant = m_tot * wt_percent / weighted_molecular_weight
|
||||||
|
|
||||||
|
for name, ratio_val, mw, order_index in diamine_compounds:
|
||||||
|
# 跳过 SIDA
|
||||||
|
if name == "SIDA":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 计算该二胺需要的摩尔数
|
||||||
|
n_diamine_needed = n_reactant * ratio_val
|
||||||
|
|
||||||
|
# 二胺溶液配制参数 (每种二胺固定配制参数)
|
||||||
|
m_diamine_solid = 5.0 * self.ureg.g # 每种二胺固体质量
|
||||||
|
V_solvent_for_this = 20 * self.ureg.ml # 每种二胺溶剂体积
|
||||||
|
m_solvent_for_this = ρ_solvent * V_solvent_for_this
|
||||||
|
|
||||||
|
# 计算该二胺溶液的浓度
|
||||||
|
c_diamine = (m_diamine_solid / mw) / V_solvent_for_this
|
||||||
|
|
||||||
|
# 计算需要移取的溶液体积
|
||||||
|
V_diamine_needed = n_diamine_needed / c_diamine
|
||||||
|
|
||||||
|
diamine_solutions.append({
|
||||||
|
"name": name,
|
||||||
|
"order": order_index,
|
||||||
|
"solid_mass": m_diamine_solid.magnitude,
|
||||||
|
"solvent_volume": V_solvent_for_this.magnitude,
|
||||||
|
"concentration": c_diamine.magnitude,
|
||||||
|
"volume_needed": V_diamine_needed.magnitude,
|
||||||
|
"molar_ratio": ratio_val
|
||||||
|
})
|
||||||
|
|
||||||
|
total_diamine_volume += V_diamine_needed
|
||||||
|
|
||||||
|
# 按原始顺序排序
|
||||||
|
diamine_solutions.sort(key=lambda x: x["order"])
|
||||||
|
|
||||||
|
# 计算滴定二酐的质量
|
||||||
|
titration_name, titration_ratio, titration_mw, _ = titration_anhydride
|
||||||
|
m_titration_anhydride = n_reactant * titration_ratio * titration_mw
|
||||||
|
m_titration_90 = m_titration_anhydride * (1 - titration_percent)
|
||||||
|
m_titration_10 = m_titration_anhydride * titration_percent
|
||||||
|
|
||||||
|
# 计算其他固体二酐的质量 (按顺序)
|
||||||
|
solid_anhydride_masses = []
|
||||||
|
for name, ratio_val, mw, order_index in solid_anhydrides:
|
||||||
|
mass = n_reactant * ratio_val * mw
|
||||||
|
solid_anhydride_masses.append({
|
||||||
|
"name": name,
|
||||||
|
"order": order_index,
|
||||||
|
"mass": mass.magnitude,
|
||||||
|
"molar_ratio": ratio_val
|
||||||
|
})
|
||||||
|
|
||||||
|
# 按原始顺序排序
|
||||||
|
solid_anhydride_masses.sort(key=lambda x: x["order"])
|
||||||
|
|
||||||
|
# 计算溶剂用量
|
||||||
|
total_diamine_solution_mass = sum(
|
||||||
|
sol["volume_needed"] * ρ_solvent for sol in diamine_solutions
|
||||||
|
) * self.ureg.ml
|
||||||
|
|
||||||
|
# 预估滴定溶剂量、计算补加溶剂量
|
||||||
|
m_solvent_titration = m_titration_10 / solubility * ρ_solvent
|
||||||
|
m_solvent_add = m_tot * (1 - wt_percent) - total_diamine_solution_mass - m_solvent_titration
|
||||||
|
|
||||||
|
# 检查最小溶剂体积要求
|
||||||
|
total_liquid_volume = (total_diamine_solution_mass + m_solvent_add) / ρ_solvent
|
||||||
|
m_tot_min = V_min / total_liquid_volume * m_tot
|
||||||
|
|
||||||
|
# 如果需要,按比例放大
|
||||||
|
scale_factor = 1.0
|
||||||
|
if m_tot_min > m_tot:
|
||||||
|
scale_factor = (m_tot_min / m_tot).magnitude
|
||||||
|
m_titration_90 *= scale_factor
|
||||||
|
m_titration_10 *= scale_factor
|
||||||
|
m_solvent_add *= scale_factor
|
||||||
|
m_solvent_titration *= scale_factor
|
||||||
|
|
||||||
|
# 更新二胺溶液用量
|
||||||
|
for sol in diamine_solutions:
|
||||||
|
sol["volume_needed"] *= scale_factor
|
||||||
|
|
||||||
|
# 更新固体二酐用量
|
||||||
|
for anhydride in solid_anhydride_masses:
|
||||||
|
anhydride["mass"] *= scale_factor
|
||||||
|
|
||||||
|
m_tot = m_tot_min
|
||||||
|
|
||||||
|
# 生成投料顺序
|
||||||
|
feeding_order = []
|
||||||
|
|
||||||
|
# 1. 固体二酐 (按顺序)
|
||||||
|
for anhydride in solid_anhydride_masses:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "solid_anhydride",
|
||||||
|
"name": anhydride["name"],
|
||||||
|
"amount": anhydride["mass"],
|
||||||
|
"order": anhydride["order"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 2. 二胺溶液 (按顺序)
|
||||||
|
for sol in diamine_solutions:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "diamine_solution",
|
||||||
|
"name": sol["name"],
|
||||||
|
"amount": sol["volume_needed"],
|
||||||
|
"order": sol["order"]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. 主要二酐粉末
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "main_anhydride",
|
||||||
|
"name": titration_name,
|
||||||
|
"amount": m_titration_90.magnitude,
|
||||||
|
"order": titration_anhydride[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. 补加溶剂
|
||||||
|
if m_solvent_add > 0:
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "additional_solvent",
|
||||||
|
"name": "溶剂",
|
||||||
|
"amount": m_solvent_add.magnitude,
|
||||||
|
"order": 999
|
||||||
|
})
|
||||||
|
|
||||||
|
# 5. 滴定二酐溶液
|
||||||
|
feeding_order.append({
|
||||||
|
"step": len(feeding_order) + 1,
|
||||||
|
"type": "titration_anhydride",
|
||||||
|
"name": f"{titration_name} 滴定液",
|
||||||
|
"amount": m_titration_10.magnitude,
|
||||||
|
"titration_solvent": m_solvent_titration.magnitude,
|
||||||
|
"order": titration_anhydride[3]
|
||||||
|
})
|
||||||
|
|
||||||
|
# 返回实验设计结果
|
||||||
|
results = {
|
||||||
|
"total_mass": m_tot.magnitude,
|
||||||
|
"scale_factor": scale_factor,
|
||||||
|
"solutions": diamine_solutions,
|
||||||
|
"solids": solid_anhydride_masses,
|
||||||
|
"titration": {
|
||||||
|
"name": titration_name,
|
||||||
|
"main_portion": m_titration_90.magnitude,
|
||||||
|
"titration_portion": m_titration_10.magnitude,
|
||||||
|
"titration_solvent": m_solvent_titration.magnitude,
|
||||||
|
},
|
||||||
|
"solvents": {
|
||||||
|
"additional_solvent": m_solvent_add.magnitude,
|
||||||
|
"total_liquid_volume": total_liquid_volume.magnitude
|
||||||
|
},
|
||||||
|
"feeding_order": feeding_order,
|
||||||
|
"minimum_required_mass": m_tot_min.magnitude
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
# 90%10%小瓶投料任务创建方法
|
# 90%10%小瓶投料任务创建方法
|
||||||
def create_90_10_vial_feeding_task(self,
|
def create_90_10_vial_feeding_task(self,
|
||||||
order_name: str = None,
|
order_name: str = None,
|
||||||
@@ -961,6 +1236,108 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
'actualVolume': actual_volume
|
'actualVolume': actual_volume
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _simplify_report(self, report) -> Dict[str, Any]:
|
||||||
|
"""简化实验报告,只保留关键信息,去除冗余的工作流参数"""
|
||||||
|
if not isinstance(report, dict):
|
||||||
|
return report
|
||||||
|
|
||||||
|
data = report.get('data', {})
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return report
|
||||||
|
|
||||||
|
# 提取关键信息
|
||||||
|
simplified = {
|
||||||
|
'name': data.get('name'),
|
||||||
|
'code': data.get('code'),
|
||||||
|
'requester': data.get('requester'),
|
||||||
|
'workflowName': data.get('workflowName'),
|
||||||
|
'workflowStep': data.get('workflowStep'),
|
||||||
|
'requestTime': data.get('requestTime'),
|
||||||
|
'startPreparationTime': data.get('startPreparationTime'),
|
||||||
|
'completeTime': data.get('completeTime'),
|
||||||
|
'useTime': data.get('useTime'),
|
||||||
|
'status': data.get('status'),
|
||||||
|
'statusName': data.get('statusName'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 提取物料信息(简化版)
|
||||||
|
pre_intakes = data.get('preIntakes', [])
|
||||||
|
if pre_intakes and isinstance(pre_intakes, list):
|
||||||
|
first_intake = pre_intakes[0]
|
||||||
|
sample_materials = first_intake.get('sampleMaterials', [])
|
||||||
|
|
||||||
|
# 简化物料信息
|
||||||
|
simplified_materials = []
|
||||||
|
for material in sample_materials:
|
||||||
|
if isinstance(material, dict):
|
||||||
|
mat_info = {
|
||||||
|
'materialName': material.get('materialName'),
|
||||||
|
'materialTypeName': material.get('materialTypeName'),
|
||||||
|
'materialCode': material.get('materialCode'),
|
||||||
|
'materialLocation': material.get('materialLocation'),
|
||||||
|
}
|
||||||
|
|
||||||
|
# 解析parameters中的关键信息(如密度、加料历史等)
|
||||||
|
params_str = material.get('parameters', '{}')
|
||||||
|
try:
|
||||||
|
params = json.loads(params_str) if isinstance(params_str, str) else params_str
|
||||||
|
if isinstance(params, dict):
|
||||||
|
# 只保留关键参数
|
||||||
|
if 'density' in params:
|
||||||
|
mat_info['density'] = params['density']
|
||||||
|
if 'feedingHistory' in params:
|
||||||
|
mat_info['feedingHistory'] = params['feedingHistory']
|
||||||
|
if 'liquidVolume' in params:
|
||||||
|
mat_info['liquidVolume'] = params['liquidVolume']
|
||||||
|
if 'm_diamine_tot' in params:
|
||||||
|
mat_info['m_diamine_tot'] = params['m_diamine_tot']
|
||||||
|
if 'wt_diamine' in params:
|
||||||
|
mat_info['wt_diamine'] = params['wt_diamine']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
simplified_materials.append(mat_info)
|
||||||
|
|
||||||
|
simplified['sampleMaterials'] = simplified_materials
|
||||||
|
|
||||||
|
# 提取extraProperties中的实际值
|
||||||
|
extra_props = first_intake.get('extraProperties', {})
|
||||||
|
if isinstance(extra_props, dict):
|
||||||
|
simplified_extra = {}
|
||||||
|
for key, value in extra_props.items():
|
||||||
|
try:
|
||||||
|
parsed_value = json.loads(value) if isinstance(value, str) else value
|
||||||
|
simplified_extra[key] = parsed_value
|
||||||
|
except:
|
||||||
|
simplified_extra[key] = value
|
||||||
|
simplified['extraProperties'] = simplified_extra
|
||||||
|
|
||||||
|
return {
|
||||||
|
'data': simplified,
|
||||||
|
'code': report.get('code'),
|
||||||
|
'message': report.get('message'),
|
||||||
|
'timestamp': report.get('timestamp')
|
||||||
|
}
|
||||||
|
|
||||||
|
def scheduler_start(self) -> dict:
|
||||||
|
"""启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含return_info的字典,return_info为整型(1=成功)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
BioyondException: 调度器启动失败时抛出异常
|
||||||
|
"""
|
||||||
|
result = self.hardware_interface.scheduler_start()
|
||||||
|
self.hardware_interface._logger.info(f"调度器启动结果: {result}")
|
||||||
|
|
||||||
|
if result != 1:
|
||||||
|
error_msg = "启动调度器失败: 有未处理错误,调度无法启动。请检查Bioyond系统状态。"
|
||||||
|
self.hardware_interface._logger.error(error_msg)
|
||||||
|
raise BioyondException(error_msg)
|
||||||
|
|
||||||
|
return {"return_info": result}
|
||||||
|
|
||||||
# 等待多个任务完成并获取实验报告
|
# 等待多个任务完成并获取实验报告
|
||||||
def wait_for_multiple_orders_and_get_reports(self,
|
def wait_for_multiple_orders_and_get_reports(self,
|
||||||
batch_create_result: str = None,
|
batch_create_result: str = None,
|
||||||
@@ -1002,7 +1379,12 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
|
|
||||||
# 验证batch_create_result参数
|
# 验证batch_create_result参数
|
||||||
if not batch_create_result or batch_create_result == "":
|
if not batch_create_result or batch_create_result == "":
|
||||||
raise BioyondException("batch_create_result参数为空,请确保从batch_create节点正确连接handle")
|
raise BioyondException(
|
||||||
|
"batch_create_result参数为空,请确保:\n"
|
||||||
|
"1. batch_create节点与wait节点之间正确连接了handle\n"
|
||||||
|
"2. batch_create节点成功执行并返回了结果\n"
|
||||||
|
"3. 检查上游batch_create任务是否成功创建了订单"
|
||||||
|
)
|
||||||
|
|
||||||
# 解析batch_create_result JSON对象
|
# 解析batch_create_result JSON对象
|
||||||
try:
|
try:
|
||||||
@@ -1031,7 +1413,17 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
|
|
||||||
# 验证提取的数据
|
# 验证提取的数据
|
||||||
if not order_codes:
|
if not order_codes:
|
||||||
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
|
self.hardware_interface._logger.error(
|
||||||
|
f"batch_create任务未生成任何订单。batch_create_result内容: {batch_create_result}"
|
||||||
|
)
|
||||||
|
raise BioyondException(
|
||||||
|
"batch_create_result中未找到order_codes或为空。\n"
|
||||||
|
"可能的原因:\n"
|
||||||
|
"1. batch_create任务执行失败(检查任务是否报错)\n"
|
||||||
|
"2. 物料配置问题(如'物料样品板分配失败')\n"
|
||||||
|
"3. Bioyond系统状态异常\n"
|
||||||
|
f"请检查batch_create任务的执行结果"
|
||||||
|
)
|
||||||
if not order_ids:
|
if not order_ids:
|
||||||
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
|
||||||
|
|
||||||
@@ -1114,6 +1506,8 @@ class BioyondDispensingStation(BioyondWorkstation):
|
|||||||
self.hardware_interface._logger.info(
|
self.hardware_interface._logger.info(
|
||||||
f"成功获取任务 {order_code} 的实验报告"
|
f"成功获取任务 {order_code} 的实验报告"
|
||||||
)
|
)
|
||||||
|
# 简化报告,去除冗余信息
|
||||||
|
report = self._simplify_report(report)
|
||||||
|
|
||||||
reports.append({
|
reports.append({
|
||||||
"order_code": order_code,
|
"order_code": order_code,
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ Bioyond Workstation Implementation
|
|||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Any, List, Optional, Union
|
from typing import Dict, Any, List, Optional, Union
|
||||||
import json
|
import json
|
||||||
@@ -29,6 +30,90 @@ from unilabos.devices.workstation.bioyond_studio.config import (
|
|||||||
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionMonitor:
|
||||||
|
"""Bioyond连接监控器"""
|
||||||
|
def __init__(self, workstation, check_interval=30):
|
||||||
|
self.workstation = workstation
|
||||||
|
self.check_interval = check_interval
|
||||||
|
self._running = False
|
||||||
|
self._thread = None
|
||||||
|
self._last_status = "unknown"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self._running:
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
|
self._thread = threading.Thread(target=self._monitor_loop, daemon=True, name="BioyondConnectionMonitor")
|
||||||
|
self._thread.start()
|
||||||
|
logger.info("Bioyond连接监控器已启动")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self._running = False
|
||||||
|
if self._thread:
|
||||||
|
self._thread.join(timeout=2)
|
||||||
|
logger.info("Bioyond连接监控器已停止")
|
||||||
|
|
||||||
|
def _monitor_loop(self):
|
||||||
|
while self._running:
|
||||||
|
try:
|
||||||
|
# 使用 lightweight API 检查连接
|
||||||
|
# query_matial_type_list 是比较快的查询
|
||||||
|
start_time = time.time()
|
||||||
|
result = self.workstation.hardware_interface.material_type_list()
|
||||||
|
|
||||||
|
status = "online" if result else "offline"
|
||||||
|
msg = "Connection established" if status == "online" else "Failed to get material type list"
|
||||||
|
|
||||||
|
if status != self._last_status:
|
||||||
|
logger.info(f"Bioyond连接状态变更: {self._last_status} -> {status}")
|
||||||
|
self._publish_event(status, msg)
|
||||||
|
self._last_status = status
|
||||||
|
|
||||||
|
# 发布心跳 (可选,或者只在状态变更时发布)
|
||||||
|
# self._publish_event(status, msg)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Bioyond连接检查异常: {e}")
|
||||||
|
if self._last_status != "error":
|
||||||
|
self._publish_event("error", str(e))
|
||||||
|
self._last_status = "error"
|
||||||
|
|
||||||
|
time.sleep(self.check_interval)
|
||||||
|
|
||||||
|
def _publish_event(self, status, message):
|
||||||
|
try:
|
||||||
|
if hasattr(self.workstation, "_ros_node") and self.workstation._ros_node:
|
||||||
|
event_data = {
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 动态发布消息,需要在 ROS2DeviceNode 中有对应支持
|
||||||
|
# 这里假设通用事件发布机制,使用 String 类型的 topic
|
||||||
|
# 话题: /<namespace>/events/device_status
|
||||||
|
ns = self.workstation._ros_node.namespace
|
||||||
|
topic = f"{ns}/events/device_status"
|
||||||
|
|
||||||
|
# 使用 ROS2DeviceNode 的发布功能
|
||||||
|
# 如果没有预定义的 publisher,需要动态创建
|
||||||
|
# 注意:workstation base node 可能没有自动创建 arbitrary publishers 的机制
|
||||||
|
# 这里我们先尝试用 String json 发布
|
||||||
|
|
||||||
|
# 在 ROS2DeviceNode 中通常需要先 create_publisher
|
||||||
|
# 为了简单起见,我们检查是否已有 publisher,没有则创建
|
||||||
|
if not hasattr(self.workstation, "_device_status_pub"):
|
||||||
|
self.workstation._device_status_pub = self.workstation._ros_node.create_publisher(
|
||||||
|
String, topic, 10
|
||||||
|
)
|
||||||
|
|
||||||
|
self.workstation._device_status_pub.publish(
|
||||||
|
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发布设备状态事件失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
class BioyondResourceSynchronizer(ResourceSynchronizer):
|
||||||
"""Bioyond资源同步器
|
"""Bioyond资源同步器
|
||||||
|
|
||||||
@@ -239,13 +324,18 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...")
|
logger.info(f"[同步→Bioyond] 🔄 转换物料为 Bioyond 格式...")
|
||||||
|
|
||||||
# 导入物料默认参数配置
|
# 导入物料默认参数配置
|
||||||
from .config import MATERIAL_DEFAULT_PARAMETERS
|
from .config import MATERIAL_DEFAULT_PARAMETERS, MATERIAL_TYPE_PARAMETERS
|
||||||
|
|
||||||
|
# 合并参数配置:物料名称参数 + typeId参数(转换为 type:<uuid> 格式)
|
||||||
|
merged_params = MATERIAL_DEFAULT_PARAMETERS.copy()
|
||||||
|
for type_id, params in MATERIAL_TYPE_PARAMETERS.items():
|
||||||
|
merged_params[f"type:{type_id}"] = params
|
||||||
|
|
||||||
bioyond_material = resource_plr_to_bioyond(
|
bioyond_material = resource_plr_to_bioyond(
|
||||||
[resource],
|
[resource],
|
||||||
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
||||||
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
||||||
material_params=MATERIAL_DEFAULT_PARAMETERS
|
material_params=merged_params
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段,目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...")
|
logger.info(f"[同步→Bioyond] 🔧 准备覆盖locations字段,目标仓库: {parent_name}, 库位: {update_site}, UUID: {target_location_uuid[:8]}...")
|
||||||
@@ -468,13 +558,18 @@ class BioyondResourceSynchronizer(ResourceSynchronizer):
|
|||||||
return material_bioyond_id
|
return material_bioyond_id
|
||||||
|
|
||||||
# 转换为 Bioyond 格式
|
# 转换为 Bioyond 格式
|
||||||
from .config import MATERIAL_DEFAULT_PARAMETERS
|
from .config import MATERIAL_DEFAULT_PARAMETERS, MATERIAL_TYPE_PARAMETERS
|
||||||
|
|
||||||
|
# 合并参数配置:物料名称参数 + typeId参数(转换为 type:<uuid> 格式)
|
||||||
|
merged_params = MATERIAL_DEFAULT_PARAMETERS.copy()
|
||||||
|
for type_id, params in MATERIAL_TYPE_PARAMETERS.items():
|
||||||
|
merged_params[f"type:{type_id}"] = params
|
||||||
|
|
||||||
bioyond_material = resource_plr_to_bioyond(
|
bioyond_material = resource_plr_to_bioyond(
|
||||||
[resource],
|
[resource],
|
||||||
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
type_mapping=self.workstation.bioyond_config["material_type_mappings"],
|
||||||
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
warehouse_mapping=self.workstation.bioyond_config["warehouse_mapping"],
|
||||||
material_params=MATERIAL_DEFAULT_PARAMETERS
|
material_params=merged_params
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
# ⚠️ 关键:创建物料时不设置 locations,让 Bioyond 系统暂不分配库位
|
# ⚠️ 关键:创建物料时不设置 locations,让 Bioyond 系统暂不分配库位
|
||||||
@@ -584,6 +679,44 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
集成Bioyond物料管理的工作站实现
|
集成Bioyond物料管理的工作站实现
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _publish_task_status(
|
||||||
|
self,
|
||||||
|
task_id: str,
|
||||||
|
task_type: str,
|
||||||
|
status: str,
|
||||||
|
result: dict = None,
|
||||||
|
progress: float = 0.0,
|
||||||
|
task_code: str = None
|
||||||
|
):
|
||||||
|
"""发布任务状态事件"""
|
||||||
|
try:
|
||||||
|
if not getattr(self, "_ros_node", None):
|
||||||
|
return
|
||||||
|
|
||||||
|
event_data = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"task_code": task_code,
|
||||||
|
"task_type": task_type,
|
||||||
|
"status": status,
|
||||||
|
"progress": progress,
|
||||||
|
"timestamp": datetime.now().isoformat()
|
||||||
|
}
|
||||||
|
if result:
|
||||||
|
event_data["result"] = result
|
||||||
|
|
||||||
|
topic = f"{self._ros_node.namespace}/events/task_status"
|
||||||
|
|
||||||
|
if not hasattr(self, "_task_status_pub"):
|
||||||
|
self._task_status_pub = self._ros_node.create_publisher(
|
||||||
|
String, topic, 10
|
||||||
|
)
|
||||||
|
|
||||||
|
self._task_status_pub.publish(
|
||||||
|
convert_to_ros_msg(String, json.dumps(event_data, ensure_ascii=False))
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"发布任务状态事件失败: {e}")
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
bioyond_config: Optional[Dict[str, Any]] = None,
|
bioyond_config: Optional[Dict[str, Any]] = None,
|
||||||
@@ -632,13 +765,16 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]),
|
"host": bioyond_config.get("http_service_host", HTTP_SERVICE_CONFIG["http_service_host"]),
|
||||||
"port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG["http_service_port"])
|
"port": bioyond_config.get("http_service_port", HTTP_SERVICE_CONFIG["http_service_port"])
|
||||||
}
|
}
|
||||||
self.http_service = None # 将在 post_init 中启动
|
self.http_service = None # 将在 post_init 启动
|
||||||
|
self.connection_monitor = None # 将在 post_init 启动
|
||||||
|
|
||||||
logger.info(f"Bioyond工作站初始化完成")
|
logger.info(f"Bioyond工作站初始化完成")
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
"""析构函数:清理资源,停止 HTTP 服务"""
|
"""析构函数:清理资源,停止 HTTP 服务"""
|
||||||
try:
|
try:
|
||||||
|
if hasattr(self, 'connection_monitor') and self.connection_monitor:
|
||||||
|
self.connection_monitor.stop()
|
||||||
if hasattr(self, 'http_service') and self.http_service is not None:
|
if hasattr(self, 'http_service') and self.http_service is not None:
|
||||||
logger.info("正在停止 HTTP 报送服务...")
|
logger.info("正在停止 HTTP 报送服务...")
|
||||||
self.http_service.stop()
|
self.http_service.stop()
|
||||||
@@ -648,6 +784,13 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
def post_init(self, ros_node: ROS2WorkstationNode):
|
def post_init(self, ros_node: ROS2WorkstationNode):
|
||||||
self._ros_node = ros_node
|
self._ros_node = ros_node
|
||||||
|
|
||||||
|
# 启动连接监控
|
||||||
|
try:
|
||||||
|
self.connection_monitor = ConnectionMonitor(self)
|
||||||
|
self.connection_monitor.start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动连接监控失败: {e}")
|
||||||
|
|
||||||
# 启动 HTTP 报送接收服务(现在 device_id 已可用)
|
# 启动 HTTP 报送接收服务(现在 device_id 已可用)
|
||||||
if hasattr(self, '_http_service_config'):
|
if hasattr(self, '_http_service_config'):
|
||||||
try:
|
try:
|
||||||
@@ -1014,7 +1157,15 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
workflow_id = self._get_workflow(actual_workflow_name)
|
workflow_id = self._get_workflow(actual_workflow_name)
|
||||||
if workflow_id:
|
if workflow_id:
|
||||||
self.workflow_sequence.append(workflow_id)
|
# 兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property 的情况
|
||||||
|
if isinstance(self.workflow_sequence, list):
|
||||||
|
self.workflow_sequence.append(workflow_id)
|
||||||
|
elif hasattr(self, "_cached_workflow_sequence") and isinstance(self._cached_workflow_sequence, list):
|
||||||
|
self._cached_workflow_sequence.append(workflow_id)
|
||||||
|
else:
|
||||||
|
print(f"❌ 无法添加工作流: workflow_sequence 类型错误 {type(self.workflow_sequence)}")
|
||||||
|
return False
|
||||||
|
|
||||||
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
|
print(f"添加工作流到执行顺序: {actual_workflow_name} -> {workflow_id}")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@@ -1215,6 +1366,22 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
# TODO: 根据实际业务需求处理步骤完成逻辑
|
# TODO: 根据实际业务需求处理步骤完成逻辑
|
||||||
# 例如:更新数据库、触发后续流程等
|
# 例如:更新数据库、触发后续流程等
|
||||||
|
|
||||||
|
# 发布任务状态事件 (running/progress update)
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'), # 使用 OrderCode 作为关联 ID
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_step",
|
||||||
|
status="running",
|
||||||
|
progress=0.5, # 步骤完成视为任务进行中
|
||||||
|
result={"step_name": data.get('stepName'), "step_id": data.get('stepId')}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新物料信息
|
||||||
|
# 步骤完成后,物料状态可能发生变化(如位置、用量等),触发同步
|
||||||
|
logger.info(f"[步骤完成报送] 触发物料同步...")
|
||||||
|
self.resource_synchronizer.sync_from_external()
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"step_id": data.get('stepId'),
|
"step_id": data.get('stepId'),
|
||||||
@@ -1249,6 +1416,17 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
|
|
||||||
# TODO: 根据实际业务需求处理通量完成逻辑
|
# TODO: 根据实际业务需求处理通量完成逻辑
|
||||||
|
|
||||||
|
# 发布任务状态事件
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'),
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_sample",
|
||||||
|
status="running",
|
||||||
|
progress=0.7,
|
||||||
|
result={"sample_id": data.get('sampleId'), "status": status_desc}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"sample_id": data.get('sampleId'),
|
"sample_id": data.get('sampleId'),
|
||||||
@@ -1288,6 +1466,32 @@ class BioyondWorkstation(WorkstationBase):
|
|||||||
# TODO: 根据实际业务需求处理任务完成逻辑
|
# TODO: 根据实际业务需求处理任务完成逻辑
|
||||||
# 例如:更新物料库存、生成报表等
|
# 例如:更新物料库存、生成报表等
|
||||||
|
|
||||||
|
# 映射状态到事件状态
|
||||||
|
event_status = "completed"
|
||||||
|
if str(data.get('status')) in ["-11", "-12"]:
|
||||||
|
event_status = "error"
|
||||||
|
elif str(data.get('status')) == "30":
|
||||||
|
event_status = "completed"
|
||||||
|
else:
|
||||||
|
event_status = "running" # 其他状态视为运行中(或根据实际定义)
|
||||||
|
|
||||||
|
# 发布任务状态事件
|
||||||
|
self._publish_task_status(
|
||||||
|
task_id=data.get('orderCode'),
|
||||||
|
task_code=data.get('orderCode'),
|
||||||
|
task_type="bioyond_order",
|
||||||
|
status=event_status,
|
||||||
|
progress=1.0 if event_status in ["completed", "error"] else 0.9,
|
||||||
|
result={"order_name": data.get('orderName'), "status": status_desc, "materials_count": len(used_materials)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# 更新物料信息
|
||||||
|
# 任务完成后,且状态为完成时,触发同步以更新最终物料状态
|
||||||
|
if event_status == "completed":
|
||||||
|
logger.info(f"[任务完成报送] 触发物料同步...")
|
||||||
|
self.resource_synchronizer.sync_from_external()
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"processed": True,
|
"processed": True,
|
||||||
"order_code": data.get('orderCode'),
|
"order_code": data.get('orderCode'),
|
||||||
|
|||||||
@@ -459,12 +459,12 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
|||||||
# 验证必需字段
|
# 验证必需字段
|
||||||
if 'brand' in request_data:
|
if 'brand' in request_data:
|
||||||
if request_data['brand'] == "bioyond": # 奔曜
|
if request_data['brand'] == "bioyond": # 奔曜
|
||||||
error_msg = request_data["text"]
|
material_data = request_data["text"]
|
||||||
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
logger.info(f"收到奔曜物料变更报送: {material_data}")
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
success=True,
|
success=True,
|
||||||
message=f"错误处理报送已收到: {error_msg}",
|
message=f"物料变更报送已收到: {material_data}",
|
||||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{material_data.get('id', 'unknown')}",
|
||||||
data=None
|
data=None
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,229 +5,6 @@ bioyond_dispensing_station:
|
|||||||
- bioyond_dispensing_station
|
- bioyond_dispensing_station
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-brief_step_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
data: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
data:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- data
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: brief_step_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-compute_experiment_design:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
m_tot: '70'
|
|
||||||
ratio: null
|
|
||||||
titration_percent: '0.03'
|
|
||||||
wt_percent: '0.25'
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
m_tot:
|
|
||||||
default: '70'
|
|
||||||
type: string
|
|
||||||
ratio:
|
|
||||||
type: object
|
|
||||||
titration_percent:
|
|
||||||
default: '0.03'
|
|
||||||
type: string
|
|
||||||
wt_percent:
|
|
||||||
default: '0.25'
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- ratio
|
|
||||||
type: object
|
|
||||||
result:
|
|
||||||
properties:
|
|
||||||
feeding_order:
|
|
||||||
items: {}
|
|
||||||
title: Feeding Order
|
|
||||||
type: array
|
|
||||||
return_info:
|
|
||||||
title: Return Info
|
|
||||||
type: string
|
|
||||||
solutions:
|
|
||||||
items: {}
|
|
||||||
title: Solutions
|
|
||||||
type: array
|
|
||||||
solvents:
|
|
||||||
additionalProperties: true
|
|
||||||
title: Solvents
|
|
||||||
type: object
|
|
||||||
titration:
|
|
||||||
additionalProperties: true
|
|
||||||
title: Titration
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- solutions
|
|
||||||
- titration
|
|
||||||
- solvents
|
|
||||||
- feeding_order
|
|
||||||
- return_info
|
|
||||||
title: ComputeExperimentDesignReturn
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: compute_experiment_design参数
|
|
||||||
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
|
|
||||||
- used_materials
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_order_finish_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-project_order_report:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
order_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
order_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- order_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: project_order_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-query_resource_by_name:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
material_name: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
material_name:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- material_name
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: query_resource_by_name参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-transfer_materials_to_reaction_station:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
target_device_id: null
|
|
||||||
transfer_groups: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
target_device_id:
|
|
||||||
type: string
|
|
||||||
transfer_groups:
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- target_device_id
|
|
||||||
- transfer_groups
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: transfer_materials_to_reaction_station参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_sample_locations:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: workflow_sample_locations参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
batch_create_90_10_vial_feeding_tasks:
|
batch_create_90_10_vial_feeding_tasks:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -394,6 +171,99 @@ bioyond_dispensing_station:
|
|||||||
title: BatchCreateDiamineSolutionTasks
|
title: BatchCreateDiamineSolutionTasks
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
compute_experiment_design:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
m_tot: m_tot
|
||||||
|
ratio: ratio
|
||||||
|
titration_percent: titration_percent
|
||||||
|
wt_percent: wt_percent
|
||||||
|
goal_default:
|
||||||
|
m_tot: '70'
|
||||||
|
ratio: ''
|
||||||
|
titration_percent: '0.03'
|
||||||
|
wt_percent: '0.25'
|
||||||
|
handles:
|
||||||
|
output:
|
||||||
|
- data_key: solutions
|
||||||
|
data_source: executor
|
||||||
|
data_type: array
|
||||||
|
handler_key: solutions
|
||||||
|
io_type: sink
|
||||||
|
label: Solution Data From Python
|
||||||
|
- data_key: titration
|
||||||
|
data_source: executor
|
||||||
|
data_type: object
|
||||||
|
handler_key: titration
|
||||||
|
io_type: sink
|
||||||
|
label: Titration Data From Calculation Node
|
||||||
|
- data_key: solvents
|
||||||
|
data_source: executor
|
||||||
|
data_type: object
|
||||||
|
handler_key: solvents
|
||||||
|
io_type: sink
|
||||||
|
label: Solvents Data From Calculation Node
|
||||||
|
- data_key: feeding_order
|
||||||
|
data_source: executor
|
||||||
|
data_type: array
|
||||||
|
handler_key: feeding_order
|
||||||
|
io_type: sink
|
||||||
|
label: Feeding Order Data From Calculation Node
|
||||||
|
result:
|
||||||
|
feeding_order: feeding_order
|
||||||
|
return_info: return_info
|
||||||
|
solutions: solutions
|
||||||
|
solvents: solvents
|
||||||
|
titration: titration
|
||||||
|
schema:
|
||||||
|
description: 计算实验设计,输出solutions/titration/solvents/feeding_order用于后续节点。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
m_tot:
|
||||||
|
default: '70'
|
||||||
|
description: 总质量(g)
|
||||||
|
type: string
|
||||||
|
ratio:
|
||||||
|
description: 组分摩尔比的对象,保持输入顺序,如{"MDA":1,"BTDA":1}
|
||||||
|
type: string
|
||||||
|
titration_percent:
|
||||||
|
default: '0.03'
|
||||||
|
description: 滴定比例(10%部分)
|
||||||
|
type: string
|
||||||
|
wt_percent:
|
||||||
|
default: '0.25'
|
||||||
|
description: 目标固含质量分数
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- ratio
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
feeding_order:
|
||||||
|
type: array
|
||||||
|
return_info:
|
||||||
|
type: string
|
||||||
|
solutions:
|
||||||
|
type: array
|
||||||
|
solvents:
|
||||||
|
type: object
|
||||||
|
titration:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- solutions
|
||||||
|
- titration
|
||||||
|
- solvents
|
||||||
|
- feeding_order
|
||||||
|
- return_info
|
||||||
|
title: ComputeExperimentDesign_Result
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: ComputeExperimentDesign
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
create_90_10_vial_feeding_task:
|
create_90_10_vial_feeding_task:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -620,6 +490,89 @@ bioyond_dispensing_station:
|
|||||||
title: DispenStationSolnPrep
|
title: DispenStationSolnPrep
|
||||||
type: object
|
type: object
|
||||||
type: DispenStationSolnPrep
|
type: DispenStationSolnPrep
|
||||||
|
scheduler_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: 启动调度器 - 启动Bioyond配液站的任务调度器,开始执行队列中的任务
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 调度器启动结果,成功返回1,失败返回0
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: scheduler_start结果
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: scheduler_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
transfer_materials_to_reaction_station:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
target_device_id: target_device_id
|
||||||
|
transfer_groups: transfer_groups
|
||||||
|
goal_default:
|
||||||
|
target_device_id: ''
|
||||||
|
transfer_groups: ''
|
||||||
|
handles: {}
|
||||||
|
placeholder_keys:
|
||||||
|
target_device_id: unilabos_devices
|
||||||
|
result: {}
|
||||||
|
schema:
|
||||||
|
description: 将配液站完成的物料(溶液、样品等)转移到指定反应站的堆栈库位。支持配置多组转移任务,每组包含物料名称、目标堆栈和目标库位。
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties:
|
||||||
|
target_device_id:
|
||||||
|
description: 目标反应站设备ID(从设备列表中选择,所有转移组都使用同一个目标设备)
|
||||||
|
type: string
|
||||||
|
transfer_groups:
|
||||||
|
description: 转移任务组列表,每组包含物料名称、目标堆栈和目标库位,可以添加多组
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
materials:
|
||||||
|
description: 物料名称(手动输入,系统将通过RPC查询验证)
|
||||||
|
type: string
|
||||||
|
target_sites:
|
||||||
|
description: 目标库位(手动输入,如"A01")
|
||||||
|
type: string
|
||||||
|
target_stack:
|
||||||
|
description: 目标堆栈名称(从列表选择)
|
||||||
|
enum:
|
||||||
|
- 堆栈1左
|
||||||
|
- 堆栈1右
|
||||||
|
- 站内试剂存放堆栈
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- materials
|
||||||
|
- target_stack
|
||||||
|
- target_sites
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
required:
|
||||||
|
- target_device_id
|
||||||
|
- transfer_groups
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: transfer_materials_to_reaction_station参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
wait_for_multiple_orders_and_get_reports:
|
wait_for_multiple_orders_and_get_reports:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -688,7 +641,7 @@ bioyond_dispensing_station:
|
|||||||
title: WaitForMultipleOrdersAndGetReports
|
title: WaitForMultipleOrdersAndGetReports
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
|
module: unilabos.devices.workstation.bioyond_studio.dispensing_station.dispensing_station:BioyondDispensingStation
|
||||||
status_types: {}
|
status_types: {}
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
|
|||||||
@@ -4,213 +4,88 @@ reaction_station.bioyond:
|
|||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings:
|
||||||
auto-create_order:
|
add_time_constraint:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal: {}
|
goal:
|
||||||
|
duration: duration
|
||||||
|
end_point: end_point
|
||||||
|
end_step_key: end_step_key
|
||||||
|
start_point: start_point
|
||||||
|
start_step_key: start_step_key
|
||||||
goal_default:
|
goal_default:
|
||||||
json_str: null
|
duration: 0
|
||||||
|
end_point: 0
|
||||||
|
end_step_key: ''
|
||||||
|
start_point: 0
|
||||||
|
start_step_key: ''
|
||||||
handles: {}
|
handles: {}
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: ''
|
description: 添加时间约束 - 在两个工作流之间添加时间约束
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
properties:
|
properties:
|
||||||
json_str:
|
duration:
|
||||||
type: string
|
description: 时间(秒)
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: create_order参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-hard_delete_merged_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_ids: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
workflow_ids:
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
type: array
|
|
||||||
required:
|
|
||||||
- workflow_ids
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: hard_delete_merged_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-merge_workflow_with_parameters:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
json_str: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
json_str:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- json_str
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: merge_workflow_with_parameters参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_temperature_cutoff_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_temperature_cutoff_report参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-process_web_workflows:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
web_workflow_json: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
web_workflow_json:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- web_workflow_json
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: process_web_workflows参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-skip_titration_steps:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
preintake_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
preintake_id:
|
|
||||||
type: string
|
|
||||||
required:
|
|
||||||
- preintake_id
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: skip_titration_steps参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-wait_for_multiple_orders_and_get_reports:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
batch_create_result: null
|
|
||||||
check_interval: 10
|
|
||||||
timeout: 7200
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
batch_create_result:
|
|
||||||
type: string
|
|
||||||
check_interval:
|
|
||||||
default: 10
|
|
||||||
type: integer
|
|
||||||
timeout:
|
|
||||||
default: 7200
|
|
||||||
type: integer
|
type: integer
|
||||||
|
end_point:
|
||||||
|
default: Start
|
||||||
|
description: 终点计时点 (Start=开始前, End=结束后)
|
||||||
|
enum:
|
||||||
|
- Start
|
||||||
|
- End
|
||||||
|
type: string
|
||||||
|
end_step_key:
|
||||||
|
description: 终点步骤Key (可选, 默认为空则自动选择)
|
||||||
|
type: string
|
||||||
|
start_point:
|
||||||
|
default: Start
|
||||||
|
description: 起点计时点 (Start=开始前, End=结束后)
|
||||||
|
enum:
|
||||||
|
- Start
|
||||||
|
- End
|
||||||
|
type: string
|
||||||
|
start_step_key:
|
||||||
|
description: 起点步骤Key (例如 "feeding", "liquid", 可选, 默认为空则自动选择)
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- duration
|
||||||
|
type: object
|
||||||
|
result: {}
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: add_time_constraint参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
|
clean_all_server_workflows:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
code: code
|
||||||
|
message: message
|
||||||
|
schema:
|
||||||
|
description: 清空服务端所有非核心工作流 (保留核心流程)
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
required: []
|
required: []
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
result:
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: wait_for_multiple_orders_and_get_reports参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
auto-workflow_step_query:
|
|
||||||
feedback: {}
|
|
||||||
goal: {}
|
|
||||||
goal_default:
|
|
||||||
workflow_id: null
|
|
||||||
handles: {}
|
|
||||||
placeholder_keys: {}
|
|
||||||
result: {}
|
|
||||||
schema:
|
|
||||||
description: ''
|
|
||||||
properties:
|
|
||||||
feedback: {}
|
|
||||||
goal:
|
|
||||||
properties:
|
properties:
|
||||||
workflow_id:
|
code:
|
||||||
|
description: 操作结果代码(1表示成功)
|
||||||
|
type: integer
|
||||||
|
message:
|
||||||
|
description: 结果描述
|
||||||
type: string
|
type: string
|
||||||
required:
|
|
||||||
- workflow_id
|
|
||||||
type: object
|
type: object
|
||||||
result: {}
|
|
||||||
required:
|
required:
|
||||||
- goal
|
- goal
|
||||||
title: workflow_step_query参数
|
title: clean_all_server_workflows参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
drip_back:
|
drip_back:
|
||||||
@@ -247,13 +122,19 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
description: 是否滴定(NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
volume:
|
volume:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume
|
- volume
|
||||||
@@ -353,13 +234,19 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
description: 是否滴定(NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
volume:
|
volume:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume
|
- volume
|
||||||
@@ -403,7 +290,7 @@ reaction_station.bioyond:
|
|||||||
label: Solvents Data From Calculation Node
|
label: Solvents Data From Calculation Node
|
||||||
result: {}
|
result: {}
|
||||||
schema:
|
schema:
|
||||||
description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
description: 液体投料-溶剂。可以直接提供volume(mL),或通过solvents对象自动从additional_solvent(mL)计算volume。
|
||||||
properties:
|
properties:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -423,15 +310,21 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟),默认360
|
description: 观察时间(分钟),默认360
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '1'
|
default: 'NO'
|
||||||
description: 是否滴定(1=否, 2=是),默认1
|
description: 是否滴定(NO=否, YES=是),默认NO
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: '2'
|
default: 'YES'
|
||||||
description: 是否观察 (1=否, 2=是),默认2
|
description: 是否观察 (NO=否, YES=是),默认YES
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
volume:
|
volume:
|
||||||
description: 分液量(μL)。可直接提供,或通过solvents参数自动计算
|
description: 分液量(mL)。可直接提供,或通过solvents参数自动计算
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
@@ -504,15 +397,21 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟),默认90
|
description: 观察时间(分钟),默认90
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
default: '2'
|
default: 'YES'
|
||||||
description: 是否滴定(1=否, 2=是),默认2
|
description: 是否滴定(NO=否, YES=是),默认YES
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
default: '2'
|
default: 'YES'
|
||||||
description: 是否观察 (1=否, 2=是),默认2
|
description: 是否观察 (NO=否, YES=是),默认YES
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(μL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
description: 分液公式(mL)。可直接提供固定公式,或留空由系统根据x_value、feeding_order_data、extracted_actuals自动生成
|
||||||
type: string
|
type: string
|
||||||
x_value:
|
x_value:
|
||||||
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
description: 公式中的x值,手工输入,格式为"{{1-2-3}}"(包含双花括号)。用于自动公式计算
|
||||||
@@ -560,13 +459,19 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
titration_type:
|
titration_type:
|
||||||
description: 是否滴定(1=否, 2=是)
|
description: 是否滴定(NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
volume_formula:
|
volume_formula:
|
||||||
description: 分液公式(μL)
|
description: 分液公式(mL)
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- volume_formula
|
- volume_formula
|
||||||
@@ -680,6 +585,35 @@ reaction_station.bioyond:
|
|||||||
title: reactor_taken_out参数
|
title: reactor_taken_out参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
|
scheduler_start:
|
||||||
|
feedback: {}
|
||||||
|
goal: {}
|
||||||
|
goal_default: {}
|
||||||
|
handles: {}
|
||||||
|
result:
|
||||||
|
return_info: return_info
|
||||||
|
schema:
|
||||||
|
description: 启动调度器 - 启动Bioyond工作站的任务调度器,开始执行队列中的任务
|
||||||
|
properties:
|
||||||
|
feedback: {}
|
||||||
|
goal:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
type: object
|
||||||
|
result:
|
||||||
|
properties:
|
||||||
|
return_info:
|
||||||
|
description: 调度器启动结果,成功返回1,失败返回0
|
||||||
|
type: integer
|
||||||
|
required:
|
||||||
|
- return_info
|
||||||
|
title: scheduler_start结果
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- goal
|
||||||
|
title: scheduler_start参数
|
||||||
|
type: object
|
||||||
|
type: UniLabJsonCommand
|
||||||
solid_feeding_vials:
|
solid_feeding_vials:
|
||||||
feedback: {}
|
feedback: {}
|
||||||
goal:
|
goal:
|
||||||
@@ -706,7 +640,11 @@ reaction_station.bioyond:
|
|||||||
description: 物料名称(用于获取试剂瓶位ID)
|
description: 物料名称(用于获取试剂瓶位ID)
|
||||||
type: string
|
type: string
|
||||||
material_id:
|
material_id:
|
||||||
description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
description: 粉末类型ID,Salt=盐(21分钟),Flour=面粉(27分钟),BTDA=BTDA(38分钟)
|
||||||
|
enum:
|
||||||
|
- Salt
|
||||||
|
- Flour
|
||||||
|
- BTDA
|
||||||
type: string
|
type: string
|
||||||
temperature:
|
temperature:
|
||||||
description: 温度设定(°C)
|
description: 温度设定(°C)
|
||||||
@@ -715,7 +653,10 @@ reaction_station.bioyond:
|
|||||||
description: 观察时间(分钟)
|
description: 观察时间(分钟)
|
||||||
type: string
|
type: string
|
||||||
torque_variation:
|
torque_variation:
|
||||||
description: 是否观察 (1=否, 2=是)
|
description: 是否观察 (NO=否, YES=是)
|
||||||
|
enum:
|
||||||
|
- 'NO'
|
||||||
|
- 'YES'
|
||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- assign_material_name
|
- assign_material_name
|
||||||
@@ -730,9 +671,19 @@ reaction_station.bioyond:
|
|||||||
title: solid_feeding_vials参数
|
title: solid_feeding_vials参数
|
||||||
type: object
|
type: object
|
||||||
type: UniLabJsonCommand
|
type: UniLabJsonCommand
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactionStation
|
||||||
protocol_type: []
|
protocol_type: []
|
||||||
status_types:
|
status_types:
|
||||||
|
average_viscosity: Float64
|
||||||
|
force: Float64
|
||||||
|
in_temperature: Float64
|
||||||
|
out_temperature: Float64
|
||||||
|
pt100_temperature: Float64
|
||||||
|
sensor_average_temperature: Float64
|
||||||
|
setting_temperature: Float64
|
||||||
|
speed: Float64
|
||||||
|
target_temperature: Float64
|
||||||
|
viscosity: Float64
|
||||||
workflow_sequence: String
|
workflow_sequence: String
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
@@ -765,34 +716,19 @@ reaction_station.reactor:
|
|||||||
- reactor
|
- reactor
|
||||||
- reaction_station_bioyond
|
- reaction_station_bioyond
|
||||||
class:
|
class:
|
||||||
action_value_mappings:
|
action_value_mappings: {}
|
||||||
auto-update_metrics:
|
module: unilabos.devices.workstation.bioyond_studio.reaction_station.reaction_station:BioyondReactor
|
||||||
feedback: {}
|
status_types:
|
||||||
goal: {}
|
average_viscosity: Float64
|
||||||
goal_default:
|
force: Float64
|
||||||
payload: null
|
in_temperature: Float64
|
||||||
handles: {}
|
out_temperature: Float64
|
||||||
placeholder_keys: {}
|
pt100_temperature: Float64
|
||||||
result: {}
|
sensor_average_temperature: Float64
|
||||||
schema:
|
setting_temperature: Float64
|
||||||
description: ''
|
speed: Float64
|
||||||
properties:
|
target_temperature: Float64
|
||||||
feedback: {}
|
viscosity: Float64
|
||||||
goal:
|
|
||||||
properties:
|
|
||||||
payload:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- payload
|
|
||||||
type: object
|
|
||||||
result: {}
|
|
||||||
required:
|
|
||||||
- goal
|
|
||||||
title: update_metrics参数
|
|
||||||
type: object
|
|
||||||
type: UniLabJsonCommand
|
|
||||||
module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactor
|
|
||||||
status_types: {}
|
|
||||||
type: python
|
type: python
|
||||||
config_info: []
|
config_info: []
|
||||||
description: 反应站子设备-反应器
|
description: 反应站子设备-反应器
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ BIOYOND_PolymerStation_Liquid_Vial:
|
|||||||
icon: ''
|
icon: ''
|
||||||
init_param_schema: {}
|
init_param_schema: {}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
|
BIOYOND_PolymerStation_Measurement_Vial:
|
||||||
|
category:
|
||||||
|
- bottles
|
||||||
|
class:
|
||||||
|
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Measurement_Vial
|
||||||
|
type: pylabrobot
|
||||||
|
description: 聚合站-测量小瓶(测密度)
|
||||||
|
handles: []
|
||||||
|
icon: ''
|
||||||
|
init_param_schema: {}
|
||||||
|
version: 1.0.0
|
||||||
BIOYOND_PolymerStation_Reactor:
|
BIOYOND_PolymerStation_Reactor:
|
||||||
category:
|
category:
|
||||||
- bottles
|
- bottles
|
||||||
|
|||||||
@@ -193,3 +193,20 @@ def BIOYOND_PolymerStation_Flask(
|
|||||||
barcode=barcode,
|
barcode=barcode,
|
||||||
model="BIOYOND_PolymerStation_Flask",
|
model="BIOYOND_PolymerStation_Flask",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def BIOYOND_PolymerStation_Measurement_Vial(
|
||||||
|
name: str,
|
||||||
|
diameter: float = 25.0,
|
||||||
|
height: float = 60.0,
|
||||||
|
max_volume: float = 20000.0, # 20mL
|
||||||
|
barcode: str = None,
|
||||||
|
) -> Bottle:
|
||||||
|
"""创建测量小瓶"""
|
||||||
|
return Bottle(
|
||||||
|
name=name,
|
||||||
|
diameter=diameter,
|
||||||
|
height=height,
|
||||||
|
max_volume=max_volume,
|
||||||
|
barcode=barcode,
|
||||||
|
model="BIOYOND_PolymerStation_Measurement_Vial",
|
||||||
|
)
|
||||||
|
|||||||
@@ -49,20 +49,17 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
|
|||||||
"测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01~B03
|
"测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01~B03
|
||||||
}
|
}
|
||||||
self.warehouse_locations = {
|
self.warehouse_locations = {
|
||||||
"堆栈1左": Coordinate(0.0, 430.0, 0.0), # 左侧位置
|
"堆栈1左": Coordinate(-200.0, 450.0, 0.0), # 左侧位置
|
||||||
"堆栈1右": Coordinate(2500.0, 430.0, 0.0), # 右侧位置
|
"堆栈1右": Coordinate(2350.0, 450.0, 0.0), # 右侧位置
|
||||||
"站内试剂存放堆栈": Coordinate(640.0, 480.0, 0.0),
|
"站内试剂存放堆栈": Coordinate(730.0, 390.0, 0.0),
|
||||||
# "移液站内10%分装液体准备仓库": Coordinate(1200.0, 600.0, 0.0),
|
# "移液站内10%分装液体准备仓库": Coordinate(1200.0, 600.0, 0.0),
|
||||||
"站内Tip盒堆栈": Coordinate(300.0, 150.0, 0.0),
|
"站内Tip盒堆栈": Coordinate(300.0, 150.0, 0.0),
|
||||||
"测量小瓶仓库(测密度)": Coordinate(922.0, 552.0, 0.0),
|
"测量小瓶仓库(测密度)": Coordinate(940.0, 530.0, 0.0),
|
||||||
}
|
}
|
||||||
self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90)
|
|
||||||
self.warehouses["测量小瓶仓库(测密度)"].rotation = Rotation(z=270)
|
|
||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
|
|
||||||
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
class BIOYOND_PolymerPreparationStation_Deck(Deck):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -144,6 +141,7 @@ class BIOYOND_YB_Deck(Deck):
|
|||||||
|
|
||||||
for warehouse_name, warehouse in self.warehouses.items():
|
for warehouse_name, warehouse in self.warehouses.items():
|
||||||
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
|
||||||
|
|
||||||
def YB_Deck(name: str) -> Deck:
|
def YB_Deck(name: str) -> Deck:
|
||||||
by=BIOYOND_YB_Deck(name=name)
|
by=BIOYOND_YB_Deck(name=name)
|
||||||
by.setup()
|
by.setup()
|
||||||
|
|||||||
@@ -46,41 +46,55 @@ def bioyond_warehouse_1x4x4_right(name: str) -> WareHouse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
def bioyond_warehouse_density_vial(name: str) -> WareHouse:
|
||||||
"""创建测量小瓶仓库(测密度) A01~B03"""
|
"""创建测量小瓶仓库(测密度) - 竖向排列2列3行
|
||||||
|
布局(从下到上,从左到右):
|
||||||
|
| A03 | B03 | ← 顶部
|
||||||
|
| A02 | B02 | ← 中部
|
||||||
|
| A01 | B01 | ← 底部
|
||||||
|
"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=3, # 3列(01-03)
|
num_items_x=2, # 2列(A, B)
|
||||||
num_items_y=2, # 2行(A-B)
|
num_items_y=3, # 3行(01-03,从下到上)
|
||||||
num_items_z=1, # 1层
|
num_items_z=1, # 1层
|
||||||
dx=10.0,
|
dx=10.0,
|
||||||
dy=10.0,
|
dy=10.0,
|
||||||
dz=10.0,
|
dz=10.0,
|
||||||
item_dx=40.0,
|
item_dx=40.0, # 列间距(A到B的横向距离)
|
||||||
item_dy=40.0,
|
item_dy=40.0, # 行间距(01到02到03的竖向距离)
|
||||||
item_dz=50.0,
|
item_dz=50.0,
|
||||||
# 用更小的 resource_size 来表现 "小点的孔位"
|
# ⭐ 竖向warehouse:槽位尺寸也是竖向的(小瓶已经是正方形,无需调整)
|
||||||
resource_size_x=30.0,
|
resource_size_x=30.0,
|
||||||
resource_size_y=30.0,
|
resource_size_y=30.0,
|
||||||
resource_size_z=12.0,
|
resource_size_z=12.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
col_offset=0,
|
col_offset=0,
|
||||||
layout="row-major",
|
layout="vertical-col-major", # ⭐ 竖向warehouse专用布局
|
||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
def bioyond_warehouse_reagent_storage(name: str) -> WareHouse:
|
||||||
"""创建BioYond站内试剂存放堆栈(A01~A02, 1行×2列)"""
|
"""创建BioYond站内试剂存放堆栈 - 竖向排列1列2行
|
||||||
|
布局(竖向,从下到上):
|
||||||
|
| A02 | ← 顶部
|
||||||
|
| A01 | ← 底部
|
||||||
|
"""
|
||||||
return warehouse_factory(
|
return warehouse_factory(
|
||||||
name=name,
|
name=name,
|
||||||
num_items_x=2, # 2列(01-02)
|
num_items_x=1, # 1列
|
||||||
num_items_y=1, # 1行(A)
|
num_items_y=2, # 2行(01-02,从下到上)
|
||||||
num_items_z=1, # 1层
|
num_items_z=1, # 1层
|
||||||
dx=10.0,
|
dx=10.0,
|
||||||
dy=10.0,
|
dy=10.0,
|
||||||
dz=10.0,
|
dz=10.0,
|
||||||
item_dx=137.0,
|
item_dx=96.0, # 列间距(这里只有1列,不重要)
|
||||||
item_dy=96.0,
|
item_dy=137.0, # 行间距(A01到A02的竖向距离)
|
||||||
item_dz=120.0,
|
item_dz=120.0,
|
||||||
|
# ⭐ 竖向warehouse:交换槽位尺寸,使槽位框也是竖向的
|
||||||
|
resource_size_x=86.0, # 原来的 resource_size_y
|
||||||
|
resource_size_y=127.0, # 原来的 resource_size_x
|
||||||
|
resource_size_z=25.0,
|
||||||
category="warehouse",
|
category="warehouse",
|
||||||
|
layout="vertical-col-major", # ⭐ 竖向warehouse专用布局
|
||||||
)
|
)
|
||||||
|
|
||||||
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
def bioyond_warehouse_tipbox_storage(name: str) -> WareHouse:
|
||||||
|
|||||||
@@ -779,6 +779,22 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
if not locations:
|
if not locations:
|
||||||
logger.debug(f"[物料位置] {unique_name} 没有location信息,跳过warehouse放置")
|
logger.debug(f"[物料位置] {unique_name} 没有location信息,跳过warehouse放置")
|
||||||
|
|
||||||
|
# ⭐ 预先检查:如果物料的任何location在竖向warehouse中,提前交换尺寸
|
||||||
|
# 这样可以避免多个location时尺寸不一致的问题
|
||||||
|
needs_size_swap = False
|
||||||
|
for loc in locations:
|
||||||
|
wh_name_check = loc.get("whName")
|
||||||
|
if wh_name_check in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]:
|
||||||
|
needs_size_swap = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if needs_size_swap and hasattr(plr_material, 'size_x') and hasattr(plr_material, 'size_y'):
|
||||||
|
original_x = plr_material.size_x
|
||||||
|
original_y = plr_material.size_y
|
||||||
|
plr_material.size_x = original_y
|
||||||
|
plr_material.size_y = original_x
|
||||||
|
logger.debug(f" 物料 {unique_name} 将放入竖向warehouse,预先交换尺寸: {original_x}×{original_y} → {plr_material.size_x}×{plr_material.size_y}")
|
||||||
|
|
||||||
for loc in locations:
|
for loc in locations:
|
||||||
wh_name = loc.get("whName")
|
wh_name = loc.get("whName")
|
||||||
logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})")
|
logger.debug(f"[物料位置] {unique_name} 尝试放置到 warehouse: {wh_name} (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')}, z={loc.get('z')})")
|
||||||
@@ -800,7 +816,6 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})")
|
logger.debug(f"[Warehouse匹配] 找到warehouse: {wh_name} (容量: {warehouse.capacity}, 行×列: {warehouse.num_items_x}×{warehouse.num_items_y})")
|
||||||
|
|
||||||
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
# Bioyond坐标映射 (重要!): x→行(1=A,2=B...), y→列(1=01,2=02...), z→层(通常=1)
|
||||||
# PyLabRobot warehouse是列优先存储: A01,B01,C01,D01, A02,B02,C02,D02, ...
|
|
||||||
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
x = loc.get("x", 1) # 行号 (1-based: 1=A, 2=B, 3=C, 4=D)
|
||||||
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
y = loc.get("y", 1) # 列号 (1-based: 1=01, 2=02, 3=03...)
|
||||||
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
z = loc.get("z", 1) # 层号 (1-based, 通常为1)
|
||||||
@@ -809,12 +824,23 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
if wh_name == "堆栈1右":
|
if wh_name == "堆栈1右":
|
||||||
y = y - 4 # 将5-8映射到1-4
|
y = y - 4 # 将5-8映射到1-4
|
||||||
|
|
||||||
# 特殊处理:对于1行×N列的横向warehouse(如站内试剂存放堆栈)
|
# 特殊处理竖向warehouse(站内试剂存放堆栈、测量小瓶仓库)
|
||||||
# Bioyond的y坐标表示线性位置序号,而不是列号
|
# 这些warehouse使用 vertical-col-major 布局
|
||||||
if warehouse.num_items_y == 1:
|
if wh_name in ["站内试剂存放堆栈", "测量小瓶仓库(测密度)"]:
|
||||||
# 1行warehouse: 直接用y作为线性索引
|
# vertical-col-major 布局的坐标映射:
|
||||||
idx = y - 1
|
# - Bioyond的x(1=A,2=B)对应warehouse的列(col, x方向)
|
||||||
logger.debug(f"1行warehouse {wh_name}: y={y} → idx={idx}")
|
# - Bioyond的y(1=01,2=02,3=03)对应warehouse的行(row, y方向),从下到上
|
||||||
|
# vertical-col-major 中: row=0 对应底部,row=n-1 对应顶部
|
||||||
|
# Bioyond y=1(01) 对应底部 → row=0, y=2(02) 对应中间 → row=1
|
||||||
|
# 索引计算: idx = row * num_cols + col
|
||||||
|
col_idx = x - 1 # Bioyond的x(A,B) → col索引(0,1)
|
||||||
|
row_idx = y - 1 # Bioyond的y(01,02,03) → row索引(0,1,2)
|
||||||
|
layer_idx = z - 1
|
||||||
|
|
||||||
|
idx = layer_idx * (warehouse.num_items_x * warehouse.num_items_y) + row_idx * warehouse.num_items_x + col_idx
|
||||||
|
logger.debug(f"🔍 竖向warehouse {wh_name}: Bioyond(x={x},y={y},z={z}) → warehouse(col={col_idx},row={row_idx},layer={layer_idx}) → idx={idx}, capacity={warehouse.capacity}")
|
||||||
|
|
||||||
|
# 普通横向warehouse的处理
|
||||||
else:
|
else:
|
||||||
# 多行warehouse: 根据 layout 使用不同的索引计算
|
# 多行warehouse: 根据 layout 使用不同的索引计算
|
||||||
row_idx = x - 1 # x表示行: 转为0-based
|
row_idx = x - 1 # x表示行: 转为0-based
|
||||||
@@ -838,6 +864,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
|
|||||||
|
|
||||||
if 0 <= idx < warehouse.capacity:
|
if 0 <= idx < warehouse.capacity:
|
||||||
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder):
|
||||||
|
# 物料尺寸已在放入warehouse前根据需要进行了交换
|
||||||
warehouse[idx] = plr_material
|
warehouse[idx] = plr_material
|
||||||
logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
logger.debug(f"✅ 物料 {unique_name} 放置到 {wh_name}[{idx}] (Bioyond坐标: x={loc.get('x')}, y={loc.get('y')})")
|
||||||
else:
|
else:
|
||||||
@@ -1011,11 +1038,24 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
|||||||
logger.debug(f" 📭 [单瓶物料] {resource.name} 无液体,使用资源名: {material_name}")
|
logger.debug(f" 📭 [单瓶物料] {resource.name} 无液体,使用资源名: {material_name}")
|
||||||
|
|
||||||
# 🎯 处理物料默认参数和单位
|
# 🎯 处理物料默认参数和单位
|
||||||
# 检查是否有该物料名称的默认参数配置
|
# 优先级: typeId参数 > 物料名称参数 > 默认值
|
||||||
default_unit = "个" # 默认单位
|
default_unit = "个" # 默认单位
|
||||||
material_parameters = {}
|
material_parameters = {}
|
||||||
|
|
||||||
if material_name in material_params:
|
# 1️⃣ 首先检查是否有 typeId 对应的参数配置(从 material_params 中获取,key 格式为 "type:<typeId>")
|
||||||
|
type_params_key = f"type:{type_id}"
|
||||||
|
if type_params_key in material_params:
|
||||||
|
params_config = material_params[type_params_key].copy()
|
||||||
|
|
||||||
|
# 提取 unit 字段(如果有)
|
||||||
|
if "unit" in params_config:
|
||||||
|
default_unit = params_config.pop("unit") # 从参数中移除,放到外层
|
||||||
|
|
||||||
|
# 剩余的字段放入 Parameters
|
||||||
|
material_parameters = params_config
|
||||||
|
logger.debug(f" 🔧 [物料参数-按typeId] 为 typeId={type_id[:8]}... 应用配置: unit={default_unit}, parameters={material_parameters}")
|
||||||
|
# 2️⃣ 其次检查是否有该物料名称的默认参数配置
|
||||||
|
elif material_name in material_params:
|
||||||
params_config = material_params[material_name].copy()
|
params_config = material_params[material_name].copy()
|
||||||
|
|
||||||
# 提取 unit 字段(如果有)
|
# 提取 unit 字段(如果有)
|
||||||
@@ -1024,7 +1064,7 @@ def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict
|
|||||||
|
|
||||||
# 剩余的字段放入 Parameters
|
# 剩余的字段放入 Parameters
|
||||||
material_parameters = params_config
|
material_parameters = params_config
|
||||||
logger.debug(f" 🔧 [物料参数] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}")
|
logger.debug(f" 🔧 [物料参数-按名称] 为 {material_name} 应用配置: unit={default_unit}, parameters={material_parameters}")
|
||||||
|
|
||||||
# 转换为 JSON 字符串
|
# 转换为 JSON 字符串
|
||||||
parameters_json = json.dumps(material_parameters) if material_parameters else "{}"
|
parameters_json = json.dumps(material_parameters) if material_parameters else "{}"
|
||||||
|
|||||||
@@ -50,13 +50,46 @@ class Bottle(Well):
|
|||||||
self.barcode = barcode
|
self.barcode = barcode
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
|
# Pylabrobot expects barcode to be an object with serialize(), but here it is a str.
|
||||||
|
# We temporarily unset it to avoid AttributeError in super().serialize().
|
||||||
|
_barcode = self.barcode
|
||||||
|
self.barcode = None
|
||||||
|
try:
|
||||||
|
data = super().serialize()
|
||||||
|
finally:
|
||||||
|
self.barcode = _barcode
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**super().serialize(),
|
**data,
|
||||||
"diameter": self.diameter,
|
"diameter": self.diameter,
|
||||||
"height": self.height,
|
"height": self.height,
|
||||||
"barcode": self.barcode,
|
"barcode": self.barcode,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def deserialize(cls, data: dict, allow_marshal: bool = False):
|
||||||
|
# Extract barcode before calling parent deserialize to avoid type error
|
||||||
|
barcode_data = data.pop("barcode", None)
|
||||||
|
|
||||||
|
# Call parent deserialize
|
||||||
|
instance = super(Bottle, cls).deserialize(data, allow_marshal=allow_marshal)
|
||||||
|
|
||||||
|
# Set barcode as string (not as Barcode object)
|
||||||
|
if barcode_data:
|
||||||
|
if isinstance(barcode_data, str):
|
||||||
|
instance.barcode = barcode_data
|
||||||
|
elif isinstance(barcode_data, dict):
|
||||||
|
# If it's a dict (Barcode serialized format), extract the data field
|
||||||
|
instance.barcode = barcode_data.get("data", "")
|
||||||
|
else:
|
||||||
|
instance.barcode = ""
|
||||||
|
|
||||||
|
# Set additional attributes
|
||||||
|
instance.diameter = data.get("diameter", instance._size_x)
|
||||||
|
instance.height = data.get("height", instance._size_z)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
T = TypeVar("T", bound=ResourceHolder)
|
T = TypeVar("T", bound=ResourceHolder)
|
||||||
|
|
||||||
S = TypeVar("S", bound=ResourceHolder)
|
S = TypeVar("S", bound=ResourceHolder)
|
||||||
|
|||||||
@@ -621,6 +621,16 @@ class ResourceTreeSet(object):
|
|||||||
"""
|
"""
|
||||||
return [tree.root_node for tree in self.trees]
|
return [tree.root_node for tree in self.trees]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_nodes_uuid(self) -> List[ResourceDictInstance]:
|
||||||
|
"""
|
||||||
|
获取所有树的根节点
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
所有根节点的资源实例列表
|
||||||
|
"""
|
||||||
|
return [tree.root_node.res_content.uuid for tree in self.trees]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all_nodes(self) -> List[ResourceDictInstance]:
|
def all_nodes(self) -> List[ResourceDictInstance]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ def warehouse_factory(
|
|||||||
if layout == "row-major":
|
if layout == "row-major":
|
||||||
# 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值
|
# 行优先:row=0(A行) 应该显示在上方,需要较小的 y 值
|
||||||
y = dy + row * item_dy
|
y = dy + row * item_dy
|
||||||
|
elif layout == "vertical-col-major":
|
||||||
|
# 竖向warehouse: row=0 对应顶部(y小),row=n-1 对应底部(y大)
|
||||||
|
# 但标签 01 应该在底部,所以使用反向映射
|
||||||
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
else:
|
else:
|
||||||
# 列优先:保持原逻辑(row=0 对应较大的 y)
|
# 列优先:保持原逻辑(row=0 对应较大的 y)
|
||||||
y = dy + (num_items_y - row - 1) * item_dy
|
y = dy + (num_items_y - row - 1) * item_dy
|
||||||
@@ -66,6 +70,14 @@ def warehouse_factory(
|
|||||||
# 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04
|
# 行优先顺序: A01,A02,A03,A04, B01,B02,B03,B04
|
||||||
# locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01
|
# locations[0] 对应 row=0, y最大(前端顶部)→ 应该是 A01
|
||||||
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)]
|
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for j in range(len_y) for i in range(len_x)]
|
||||||
|
elif layout == "vertical-col-major":
|
||||||
|
# ⭐ 竖向warehouse专用布局:
|
||||||
|
# 字母(A,B,C...)对应列(横向, x方向),数字(01,02,03...)对应行(竖向, y方向,从下到上)
|
||||||
|
# locations 生成顺序: row→col (row=0,col=0 → row=0,col=1 → row=1,col=0 → ...)
|
||||||
|
# 其中 row=0 对应底部(y大),row=n-1 对应顶部(y小)
|
||||||
|
# 标签中 01 对应底部(row=0),02 对应中间(row=1),03 对应顶部(row=2)
|
||||||
|
# 标签顺序: A01,B01,A02,B02,A03,B03
|
||||||
|
keys = [f"{LETTERS[col]}{row + 1 + col_offset:02d}" for row in range(len_y) for col in range(len_x)]
|
||||||
else:
|
else:
|
||||||
# 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02
|
# 列优先顺序: A01,B01,C01,D01, A02,B02,C02,D02
|
||||||
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
keys = [f"{LETTERS[j]}{i + 1 + col_offset:02d}" for i in range(len_x) for j in range(len_y)]
|
||||||
|
|||||||
@@ -622,7 +622,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
|
|||||||
) # type: ignore
|
) # type: ignore
|
||||||
raw_nodes = json.loads(response.response)
|
raw_nodes = json.loads(response.response)
|
||||||
tree_set = ResourceTreeSet.from_raw_dict_list(raw_nodes)
|
tree_set = ResourceTreeSet.from_raw_dict_list(raw_nodes)
|
||||||
self.lab_logger().debug(f"获取资源结果: {len(tree_set.trees)} 个资源树")
|
self.lab_logger().trace(f"获取资源结果: {len(tree_set.trees)} 个资源树 {tree_set.root_nodes}")
|
||||||
return tree_set
|
return tree_set
|
||||||
|
|
||||||
async def get_resource_with_dir(self, resource_id: str, with_children: bool = True) -> "ResourcePLR":
|
async def get_resource_with_dir(self, resource_id: str, with_children: bool = True) -> "ResourcePLR":
|
||||||
|
|||||||
@@ -14,7 +14,11 @@
|
|||||||
],
|
],
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.bioyond",
|
"class": "reaction_station.bioyond",
|
||||||
"position": {"x": 0, "y": 3800, "z": 0},
|
"position": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 1100,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"config": {
|
"config": {
|
||||||
"api_key": "DE9BDDA0",
|
"api_key": "DE9BDDA0",
|
||||||
@@ -57,6 +61,10 @@
|
|||||||
"BIOYOND_PolymerStation_TipBox": [
|
"BIOYOND_PolymerStation_TipBox": [
|
||||||
"枪头盒",
|
"枪头盒",
|
||||||
"3a143890-9d51-60ac-6d6f-6edb43c12041"
|
"3a143890-9d51-60ac-6d6f-6edb43c12041"
|
||||||
|
],
|
||||||
|
"BIOYOND_PolymerStation_Measurement_Vial": [
|
||||||
|
"测量小瓶",
|
||||||
|
"b1fc79c9-5864-4f05-8052-6ed3abc18a97"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -66,6 +74,9 @@
|
|||||||
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"size_x": 2700.0,
|
||||||
|
"size_y": 1080.0,
|
||||||
|
"size_z": 2000.0,
|
||||||
"protocol_type": []
|
"protocol_type": []
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
@@ -77,7 +88,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1150, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1150,
|
||||||
|
"y": 380,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -88,7 +103,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1365, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1365,
|
||||||
|
"y": 380,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -99,7 +118,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1580, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1580,
|
||||||
|
"y": 380,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -110,7 +133,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 1790, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 1790,
|
||||||
|
"y": 380,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -121,7 +148,11 @@
|
|||||||
"parent": "reaction_station_bioyond",
|
"parent": "reaction_station_bioyond",
|
||||||
"type": "device",
|
"type": "device",
|
||||||
"class": "reaction_station.reactor",
|
"class": "reaction_station.reactor",
|
||||||
"position": {"x": 2010, "y": 380, "z": 0},
|
"position": {
|
||||||
|
"x": 2010,
|
||||||
|
"y": 380,
|
||||||
|
"z": 0
|
||||||
|
},
|
||||||
"config": {},
|
"config": {},
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
@@ -134,7 +165,7 @@
|
|||||||
"class": "BIOYOND_PolymerReactionStation_Deck",
|
"class": "BIOYOND_PolymerReactionStation_Deck",
|
||||||
"position": {
|
"position": {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 1100,
|
||||||
"z": 0
|
"z": 0
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
|||||||
Reference in New Issue
Block a user