Compare commits

...

41 Commits

Author SHA1 Message Date
ZiWei
9a06ef3836 Refactor module paths for Bioyond devices in YAML configuration files
- Updated the module path for BioyondDispensingStation in bioyond_dispensing_station.yaml to reflect the new directory structure.
- Updated the module path for BioyondReactionStation and BioyondReactor in reaction_station_bioyond.yaml to align with the revised organization of the codebase.
2026-01-14 12:44:19 +08:00
ZiWei
ce3f2b33c5 Merge branch 'dev' into pr/169 2026-01-14 11:44:04 +08:00
Xuwznln
6f143b068b Add debug log 2026-01-13 17:51:18 +08:00
ZiWei
03423e4791 feat:增强材料缓存更新逻辑,支持处理返回数据中的详细信息 2026-01-13 11:24:10 +08:00
ZiWei
9fc6781406 Merge branch 'dev' into pr/169 2026-01-12 11:37:50 +08:00
ZiWei
9753ef02c3 fix:修复Bottle类的序列化和反序列化方法 2026-01-12 10:50:28 +08:00
ZiWei
6a0614c0c9 Merge branch 'dev' into pr/169 and fix conflicts 2026-01-11 18:14:19 +08:00
ZiWei
a25e8f6853 feat: 添加清空服务端所有非核心工作流功能 2026-01-08 11:40:42 +08:00
ZiWei
5c249e66a2 feat: 动态获取工作流步骤ID,优化工作流配置 2025-12-31 14:42:43 +08:00
ZiWei
93ac095e0a fix:refresh_material_cache 2025-12-29 22:19:26 +08:00
ZiWei
288d9fea91 fix:Change the material unit from μL to mL 2025-12-29 21:33:38 +08:00
ZiWei
81b28cef71 Merge branch 'dev' into pr/169
# Conflicts:
#	unilabos/device_comms/opcua_client/client.py
#	unilabos/device_comms/opcua_client/node/uniopcua.py
#	unilabos/registry/devices/post_process_station.yaml
2025-12-25 13:55:22 +08:00
ZiWei
d57e5ffdae Refactor bioyond_dispensing_station and reaction_station_bioyond YAML configurations
- Removed redundant action value mappings from bioyond_dispensing_station.
- Updated goal properties in bioyond_dispensing_station to use enums for target_stack and other parameters.
- Changed data types for end_point and start_point in reaction_station_bioyond to use string enums (Start, End).
- Simplified descriptions and updated measurement units from μL to mL where applicable.
- Removed unused commands from reaction_station_bioyond to streamline the configuration.
2025-12-25 12:19:37 +08:00
ZiWei
d5e0d76311 fix: 修复添加物料时数据格式错误 2025-12-25 11:23:59 +08:00
ZiWei
beaa1d7213 feat: 添加任务状态事件发布功能,监控并报告任务运行、超时、完成和错误状态 2025-12-25 09:58:03 +08:00
ZiWei
1e5f6b0c04 feat:添加实验报告简化功能,去除冗余信息并保留关键信息 2025-12-24 11:28:40 +08:00
ZiWei
5ae89d8607 fix:更新奔曜错误处理报送为物料变更报送,调整日志记录和响应消息 2025-12-23 10:55:25 +08:00
ZiWei
74d0ea3379 fix:在添加物料时处理字符串和字典返回值,确保正确更新缓存 2025-12-22 14:37:04 +08:00
ZiWei
440c9965fd fix:自动更新物料缓存功能,添加物料时更新缓存并在删除时移除缓存项 2025-12-18 11:40:59 +08:00
ZiWei
9cac852bc3 添加时间约束功能及相关配置 2025-12-16 13:03:01 +08:00
ZiWei
de662a42aa Merge branch 'dev' into hrdev 2025-12-16 10:03:13 +08:00
ZiWei
632f9b90d1 feat: remove commented workflow synchronization from reaction_station.py. 2025-12-10 17:43:16 +08:00
ZiWei
d7c970d244 fix:同步工作流序列 2025-12-10 17:32:11 +08:00
ZiWei
2c69e663a7 fix:兼容 BioyondReactionStation 中 workflow_sequence 被重写为 property 2025-12-10 11:10:57 +08:00
ZiWei
f03ff96ae4 Merge branch 'dev' into hrdev 2025-12-08 20:25:34 +08:00
ZiWei
c68903ed83 Merge branch 'dev' into hrdev 2025-12-08 16:37:26 +08:00
ZiWei
8efbbbe72a 添加从 Bioyond 系统自动同步工作流序列的功能,并更新相关配置 2025-12-08 16:37:01 +08:00
ZiWei
4a23b05abc 添加调度器启动功能,合并物料参数配置,优化物料参数处理逻辑 2025-12-01 14:00:58 +08:00
ZiWei
6d8884a2c7 Merge remote-tracking branch 'upstream/dev' into hrdev 2025-11-28 22:51:09 +08:00
ZiWei
c0e7a69553 feat(registry): 新增后处理站的设备配置文件
添加后处理站的YAML配置文件,包含动作映射、状态类型和设备描述
2025-11-26 19:59:30 +08:00
ZiWei
fb6ee79577 feat(opcua): 增强节点ID解析兼容性和数据类型处理
改进节点ID解析逻辑以支持多种格式,包括字符串和数字标识符
添加数据类型转换处理,确保写入值时类型匹配
优化错误提示信息,便于调试节点连接问题
2025-11-26 19:57:06 +08:00
ZiWei
dbe129caab feat(bioyond): 优化调度器启动功能,添加异常处理并更新相关配置 2025-11-26 16:32:10 +08:00
ZiWei
7250995891 feat(bioyond): 添加调度器启动功能,支持任务队列执行并处理异常 2025-11-26 16:09:16 +08:00
ZiWei
68eddbdffd feat(bioyond): 调整反应器位置配置,统一坐标格式 2025-11-23 18:10:56 +08:00
ZiWei
32bd234176 feat(bioyond): 添加设置反应器温度功能,支持温度范围和异常处理 2025-11-23 17:16:51 +08:00
ZiWei
3d62e8bf6c feat(bioyond): 优化任务创建流程,确保无论成功与否都清理任务队列以避免重复累积 2025-11-23 13:27:31 +08:00
ZiWei
efec1dd501 Merge remote-tracking branch 'upstream/dev' into hrdev 2025-11-21 11:39:17 +08:00
ZiWei
c16756ddb3 feat(bioyond): 更新仓库布局和尺寸,支持竖向排列的测量小瓶和试剂存放堆栈 2025-11-21 11:33:58 +08:00
ZiWei
daf41871a1 feat(bioyond): 添加测量小瓶配置,支持新设备参数 2025-11-21 11:33:32 +08:00
ZiWei
6b0b28becf feat(bioyond): 添加测量小瓶功能,支持基本参数配置 2025-11-21 11:32:53 +08:00
ZiWei
0f7366f3ee feat(bioyond): 添加计算实验设计功能,支持化合物配比和滴定比例参数 2025-11-20 12:10:18 +08:00
20 changed files with 2437 additions and 850 deletions

