Files
Uni-Lab-OS/unilabos/devices/workstation/workstation_base.py
2025-08-25 22:09:37 +08:00

468 lines
18 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.

"""
工作站基类
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