Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions test/experiments/reaction_station_bioyond.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,42 @@
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
},
"material_type_mappings": {
"烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
"试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
"样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
"烧杯": [
"BIOYOND_PolymerStation_1FlaskCarrier",
"3a14196b-24f2-ca49-9081-0cab8021bf1a"
],
"试剂瓶": [
"BIOYOND_PolymerStation_1BottleCarrier",
""
],
"样品板": [
"BIOYOND_PolymerStation_6StockCarrier",
"3a14196e-b7a0-a5da-1931-35f3000281e9"
],
"分装板": [
"BIOYOND_PolymerStation_6VialCarrier",
"3a14196e-5dfe-6e21-0c79-fe2036d052c4"
],
"样品瓶": [
"BIOYOND_PolymerStation_Solid_Stock",
"3a14196a-cf7d-8aea-48d8-b9662c7dba94"
],
"90%分装小瓶": [
"BIOYOND_PolymerStation_Solid_Vial",
"3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"
],
"10%分装小瓶": [
"BIOYOND_PolymerStation_Liquid_Vial",
"3a14196c-76be-2279-4e22-7310d69aed68"
],
"枪头盒": [
"BIOYOND_PolymerStation_TipBox",
""
],
"反应器": [
"BIOYOND_PolymerStation_Reactor",
""
]
}
},
"deck": {
Expand All @@ -46,8 +75,7 @@
{
"id": "Bioyond_Deck",
"name": "Bioyond_Deck",
"children": [
],
"children": [],
"parent": "reaction_station_bioyond",
"type": "deck",
"class": "BIOYOND_PolymerReactionStation_Deck",
Expand All @@ -69,4 +97,4 @@
"data": {}
}
]
}
}
39 changes: 34 additions & 5 deletions unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def delete_material(self, material_id: str) -> dict:
return response.get("data", {})

def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict:
"""指定库位出库物料"""
"""指定库位出库物料(通过库位名称)"""
location_id = LOCATION_MAPPING.get(location_name, location_name)

params = {
Expand All @@ -251,7 +251,36 @@ def material_outbound(self, material_id: str, location_name: str, quantity: int)
})

if not response or response['code'] != 1:
return {}
return None
return response

def material_outbound_by_id(self, material_id: str, location_id: str, quantity: int) -> dict:
"""指定库位出库物料(直接使用location_id)

Args:
material_id: 物料ID
location_id: 库位ID(不是库位名称,是UUID)
quantity: 数量

Returns:
dict: API响应,失败返回None
"""
params = {
"materialId": material_id,
"locationId": location_id,
"quantity": quantity
}

response = self.post(
url=f'{self.host}/api/lims/storage/outbound',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
"data": params
})

if not response or response['code'] != 1:
return None
return response

# ==================== 工作流查询相关接口 ====================
Expand Down Expand Up @@ -703,10 +732,10 @@ def _load_material_cache(self):
"""预加载材料列表到缓存中"""
try:
print("正在加载材料列表缓存...")

# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
material_types = [1, 2]

for type_mode in material_types:
print(f"正在加载类型 {type_mode} 的材料...")
stock_query = f'{{"typeMode": {type_mode}, "includeDetail": true}}'
Expand All @@ -723,7 +752,7 @@ def _load_material_cache(self):
material_id = material.get("id")
if material_name and material_id:
self.material_cache[material_name] = material_id

