mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
refactor: ProtocolNode→WorkstationNode
This commit is contained in:
649
unilabos/devices/workstation/workflow_executors.py
Normal file
649
unilabos/devices/workstation/workflow_executors.py
Normal file
@@ -0,0 +1,649 @@
|
||||
"""
|
||||
工作流执行器模块
|
||||
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._protocol_node:
|
||||
return self.workstation._protocol_node.call_device_method(
|
||||
self.device_id, 'execute_workflow', workflow_name, parameters
|
||||
)
|
||||
else:
|
||||
logger.error("代理模式需要protocol_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._protocol_node:
|
||||
return self.workstation._protocol_node.call_device_method(
|
||||
self.device_id, 'stop_workflow', emergency
|
||||
)
|
||||
else:
|
||||
logger.error("代理模式需要protocol_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"
|
||||
@@ -1,74 +1,25 @@
|
||||
"""
|
||||
工作站基类
|
||||
Workstation Base Class
|
||||
Workstation Base Class - 单接口模式
|
||||
|
||||
集成通信、物料管理和工作流的工作站基类
|
||||
融合子设备管理、动态工作流注册等高级功能
|
||||
基于单一硬件接口的简化工作站架构
|
||||
支持直接模式和代理模式的自动工作流执行器选择
|
||||
"""
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
import traceback
|
||||
from typing import Dict, Any, List, Optional, Union, Callable
|
||||
from typing import Dict, Any, List, Optional, Union, TYPE_CHECKING
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
from rclpy.action import ActionServer, ActionClient
|
||||
from rclpy.action.server import ServerGoalHandle
|
||||
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||
from rclpy.service import Service
|
||||
from unilabos_msgs.srv import SerialCommand
|
||||
from unilabos_msgs.msg import Resource
|
||||
if TYPE_CHECKING:
|
||||
from unilabos.ros.nodes.presets.protocol_node import ROS2WorkstationNode
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.device_comms.workstation_material_management import MaterialManagementBase
|
||||
from unilabos.device_comms.workstation_http_service import (
|
||||
from unilabos.devices.work_station.workstation_material_management import MaterialManagementBase
|
||||
from unilabos.devices.work_station.workstation_http_service import (
|
||||
WorkstationHTTPService, WorkstationReportRequest, MaterialUsage
|
||||
)
|
||||
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, convert_from_ros_msg
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.utils.type_check import serialize_result_info
|
||||
|
||||
|
||||
class DeviceType(Enum):
|
||||
"""设备类型枚举"""
|
||||
LOGICAL = "logical" # 逻辑设备
|
||||
COMMUNICATION = "communication" # 通信设备 (modbus/opcua/serial)
|
||||
PROTOCOL = "protocol" # 协议设备
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommunicationInterface:
|
||||
"""通信接口配置"""
|
||||
device_id: str # 通信设备ID
|
||||
read_method: str # 读取方法名
|
||||
write_method: str # 写入方法名
|
||||
protocol_type: str # 协议类型 (modbus/opcua/serial)
|
||||
config: Dict[str, Any] # 协议特定配置
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowStep:
|
||||
"""工作流步骤定义"""
|
||||
device_id: str
|
||||
action_name: str
|
||||
action_kwargs: Dict[str, Any]
|
||||
depends_on: Optional[List[str]] = None # 依赖的步骤ID
|
||||
step_id: Optional[str] = None
|
||||
timeout: Optional[float] = None
|
||||
retry_count: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkflowDefinition:
|
||||
"""工作流定义"""
|
||||
name: str
|
||||
description: str
|
||||
steps: List[WorkflowStep]
|
||||
input_schema: Dict[str, Any]
|
||||
output_schema: Dict[str, Any]
|
||||
metadata: Dict[str, Any]
|
||||
|
||||
|
||||
class WorkflowStatus(Enum):
|
||||
@@ -94,65 +45,57 @@ class WorkflowInfo:
|
||||
parameters_schema: Dict[str, Any] # 参数架构
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommunicationConfig:
|
||||
"""通信配置"""
|
||||
protocol: str
|
||||
host: str
|
||||
port: int
|
||||
timeout: float = 5.0
|
||||
retry_count: int = 3
|
||||
extra_params: Dict[str, Any] = None
|
||||
|
||||
|
||||
class WorkstationBase(ABC):
|
||||
"""工作站基类
|
||||
"""工作站基类 - 单接口模式
|
||||
|
||||
提供工作站的核心功能:
|
||||
1. 物料管理 - 基于PyLabRobot的物料系统
|
||||
2. 工作流控制 - 支持动态注册和静态预定义工作流
|
||||
3. 状态监控 - 设备状态和生产数据监控
|
||||
4. HTTP服务 - 接收外部报送和状态查询
|
||||
|
||||
注意:子设备管理和通信转发功能已移入ROS2ProtocolNode
|
||||
核心设计原则:
|
||||
1. 每个工作站只有一个 hardware_interface
|
||||
2. 根据接口类型自动选择工作流执行器
|
||||
3. 支持直接模式和代理模式
|
||||
4. 统一的设备操作接口
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
deck_config: Optional[Dict[str, Any]] = None,
|
||||
http_service_config: Optional[Dict[str, Any]] = None, # HTTP服务配置
|
||||
http_service_config: Optional[Dict[str, Any]] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
# 保存工作站基本配置
|
||||
# 基本配置
|
||||
self.device_id = device_id
|
||||
self.deck_config = deck_config or {"size_x": 1000.0, "size_y": 1000.0, "size_z": 500.0}
|
||||
|
||||
# HTTP服务配置 - 现在专门用于报送接收
|
||||
# HTTP服务配置
|
||||
self.http_service_config = http_service_config or {
|
||||
"enabled": True,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8081 # 默认使用8081端口作为报送接收服务
|
||||
"port": 8081
|
||||
}
|
||||
|
||||
# 错误处理和动作追踪
|
||||
self.current_action_context = None # 当前正在执行的动作上下文
|
||||
self.error_history = [] # 错误历史记录
|
||||
self.action_results = {} # 动作结果缓存
|
||||
# 单一硬件接口 - 可以是具体客户端对象或代理字符串
|
||||
self.hardware_interface: Union[Any, str] = None
|
||||
|
||||
# 工作流状态 - 支持静态和动态工作流
|
||||
# 协议节点引用(用于代理模式)
|
||||
self._protocol_node: Optional['ROS2WorkstationNode'] = None
|
||||
|
||||
# 工作流执行器(基于通信接口类型自动选择)
|
||||
self.workflow_executor: Optional['WorkflowExecutor'] = None
|
||||
|
||||
# 工作流状态
|
||||
self.current_workflow_status = WorkflowStatus.IDLE
|
||||
self.current_workflow_info = None
|
||||
self.workflow_start_time = None
|
||||
self.workflow_parameters = {}
|
||||
|
||||
# 错误处理
|
||||
self.error_history = []
|
||||
self.action_results = {}
|
||||
|
||||
# 支持的工作流(静态预定义)
|
||||
self.supported_workflows: Dict[str, WorkflowInfo] = {}
|
||||
|
||||
# 动态注册的工作流
|
||||
self.registered_workflows: Dict[str, WorkflowDefinition] = {}
|
||||
|
||||
# 初始化工作站模块
|
||||
self.material_management: MaterialManagementBase = self._create_material_management_module()
|
||||
|
||||
@@ -163,113 +106,166 @@ class WorkstationBase(ABC):
|
||||
self.http_service = None
|
||||
self._start_http_service()
|
||||
|
||||
logger.info(f"工作站基类 {device_id} 初始化完成")
|
||||
logger.info(f"工作站 {device_id} 初始化完成(单接口模式)")
|
||||
|
||||
@abstractmethod
|
||||
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||
"""创建物料管理模块 - 子类必须实现"""
|
||||
pass
|
||||
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
|
||||
"""设置硬件接口"""
|
||||
self.hardware_interface = hardware_interface
|
||||
|
||||
# 根据接口类型自动创建工作流执行器
|
||||
self._setup_workflow_executor()
|
||||
|
||||
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
||||
|
||||
@abstractmethod
|
||||
def _register_supported_workflows(self):
|
||||
"""注册支持的工作流 - 子类必须实现"""
|
||||
pass
|
||||
def set_protocol_node(self, protocol_node: 'ROS2WorkstationNode'):
|
||||
"""设置协议节点引用(用于代理模式)"""
|
||||
self._protocol_node = protocol_node
|
||||
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
||||
|
||||
def _create_workstation_services(self):
|
||||
"""创建工作站ROS服务"""
|
||||
def _start_http_service(self):
|
||||
"""启动HTTP报送接收服务"""
|
||||
if self.http_service_config.get("enabled", True):
|
||||
try:
|
||||
self.http_service = WorkstationHTTPService(
|
||||
host=self.http_service_config.get("host", "127.0.0.1"),
|
||||
port=self.http_service_config.get("port", 8081),
|
||||
workstation_handler=self
|
||||
)
|
||||
logger.info(f"HTTP报送接收服务已启动: {self.http_service_config['host']}:{self.http_service_config['port']}")
|
||||
except Exception as e:
|
||||
logger.error(f"启动HTTP报送接收服务失败: {e}")
|
||||
else:
|
||||
logger.info("HTTP报送接收服务已禁用")
|
||||
|
||||
def _stop_http_service(self):
|
||||
"""停止HTTP报送接收服务"""
|
||||
if self.http_service:
|
||||
try:
|
||||
self.http_service.stop()
|
||||
logger.info("HTTP报送接收服务已停止")
|
||||
except Exception as e:
|
||||
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||
|
||||
# ============ 核心业务方法 ============
|
||||
|
||||
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||
"""启动工作流 - 业务逻辑层"""
|
||||
def _setup_workflow_executor(self):
|
||||
"""根据硬件接口类型自动设置工作流执行器"""
|
||||
if self.hardware_interface is None:
|
||||
return
|
||||
|
||||
# 动态导入工作流执行器类
|
||||
try:
|
||||
if self.current_workflow_status != WorkflowStatus.IDLE:
|
||||
logger.warning(f"工作流 {workflow_type} 启动失败:当前状态为 {self.current_workflow_status}")
|
||||
return False
|
||||
from unilabos.devices.work_station.workflow_executors import (
|
||||
ProxyWorkflowExecutor, ModbusWorkflowExecutor,
|
||||
HttpWorkflowExecutor, PyLabRobotWorkflowExecutor
|
||||
)
|
||||
except ImportError:
|
||||
logger.warning("工作流执行器模块未找到,将使用基础执行器")
|
||||
self.workflow_executor = None
|
||||
return
|
||||
|
||||
# 检查是否为代理字符串
|
||||
if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"):
|
||||
self.workflow_executor = ProxyWorkflowExecutor(self)
|
||||
logger.info(f"工作站 {self.device_id} 使用代理工作流执行器")
|
||||
|
||||
# 检查是否为Modbus客户端
|
||||
elif hasattr(self.hardware_interface, 'write_register') and hasattr(self.hardware_interface, 'read_register'):
|
||||
self.workflow_executor = ModbusWorkflowExecutor(self)
|
||||
logger.info(f"工作站 {self.device_id} 使用Modbus工作流执行器")
|
||||
|
||||
# 检查是否为HTTP客户端
|
||||
elif hasattr(self.hardware_interface, 'post') or hasattr(self.hardware_interface, 'get'):
|
||||
self.workflow_executor = HttpWorkflowExecutor(self)
|
||||
logger.info(f"工作站 {self.device_id} 使用HTTP工作流执行器")
|
||||
|
||||
# 检查是否为PyLabRobot设备
|
||||
elif hasattr(self.hardware_interface, 'transfer_liquid') or hasattr(self.hardware_interface, 'pickup_tips'):
|
||||
self.workflow_executor = PyLabRobotWorkflowExecutor(self)
|
||||
logger.info(f"工作站 {self.device_id} 使用PyLabRobot工作流执行器")
|
||||
|
||||
else:
|
||||
logger.warning(f"工作站 {self.device_id} 无法识别硬件接口类型: {type(self.hardware_interface)}")
|
||||
self.workflow_executor = None
|
||||
|
||||
# ============ 统一的设备操作接口 ============
|
||||
|
||||
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._protocol_node:
|
||||
raise RuntimeError("代理模式需要设置protocol_node")
|
||||
|
||||
device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
|
||||
return self._protocol_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 execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行工作流 - 委托给工作流执行器"""
|
||||
if not self.workflow_executor:
|
||||
logger.error(f"工作站 {self.device_id} 工作流执行器未初始化")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 设置工作流状态
|
||||
self.current_workflow_status = WorkflowStatus.INITIALIZING
|
||||
self.workflow_parameters = parameters or {}
|
||||
self.workflow_parameters = parameters
|
||||
self.workflow_start_time = time.time()
|
||||
|
||||
# 执行具体的工作流启动逻辑
|
||||
success = self._execute_start_workflow(workflow_type, parameters or {})
|
||||
# 委托给工作流执行器
|
||||
success = self.workflow_executor.execute_workflow(workflow_name, parameters)
|
||||
|
||||
if success:
|
||||
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||
logger.info(f"工作流 {workflow_type} 启动成功")
|
||||
logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功")
|
||||
else:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作流 {workflow_type} 启动失败")
|
||||
logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"启动工作流失败: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}")
|
||||
return False
|
||||
|
||||
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||
"""启动工作流 - 兼容旧接口"""
|
||||
return self.execute_workflow(workflow_type, parameters or {})
|
||||
|
||||
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流 - 业务逻辑层"""
|
||||
"""停止工作流"""
|
||||
if not self.workflow_executor:
|
||||
logger.warning(f"工作站 {self.device_id} 工作流执行器未初始化")
|
||||
return True
|
||||
|
||||
try:
|
||||
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
|
||||
logger.warning("没有正在运行的工作流")
|
||||
logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流")
|
||||
return True
|
||||
|
||||
self.current_workflow_status = WorkflowStatus.STOPPING
|
||||
|
||||
# 执行具体的工作流停止逻辑
|
||||
success = self._execute_stop_workflow(emergency)
|
||||
# 委托给工作流执行器
|
||||
success = self.workflow_executor.stop_workflow(emergency)
|
||||
|
||||
if success:
|
||||
self.current_workflow_status = WorkflowStatus.STOPPED
|
||||
logger.info(f"工作流停止成功 (紧急: {emergency})")
|
||||
logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})")
|
||||
else:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"工作流停止失败")
|
||||
logger.error(f"工作站 {self.device_id} 工作流停止失败")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.current_workflow_status = WorkflowStatus.ERROR
|
||||
logger.error(f"停止工作流失败: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 抽象方法 - 子类必须实现具体的工作流控制 ============
|
||||
|
||||
@abstractmethod
|
||||
def _execute_start_workflow(self, workflow_type: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行启动工作流的具体逻辑 - 子类实现"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _execute_stop_workflow(self, emergency: bool) -> bool:
|
||||
"""执行停止工作流的具体逻辑 - 子类实现"""
|
||||
pass
|
||||
|
||||
# ============ 状态属性 ============
|
||||
|
||||
@property
|
||||
@@ -303,11 +299,25 @@ class WorkstationBase(ABC):
|
||||
"""获取最后一个错误"""
|
||||
return self.error_history[-1] if self.error_history else None
|
||||
|
||||
# ============ 抽象方法 - 子类必须实现 ============
|
||||
|
||||
@abstractmethod
|
||||
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||
"""创建物料管理模块 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _register_supported_workflows(self):
|
||||
"""注册支持的工作流 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
# ============ HTTP服务管理 ============
|
||||
|
||||
def _start_http_service(self):
|
||||
"""启动HTTP报送接收服务"""
|
||||
try:
|
||||
if not self.http_service_config.get("enabled", True):
|
||||
logger.info("HTTP报送接收服务已禁用")
|
||||
logger.info(f"工作站 {self.device_id} HTTP报送接收服务已禁用")
|
||||
return
|
||||
|
||||
host = self.http_service_config.get("host", "127.0.0.1")
|
||||
@@ -322,7 +332,7 @@ class WorkstationBase(ABC):
|
||||
logger.info(f"工作站 {self.device_id} HTTP报送接收服务启动成功: {host}:{port}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动HTTP报送接收服务失败: {e}")
|
||||
logger.error(f"工作站 {self.device_id} 启动HTTP报送接收服务失败: {e}")
|
||||
self.http_service = None
|
||||
|
||||
def _stop_http_service(self):
|
||||
@@ -331,12 +341,9 @@ class WorkstationBase(ABC):
|
||||
if self.http_service:
|
||||
self.http_service.stop()
|
||||
self.http_service = None
|
||||
logger.info("HTTP报送接收服务已停止")
|
||||
logger.info(f"工作站 {self.device_id} HTTP报送接收服务已停止")
|
||||
except Exception as e:
|
||||
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||
|
||||
# ============ 报送处理方法 ============
|
||||
logger.error(f"工作站 {self.device_id} 停止HTTP报送接收服务失败: {e}")
|
||||
|
||||
# ============ 报送处理方法 ============
|
||||
|
||||
Reference in New Issue
Block a user