refactor: ProtocolNode→WorkstationNode

This commit is contained in:
Junhan Chang
2025-08-25 22:09:37 +08:00
parent ae3c1100ae
commit 5ec8a57a1f
19 changed files with 1089 additions and 365 deletions

View File

@@ -0,0 +1,467 @@
"""
工作站基类
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