# 处理样品板等容器中的detail材料
detail_materials = material.get("detail", [])
for detail_material in detail_materials:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def liquid_feeding_solvents(
temperature: 温度设定(°C)
"""
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
if volume is None and solvents is not None:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Changing 'if volume is None' to 'if not volume' may cause issues with falsy values.

This change will cause both None and 0 to trigger the condition. If 0 is a valid volume, use 'is None' to avoid unintended behavior.

if not volume and solvents is not None:
# 参数类型转换:如果是字符串则解析为字典
if isinstance(solvents, str):
try:
Expand Down
107 changes: 100 additions & 7 deletions unilabos/devices/workstation/bioyond_studio/station.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,90 @@ def sync_from_external(self) -> bool:
def sync_to_external(self, resource: Any) -> bool:
"""将本地物料数据变更同步到Bioyond系统"""
try:
if self.bioyond_api_client is None:
logger.error("Bioyond API客户端未初始化")
# ✅ 跳过仓库类型的资源 - 仓库是容器,不是物料
resource_category = getattr(resource, "category", None)
if resource_category == "warehouse":
logger.debug(f"[同步→Bioyond] 跳过仓库类型资源: {resource.name} (仓库是容器,不需要同步为物料)")
return True

logger.info(f"[同步→Bioyond] 收到物料变更: {resource.name}")

# 获取物料的 Bioyond ID
extra_info = getattr(resource, "unilabos_extra", {})
material_bioyond_id = extra_info.get("material_bioyond_id")

# ⭐ 如果没有 Bioyond ID,尝试从 Bioyond 系统中按名称查询
if not material_bioyond_id:
logger.warning(f"[同步→Bioyond] 物料 {resource.name} 没有 Bioyond ID,尝试按名称查询...")
try:
# 查询所有类型的物料:0=耗材, 1=样品, 2=试剂
import json
all_materials = []

for type_mode in [0, 1, 2]:
query_params = json.dumps({
"typeMode": type_mode,
"filter": "", # 空字符串表示查询所有
"includeDetail": True
})
materials = self.bioyond_api_client.stock_material(query_params)
if materials:
all_materials.extend(materials)

logger.info(f"[同步→Bioyond] 查询到 {len(all_materials)} 个物料")

# 按名称匹配
for mat in all_materials:
if mat.get("name") == resource.name:
material_bioyond_id = mat.get("id")
mat_type = mat.get("typeName", "未知")
logger.info(f"✅ 找到物料 {resource.name} ({mat_type}) 的 Bioyond ID: {material_bioyond_id[:8]}...")
# 保存 ID 到资源对象
extra_info["material_bioyond_id"] = material_bioyond_id
setattr(resource, "unilabos_extra", extra_info)
break

if not material_bioyond_id:
logger.warning(f"⚠️ 在 Bioyond 系统中未找到名为 {resource.name} 的物料")
logger.info(f"[同步→Bioyond] 这是一个新物料,将创建并入库到 Bioyond 系统")
# 不返回,继续执行后续的创建+入库流程
except Exception as e:
logger.error(f"查询 Bioyond 物料失败: {e}")
import traceback
traceback.print_exc()
return False

# 检查是否有位置更新请求
update_site = extra_info.get("update_resource_site")

if not update_site:
logger.debug(f"[同步→Bioyond] 无位置更新请求")
return True

# ===== 物料移动/创建流程 =====
if material_bioyond_id:
logger.info(f"[同步→Bioyond] 🔄 开始移动物料 {resource.name} 到 {update_site}")
else:
logger.info(f"[同步→Bioyond] ➕ 开始创建新物料 {resource.name} 并入库到 {update_site}") # 第1步:获取仓库配置
from .config import WAREHOUSE_MAPPING
warehouse_mapping = WAREHOUSE_MAPPING

# 确定目标仓库名称(通过遍历所有仓库的库位配置)
parent_name = None
target_location_uuid = None

for warehouse_name, warehouse_info in warehouse_mapping.items():
site_uuids = warehouse_info.get("site_uuids", {})
if update_site in site_uuids:
parent_name = warehouse_name
target_location_uuid = site_uuids[update_site]
logger.info(f"[同步] 目标仓库: {parent_name}/{update_site}")
logger.info(f"[同步] 目标库位UUID: {target_location_uuid[:8]}...")
break

if not parent_name or not target_location_uuid:
logger.error(f"❌ 库位 {update_site} 没有在 WAREHOUSE_MAPPING 中配置")
logger.debug(f"可用仓库: {list(warehouse_mapping.keys())}")
return False

bioyond_material = resource_plr_to_bioyond(
Expand Down Expand Up @@ -171,11 +253,22 @@ def __init__(

def post_init(self, ros_node: ROS2WorkstationNode):
self._ros_node = ros_node

# ⭐ 上传 deck(包括所有 warehouses 及其中的物料)
# 注意:如果有从 Bioyond 同步的物料,它们已经被放置到 warehouse 中了
# 所以只需要上传 deck,物料会作为 warehouse 的 children 一起上传
logger.info("正在上传 deck(包括 warehouses 和物料)到云端...")
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})

# 清理临时变量(物料已经在 deck 的 warehouse children 中,不需要单独上传)
if hasattr(self, "_synced_resources"):
logger.info(f"✅ {len(self._synced_resources)} 个从Bioyond同步的物料已包含在 deck 中")
self._synced_resources = []

def transfer_resource_to_another(self, resource: List[ResourceSlot], mount_resource: List[ResourceSlot], sites: List[str], mount_device_id: DeviceSlot):
time.sleep(3)
ROS2DeviceNode.run_async_func(self._ros_node.transfer_resource_to_another, True, **{
"plr_resources": resource,
"target_device_id": mount_device_id,
Expand Down Expand Up @@ -246,7 +339,7 @@ def bioyond_status(self) -> Dict[str, Any]:
}

# ==================== 工作流合并与参数设置 API ====================

def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
# 检查是否为JSON格式的字符串
actual_workflow_name = web_workflow_name
Expand All @@ -257,7 +350,7 @@ def append_to_workflow_sequence(self, web_workflow_name: str) -> bool:
print(f"解析JSON格式工作流名称: {web_workflow_name} -> {actual_workflow_name}")
except json.JSONDecodeError:
print(f"JSON解析失败,使用原始字符串: {web_workflow_name}")

workflow_id = self._get_workflow(actual_workflow_name)
if workflow_id:
self.workflow_sequence.append(workflow_id)
Expand Down Expand Up @@ -322,7 +415,7 @@ def clear_workflows(self):
# ============ 工作站状态管理 ============
def get_station_info(self) -> Dict[str, Any]:
"""获取工作站基础信息

Returns:
Dict[str, Any]: 工作站基础信息,包括设备ID、状态等
"""
Expand Down Expand Up @@ -450,8 +543,8 @@ def load_bioyond_data_from_file(self, file_path: str) -> bool:

# 转换为UniLab格式
unilab_resources = resource_bioyond_to_plr(
bioyond_data,
type_mapping=self.bioyond_config["material_type_mappings"],
bioyond_data,
type_mapping=self.bioyond_config["material_type_mappings"],
deck=self.deck
)

Expand Down
22 changes: 22 additions & 0 deletions unilabos/registry/resources/bioyond/bottles.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,25 @@ BIOYOND_PolymerStation_Solution_Beaker:
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_TipBox:
category:
- bottles
- tip_boxes
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_TipBox
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Reactor:
category:
- bottles
- reactors
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reactor
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
Loading
Loading