mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-18 05:21:19 +00:00
Merge branch 'workstation_dev_YB3' into fix/yb3-material-names-and-model
This commit is contained in:
@@ -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,# 调试模式
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟)
|
||||
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}")
|
||||
|
||||
@@ -4,6 +4,7 @@ Bioyond Workstation Implementation
|
||||
|
||||
集成Bioyond物料管理的工作站示例
|
||||
"""
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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": []
|
||||
}
|
||||
@@ -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,负极片摆盘方式
|
||||
|
@@ -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": []
|
||||
}
|
||||
@@ -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进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||
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
|
||||
Reference in New Issue
Block a user