mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
468 lines
18 KiB
Python
468 lines
18 KiB
Python
"""
|
||
工作站基类
|
||
Workstation Base Class - 单接口模式
|
||
|
||
基于单一硬件接口的简化工作站架构
|
||
支持直接模式和代理模式的自动工作流执行器选择
|
||
"""
|
||
import time
|
||
import traceback
|
||
from typing import Dict, Any, List, Optional, Union, TYPE_CHECKING
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
|
||
if TYPE_CHECKING:
|
||
from unilabos.ros.nodes.presets.protocol_node import ROS2WorkstationNode
|
||
|
||
from unilabos.devices.work_station.workstation_material_management import MaterialManagementBase
|
||
from unilabos.devices.work_station.workstation_http_service import (
|
||
WorkstationHTTPService, WorkstationReportRequest, MaterialUsage
|
||
)
|
||
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 WorkstationBase(ABC):
|
||
"""工作站基类 - 单接口模式
|
||
|
||
核心设计原则:
|
||
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,
|
||
*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服务配置
|
||
self.http_service_config = http_service_config or {
|
||
"enabled": True,
|
||
"host": "127.0.0.1",
|
||
"port": 8081
|
||
}
|
||
|
||
# 单一硬件接口 - 可以是具体客户端对象或代理字符串
|
||
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.material_management: MaterialManagementBase = self._create_material_management_module()
|
||
|
||
# 注册支持的工作流
|
||
self._register_supported_workflows()
|
||
|
||
# 启动HTTP报送接收服务
|
||
self.http_service = None
|
||
self._start_http_service()
|
||
|
||
logger.info(f"工作站 {device_id} 初始化完成(单接口模式)")
|
||
|
||
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__}")
|
||
|
||
def set_protocol_node(self, protocol_node: 'ROS2WorkstationNode'):
|
||
"""设置协议节点引用(用于代理模式)"""
|
||
self._protocol_node = protocol_node
|
||
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
||
|
||
def _setup_workflow_executor(self):
|
||
"""根据硬件接口类型自动设置工作流执行器"""
|
||
if self.hardware_interface is None:
|
||
return
|
||
|
||
# 动态导入工作流执行器类
|
||
try:
|
||
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
|
||
self.workflow_start_time = time.time()
|
||
|
||
# 委托给工作流执行器
|
||
success = self.workflow_executor.execute_workflow(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 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(f"工作站 {self.device_id} 没有正在运行的工作流")
|
||
return True
|
||
|
||
self.current_workflow_status = WorkflowStatus.STOPPING
|
||
|
||
# 委托给工作流执行器
|
||
success = self.workflow_executor.stop_workflow(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
|
||
|
||
@property
|
||
def error_count(self) -> int:
|
||
"""获取错误计数"""
|
||
return len(self.error_history)
|
||
|
||
@property
|
||
def last_error(self) -> Optional[Dict[str, Any]]:
|
||
"""获取最后一个错误"""
|
||
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(f"工作站 {self.device_id} HTTP报送接收服务已禁用")
|
||
return
|
||
|
||
host = self.http_service_config.get("host", "127.0.0.1")
|
||
port = self.http_service_config.get("port", 8081)
|
||
|
||
self.http_service = WorkstationHTTPService(
|
||
workstation_handler=self,
|
||
host=host,
|
||
port=port
|
||
)
|
||
|
||
logger.info(f"工作站 {self.device_id} HTTP报送接收服务启动成功: {host}:{port}")
|
||
|
||
except Exception as e:
|
||
logger.error(f"工作站 {self.device_id} 启动HTTP报送接收服务失败: {e}")
|
||
self.http_service = None
|
||
|
||
def _stop_http_service(self):
|
||
"""停止HTTP报送接收服务"""
|
||
try:
|
||
if self.http_service:
|
||
self.http_service.stop()
|
||
self.http_service = None
|
||
logger.info(f"工作站 {self.device_id} HTTP报送接收服务已停止")
|
||
except Exception as e:
|
||
logger.error(f"工作站 {self.device_id} 停止HTTP报送接收服务失败: {e}")
|
||
|
||
# ============ 报送处理方法 ============
|
||
|
||
def process_material_change_report(self, report) -> Dict[str, Any]:
|
||
"""处理物料变更报送"""
|
||
try:
|
||
logger.info(f"处理物料变更报送: {report.workstation_id} -> {report.resource_id} ({report.change_type})")
|
||
|
||
result = {
|
||
'processed': True,
|
||
'resource_id': report.resource_id,
|
||
'change_type': report.change_type,
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
# 更新本地物料管理系统
|
||
if hasattr(self, 'material_management'):
|
||
try:
|
||
self.material_management.sync_external_material_change(report)
|
||
except Exception as e:
|
||
logger.warning(f"同步物料变更到本地管理系统失败: {e}")
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理物料变更报送失败: {e}")
|
||
return {'processed': False, 'error': str(e)}
|
||
|
||
def process_step_finish_report(self, request: WorkstationReportRequest) -> Dict[str, Any]:
|
||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||
try:
|
||
data = request.data
|
||
logger.info(f"处理步骤完成报送: {data['orderCode']} - {data['stepName']}")
|
||
|
||
result = {
|
||
'processed': True,
|
||
'order_code': data['orderCode'],
|
||
'step_id': data['stepId'],
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理步骤完成报送失败: {e}")
|
||
return {'processed': False, 'error': str(e)}
|
||
|
||
def process_sample_finish_report(self, request: WorkstationReportRequest) -> Dict[str, Any]:
|
||
"""处理样品完成报送"""
|
||
try:
|
||
data = request.data
|
||
logger.info(f"处理样品完成报送: {data['sampleId']}")
|
||
|
||
result = {
|
||
'processed': True,
|
||
'sample_id': data['sampleId'],
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理样品完成报送失败: {e}")
|
||
return {'processed': False, 'error': str(e)}
|
||
|
||
def process_order_finish_report(self, request: WorkstationReportRequest, used_materials: List[MaterialUsage]) -> Dict[str, Any]:
|
||
"""处理订单完成报送"""
|
||
try:
|
||
data = request.data
|
||
logger.info(f"处理订单完成报送: {data['orderCode']}")
|
||
|
||
result = {
|
||
'processed': True,
|
||
'order_code': data['orderCode'],
|
||
'used_materials': len(used_materials),
|
||
'timestamp': time.time()
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理订单完成报送失败: {e}")
|
||
return {'processed': False, 'error': str(e)}
|
||
|
||
def handle_external_error(self, error_request):
|
||
"""处理外部错误报告"""
|
||
try:
|
||
logger.error(f"收到外部错误报告: {error_request}")
|
||
|
||
# 记录错误
|
||
error_record = {
|
||
'timestamp': time.time(),
|
||
'error_type': error_request.get('error_type', 'unknown'),
|
||
'error_message': error_request.get('message', ''),
|
||
'source': error_request.get('source', 'external'),
|
||
'context': error_request.get('context', {})
|
||
}
|
||
|
||
self.error_history.append(error_record)
|
||
|
||
# 处理紧急停止情况
|
||
if error_request.get('emergency_stop', False):
|
||
self._trigger_emergency_stop(error_record['error_message'])
|
||
|
||
return {'processed': True, 'error_id': len(self.error_history)}
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理外部错误失败: {e}")
|
||
return {'processed': False, 'error': str(e)}
|
||
|
||
def _trigger_emergency_stop(self, reason: str):
|
||
"""触发紧急停止"""
|
||
logger.critical(f"触发紧急停止: {reason}")
|
||
self.stop_workflow(emergency=True)
|
||
|
||
def __del__(self):
|
||
"""清理资源"""
|
||
try:
|
||
self._stop_http_service()
|
||
except:
|
||
pass
|