View File

@@ -902,28 +902,28 @@ class MessageProcessor:
async def _handle_request_restart(self, data: Dict[str, Any]): async def _handle_request_restart(self, data: Dict[str, Any]):
""" """
处理重启请求 处理重启请求
当LabGo发送request_restart时执行清理并触发重启 当LabGo发送request_restart时执行清理并触发重启
""" """
reason = data.get("reason", "unknown") reason = data.get("reason", "unknown")
delay = data.get("delay", 2) # 默认延迟2秒 delay = data.get("delay", 2) # 默认延迟2秒
logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s") logger.info(f"[MessageProcessor] Received restart request, reason: {reason}, delay: {delay}s")
# 发送确认消息 # 发送确认消息
if self.websocket_client: if self.websocket_client:
await self.websocket_client.send_message({ await self.websocket_client.send_message({
"action": "restart_acknowledged", "action": "restart_acknowledged",
"data": {"reason": reason, "delay": delay} "data": {"reason": reason, "delay": delay}
}) })
# 设置全局重启标志 # 设置全局重启标志
import unilabos.app.main as main_module import unilabos.app.main as main_module
main_module._restart_requested = True main_module._restart_requested = True
main_module._restart_reason = reason main_module._restart_reason = reason
# 延迟后执行清理 # 延迟后执行清理
await asyncio.sleep(delay) await asyncio.sleep(delay)
# 在新线程中执行清理,避免阻塞当前事件循环 # 在新线程中执行清理,避免阻塞当前事件循环
def do_cleanup(): def do_cleanup():
import time import time
@@ -937,7 +937,7 @@ class MessageProcessor:
logger.error("[MessageProcessor] Cleanup failed") logger.error("[MessageProcessor] Cleanup failed")
except Exception as e: except Exception as e:
logger.error(f"[MessageProcessor] Error during cleanup: {e}") logger.error(f"[MessageProcessor] Error during cleanup: {e}")
cleanup_thread = threading.Thread(target=do_cleanup, name="RestartCleanupThread", daemon=True) cleanup_thread = threading.Thread(target=do_cleanup, name="RestartCleanupThread", daemon=True)
cleanup_thread.start() cleanup_thread.start()
logger.info(f"[MessageProcessor] Restart cleanup scheduled") logger.info(f"[MessageProcessor] Restart cleanup scheduled")
@@ -1375,7 +1375,7 @@ class WebSocketClient(BaseCommunicationClient):
# 收集设备信息 # 收集设备信息
devices = [] devices = []
machine_name = BasicConfig.machine_name machine_name = BasicConfig.machine_name
try: try:
host_node = HostNode.get_instance(0) host_node = HostNode.get_instance(0)
if host_node: if host_node:
@@ -1383,7 +1383,7 @@ class WebSocketClient(BaseCommunicationClient):
for device_id, namespace in host_node.devices_names.items(): for device_id, namespace in host_node.devices_names.items():
device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}" device_key = f"{namespace}/{device_id}" if namespace.startswith("/") else f"/{namespace}/{device_id}"
is_online = device_key in host_node._online_devices is_online = device_key in host_node._online_devices
# 获取设备的动作信息 # 获取设备的动作信息
actions = {} actions = {}
for action_id, client in host_node._action_clients.items(): for action_id, client in host_node._action_clients.items():
@@ -1394,7 +1394,7 @@ class WebSocketClient(BaseCommunicationClient):
"action_path": action_id, "action_path": action_id,
"action_type": str(type(client).__name__), "action_type": str(type(client).__name__),
} }
devices.append({ devices.append({
"device_id": device_id, "device_id": device_id,
"namespace": namespace, "namespace": namespace,
@@ -1403,7 +1403,7 @@ class WebSocketClient(BaseCommunicationClient):
"machine_name": host_node.device_machine_names.get(device_id, machine_name), "machine_name": host_node.device_machine_names.get(device_id, machine_name),
"actions": actions, "actions": actions,
}) })
logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready") logger.info(f"[WebSocketClient] Collected {len(devices)} devices for host_ready")
except Exception as e: except Exception as e:
logger.warning(f"[WebSocketClient] Error collecting device info: {e}") logger.warning(f"[WebSocketClient] Error collecting device info: {e}")

