Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model

This commit is contained in:
Calvin Cao
2025-10-30 16:31:27 +08:00
committed by GitHub
35 changed files with 4552 additions and 4361 deletions

View File

@@ -16,7 +16,7 @@ API_CONFIG = {
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"),
# HTTP 服务配置
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.91"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")),
"debug_mode": False,# 调试模式
}

View File

@@ -7,7 +7,7 @@ from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstati
class BioyondDispensingStation(BioyondWorkstation):
def __init__(
self,
self,
config,
# 桌子
deck,
@@ -77,7 +77,7 @@ class BioyondDispensingStation(BioyondWorkstation):
- hold_m_name: 库位名称,如"C01"用于查找对应的holdMId
返回: 任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
@@ -85,7 +85,7 @@ class BioyondDispensingStation(BioyondWorkstation):
# 1. 参数验证
if not hold_m_name:
raise BioyondException("hold_m_name 是必填参数")
# 检查90%物料参数的完整性
# 90%_1物料如果有物料名称或目标重量就必须有全部参数
if percent_90_1_assign_material_name or percent_90_1_target_weigh:
@@ -93,21 +93,21 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException("90%_1物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_1_target_weigh:
raise BioyondException("90%_1物料如果提供了物料名称必须同时提供目标重量")
# 90%_2物料如果有物料名称或目标重量就必须有全部参数
if percent_90_2_assign_material_name or percent_90_2_target_weigh:
if not percent_90_2_assign_material_name:
raise BioyondException("90%_2物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_2_target_weigh:
raise BioyondException("90%_2物料如果提供了物料名称必须同时提供目标重量")
# 90%_3物料如果有物料名称或目标重量就必须有全部参数
if percent_90_3_assign_material_name or percent_90_3_target_weigh:
if not percent_90_3_assign_material_name:
raise BioyondException("90%_3物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_3_target_weigh:
raise BioyondException("90%_3物料如果提供了物料名称必须同时提供目标重量")
# 检查10%物料参数的完整性
# 10%_1物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]):
@@ -119,7 +119,7 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_1_liquid_material_name:
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供液体物料名称")
# 10%_2物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]):
if not percent_10_2_assign_material_name:
@@ -130,7 +130,7 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_2_liquid_material_name:
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供液体物料名称")
# 10%_3物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]):
if not percent_10_3_assign_material_name:
@@ -141,7 +141,7 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_3_liquid_material_name:
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供液体物料名称")
# 2. 生成任务编码和设置默认值
order_code = "task_vial_" + str(int(datetime.now().timestamp()))
if order_name is None:
@@ -152,7 +152,7 @@ class BioyondDispensingStation(BioyondWorkstation):
temperature = "40"
if delay_time is None:
delay_time = "600"
# 3. 工作流ID
workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b"
@@ -160,22 +160,22 @@ class BioyondDispensingStation(BioyondWorkstation):
material_info = self.hardware_interface.material_id_query(workflow_id)
if not material_info:
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
# 获取locations列表
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
if not locations:
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
# 查找指定名称的库位
hold_mid = None
for location in locations:
if location.get("holdMName") == hold_m_name:
hold_mid = location.get("holdMId")
break
if not hold_mid:
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
@@ -271,7 +271,7 @@ class BioyondDispensingStation(BioyondWorkstation):
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
return json.dumps({"suc": True})
except BioyondException:
# 重新抛出BioyondException
raise
@@ -307,7 +307,7 @@ class BioyondDispensingStation(BioyondWorkstation):
- hold_m_name: 库位名称,如"ODA-1"用于查找对应的holdMId
返回: 任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
@@ -321,8 +321,8 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException("volume 是必填参数")
if not hold_m_name:
raise BioyondException("hold_m_name 是必填参数")
# 2. 生成任务编码和设置默认值
order_code = "task_oda_" + str(int(datetime.now().timestamp()))
if order_name is None:
@@ -333,30 +333,30 @@ class BioyondDispensingStation(BioyondWorkstation):
temperature = "20"
if delay_time is None:
delay_time = "600"
# 3. 工作流ID - 二胺溶液配置工作流
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
# 4. 查询工作流对应的holdMID
material_info = self.material_id_query(workflow_id)
material_info = self.hardware_interface.material_id_query(workflow_id)
if not material_info:
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
# 获取locations列表
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
if not locations:
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
# 查找指定名称的库位
hold_mid = None
for location in locations:
if location.get("holdMName") == hold_m_name:
hold_mid = location.get("holdMId")
break
if not hold_mid:
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
@@ -397,9 +397,9 @@ class BioyondDispensingStation(BioyondWorkstation):
# 7. 调用create_order方法创建任务
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
return json.dumps({"suc": True})
except BioyondException:
# 重新抛出BioyondException
raise
@@ -409,17 +409,278 @@ class BioyondDispensingStation(BioyondWorkstation):
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
# 批量创建二胺溶液配置任务
def batch_create_diamine_solution_tasks(self,
solutions,
liquid_material_name: str = "NMP",
speed: str = None,
temperature: str = None,
delay_time: str = None) -> str:
"""
批量创建二胺溶液配置任务
参数说明:
- solutions: 溶液列表数组或JSON字符串格式如下:
[
{
"name": "MDA",
"order": 0,
"solid_mass": 5.0,
"solvent_volume": 20,
...
},
...
]
- liquid_material_name: 液体物料名称,默认为"NMP"
- speed: 搅拌速度如果为None则使用默认值400
- temperature: 温度如果为None则使用默认值20
- delay_time: 延迟时间如果为None则使用默认值600
返回: JSON字符串格式的任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
try:
# 参数类型转换:如果是字符串则解析为列表
if isinstance(solutions, str):
try:
solutions = json.loads(solutions)
except json.JSONDecodeError as e:
raise BioyondException(f"solutions JSON解析失败: {str(e)}")
# 参数验证
if not isinstance(solutions, list):
raise BioyondException("solutions 必须是列表类型或有效的JSON数组字符串")
if not solutions:
raise BioyondException("solutions 列表不能为空")
# 批量创建任务
results = []
success_count = 0
failed_count = 0
for idx, solution in enumerate(solutions):
try:
# 提取参数
name = solution.get("name")
solid_mass = solution.get("solid_mass")
solvent_volume = solution.get("solvent_volume")
order = solution.get("order")
if not all([name, solid_mass is not None, solvent_volume is not None]):
self.hardware_interface._logger.warning(
f"跳过第 {idx + 1} 个溶液:缺少必要参数"
)
results.append({
"index": idx + 1,
"name": name,
"success": False,
"error": "缺少必要参数"
})
failed_count += 1
continue
# 生成库位名称(直接使用物料名称)
# 如果需要其他命名规则,可以在这里调整
hold_m_name = name
# 调用单个任务创建方法
result = self.create_diamine_solution_task(
order_name=f"二胺溶液配置-{name}",
material_name=name,
target_weigh=str(solid_mass),
volume=str(solvent_volume),
liquid_material_name=liquid_material_name,
speed=speed,
temperature=temperature,
delay_time=delay_time,
hold_m_name=hold_m_name
)
results.append({
"index": idx + 1,
"name": name,
"success": True,
"hold_m_name": hold_m_name
})
success_count += 1
self.hardware_interface._logger.info(
f"成功创建二胺溶液配置任务: {name}"
)
except BioyondException as e:
results.append({
"index": idx + 1,
"name": solution.get("name", "unknown"),
"success": False,
"error": str(e)
})
failed_count += 1
self.hardware_interface._logger.error(
f"创建第 {idx + 1} 个任务失败: {str(e)}"
)
except Exception as e:
results.append({
"index": idx + 1,
"name": solution.get("name", "unknown"),
"success": False,
"error": f"未知错误: {str(e)}"
})
failed_count += 1
self.hardware_interface._logger.error(
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
)
# 返回汇总结果
summary = {
"total": len(solutions),
"success": success_count,
"failed": failed_count,
"details": results
}
self.hardware_interface._logger.info(
f"批量创建二胺溶液配置任务完成: 总数={len(solutions)}, "
f"成功={success_count}, 失败={failed_count}"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
except BioyondException:
raise
except Exception as e:
error_msg = f"批量创建二胺溶液配置任务时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
# 批量创建90%10%小瓶投料任务
def batch_create_90_10_vial_feeding_tasks(self,
titration,
hold_m_name: str = None,
speed: str = None,
temperature: str = None,
delay_time: str = None,
liquid_material_name: str = "NMP") -> str:
"""
批量创建90%10%小瓶投料任务仅创建1个任务但包含所有90%和10%物料)
参数说明:
- titration: 滴定信息的字典或JSON字符串格式如下:
{
"name": "BTDA",
"main_portion": 1.9152351915461294, # 主称固体质量(g) -> 90%物料
"titration_portion": 0.05923407808905555, # 滴定固体质量(g) -> 10%物料固体
"titration_solvent": 3.050555021586361 # 滴定溶液体积(mL) -> 10%物料液体
}
- hold_m_name: 库位名称,如"C01"。必填参数
- speed: 搅拌速度如果为None则使用默认值400
- temperature: 温度如果为None则使用默认值40
- delay_time: 延迟时间如果为None则使用默认值600
- liquid_material_name: 10%物料的液体物料名称,默认为"NMP"
返回: JSON字符串格式的任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
try:
# 参数类型转换:如果是字符串则解析为字典
if isinstance(titration, str):
try:
titration = json.loads(titration)
except json.JSONDecodeError as e:
raise BioyondException(f"titration参数JSON解析失败: {str(e)}")
# 参数验证
if not isinstance(titration, dict):
raise BioyondException("titration 必须是字典类型或有效的JSON字符串")
if not hold_m_name:
raise BioyondException("hold_m_name 是必填参数")
if not titration:
raise BioyondException("titration 参数不能为空")
# 提取滴定数据
name = titration.get("name")
main_portion = titration.get("main_portion") # 主称固体质量
titration_portion = titration.get("titration_portion") # 滴定固体质量
titration_solvent = titration.get("titration_solvent") # 滴定溶液体积
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
raise BioyondException("titration 数据缺少必要参数")
# 将main_portion平均分成3份作为90%物料3个小瓶
portion_90 = main_portion / 3
# 调用单个任务创建方法
result = self.create_90_10_vial_feeding_task(
order_name=f"90%10%小瓶投料-{name}",
speed=speed,
temperature=temperature,
delay_time=delay_time,
# 90%物料 - 主称固体平均分成3份
percent_90_1_assign_material_name=name,
percent_90_1_target_weigh=str(round(portion_90, 6)),
percent_90_2_assign_material_name=name,
percent_90_2_target_weigh=str(round(portion_90, 6)),
percent_90_3_assign_material_name=name,
percent_90_3_target_weigh=str(round(portion_90, 6)),
# 10%物料 - 滴定固体 + 滴定溶剂只使用第1个10%小瓶)
percent_10_1_assign_material_name=name,
percent_10_1_target_weigh=str(round(titration_portion, 6)),
percent_10_1_volume=str(round(titration_solvent, 6)),
percent_10_1_liquid_material_name=liquid_material_name,
hold_m_name=hold_m_name
)
summary = {
"success": True,
"hold_m_name": hold_m_name,
"material_name": name,
"90_vials": {
"count": 3,
"weight_per_vial": round(portion_90, 6),
"total_weight": round(main_portion, 6)
},
"10_vials": {
"count": 1,
"solid_weight": round(titration_portion, 6),
"liquid_volume": round(titration_solvent, 6)
}
}
self.hardware_interface._logger.info(
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
except BioyondException:
raise
except Exception as e:
error_msg = f"批量创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
if __name__ == "__main__":
bioyond = BioyondDispensingStation(config={
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388"
})
# ============ 原有示例代码 ============
# 示例1使用material_id_query查询工作流对应的holdMID
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
#示例2创建二胺溶液配置任务 - ODA指定库位名称
# bioyond.create_diamine_solution_task(
# order_code="task_oda_" + str(int(datetime.now().timestamp())),
@@ -433,7 +694,7 @@ if __name__ == "__main__":
# delay_time="600",
# hold_m_name="烧杯ODA"
# )
# bioyond.create_diamine_solution_task(
# order_code="task_pda_" + str(int(datetime.now().timestamp())),
# order_name="二胺溶液配置-PDA",
@@ -446,7 +707,7 @@ if __name__ == "__main__":
# delay_time="600",
# hold_m_name="烧杯PDA-2"
# )
# bioyond.create_diamine_solution_task(
# order_code="task_mpda_" + str(int(datetime.now().timestamp())),
# order_name="二胺溶液配置-MPDA",
@@ -462,8 +723,8 @@ if __name__ == "__main__":
bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b")
bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5")
#示例4创建90%10%小瓶投料任务
# vial_result = bioyond.create_90_10_vial_feeding_task(
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
@@ -487,7 +748,7 @@ if __name__ == "__main__":
# delay_time="1200",
# hold_m_name="8.4分装板-1"
# )
# vial_result = bioyond.create_90_10_vial_feeding_task(
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
# order_name="90%10%小瓶投料-2",
@@ -510,7 +771,7 @@ if __name__ == "__main__":
# delay_time="1200",
# hold_m_name="8.4分装板-2"
# )
#启动调度器
#bioyond.scheduler_start()
@@ -529,7 +790,7 @@ if __name__ == "__main__":
material_data_yp = {
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "8.4样品板",
"unit": "",
"quantity": 1,
@@ -540,7 +801,7 @@ if __name__ == "__main__":
"name": "BTDA-1",
"quantity": 20,
"x": 1,
"y": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -585,7 +846,7 @@ if __name__ == "__main__":
material_data_yp = {
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "8.7样品板",
"unit": "",
"quantity": 1,
@@ -596,7 +857,7 @@ if __name__ == "__main__":
"name": "mianfen",
"quantity": 13,
"x": 1,
"y": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -620,7 +881,7 @@ if __name__ == "__main__":
material_data_fzb_1 = {
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "8.7分装板",
"unit": "",
"quantity": 1,
@@ -631,7 +892,7 @@ if __name__ == "__main__":
"name": "10%小瓶1",
"quantity": 1,
"x": 1,
"y": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -642,7 +903,7 @@ if __name__ == "__main__":
"name": "10%小瓶2",
"quantity": 1,
"x": 1,
"y": 2,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -653,7 +914,7 @@ if __name__ == "__main__":
"name": "10%小瓶3",
"quantity": 1,
"x": 1,
"y": 3,
"y": 3,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -697,7 +958,7 @@ if __name__ == "__main__":
material_data_fzb_2 = {
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "8.4分装板-2",
"unit": "",
"quantity": 1,
@@ -708,7 +969,7 @@ if __name__ == "__main__":
"name": "10%小瓶1",
"quantity": 1,
"x": 1,
"y": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -719,7 +980,7 @@ if __name__ == "__main__":
"name": "10%小瓶2",
"quantity": 1,
"x": 1,
"y": 2,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -730,7 +991,7 @@ if __name__ == "__main__":
"name": "10%小瓶3",
"quantity": 1,
"x": 1,
"y": 3,
"y": 3,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
@@ -775,7 +1036,7 @@ if __name__ == "__main__":
material_data_sb_oda = {
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "mianfen1",
"unit": "",
"quantity": 1,
@@ -785,7 +1046,7 @@ if __name__ == "__main__":
material_data_sb_pda_2 = {
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
#"code": "物料编码001",
#"barCode": "物料条码001",
#"barCode": "物料条码001",
"name": "mianfen2",
"unit": "",
"quantity": 1,
@@ -795,7 +1056,7 @@ if __name__ == "__main__":
# material_data_sb_mpda = {
# "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
# #"code": "物料编码001",
# #"barCode": "物料条码001",
# #"barCode": "物料条码001",
# "name": "烧杯MPDA",
# "unit": "个",
# "quantity": 1,

View File

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

View File

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

View File

@@ -973,4 +973,4 @@ def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0,
"""
deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z)
deck.setup()
return deck
return deck

View File

@@ -1,691 +0,0 @@
{
"nodes": [
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": false,
"_comment": "protocol_type接外部工站固定写法字段一般为空station_resource写法也固定",
"protocol_type": [],
"station_resource": {
"data": {
"_resource_child_name": "coin_cell_deck",
"_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck"
}
},
"address": "192.168.1.20",
"port": 502
},
"data": {}
},
{
"id": "coin_cell_deck",
"name": "coin_cell_deck",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8"
],
"parent": null,
"type": "container",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "CoincellDeck",
"size_x": 1000,
"size_y": 1000,
"size_z": 900,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "coin_cell_deck",
"barcode": null
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8",
"name": "\u7535\u6c60\u6599\u76d8",
"sample_id": null,
"children": [
"\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"\u7535\u6c60\u6599\u76d8_materialhole_3_3"
],
"parent": "coin_cell_deck",
"type": "container",
"class": "",
"position": {
"x": 100,
"y": 100,
"z": 0
},
"config": {
"type": "MaterialPlate",
"size_x": 120.8,
"size_y": 160.5,
"size_z": 10.0,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_plate",
"model": null,
"barcode": null,
"ordering": {
"A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3"
}
},
"data": {}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 12.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 36.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 60.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 104.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 80.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 56.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
},
{
"id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3",
"sample_id": null,
"children": [],
"parent": "\u7535\u6c60\u6599\u76d8",
"type": "container",
"class": "",
"position": {
"x": 84.4,
"y": 32.25,
"z": 10.0
},
"config": {
"type": "MaterialHole",
"size_x": 16,
"size_y": 16,
"size_z": 16,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
},
"category": "material_hole",
"model": null,
"barcode": null
},
"data": {
"diameter": 20,
"depth": 10,
"max_sheets": 1,
"info": null
}
}
],
"links": []
}

View File

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

View File

@@ -0,0 +1,38 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "bettery_station_registry",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": false,
"protocol_type": []
}
}
],
"links": []
}

View File

@@ -1,489 +0,0 @@
"""
工作站基类
Workstation Base Class - 简化版
基于PLR Deck的简化工作站架构
专注于核心物料系统和工作流管理
"""
import collections
import time
from typing import Dict, Any, List, Optional, Union
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from pylabrobot.resources import Deck, Plate, Resource as PLRResource
from pylabrobot.resources.coordinate import Coordinate
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.utils.log import logger
class WorkflowStatus(Enum):
"""工作流状态"""
IDLE = "idle"
INITIALIZING = "initializing"
RUNNING = "running"
PAUSED = "paused"
STOPPING = "stopping"
STOPPED = "stopped"
ERROR = "error"
COMPLETED = "completed"
@dataclass
class WorkflowInfo:
"""工作流信息"""
name: str
description: str
estimated_duration: float # 预估持续时间(秒)
required_materials: List[str] # 所需物料类型
output_product: str # 输出产品类型
parameters_schema: Dict[str, Any] # 参数架构
class WorkStationContainer(Plate):
"""
WorkStation 专用 Container 类,继承自 Plate和TipRack
注意这个物料必须通过plr_additional_res_reg.py注册到edge才能正常序列化
"""
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
category: str,
ordering: collections.OrderedDict,
model: Optional[str] = None,
):
"""
这里的初始化入参要和plr的保持一致
"""
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(
self._unilabos_state
) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
"""
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate这里需要到注册表注册例如unilabos/registry/resources/organic/workstation.yaml
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
:param name: 资源名称
:return: Resource对象
"""
plate = WorkStationContainer(
name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
)
tip_rack = WorkStationContainer(
"tip_rack_inside_plate",
size_x=50,
size_y=50,
size_z=10,
category="tip_rack",
ordering=collections.OrderedDict(),
)
plate.assign_child_resource(tip_rack, Coordinate.zero())
return plate
class ResourceSynchronizer(ABC):
"""资源同步器基类
负责与外部物料系统的同步,并对 self.deck 做修改
"""
def __init__(self, workstation: "WorkstationBase"):
self.workstation = workstation
@abstractmethod
async def sync_from_external(self) -> bool:
"""从外部系统同步物料到本地deck"""
pass
@abstractmethod
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
"""将本地物料同步到外部系统"""
pass
@abstractmethod
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
"""处理外部系统的变更通知"""
pass
class WorkstationBase(ABC):
"""工作站基类 - 简化版
核心功能:
1. 基于 PLR Deck 的物料系统,支持格式转换
2. 可选的资源同步器支持外部物料系统
3. 简化的工作流管理
"""
_ros_node: ROS2WorkstationNode
@property
def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别
return self._ros_node.children
async def update_resource_example(self):
return await self._ros_node.update_resource([get_workstation_plate_resource("test")])
def __init__(
self,
station_resource: PLRResource,
*args,
**kwargs, # 必须有kwargs
):
# 基本配置
print(station_resource)
self.deck_config = station_resource
# PLR 物料系统
self.deck: Optional[Deck] = None
self.plr_resources: Dict[str, PLRResource] = {}
# 资源同步器(可选)
# self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化只有workstation用
# 硬件接口
self.hardware_interface: Union[Any, str] = None
# 工作流状态
self.current_workflow_status = WorkflowStatus.IDLE
self.current_workflow_info = None
self.workflow_start_time = None
self.workflow_parameters = {}
# 支持的工作流(静态预定义)
self.supported_workflows: Dict[str, WorkflowInfo] = {}
# 初始化物料系统
self._initialize_material_system()
# 注册支持的工作流
# self._register_supported_workflows()
# logger.info(f"工作站 {device_id} 初始化完成(简化版)")
def _initialize_material_system(self):
"""初始化物料系统 - 使用 graphio 转换"""
try:
from unilabos.resources.graphio import resource_ulab_to_plr
# # 1. 合并 deck_config 和 children 创建完整的资源树
# complete_resource_config = self._create_complete_resource_config()
# # 2. 使用 graphio 转换为 PLR 资源
# self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
# # 3. 建立资源映射
# self._build_resource_mappings(self.deck)
# # 4. 如果有资源同步器,执行初始同步
# if self.resource_synchronizer:
# # 这里可以异步执行,暂时跳过
# pass
# logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
pass
except Exception as e:
# logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
raise
def _create_complete_resource_config(self) -> Dict[str, Any]:
"""创建完整的资源配置 - 合并 deck_config 和 children"""
# 创建主 deck 配置
deck_resource = {
"id": f"{self.device_id}_deck",
"name": f"{self.device_id}_deck",
"type": "deck",
"position": {"x": 0, "y": 0, "z": 0},
"config": {
"size_x": self.deck_config.get("size_x", 1000.0),
"size_y": self.deck_config.get("size_y", 1000.0),
"size_z": self.deck_config.get("size_z", 100.0),
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
},
"data": {},
"children": [],
"parent": None,
}
# 添加子资源
if self._children:
children_list = []
for child_id, child_config in self._children.items():
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
children_list.append(child_resource)
deck_resource["children"] = children_list
return deck_resource
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
"""标准化子资源配置"""
return {
"id": resource_id,
"name": config.get("name", resource_id),
"type": config.get("type", "container"),
"position": self._normalize_position(config.get("position", {})),
"config": config.get("config", {}),
"data": config.get("data", {}),
"children": [], # 简化版本:只支持一层子资源
"parent": parent_id,
}
def _normalize_position(self, position: Any) -> Dict[str, float]:
"""标准化位置信息"""
if isinstance(position, dict):
return {
"x": float(position.get("x", 0)),
"y": float(position.get("y", 0)),
"z": float(position.get("z", 0)),
}
elif isinstance(position, (list, tuple)) and len(position) >= 2:
return {
"x": float(position[0]),
"y": float(position[1]),
"z": float(position[2]) if len(position) > 2 else 0.0,
}
else:
return {"x": 0.0, "y": 0.0, "z": 0.0}
def _build_resource_mappings(self, deck: Deck):
"""递归构建资源映射"""
def add_resource_recursive(resource: PLRResource):
if hasattr(resource, "name"):
self.plr_resources[resource.name] = resource
if hasattr(resource, "children"):
for child in resource.children:
add_resource_recursive(child)
add_resource_recursive(deck)
# ============ 硬件接口管理 ============
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
"""设置硬件接口"""
self.hardware_interface = hardware_interface
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
"""设置协议节点引用(用于代理模式)"""
self._ros_node = workstation_node
logger.info(f"工作站 {self.device_id} 关联协议节点")
# ============ 设备操作接口 ============
def call_device_method(self, method: str, *args, **kwargs) -> Any:
"""调用设备方法的统一接口"""
# 1. 代理模式:通过协议节点转发
if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"):
if not self._ros_node:
raise RuntimeError("代理模式需要设置workstation_node")
device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
return self._ros_node.call_device_method(device_id, method, *args, **kwargs)
# 2. 直接模式:直接调用硬件接口方法
elif self.hardware_interface and hasattr(self.hardware_interface, method):
return getattr(self.hardware_interface, method)(*args, **kwargs)
else:
raise AttributeError(f"硬件接口不支持方法: {method}")
def get_device_status(self) -> Dict[str, Any]:
"""获取设备状态"""
try:
return self.call_device_method("get_status")
except AttributeError:
# 如果设备不支持get_status方法返回基础状态
return {
"status": "unknown",
"interface_type": type(self.hardware_interface).__name__,
"timestamp": time.time(),
}
def is_device_available(self) -> bool:
"""检查设备是否可用"""
try:
self.get_device_status()
return True
except:
return False
# ============ 物料系统接口 ============
def get_deck(self) -> Deck:
"""获取主 Deck"""
return self.deck
def get_all_resources(self) -> Dict[str, PLRResource]:
"""获取所有 PLR 资源"""
return self.plr_resources.copy()
def find_resource_by_name(self, name: str) -> Optional[PLRResource]:
"""按名称查找资源"""
return self.plr_resources.get(name)
def find_resources_by_type(self, resource_type: type) -> List[PLRResource]:
"""按类型查找资源"""
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
async def sync_with_external_system(self) -> bool:
"""与外部物料系统同步"""
if not self.resource_synchronizer:
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
return True
try:
success = await self.resource_synchronizer.sync_from_external()
if success:
logger.info(f"工作站 {self.device_id} 外部同步成功")
else:
logger.warning(f"工作站 {self.device_id} 外部同步失败")
return success
except Exception as e:
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
return False
# ============ 简化的工作流控制 ============
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行工作流"""
try:
# 设置工作流状态
self.current_workflow_status = WorkflowStatus.INITIALIZING
self.workflow_parameters = parameters
self.workflow_start_time = time.time()
# 委托给子类实现
success = self._execute_workflow_impl(workflow_name, parameters)
if success:
self.current_workflow_status = WorkflowStatus.RUNNING
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
else:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
return success
except Exception as e:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
return False
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
try:
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
return True
self.current_workflow_status = WorkflowStatus.STOPPING
# 委托给子类实现
success = self._stop_workflow_impl(emergency)
if success:
self.current_workflow_status = WorkflowStatus.STOPPED
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
else:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self.device_id} 工作流停止失败")
return success
except Exception as e:
self.current_workflow_status = WorkflowStatus.ERROR
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
return False
# ============ 状态属性 ============
@property
def workflow_status(self) -> WorkflowStatus:
"""获取当前工作流状态"""
return self.current_workflow_status
@property
def is_busy(self) -> bool:
"""检查工作站是否忙碌"""
return self.current_workflow_status in [
WorkflowStatus.INITIALIZING,
WorkflowStatus.RUNNING,
WorkflowStatus.STOPPING,
]
@property
def workflow_runtime(self) -> float:
"""获取工作流运行时间(秒)"""
if self.workflow_start_time is None:
return 0.0
return time.time() - self.workflow_start_time
# ============ 抽象方法 - 子类必须实现 ============
# @abstractmethod
# def _register_supported_workflows(self):
# """注册支持的工作流 - 子类必须实现"""
# pass
# @abstractmethod
# def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
# """执行工作流的具体实现 - 子类必须实现"""
# pass
# @abstractmethod
# def _stop_workflow_impl(self, emergency: bool = False) -> bool:
# """停止工作流的具体实现 - 子类必须实现"""
# pass
class WorkstationExample(WorkstationBase):
"""工作站示例实现"""
def _register_supported_workflows(self):
"""注册支持的工作流"""
self.supported_workflows["example_workflow"] = WorkflowInfo(
name="example_workflow",
description="这是一个示例工作流",
estimated_duration=300.0,
required_materials=["sample_plate"],
output_product="processed_plate",
parameters_schema={"param1": "string", "param2": "integer"},
)
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行工作流的具体实现"""
if workflow_name not in self.supported_workflows:
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
return False
# 这里添加实际的工作流逻辑
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
return True
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
"""停止工作流的具体实现"""
# 这里添加实际的停止逻辑
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
return True