Compare commits

...

7 Commits

Author SHA1 Message Date
calvincao
e92d933968 refactor(bioyond_cell_workstation): 重构物料创建与入库逻辑- 移除从CSV读取物料名称的功能
- 新增通过参数传递物料名称列表的方式- 抽离仓库位置加载逻辑至独立方法
- 简化物料创建与入库流程- 统一使用资源同步器进行数据同步
- 更新调用示例以适配新接口
2025-10-23 22:36:21 +08:00
Calvin Cao
f0ebcc60bb Merge pull request #126 from sun7151887/fix/yb3-material-names-and-model
添加新物料类型映射:包括100ml液体、液、高粘液、5ml/20ml分液瓶、配液瓶、加样头、适配器块、枪头盒等
2025-10-23 21:59:26 +08:00
dijkstra402
e2097f0b22 添加新物料类型映射:包括100ml液体、液、高粘液、5ml/20ml分液瓶、配液瓶、加样头、适配器块、枪头盒等 2025-10-23 21:56:54 +08:00
calvincao
fd73731130 增强批量入库功能,添加物料数据同步逻辑;优化日志记录以提供更详细的同步状态信息。 2025-10-23 18:02:49 +08:00
Calvin Cao
ab7f2081c9 Merge pull request #124 from sun7151887/fix/yb3-material-names-and-model
更新载架网格布局:5ml/20ml/配液瓶(小)板改为4x2,加样头(大)板改为1x1
2025-10-23 17:45:29 +08:00
dijkstra402
9e850d8a81 更新载架网格布局:5ml/20ml/配液瓶(小)板改为4x2,加样头(大)板改为1x1 2025-10-23 17:42:10 +08:00
calvincao
1af6ffafc6 新增批量创建固体物料和从CSV文件入库的功能;更新配置文件中的 report_ip 默认值;新增 solid_materials.csv 文件以支持物料名称导入。 2025-10-23 17:32:08 +08:00
4 changed files with 271 additions and 32 deletions

View File

