Files
Uni-Lab-OS/unilabos/devices/workstation/workflow_executors.py
Xuwznln 9aeffebde1 0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>

* fix: workstation handlers and vessel_id parsing

* fix: working dir error when input config path
feat: report publish topic when error

* modify default discovery_interval to 15s

* feat: add trace log level

* feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

* feat: 优化protocol node节点运行日志

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

* refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

* update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)

* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

* fix: station自己的方法注册错误

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

* 修复工站的tracker实例追踪失效问题

* 增加handle检测,增加material edge关系上传

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

* 主机节点信息等支持自动刷新

* 注册表编辑器

* 修复status密集发送时,消息出错

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

- Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes.
- Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume.
- Both actions include response fields for return information and success status.

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

* Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

* 对于PRCXI9320的transfer_group,一对多和多对多

* 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

* Fix: run-column with correct vessel id (#86)

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

* 重新规定了版位推荐的入参

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

* 修复移液站错误的aspirate注册表

* Feature/xprbalance-zhida (#80)

* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* frontend_docs

* create/update resources with POST/PUT for big amount/ small amount data

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

* Workstation templates: Resources and its CRUD, and workstation tasks (#95)

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.

* 修复to_plr_resources

* add update remove

* 支持选择器注册表自动生成
支持转运物料

* 修复资源添加

* 修复transfer_resource_to_another生成

* 更新transfer_resource_to_another参数,支持spot入参

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

* 修复同步任务报错不显示的bug

* feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了

* update todo

* modify bioyond/plr converter, bioyond resource registry, and tests

* pass the tests

* update todo

* add conda-pack-build.yml

* add auto install script for conda-pack-build.yml

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py

* Update conda-pack-build.yml

* Update conda-pack-build.yml (with mamba)

* Update conda-pack-build.yml

* Fix FileNotFoundError

* Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined>

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

* Update registry. Update uuid loop figure method. Update install docs.

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字

* 电池装配工站二次开发教程(带目录)上传至dev (#94)

* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释

* Update prcxi driver & fix transfer_liquid mix_times (#90)

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: LccLink <1951855008@qq.com>
Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
2025-10-12 23:34:26 +08:00

650 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
工作流执行器模块
Workflow Executors Module
基于单一硬件接口的工作流执行器实现
支持Modbus、HTTP、PyLabRobot和代理模式
"""
import time
import json
import asyncio
from typing import Dict, Any, List, Optional, TYPE_CHECKING
from abc import ABC, abstractmethod
if TYPE_CHECKING:
from unilabos.devices.work_station.workstation_base import WorkstationBase
from unilabos.utils.log import logger
class WorkflowExecutor(ABC):
"""工作流执行器基类 - 基于单一硬件接口"""
def __init__(self, workstation: 'WorkstationBase'):
self.workstation = workstation
self.hardware_interface = workstation.hardware_interface
self.material_management = workstation.material_management
@abstractmethod
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行工作流"""
pass
@abstractmethod
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
pass
def call_device(self, method: str, *args, **kwargs) -> Any:
"""调用设备方法的统一接口"""
return self.workstation.call_device_method(method, *args, **kwargs)
def get_device_status(self) -> Dict[str, Any]:
"""获取设备状态"""
return self.workstation.get_device_status()
class ModbusWorkflowExecutor(WorkflowExecutor):
"""Modbus工作流执行器 - 适配 coin_cell_assembly_system"""
def __init__(self, workstation: 'WorkstationBase'):
super().__init__(workstation)
# 验证Modbus接口
if not (hasattr(self.hardware_interface, 'write_register') and
hasattr(self.hardware_interface, 'read_register')):
raise RuntimeError("工作站硬件接口不是有效的Modbus客户端")
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行Modbus工作流"""
if workflow_name == "battery_manufacturing":
return self._execute_battery_manufacturing(parameters)
elif workflow_name == "material_loading":
return self._execute_material_loading(parameters)
elif workflow_name == "quality_check":
return self._execute_quality_check(parameters)
else:
logger.warning(f"不支持的Modbus工作流: {workflow_name}")
return False
def _execute_battery_manufacturing(self, parameters: Dict[str, Any]) -> bool:
"""执行电池制造工作流"""
try:
# 1. 物料准备检查
available_slot = self._find_available_press_slot()
if not available_slot:
raise RuntimeError("没有可用的压制槽")
logger.info(f"找到可用压制槽: {available_slot}")
# 2. 设置工艺参数直接调用Modbus接口
if "electrolyte_num" in parameters:
self.hardware_interface.write_register('REG_MSG_ELECTROLYTE_NUM', parameters["electrolyte_num"])
logger.info(f"设置电解液编号: {parameters['electrolyte_num']}")
if "electrolyte_volume" in parameters:
self.hardware_interface.write_register('REG_MSG_ELECTROLYTE_VOLUME',
parameters["electrolyte_volume"],
data_type="FLOAT32")
logger.info(f"设置电解液体积: {parameters['electrolyte_volume']}")
if "assembly_pressure" in parameters:
self.hardware_interface.write_register('REG_MSG_ASSEMBLY_PRESSURE',
parameters["assembly_pressure"],
data_type="FLOAT32")
logger.info(f"设置装配压力: {parameters['assembly_pressure']}")
# 3. 启动制造流程
self.hardware_interface.write_register('COIL_SYS_START_CMD', True)
logger.info("启动电池制造流程")
# 4. 确认启动成功
time.sleep(0.5)
status = self.hardware_interface.read_register('COIL_SYS_START_STATUS', count=1)
success = status[0] if status else False
if success:
logger.info(f"电池制造工作流启动成功,参数: {parameters}")
else:
logger.error("电池制造工作流启动失败")
return success
except Exception as e:
logger.error(f"执行电池制造工作流失败: {e}")
return False
def _execute_material_loading(self, parameters: Dict[str, Any]) -> bool:
"""执行物料装载工作流"""
try:
material_type = parameters.get('material_type', 'cathode')
position = parameters.get('position', 'A1')
logger.info(f"开始物料装载: {material_type} -> {position}")
# 设置物料类型和位置
self.hardware_interface.write_register('REG_MATERIAL_TYPE', material_type)
self.hardware_interface.write_register('REG_MATERIAL_POSITION', position)
# 启动装载
self.hardware_interface.write_register('COIL_LOAD_START', True)
# 等待装载完成
timeout = parameters.get('timeout', 30)
start_time = time.time()
while time.time() - start_time < timeout:
status = self.hardware_interface.read_register('COIL_LOAD_COMPLETE', count=1)
if status and status[0]:
logger.info(f"物料装载完成: {material_type} -> {position}")
return True
time.sleep(0.5)
logger.error(f"物料装载超时: {material_type} -> {position}")
return False
except Exception as e:
logger.error(f"执行物料装载失败: {e}")
return False
def _execute_quality_check(self, parameters: Dict[str, Any]) -> bool:
"""执行质量检测工作流"""
try:
check_type = parameters.get('check_type', 'dimensional')
logger.info(f"开始质量检测: {check_type}")
# 启动质量检测
self.hardware_interface.write_register('REG_QC_TYPE', check_type)
self.hardware_interface.write_register('COIL_QC_START', True)
# 等待检测完成
timeout = parameters.get('timeout', 60)
start_time = time.time()
while time.time() - start_time < timeout:
status = self.hardware_interface.read_register('COIL_QC_COMPLETE', count=1)
if status and status[0]:
# 读取检测结果
result = self.hardware_interface.read_register('REG_QC_RESULT', count=1)
passed = result[0] if result else False
if passed:
logger.info(f"质量检测通过: {check_type}")
return True
else:
logger.warning(f"质量检测失败: {check_type}")
return False
time.sleep(1.0)
logger.error(f"质量检测超时: {check_type}")
return False
except Exception as e:
logger.error(f"执行质量检测失败: {e}")
return False
def _find_available_press_slot(self) -> Optional[str]:
"""查找可用压制槽"""
try:
press_slots = self.material_management.find_by_category("battery_press_slot")
for slot in press_slots:
if hasattr(slot, 'has_battery') and not slot.has_battery():
return slot.name
return None
except:
# 如果物料管理系统不可用,返回默认槽位
return "A1"
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
try:
if emergency:
self.hardware_interface.write_register('COIL_SYS_RESET_CMD', True)
logger.warning("执行紧急停止")
else:
self.hardware_interface.write_register('COIL_SYS_STOP_CMD', True)
logger.info("执行正常停止")
time.sleep(0.5)
status = self.hardware_interface.read_register('COIL_SYS_STOP_STATUS', count=1)
return status[0] if status else False
except Exception as e:
logger.error(f"停止Modbus工作流失败: {e}")
return False
class HttpWorkflowExecutor(WorkflowExecutor):
"""HTTP工作流执行器 - 适配 reaction_station_bioyong"""
def __init__(self, workstation: 'WorkstationBase'):
super().__init__(workstation)
# 验证HTTP接口
if not (hasattr(self.hardware_interface, 'post') or
hasattr(self.hardware_interface, 'get')):
raise RuntimeError("工作站硬件接口不是有效的HTTP客户端")
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行HTTP工作流"""
try:
if workflow_name == "reaction_synthesis":
return self._execute_reaction_synthesis(parameters)
elif workflow_name == "liquid_feeding":
return self._execute_liquid_feeding(parameters)
elif workflow_name == "temperature_control":
return self._execute_temperature_control(parameters)
else:
logger.warning(f"不支持的HTTP工作流: {workflow_name}")
return False
except Exception as e:
logger.error(f"执行HTTP工作流失败: {e}")
return False
def _execute_reaction_synthesis(self, parameters: Dict[str, Any]) -> bool:
"""执行反应合成工作流"""
try:
# 1. 设置工作流序列
sequence = self._build_reaction_sequence(parameters)
self._call_rpc_method('set_workflow_sequence', json.dumps(sequence))
# 2. 设置反应参数
if parameters.get('temperature'):
self._call_rpc_method('set_temperature', parameters['temperature'])
if parameters.get('pressure'):
self._call_rpc_method('set_pressure', parameters['pressure'])
if parameters.get('stirring_speed'):
self._call_rpc_method('set_stirring_speed', parameters['stirring_speed'])
# 3. 执行工作流
result = self._call_rpc_method('execute_current_sequence', {
"task_name": "reaction_synthesis"
})
success = result.get('success', False)
if success:
logger.info("反应合成工作流执行成功")
else:
logger.error(f"反应合成工作流执行失败: {result.get('error', '未知错误')}")
return success
except Exception as e:
logger.error(f"执行反应合成工作流失败: {e}")
return False
def _execute_liquid_feeding(self, parameters: Dict[str, Any]) -> bool:
"""执行液体投料工作流"""
try:
reagents = parameters.get('reagents', [])
volumes = parameters.get('volumes', [])
if len(reagents) != len(volumes):
raise ValueError("试剂列表和体积列表长度不匹配")
# 执行投料序列
for reagent, volume in zip(reagents, volumes):
result = self._call_rpc_method('feed_liquid', {
'reagent': reagent,
'volume': volume
})
if not result.get('success', False):
logger.error(f"投料失败: {reagent} {volume}mL")
return False
logger.info(f"投料成功: {reagent} {volume}mL")
return True
except Exception as e:
logger.error(f"执行液体投料失败: {e}")
return False
def _execute_temperature_control(self, parameters: Dict[str, Any]) -> bool:
"""执行温度控制工作流"""
try:
target_temp = parameters.get('temperature', 25)
hold_time = parameters.get('hold_time', 300) # 秒
# 设置目标温度
result = self._call_rpc_method('set_temperature', target_temp)
if not result.get('success', False):
logger.error(f"设置温度失败: {target_temp}°C")
return False
# 等待温度稳定
logger.info(f"等待温度稳定到 {target_temp}°C")
# 保持温度指定时间
if hold_time > 0:
logger.info(f"保持温度 {hold_time}")
time.sleep(hold_time)
return True
except Exception as e:
logger.error(f"执行温度控制失败: {e}")
return False
def _build_reaction_sequence(self, parameters: Dict[str, Any]) -> List[str]:
"""构建反应合成工作流序列"""
sequence = []
# 添加预处理步骤
if parameters.get('purge_with_inert'):
sequence.append("purge_inert_gas")
# 添加温度设置
if parameters.get('temperature'):
sequence.append(f"set_temperature_{parameters['temperature']}")
# 添加压力设置
if parameters.get('pressure'):
sequence.append(f"set_pressure_{parameters['pressure']}")
# 添加搅拌设置
if parameters.get('stirring_speed'):
sequence.append(f"set_stirring_{parameters['stirring_speed']}")
# 添加反应步骤
sequence.extend([
"start_reaction",
"monitor_progress",
"complete_reaction"
])
# 添加后处理步骤
if parameters.get('cooling_required'):
sequence.append("cool_down")
return sequence
def _call_rpc_method(self, method: str, params: Any = None) -> Dict[str, Any]:
"""调用RPC方法"""
try:
if hasattr(self.hardware_interface, method):
# 直接方法调用
if isinstance(params, dict):
params = json.dumps(params)
elif params is None:
params = ""
return getattr(self.hardware_interface, method)(params)
else:
# HTTP请求调用
if hasattr(self.hardware_interface, 'post'):
response = self.hardware_interface.post(f"/api/{method}", json=params)
return response.json()
else:
raise AttributeError(f"HTTP接口不支持方法: {method}")
except Exception as e:
logger.error(f"调用RPC方法失败 {method}: {e}")
return {'success': False, 'error': str(e)}
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
try:
if emergency:
result = self._call_rpc_method('scheduler_reset')
else:
result = self._call_rpc_method('scheduler_stop')
return result.get('success', False)
except Exception as e:
logger.error(f"停止HTTP工作流失败: {e}")
return False
class PyLabRobotWorkflowExecutor(WorkflowExecutor):
"""PyLabRobot工作流执行器 - 适配 prcxi.py"""
def __init__(self, workstation: 'WorkstationBase'):
super().__init__(workstation)
# 验证PyLabRobot接口
if not (hasattr(self.hardware_interface, 'transfer_liquid') or
hasattr(self.hardware_interface, 'pickup_tips')):
raise RuntimeError("工作站硬件接口不是有效的PyLabRobot设备")
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行PyLabRobot工作流"""
try:
if workflow_name == "liquid_transfer":
return self._execute_liquid_transfer(parameters)
elif workflow_name == "tip_pickup_drop":
return self._execute_tip_operations(parameters)
elif workflow_name == "plate_handling":
return self._execute_plate_handling(parameters)
else:
logger.warning(f"不支持的PyLabRobot工作流: {workflow_name}")
return False
except Exception as e:
logger.error(f"执行PyLabRobot工作流失败: {e}")
return False
def _execute_liquid_transfer(self, parameters: Dict[str, Any]) -> bool:
"""执行液体转移工作流"""
try:
# 1. 解析物料引用
sources = self._resolve_containers(parameters.get('sources', []))
targets = self._resolve_containers(parameters.get('targets', []))
tip_racks = self._resolve_tip_racks(parameters.get('tip_racks', []))
if not sources or not targets:
raise ValueError("液体转移需要指定源容器和目标容器")
if not tip_racks:
logger.warning("未指定枪头架,将尝试自动查找")
tip_racks = self._find_available_tip_racks()
# 2. 执行液体转移
volumes = parameters.get('volumes', [])
if not volumes:
volumes = [100.0] * len(sources) # 默认体积
# 如果是同步接口
if hasattr(self.hardware_interface, 'transfer_liquid'):
result = self.hardware_interface.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=tip_racks,
asp_vols=volumes,
dis_vols=volumes,
**parameters.get('options', {})
)
else:
# 异步接口需要特殊处理
asyncio.run(self._async_liquid_transfer(sources, targets, tip_racks, volumes, parameters))
result = True
if result:
logger.info(f"液体转移工作流完成: {len(sources)}个源 -> {len(targets)}个目标")
return bool(result)
except Exception as e:
logger.error(f"执行液体转移失败: {e}")
return False
async def _async_liquid_transfer(self, sources, targets, tip_racks, volumes, parameters):
"""异步液体转移"""
await self.hardware_interface.transfer_liquid(
sources=sources,
targets=targets,
tip_racks=tip_racks,
asp_vols=volumes,
dis_vols=volumes,
**parameters.get('options', {})
)
def _execute_tip_operations(self, parameters: Dict[str, Any]) -> bool:
"""执行枪头操作工作流"""
try:
operation = parameters.get('operation', 'pickup')
tip_racks = self._resolve_tip_racks(parameters.get('tip_racks', []))
if not tip_racks:
raise ValueError("枪头操作需要指定枪头架")
if operation == 'pickup':
result = self.hardware_interface.pickup_tips(tip_racks[0])
logger.info("枪头拾取完成")
elif operation == 'drop':
result = self.hardware_interface.drop_tips()
logger.info("枪头丢弃完成")
else:
raise ValueError(f"不支持的枪头操作: {operation}")
return bool(result)
except Exception as e:
logger.error(f"执行枪头操作失败: {e}")
return False
def _execute_plate_handling(self, parameters: Dict[str, Any]) -> bool:
"""执行板类处理工作流"""
try:
operation = parameters.get('operation', 'move')
source_position = parameters.get('source_position')
target_position = parameters.get('target_position')
if operation == 'move' and source_position and target_position:
# 移动板类
result = self.hardware_interface.move_plate(source_position, target_position)
logger.info(f"板类移动完成: {source_position} -> {target_position}")
else:
logger.warning(f"不支持的板类操作或参数不完整: {operation}")
return False
return bool(result)
except Exception as e:
logger.error(f"执行板类处理失败: {e}")
return False
def _resolve_containers(self, container_names: List[str]):
"""解析容器名称为实际容器对象"""
containers = []
for name in container_names:
try:
container = self.material_management.find_material_by_id(name)
if container:
containers.append(container)
else:
logger.warning(f"未找到容器: {name}")
except:
logger.warning(f"解析容器失败: {name}")
return containers
def _resolve_tip_racks(self, tip_rack_names: List[str]):
"""解析枪头架名称为实际对象"""
tip_racks = []
for name in tip_rack_names:
try:
tip_rack = self.material_management.find_by_category("tip_rack")
matching_racks = [rack for rack in tip_rack if rack.name == name]
if matching_racks:
tip_racks.extend(matching_racks)
else:
logger.warning(f"未找到枪头架: {name}")
except:
logger.warning(f"解析枪头架失败: {name}")
return tip_racks
def _find_available_tip_racks(self):
"""查找可用的枪头架"""
try:
tip_racks = self.material_management.find_by_category("tip_rack")
available_racks = [rack for rack in tip_racks if hasattr(rack, 'has_tips') and rack.has_tips()]
return available_racks[:1] # 返回第一个可用的枪头架
except:
return []
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止工作流"""
try:
if emergency:
if hasattr(self.hardware_interface, 'emergency_stop'):
return self.hardware_interface.emergency_stop()
else:
logger.warning("设备不支持紧急停止")
return False
else:
if hasattr(self.hardware_interface, 'graceful_stop'):
return self.hardware_interface.graceful_stop()
elif hasattr(self.hardware_interface, 'stop'):
return self.hardware_interface.stop()
else:
logger.warning("设备不支持优雅停止")
return False
except Exception as e:
logger.error(f"停止PyLabRobot工作流失败: {e}")
return False
class ProxyWorkflowExecutor(WorkflowExecutor):
"""代理工作流执行器 - 处理代理模式的工作流"""
def __init__(self, workstation: 'WorkstationBase'):
super().__init__(workstation)
# 验证代理接口
if not isinstance(self.hardware_interface, str) or not self.hardware_interface.startswith("proxy:"):
raise RuntimeError("工作站硬件接口不是有效的代理字符串")
self.device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
"""执行代理工作流"""
try:
# 通过协议节点调用目标设备的工作流
if self.workstation._workstation_node:
return self.workstation._workstation_node.call_device_method(
self.device_id, 'execute_workflow', workflow_name, parameters
)
else:
logger.error("代理模式需要workstation_node")
return False
except Exception as e:
logger.error(f"执行代理工作流失败: {e}")
return False
def stop_workflow(self, emergency: bool = False) -> bool:
"""停止代理工作流"""
try:
if self.workstation._workstation_node:
return self.workstation._workstation_node.call_device_method(
self.device_id, 'stop_workflow', emergency
)
else:
logger.error("代理模式需要workstation_node")
return False
except Exception as e:
logger.error(f"停止代理工作流失败: {e}")
return False
# 辅助函数
def get_executor_for_interface(hardware_interface) -> str:
"""根据硬件接口类型获取执行器类型名称"""
if isinstance(hardware_interface, str) and hardware_interface.startswith("proxy:"):
return "ProxyWorkflowExecutor"
elif hasattr(hardware_interface, 'write_register') and hasattr(hardware_interface, 'read_register'):
return "ModbusWorkflowExecutor"
elif hasattr(hardware_interface, 'post') or hasattr(hardware_interface, 'get'):
return "HttpWorkflowExecutor"
elif hasattr(hardware_interface, 'transfer_liquid') or hasattr(hardware_interface, 'pickup_tips'):
return "PyLabRobotWorkflowExecutor"
else:
return "UnknownExecutor"