File diff suppressed because it is too large Load Diff

View File

@@ -43,7 +43,7 @@ class Base(ABC):
self._type = typ self._type = typ
self._data_type = data_type self._data_type = data_type
self._node: Optional[Node] = None self._node: Optional[Node] = None
def _get_node(self) -> Node: def _get_node(self) -> Node:
if self._node is None: if self._node is None:
try: try:
@@ -66,7 +66,7 @@ class Base(ABC):
# 直接以字符串形式处理 # 直接以字符串形式处理
if isinstance(nid, str): if isinstance(nid, str):
nid = nid.strip() nid = nid.strip()
# 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)' # 处理包含类名的格式,如 'StringNodeId(ns=4;s=...)' 或 'NumericNodeId(ns=2;i=...)'
# 提取括号内的内容 # 提取括号内的内容
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
@@ -116,16 +116,16 @@ class Base(ABC):
def read(self) -> Tuple[Any, bool]: def read(self) -> Tuple[Any, bool]:
"""读取节点值,返回(值, 是否出错)""" """读取节点值,返回(值, 是否出错)"""
pass pass
@abstractmethod @abstractmethod
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""写入节点值,返回是否出错""" """写入节点值,返回是否出错"""
pass pass
@property @property
def type(self) -> NodeType: def type(self) -> NodeType:
return self._type return self._type
@property @property
def node_id(self) -> str: def node_id(self) -> str:
return self._node_id return self._node_id
@@ -210,15 +210,15 @@ class Method(Base):
super().__init__(client, name, node_id, NodeType.METHOD, data_type) super().__init__(client, name, node_id, NodeType.METHOD, data_type)
self._parent_node_id = parent_node_id self._parent_node_id = parent_node_id
self._parent_node = None self._parent_node = None
def _get_parent_node(self) -> Node: def _get_parent_node(self) -> Node:
if self._parent_node is None: if self._parent_node is None:
try: try:
# 处理父节点ID使用与_get_node相同的解析逻辑 # 处理父节点ID使用与_get_node相同的解析逻辑
import re import re
nid = self._parent_node_id nid = self._parent_node_id
# 如果已经是 NodeId 对象,直接使用 # 如果已经是 NodeId 对象,直接使用
try: try:
from opcua.ua import NodeId as UaNodeId from opcua.ua import NodeId as UaNodeId
@@ -227,16 +227,16 @@ class Method(Base):
return self._parent_node return self._parent_node
except Exception: except Exception:
pass pass
# 字符串处理 # 字符串处理
if isinstance(nid, str): if isinstance(nid, str):
nid = nid.strip() nid = nid.strip()
# 处理包含类名的格式 # 处理包含类名的格式
match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid) match_wrapped = re.match(r'(String|Numeric|Byte|Guid|TwoByteNode|FourByteNode)NodeId\((.*)\)', nid)
if match_wrapped: if match_wrapped:
nid = match_wrapped.group(2).strip() nid = match_wrapped.group(2).strip()
# 常见短格式 # 常见短格式
if re.match(r'^ns=\d+;[is]=', nid): if re.match(r'^ns=\d+;[is]=', nid):
self._parent_node = self._client.get_node(nid) self._parent_node = self._client.get_node(nid)
@@ -271,7 +271,7 @@ class Method(Base):
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""方法节点不支持写入操作""" """方法节点不支持写入操作"""
return True return True
def call(self, *args) -> Tuple[Any, bool]: def call(self, *args) -> Tuple[Any, bool]:
"""调用方法,返回(返回值, 是否出错)""" """调用方法,返回(返回值, 是否出错)"""
try: try:
@@ -285,7 +285,7 @@ class Method(Base):
class Object(Base): class Object(Base):
def __init__(self, client: Client, name: str, node_id: str): def __init__(self, client: Client, name: str, node_id: str):
super().__init__(client, name, node_id, NodeType.OBJECT, None) super().__init__(client, name, node_id, NodeType.OBJECT, None)
def read(self) -> Tuple[Any, bool]: def read(self) -> Tuple[Any, bool]:
"""对象节点不支持直接读取操作""" """对象节点不支持直接读取操作"""
return None, True return None, True
@@ -293,7 +293,7 @@ class Object(Base):
def write(self, value: Any) -> bool: def write(self, value: Any) -> bool:
"""对象节点不支持直接写入操作""" """对象节点不支持直接写入操作"""
return True return True
def get_children(self) -> Tuple[List[Node], bool]: def get_children(self) -> Tuple[List[Node], bool]:
"""获取子节点列表,返回(子节点列表, 是否出错)""" """获取子节点列表,返回(子节点列表, 是否出错)"""
try: try:
@@ -301,4 +301,4 @@ class Object(Base):
return children, False return children, False
except Exception as e: except Exception as e:
print(f"获取对象 {self._name} 的子节点失败: {e}") print(f"获取对象 {self._name} 的子节点失败: {e}")
return [], True return [], True

View File

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

View File

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

View File

@@ -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'),

View File

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

View File

@@ -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: []

View File

@@ -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: 粉末类型ID1=盐21分钟2=面粉27分钟3=BTDA38分钟 description: 粉末类型IDSalt=盐21分钟Flour=面粉27分钟BTDA=BTDA38分钟
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: 反应站子设备-反应器

View File

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

View File

@@ -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",
)

View File

@@ -49,20 +49,17 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
"测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01B03 "测量小瓶仓库(测密度)": bioyond_warehouse_density_vial("测量小瓶仓库(测密度)"), # A01B03
} }
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()

View File

@@ -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:
"""创建测量小瓶仓库(测密度) A01B03""" """创建测量小瓶仓库(测密度) - 竖向排列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站内试剂存放堆栈A01A02, 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:

View File

@@ -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 "{}"

View File

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

View File

@@ -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]:
""" """

View File

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

View File

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

View File

@@ -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": {