@@ -14,7 +14,7 @@ import socket
from urllib3 import response
from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation, BioyondResourceSynchronizer
from unilabos.devices.workstation.bioyond_studio.config import (
BIOYOND_FULL_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING
)
@@ -825,12 +825,206 @@ class BioyondCellWorkstation(BioyondWorkstation):
logger.warning("超时未找到成功的物料转移任务")
return False
def create_solid_materials(self, material_names: List[str], type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469") -> List[Dict[str, Any]]:
"""
批量创建固体物料
Args:
material_names: 物料名称列表
type_id: 物料类型ID默认为固体物料类型
Returns:
创建的物料列表每个元素包含物料信息和ID
"""
created_materials = []
total = len(material_names)
for i, name in enumerate(material_names, 1):
# 根据接口文档构建完整的请求体
material_data = {
"typeId": type_id,
"name": name,
"unit": "g", # 添加单位
"quantity": 1, # 添加数量默认1
"parameters": "" # 参数字段(空字符串表示无参数)
}
logger.info(f"正在创建第 {i}/{total} 个固体物料: {name}")
result = self._post_lims("/api/lims/storage/material", material_data)
if result and result.get("code") == 1:
# data 字段可能是字符串物料ID或字典包含id字段
data = result.get("data")
if isinstance(data, str):
# data 直接是物料ID字符串
material_id = data
elif isinstance(data, dict):
# data 是字典包含id字段
material_id = data.get("id")
else:
material_id = None
if material_id:
created_materials.append({
"name": name,
"materialId": material_id,
"typeId": type_id
})
logger.info(f"✓ 成功创建物料: {name}, ID: {material_id}")
else:
logger.error(f"✗ 创建物料失败: {name}, 未返回ID")
logger.error(f" 响应数据: {result}")
else:
error_msg = result.get("error") or result.get("message", "未知错误")
logger.error(f"✗ 创建物料失败: {name}")
logger.error(f" 错误信息: {error_msg}")
logger.error(f" 完整响应: {result}")
# 避免请求过快
time.sleep(0.3)
logger.info(f"物料创建完成,成功创建 {len(created_materials)}/{total} 个固体物料")
return created_materials
def _sync_materials_safe(self) -> bool:
"""仅使用 BioyondResourceSynchronizer 执行同步(与 station.py 保持一致)。"""
if hasattr(self, 'resource_synchronizer') and self.resource_synchronizer:
try:
return bool(self.resource_synchronizer.sync_from_external())
except Exception as e:
logger.error(f"同步失败: {e}")
return False
logger.warning("资源同步器未初始化")
return False
def _load_warehouse_locations(self, warehouse_name: str) -> tuple[List[str], List[str]]:
"""从配置加载仓库位置信息
Args:
warehouse_name: 仓库名称
Returns:
(location_ids, position_names) 元组
"""
warehouse_mapping = self.bioyond_config.get("warehouse_mapping", WAREHOUSE_MAPPING)
if warehouse_name not in warehouse_mapping:
raise ValueError(f"配置中未找到仓库: {warehouse_name}。可用: {list(warehouse_mapping.keys())}")
site_uuids = warehouse_mapping[warehouse_name].get("site_uuids", {})
if not site_uuids:
raise ValueError(f"仓库 {warehouse_name} 没有配置位置")
# 按顺序获取位置ID和名称
location_ids = []
position_names = []
for key in sorted(site_uuids.keys()):
location_ids.append(site_uuids[key])
position_names.append(key)
return location_ids, position_names
def create_and_inbound_materials(
self,
material_names: Optional[List[str]] = None,
type_id: str = "3a190ca0-b2f6-9aeb-8067-547e72c11469",
warehouse_name: str = "粉末加样头堆栈"
) -> Dict[str, Any]:
"""
传参与默认列表方式创建物料并入库不使用CSV
Args:
material_names: 物料名称列表;默认使用 [LiPF6, LiDFOB, DTD, LiFSI, LiPO2F2]
type_id: 物料类型ID
warehouse_name: 目标仓库名(用于取位置信息)
Returns:
执行结果字典
"""
logger.info("=" * 60)
logger.info(f"开始执行:从参数创建物料并批量入库到 {warehouse_name}")
logger.info("=" * 60)
try:
# 1) 准备物料名称(默认值)
default_materials = ["LiPF6", "LiDFOB", "DTD", "LiFSI", "LiPO2F2"]
mat_names = [m.strip() for m in (material_names or default_materials) if str(m).strip()]
if not mat_names:
return {"success": False, "error": "物料名称列表为空"}
# 2) 加载仓库位置信息
all_location_ids, position_names = self._load_warehouse_locations(warehouse_name)
logger.info(f"✓ 加载 {len(all_location_ids)} 个位置 ({position_names[0]} ~ {position_names[-1]})")
# 限制数量不超过可用位置
if len(mat_names) > len(all_location_ids):
logger.warning(f"物料数量超出位置数量,仅处理前 {len(all_location_ids)}")
mat_names = mat_names[:len(all_location_ids)]
# 3) 创建物料
logger.info(f"\n【步骤1/3】创建 {len(mat_names)} 个固体物料...")
created_materials = self.create_solid_materials(mat_names, type_id)
if not created_materials:
return {"success": False, "error": "没有成功创建任何物料"}
# 4) 批量入库
logger.info(f"\n【步骤2/3】批量入库物料...")
location_ids = all_location_ids[:len(created_materials)]
selected_positions = position_names[:len(created_materials)]
inbound_items = [
{"materialId": mat["materialId"], "locationId": loc_id}
for mat, loc_id in zip(created_materials, location_ids)
]
for material, position in zip(created_materials, selected_positions):
logger.info(f" - {material['name']}{position}")
result = self.storage_batch_inbound(inbound_items)
if result.get("code") != 1:
logger.error(f"✗ 批量入库失败: {result}")
return {"success": False, "error": "批量入库失败", "created_materials": created_materials, "inbound_result": result}
logger.info("✓ 批量入库成功")
# 5) 同步
logger.info(f"\n【步骤3/3】同步物料数据...")
if self._sync_materials_safe():
logger.info("✓ 物料数据同步完成")
else:
logger.warning("⚠ 物料数据同步未完成(可忽略,不影响已创建与入库的数据)")
logger.info("\n" + "=" * 60)
logger.info("流程完成")
logger.info("=" * 60 + "\n")
return {
"success": True,
"created_materials": created_materials,
"inbound_result": result,
"total_created": len(created_materials),
"total_inbound": len(inbound_items),
"warehouse": warehouse_name,
"positions": selected_positions
}
except Exception as e:
logger.error(f"✗ 执行失败: {e}")
return {"success": False, "error": str(e)}
# --------------------------------
if __name__ == "__main__":
ws = BioyondCellWorkstation()
logger.info(ws.scheduler_start())
#TODO新建入库
# 从CSV文件读取物料列表并批量创建入库
result = ws.create_and_inbound_materials()
# 继续后续流程
logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱
# 使用正斜杠或 Path 对象来指定文件路径
excel_path = Path("unilabos/devices/workstation/bioyond_studio/bioyond_cell/2025092701.xlsx")

View File

@@ -0,0 +1,7 @@
material_name
LiPF6
LiDFOB
DTD
LiFSI
LiPO2F2
1 material_name
2 LiPF6
3 LiDFOB
4 DTD
5 LiFSI
6 LiPO2F2

View File

@@ -25,8 +25,7 @@ BIOYOND_FULL_CONFIG = {
# HTTP 服务配置
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "0.0.0.0"), # HTTP服务监听地址0.0.0.0 表示监听所有网络接口)
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"report_ip": os.getenv("BIOYOND_REPORT_IP", "172.21.32.57"), # 报送给 Bioyond 的本机IP地址留空则自动检测
"report_ip": os.getenv("BIOYOND_REPORT_IP", "172.21.32.172"), # 报送给 Bioyond 的本机IP地址留空则自动检测
# 调试模式
"debug_mode": os.getenv("BIOYOND_DEBUG_MODE", "False").lower() == "true",
}
@@ -112,6 +111,31 @@ WAREHOUSE_MAPPING = {
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
}
},
"粉末加样头堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da56-1379-20c8-5886-f7c4fbcb5733",
"B01": "3a19da56-1379-2424-d751-fe6e94cef938",
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e",
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6",
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a",
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d",
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1",
"H01": "3a19da56-1379-6077-8258-bdc036870b78",
"I01": "3a19da56-1379-863b-a120-f606baf04617",
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5",
"K01": "3a19da56-1379-b270-b7af-f18773918abe",
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc",
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04",
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888",
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e",
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9",
"Q01": "3a19da56-1379-edf6-1472-802ddb628774",
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97",
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4",
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2"
}
}
}
@@ -124,6 +148,22 @@ MATERIAL_TYPE_MAPPINGS = {
"样品瓶": ("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"),
"20ml分液瓶": ("BIOYOND_PolymerStation_6x20ml_DispensingVialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"100ml液体": ("BIOYOND_PolymerStation_100ml_Liquid_Bottle", "d37166b3-ecaa-481e-bd84-3032b795ba07"),
"": ("BIOYOND_PolymerStation_Liquid_Bottle", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"高粘液": ("BIOYOND_PolymerStation_High_Viscosity_Liquid_Bottle", "abe8df30-563d-43d2-85e0-cabec59ddc16"),
"加样头(大)": ("BIOYOND_PolymerStation_Large_Dispense_Head", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"5ml分液瓶板": ("BIOYOND_PolymerStation_6x5ml_DispensingVialCarrier", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液瓶": ("BIOYOND_PolymerStation_5ml_Dispensing_Vial", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
"20ml分液瓶板": ("BIOYOND_PolymerStation_6x20ml_DispensingVialCarrier", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
"配液瓶(小)板": ("BIOYOND_PolymerStation_6x_SmallSolutionBottleCarrier", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("BIOYOND_PolymerStation_Small_Solution_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"配液瓶(大)板": ("BIOYOND_PolymerStation_4x_LargeSolutionBottleCarrier", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
"配液瓶(大)": ("BIOYOND_PolymerStation_Large_Solution_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
"加样头(大)板": ("BIOYOND_PolymerStation_6x_LargeDispenseHeadCarrier", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"适配器块": ("BIOYOND_PolymerStation_AdapterBlock", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
"枪头盒": ("BIOYOND_PolymerStation_TipBox", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
"枪头": ("BIOYOND_PolymerStation_Pipette_Tip", "b6196971-1050-46da-9927-333e8dea062d"),
}
# 步骤参数配置各工作流的步骤UUID

View File

@@ -283,7 +283,7 @@ def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier:
def BIOYOND_PolymerStation_6x5ml_DispensingVialCarrier(name: str) -> BottleCarrier:
"""5ml分液瓶板 - 2x3布局,6个位置"""
"""5ml分液瓶板 - 4x2布局,8个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -296,12 +296,12 @@ def BIOYOND_PolymerStation_6x5ml_DispensingVialCarrier(name: str) -> BottleCarri
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
@@ -323,17 +323,17 @@ def BIOYOND_PolymerStation_6x5ml_DispensingVialCarrier(name: str) -> BottleCarri
sites=sites,
model="6x5ml_DispensingVialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"]
for i in range(6):
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = BIOYOND_PolymerStation_5ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_6x20ml_DispensingVialCarrier(name: str) -> BottleCarrier:
"""20ml分液瓶板 - 2x3布局,6个位置"""
"""20ml分液瓶板 - 4x2布局,8个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -346,12 +346,12 @@ def BIOYOND_PolymerStation_6x20ml_DispensingVialCarrier(name: str) -> BottleCarr
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
@@ -373,17 +373,17 @@ def BIOYOND_PolymerStation_6x20ml_DispensingVialCarrier(name: str) -> BottleCarr
sites=sites,
model="6x20ml_DispensingVialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"]
for i in range(6):
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = BIOYOND_PolymerStation_20ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_6x_SmallSolutionBottleCarrier(name: str) -> BottleCarrier:
"""配液瓶(小)板 - 2x3布局,6个位置"""
"""配液瓶(小)板 - 4x2布局,8个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -396,12 +396,12 @@ def BIOYOND_PolymerStation_6x_SmallSolutionBottleCarrier(name: str) -> BottleCar
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_x=4,
num_items_y=2,
dx=start_x,
dy=start_y,
@@ -423,11 +423,11 @@ def BIOYOND_PolymerStation_6x_SmallSolutionBottleCarrier(name: str) -> BottleCar
sites=sites,
model="6x_SmallSolutionBottleCarrier",
)
carrier.num_items_x = 3
carrier.num_items_x = 4
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"]
for i in range(6):
ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"]
for i in range(8):
carrier[i] = BIOYOND_PolymerStation_Small_Solution_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
@@ -483,7 +483,7 @@ def BIOYOND_PolymerStation_4x_LargeSolutionBottleCarrier(name: str) -> BottleCar
def BIOYOND_PolymerStation_6x_LargeDispenseHeadCarrier(name: str) -> BottleCarrier:
"""加样头(大)板 - 2x3布局,6个位置"""
"""加样头(大)板 - 1x1布局,1个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
@@ -496,13 +496,13 @@ def BIOYOND_PolymerStation_6x_LargeDispenseHeadCarrier(name: str) -> BottleCarri
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=2,
num_items_x=1,
num_items_y=1,
dx=start_x,
dy=start_y,
dz=5.0,
@@ -523,12 +523,10 @@ def BIOYOND_PolymerStation_6x_LargeDispenseHeadCarrier(name: str) -> BottleCarri
sites=sites,
model="6x_LargeDispenseHeadCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"]
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Large_Dispense_Head(f"{name}_head_{ordering[i]}")
carrier[0] = BIOYOND_PolymerStation_Large_Dispense_Head(f"{name}_head_1")
return carrier