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"
|
||||
467
unilabos/devices/workstation/workstation_base.py
Normal file
467
unilabos/devices/workstation/workstation_base.py
Normal 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
|
||||
605
unilabos/devices/workstation/workstation_http_service.py
Normal file
605
unilabos/devices/workstation/workstation_http_service.py
Normal file
@@ -0,0 +1,605 @@
|
||||
"""
|
||||
工作站HTTP服务模块
|
||||
Workstation HTTP Service Module
|
||||
|
||||
统一的工作站报送接收服务,基于LIMS协议规范:
|
||||
1. 步骤完成报送 - POST /report/step_finish
|
||||
2. 通量完成报送 - POST /report/sample_finish
|
||||
3. 任务完成报送 - POST /report/order_finish
|
||||
4. 批量更新报送 - POST /report/batch_update
|
||||
5. 物料变更报送 - POST /report/material_change
|
||||
6. 错误处理报送 - POST /report/error_handling
|
||||
7. 健康检查和状态查询
|
||||
|
||||
统一使用LIMS协议字段规范,简化接口避免功能重复
|
||||
"""
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
from typing import Dict, Any, Optional, List
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from urllib.parse import urlparse
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
|
||||
from unilabos.utils.log import logger
|
||||
|
||||
|
||||
@dataclass
|
||||
class WorkstationReportRequest:
|
||||
"""统一工作站报送请求(基于LIMS协议规范)"""
|
||||
token: str # 授权令牌
|
||||
request_time: str # 请求时间,格式:2024-12-12 12:12:12.xxx
|
||||
data: Dict[str, Any] # 报送数据
|
||||
|
||||
|
||||
@dataclass
|
||||
class MaterialUsage:
|
||||
"""物料使用记录"""
|
||||
materialId: str # 物料Id(GUID)
|
||||
locationId: str # 库位Id(GUID)
|
||||
typeMode: str # 物料类型(样品1、试剂2、耗材0)
|
||||
usedQuantity: float # 使用的数量(数字)
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpResponse:
|
||||
"""HTTP响应"""
|
||||
success: bool
|
||||
message: str
|
||||
data: Optional[Dict[str, Any]] = None
|
||||
acknowledgment_id: Optional[str] = None
|
||||
|
||||
|
||||
class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||||
"""工作站HTTP请求处理器"""
|
||||
|
||||
def __init__(self, workstation_instance, *args, **kwargs):
|
||||
self.workstation = workstation_instance
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def do_POST(self):
|
||||
"""处理POST请求 - 统一的工作站报送接口"""
|
||||
try:
|
||||
# 解析请求路径
|
||||
parsed_path = urlparse(self.path)
|
||||
endpoint = parsed_path.path
|
||||
|
||||
# 读取请求体
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
if content_length > 0:
|
||||
post_data = self.rfile.read(content_length)
|
||||
request_data = json.loads(post_data.decode('utf-8'))
|
||||
else:
|
||||
request_data = {}
|
||||
|
||||
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||||
|
||||
# 统一的报送端点路由(基于LIMS协议规范)
|
||||
if endpoint == '/report/step_finish':
|
||||
response = self._handle_step_finish_report(request_data)
|
||||
elif endpoint == '/report/sample_finish':
|
||||
response = self._handle_sample_finish_report(request_data)
|
||||
elif endpoint == '/report/order_finish':
|
||||
response = self._handle_order_finish_report(request_data)
|
||||
elif endpoint == '/report/batch_update':
|
||||
response = self._handle_batch_update_report(request_data)
|
||||
# 扩展报送端点
|
||||
elif endpoint == '/report/material_change':
|
||||
response = self._handle_material_change_report(request_data)
|
||||
elif endpoint == '/report/error_handling':
|
||||
response = self._handle_error_handling_report(request_data)
|
||||
# 保留LIMS协议端点以兼容现有系统
|
||||
elif endpoint == '/LIMS/step_finish':
|
||||
response = self._handle_step_finish_report(request_data)
|
||||
elif endpoint == '/LIMS/preintake_finish':
|
||||
response = self._handle_sample_finish_report(request_data)
|
||||
elif endpoint == '/LIMS/order_finish':
|
||||
response = self._handle_order_finish_report(request_data)
|
||||
else:
|
||||
response = HttpResponse(
|
||||
success=False,
|
||||
message=f"不支持的报送端点: {endpoint}",
|
||||
data={"supported_endpoints": [
|
||||
"/report/step_finish",
|
||||
"/report/sample_finish",
|
||||
"/report/order_finish",
|
||||
"/report/batch_update",
|
||||
"/report/material_change",
|
||||
"/report/error_handling"
|
||||
]}
|
||||
)
|
||||
|
||||
# 发送响应
|
||||
self._send_response(response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理工作站报送失败: {e}\\n{traceback.format_exc()}")
|
||||
error_response = HttpResponse(
|
||||
success=False,
|
||||
message=f"请求处理失败: {str(e)}"
|
||||
)
|
||||
self._send_response(error_response)
|
||||
|
||||
def do_GET(self):
|
||||
"""处理GET请求 - 健康检查和状态查询"""
|
||||
try:
|
||||
parsed_path = urlparse(self.path)
|
||||
endpoint = parsed_path.path
|
||||
|
||||
if endpoint == '/status':
|
||||
response = self._handle_status_check()
|
||||
elif endpoint == '/health':
|
||||
response = HttpResponse(success=True, message="服务健康")
|
||||
else:
|
||||
response = HttpResponse(
|
||||
success=False,
|
||||
message=f"不支持的查询端点: {endpoint}",
|
||||
data={"supported_endpoints": ["/status", "/health"]}
|
||||
)
|
||||
|
||||
self._send_response(response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"GET请求处理失败: {e}")
|
||||
error_response = HttpResponse(
|
||||
success=False,
|
||||
message=f"GET请求处理失败: {str(e)}"
|
||||
)
|
||||
self._send_response(error_response)
|
||||
|
||||
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||
try:
|
||||
# 验证基本字段
|
||||
required_fields = ['token', 'request_time', 'data']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 验证data字段内容
|
||||
data = request_data['data']
|
||||
data_required_fields = ['orderCode', 'orderName', 'stepName', 'stepId', 'sampleId', 'startTime', 'endTime']
|
||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||
)
|
||||
|
||||
# 创建统一请求对象
|
||||
report_request = WorkstationReportRequest(
|
||||
token=request_data['token'],
|
||||
request_time=request_data['request_time'],
|
||||
data=data
|
||||
)
|
||||
|
||||
# 调用工作站处理方法
|
||||
result = self.workstation.process_step_finish_report(report_request)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"步骤完成报送已处理: {data['stepName']} ({data['orderCode']})",
|
||||
acknowledgment_id=f"STEP_{int(time.time() * 1000)}_{data['stepId']}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理步骤完成报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"步骤完成报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_sample_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理通量完成报送(统一LIMS协议规范)"""
|
||||
try:
|
||||
# 验证基本字段
|
||||
required_fields = ['token', 'request_time', 'data']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 验证data字段内容
|
||||
data = request_data['data']
|
||||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status']
|
||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||
)
|
||||
|
||||
# 创建统一请求对象
|
||||
report_request = WorkstationReportRequest(
|
||||
token=request_data['token'],
|
||||
request_time=request_data['request_time'],
|
||||
data=data
|
||||
)
|
||||
|
||||
# 调用工作站处理方法
|
||||
result = self.workstation.process_sample_finish_report(report_request)
|
||||
|
||||
status_names = {
|
||||
"0": "待生产", "2": "进样", "10": "开始",
|
||||
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||||
}
|
||||
status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}")
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"通量完成报送已处理: {data['sampleId']} ({data['orderCode']}) - {status_desc}",
|
||||
acknowledgment_id=f"SAMPLE_{int(time.time() * 1000)}_{data['sampleId']}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理通量完成报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"通量完成报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_order_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理任务完成报送(统一LIMS协议规范)"""
|
||||
try:
|
||||
# 验证基本字段
|
||||
required_fields = ['token', 'request_time', 'data']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 验证data字段内容
|
||||
data = request_data['data']
|
||||
data_required_fields = ['orderCode', 'orderName', 'startTime', 'endTime', 'status']
|
||||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||||
)
|
||||
|
||||
# 处理物料使用记录
|
||||
used_materials = []
|
||||
if 'usedMaterials' in data:
|
||||
for material_data in data['usedMaterials']:
|
||||
material = MaterialUsage(
|
||||
materialId=material_data.get('materialId', ''),
|
||||
locationId=material_data.get('locationId', ''),
|
||||
typeMode=material_data.get('typeMode', ''),
|
||||
usedQuantity=material_data.get('usedQuantity', 0.0)
|
||||
)
|
||||
used_materials.append(material)
|
||||
|
||||
# 创建统一请求对象
|
||||
report_request = WorkstationReportRequest(
|
||||
token=request_data['token'],
|
||||
request_time=request_data['request_time'],
|
||||
data=data
|
||||
)
|
||||
|
||||
# 调用工作站处理方法
|
||||
result = self.workstation.process_order_finish_report(report_request, used_materials)
|
||||
|
||||
status_names = {"30": "完成", "-11": "异常停止", "-12": "人工停止"}
|
||||
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"任务完成报送已处理: {data['orderName']} ({data['orderCode']}) - {status_desc}",
|
||||
acknowledgment_id=f"ORDER_{int(time.time() * 1000)}_{data['orderCode']}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理任务完成报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"任务完成报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理批量报送"""
|
||||
try:
|
||||
step_updates = request_data.get('step_updates', [])
|
||||
sample_updates = request_data.get('sample_updates', [])
|
||||
order_updates = request_data.get('order_updates', [])
|
||||
|
||||
results = {
|
||||
'step_results': [],
|
||||
'sample_results': [],
|
||||
'order_results': [],
|
||||
'total_processed': 0,
|
||||
'total_failed': 0
|
||||
}
|
||||
|
||||
# 处理批量步骤更新
|
||||
for step_data in step_updates:
|
||||
try:
|
||||
step_data['token'] = request_data.get('token', step_data.get('token'))
|
||||
step_data['request_time'] = request_data.get('request_time', step_data.get('request_time'))
|
||||
result = self._handle_step_finish_report(step_data)
|
||||
results['step_results'].append(result)
|
||||
if result.success:
|
||||
results['total_processed'] += 1
|
||||
else:
|
||||
results['total_failed'] += 1
|
||||
except Exception as e:
|
||||
results['step_results'].append(HttpResponse(success=False, message=str(e)))
|
||||
results['total_failed'] += 1
|
||||
|
||||
# 处理批量通量更新
|
||||
for sample_data in sample_updates:
|
||||
try:
|
||||
sample_data['token'] = request_data.get('token', sample_data.get('token'))
|
||||
sample_data['request_time'] = request_data.get('request_time', sample_data.get('request_time'))
|
||||
result = self._handle_sample_finish_report(sample_data)
|
||||
results['sample_results'].append(result)
|
||||
if result.success:
|
||||
results['total_processed'] += 1
|
||||
else:
|
||||
results['total_failed'] += 1
|
||||
except Exception as e:
|
||||
results['sample_results'].append(HttpResponse(success=False, message=str(e)))
|
||||
results['total_failed'] += 1
|
||||
|
||||
# 处理批量任务更新
|
||||
for order_data in order_updates:
|
||||
try:
|
||||
order_data['token'] = request_data.get('token', order_data.get('token'))
|
||||
order_data['request_time'] = request_data.get('request_time', order_data.get('request_time'))
|
||||
result = self._handle_order_finish_report(order_data)
|
||||
results['order_results'].append(result)
|
||||
if result.success:
|
||||
results['total_processed'] += 1
|
||||
else:
|
||||
results['total_failed'] += 1
|
||||
except Exception as e:
|
||||
results['order_results'].append(HttpResponse(success=False, message=str(e)))
|
||||
results['total_failed'] += 1
|
||||
|
||||
return HttpResponse(
|
||||
success=results['total_failed'] == 0,
|
||||
message=f"批量报送处理完成: {results['total_processed']} 成功, {results['total_failed']} 失败",
|
||||
acknowledgment_id=f"BATCH_{int(time.time() * 1000)}",
|
||||
data=results
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理批量报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"批量报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_material_change_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理物料变更报送"""
|
||||
try:
|
||||
# 验证必需字段
|
||||
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.process_material_change_report(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"物料变更报送已处理: {request_data['resource_id']} ({request_data['change_type']})",
|
||||
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{request_data['resource_id']}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理物料变更报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"物料变更报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||||
"""处理错误处理报送"""
|
||||
try:
|
||||
# 验证必需字段
|
||||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||||
)
|
||||
|
||||
# 调用工作站的处理方法
|
||||
result = self.workstation.handle_external_error(request_data)
|
||||
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||||
data=result
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理错误处理报送失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"错误处理报送处理失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _handle_status_check(self) -> HttpResponse:
|
||||
"""处理状态查询"""
|
||||
try:
|
||||
return HttpResponse(
|
||||
success=True,
|
||||
message="工作站报送服务正常运行",
|
||||
data={
|
||||
"workstation_id": self.workstation.device_id,
|
||||
"service_type": "unified_reporting_service",
|
||||
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
||||
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
||||
"supported_endpoints": [
|
||||
"POST /report/step_finish",
|
||||
"POST /report/sample_finish",
|
||||
"POST /report/order_finish",
|
||||
"POST /report/batch_update",
|
||||
"POST /report/material_change",
|
||||
"POST /report/error_handling",
|
||||
"GET /status",
|
||||
"GET /health"
|
||||
]
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"处理状态查询失败: {e}")
|
||||
return HttpResponse(
|
||||
success=False,
|
||||
message=f"状态查询失败: {str(e)}"
|
||||
)
|
||||
|
||||
def _send_response(self, response: HttpResponse):
|
||||
"""发送响应"""
|
||||
try:
|
||||
# 设置响应状态码
|
||||
status_code = 200 if response.success else 400
|
||||
self.send_response(status_code)
|
||||
|
||||
# 设置响应头
|
||||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
# 发送响应体
|
||||
response_json = json.dumps(asdict(response), ensure_ascii=False, indent=2)
|
||||
self.wfile.write(response_json.encode('utf-8'))
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"发送响应失败: {e}")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""重写日志方法"""
|
||||
logger.debug(f"HTTP请求: {format % args}")
|
||||
|
||||
|
||||
class WorkstationHTTPService:
|
||||
"""工作站HTTP服务"""
|
||||
|
||||
def __init__(self, workstation_instance, host: str = "127.0.0.1", port: int = 8080):
|
||||
self.workstation = workstation_instance
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server = None
|
||||
self.server_thread = None
|
||||
self.running = False
|
||||
|
||||
# 初始化统计信息
|
||||
self.workstation._start_time = time.time()
|
||||
self.workstation._reports_received_count = 0
|
||||
|
||||
def start(self):
|
||||
"""启动HTTP服务"""
|
||||
try:
|
||||
# 创建处理器工厂函数
|
||||
def handler_factory(*args, **kwargs):
|
||||
return WorkstationHTTPHandler(self.workstation, *args, **kwargs)
|
||||
|
||||
# 创建HTTP服务器
|
||||
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||||
|
||||
# 在单独线程中运行服务器
|
||||
self.server_thread = threading.Thread(
|
||||
target=self._run_server,
|
||||
daemon=True,
|
||||
name=f"WorkstationHTTP-{self.workstation.device_id}"
|
||||
)
|
||||
|
||||
self.running = True
|
||||
self.server_thread.start()
|
||||
|
||||
logger.info(f"工作站HTTP报送服务已启动: http://{self.host}:{self.port}")
|
||||
logger.info("统一的报送端点 (基于LIMS协议规范):")
|
||||
logger.info(" - POST /report/step_finish # 步骤完成报送")
|
||||
logger.info(" - POST /report/sample_finish # 通量完成报送")
|
||||
logger.info(" - POST /report/order_finish # 任务完成报送")
|
||||
logger.info(" - POST /report/batch_update # 批量更新报送")
|
||||
logger.info("扩展报送端点:")
|
||||
logger.info(" - POST /report/material_change # 物料变更报送")
|
||||
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||||
logger.info("兼容端点:")
|
||||
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||||
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||||
logger.info(" - POST /LIMS/order_finish # 兼容LIMS任务完成")
|
||||
logger.info("服务端点:")
|
||||
logger.info(" - GET /status # 服务状态查询")
|
||||
logger.info(" - GET /health # 健康检查")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"启动HTTP服务失败: {e}")
|
||||
raise
|
||||
|
||||
def stop(self):
|
||||
"""停止HTTP服务"""
|
||||
try:
|
||||
if self.running and self.server:
|
||||
self.running = False
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
|
||||
if self.server_thread and self.server_thread.is_alive():
|
||||
self.server_thread.join(timeout=5.0)
|
||||
|
||||
logger.info("工作站HTTP报送服务已停止")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"停止HTTP服务失败: {e}")
|
||||
|
||||
def _run_server(self):
|
||||
"""运行HTTP服务器"""
|
||||
try:
|
||||
while self.running:
|
||||
self.server.handle_request()
|
||||
except Exception as e:
|
||||
if self.running: # 只在非正常停止时记录错误
|
||||
logger.error(f"HTTP服务运行错误: {e}")
|
||||
|
||||
@property
|
||||
def is_running(self) -> bool:
|
||||
"""检查服务是否正在运行"""
|
||||
return self.running and self.server_thread and self.server_thread.is_alive()
|
||||
|
||||
@property
|
||||
def service_url(self) -> str:
|
||||
"""获取服务URL"""
|
||||
return f"http://{self.host}:{self.port}"
|
||||
|
||||
|
||||
# 导出主要类 - 保持向后兼容
|
||||
@dataclass
|
||||
class MaterialChangeReport:
|
||||
"""已废弃:物料变更报送,请使用统一的WorkstationReportRequest"""
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskExecutionReport:
|
||||
"""已废弃:任务执行报送,请使用统一的WorkstationReportRequest"""
|
||||
pass
|
||||
|
||||
|
||||
# 导出列表
|
||||
__all__ = [
|
||||
'WorkstationReportRequest',
|
||||
'MaterialUsage',
|
||||
'HttpResponse',
|
||||
'WorkstationHTTPService',
|
||||
# 向后兼容
|
||||
'MaterialChangeReport',
|
||||
'TaskExecutionReport'
|
||||
]
|
||||
583
unilabos/devices/workstation/workstation_material_management.py
Normal file
583
unilabos/devices/workstation/workstation_material_management.py
Normal file
@@ -0,0 +1,583 @@
|
||||
"""
|
||||
工作站物料管理基类
|
||||
Workstation Material Management Base Class
|
||||
|
||||
基于PyLabRobot的物料管理系统
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional, Union, Type
|
||||
from abc import ABC, abstractmethod
|
||||
import json
|
||||
|
||||
from pylabrobot.resources import (
|
||||
Resource as PLRResource,
|
||||
Container,
|
||||
Deck,
|
||||
Coordinate as PLRCoordinate,
|
||||
)
|
||||
|
||||
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||
from unilabos.utils.log import logger
|
||||
from unilabos.resources.graphio import resource_plr_to_ulab, resource_ulab_to_plr
|
||||
|
||||
|
||||
class MaterialManagementBase(ABC):
|
||||
"""物料管理基类
|
||||
|
||||
定义工作站物料管理的标准接口:
|
||||
1. 物料初始化 - 根据配置创建物料资源
|
||||
2. 物料追踪 - 实时跟踪物料位置和状态
|
||||
3. 物料查找 - 按类型、位置、状态查找物料
|
||||
4. 物料转换 - PyLabRobot与UniLab资源格式转换
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
deck_config: Dict[str, Any],
|
||||
resource_tracker: DeviceNodeResourceTracker,
|
||||
children_config: Dict[str, Dict[str, Any]] = None
|
||||
):
|
||||
self.device_id = device_id
|
||||
self.deck_config = deck_config
|
||||
self.resource_tracker = resource_tracker
|
||||
self.children_config = children_config or {}
|
||||
|
||||
# 创建主台面
|
||||
self.plr_deck = self._create_deck()
|
||||
|
||||
# 扩展ResourceTracker
|
||||
self._extend_resource_tracker()
|
||||
|
||||
# 注册deck到resource tracker
|
||||
self.resource_tracker.add_resource(self.plr_deck)
|
||||
|
||||
# 初始化子资源
|
||||
self.plr_resources = {}
|
||||
self._initialize_materials()
|
||||
|
||||
def _create_deck(self) -> Deck:
|
||||
"""创建主台面"""
|
||||
return Deck(
|
||||
name=f"{self.device_id}_deck",
|
||||
size_x=self.deck_config.get("size_x", 1000.0),
|
||||
size_y=self.deck_config.get("size_y", 1000.0),
|
||||
size_z=self.deck_config.get("size_z", 500.0),
|
||||
origin=PLRCoordinate(0, 0, 0)
|
||||
)
|
||||
|
||||
def _extend_resource_tracker(self):
|
||||
"""扩展ResourceTracker以支持PyLabRobot特定功能"""
|
||||
|
||||
def find_by_type(resource_type):
|
||||
"""按类型查找资源"""
|
||||
return self._find_resources_by_type_recursive(self.plr_deck, resource_type)
|
||||
|
||||
def find_by_category(category: str):
|
||||
"""按类别查找资源"""
|
||||
found = []
|
||||
for resource in self._get_all_resources():
|
||||
if hasattr(resource, 'category') and resource.category == category:
|
||||
found.append(resource)
|
||||
return found
|
||||
|
||||
def find_by_name_pattern(pattern: str):
|
||||
"""按名称模式查找资源"""
|
||||
import re
|
||||
found = []
|
||||
for resource in self._get_all_resources():
|
||||
if re.search(pattern, resource.name):
|
||||
found.append(resource)
|
||||
return found
|
||||
|
||||
# 动态添加方法到resource_tracker
|
||||
self.resource_tracker.find_by_type = find_by_type
|
||||
self.resource_tracker.find_by_category = find_by_category
|
||||
self.resource_tracker.find_by_name_pattern = find_by_name_pattern
|
||||
|
||||
def _find_resources_by_type_recursive(self, resource, target_type):
|
||||
"""递归查找指定类型的资源"""
|
||||
found = []
|
||||
if isinstance(resource, target_type):
|
||||
found.append(resource)
|
||||
|
||||
# 递归查找子资源
|
||||
children = getattr(resource, "children", [])
|
||||
for child in children:
|
||||
found.extend(self._find_resources_by_type_recursive(child, target_type))
|
||||
|
||||
return found
|
||||
|
||||
def _get_all_resources(self) -> List[PLRResource]:
|
||||
"""获取所有资源"""
|
||||
all_resources = []
|
||||
|
||||
def collect_resources(resource):
|
||||
all_resources.append(resource)
|
||||
children = getattr(resource, "children", [])
|
||||
for child in children:
|
||||
collect_resources(child)
|
||||
|
||||
collect_resources(self.plr_deck)
|
||||
return all_resources
|
||||
|
||||
def _initialize_materials(self):
|
||||
"""初始化物料"""
|
||||
try:
|
||||
# 确定创建顺序,确保父资源先于子资源创建
|
||||
creation_order = self._determine_creation_order()
|
||||
|
||||
# 按顺序创建资源
|
||||
for resource_id in creation_order:
|
||||
config = self.children_config[resource_id]
|
||||
self._create_plr_resource(resource_id, config)
|
||||
|
||||
logger.info(f"物料管理系统初始化完成,共创建 {len(self.plr_resources)} 个资源")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"物料初始化失败: {e}")
|
||||
|
||||
def _determine_creation_order(self) -> List[str]:
|
||||
"""确定资源创建顺序"""
|
||||
order = []
|
||||
visited = set()
|
||||
|
||||
def visit(resource_id: str):
|
||||
if resource_id in visited:
|
||||
return
|
||||
visited.add(resource_id)
|
||||
|
||||
config = self.children_config.get(resource_id, {})
|
||||
parent_id = config.get("parent")
|
||||
|
||||
# 如果有父资源,先访问父资源
|
||||
if parent_id and parent_id in self.children_config:
|
||||
visit(parent_id)
|
||||
|
||||
order.append(resource_id)
|
||||
|
||||
for resource_id in self.children_config:
|
||||
visit(resource_id)
|
||||
|
||||
return order
|
||||
|
||||
def _create_plr_resource(self, resource_id: str, config: Dict[str, Any]):
|
||||
"""创建PyLabRobot资源"""
|
||||
try:
|
||||
resource_type = config.get("type", "unknown")
|
||||
data = config.get("data", {})
|
||||
location_config = config.get("location", {})
|
||||
|
||||
# 创建位置坐标
|
||||
location = PLRCoordinate(
|
||||
x=location_config.get("x", 0.0),
|
||||
y=location_config.get("y", 0.0),
|
||||
z=location_config.get("z", 0.0)
|
||||
)
|
||||
|
||||
# 根据类型创建资源
|
||||
resource = self._create_resource_by_type(resource_id, resource_type, config, data, location)
|
||||
|
||||
if resource:
|
||||
# 设置父子关系
|
||||
parent_id = config.get("parent")
|
||||
if parent_id and parent_id in self.plr_resources:
|
||||
parent_resource = self.plr_resources[parent_id]
|
||||
parent_resource.assign_child_resource(resource, location)
|
||||
else:
|
||||
# 直接放在deck上
|
||||
self.plr_deck.assign_child_resource(resource, location)
|
||||
|
||||
# 保存资源引用
|
||||
self.plr_resources[resource_id] = resource
|
||||
|
||||
# 注册到resource tracker
|
||||
self.resource_tracker.add_resource(resource)
|
||||
|
||||
logger.debug(f"创建资源成功: {resource_id} ({resource_type})")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建资源失败 {resource_id}: {e}")
|
||||
|
||||
@abstractmethod
|
||||
def _create_resource_by_type(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""根据类型创建资源 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
# ============ 物料查找接口 ============
|
||||
|
||||
def find_materials_by_type(self, material_type: str) -> List[PLRResource]:
|
||||
"""按材料类型查找物料"""
|
||||
return self.resource_tracker.find_by_category(material_type)
|
||||
|
||||
def find_material_by_id(self, resource_id: str) -> Optional[PLRResource]:
|
||||
"""按ID查找物料"""
|
||||
return self.plr_resources.get(resource_id)
|
||||
|
||||
def find_available_positions(self, position_type: str) -> List[PLRResource]:
|
||||
"""查找可用位置"""
|
||||
positions = self.resource_tracker.find_by_category(position_type)
|
||||
available = []
|
||||
|
||||
for pos in positions:
|
||||
if hasattr(pos, 'is_available') and pos.is_available():
|
||||
available.append(pos)
|
||||
elif hasattr(pos, 'children') and len(pos.children) == 0:
|
||||
available.append(pos)
|
||||
|
||||
return available
|
||||
|
||||
def get_material_inventory(self) -> Dict[str, int]:
|
||||
"""获取物料库存统计"""
|
||||
inventory = {}
|
||||
|
||||
for resource in self._get_all_resources():
|
||||
if hasattr(resource, 'category'):
|
||||
category = resource.category
|
||||
inventory[category] = inventory.get(category, 0) + 1
|
||||
|
||||
return inventory
|
||||
|
||||
# ============ 物料状态更新接口 ============
|
||||
|
||||
def update_material_location(self, material_id: str, new_location: PLRCoordinate) -> bool:
|
||||
"""更新物料位置"""
|
||||
try:
|
||||
material = self.find_material_by_id(material_id)
|
||||
if material:
|
||||
material.location = new_location
|
||||
return True
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"更新物料位置失败: {e}")
|
||||
return False
|
||||
|
||||
def move_material(self, material_id: str, target_container_id: str) -> bool:
|
||||
"""移动物料到目标容器"""
|
||||
try:
|
||||
material = self.find_material_by_id(material_id)
|
||||
target = self.find_material_by_id(target_container_id)
|
||||
|
||||
if material and target:
|
||||
# 从原位置移除
|
||||
if material.parent:
|
||||
material.parent.unassign_child_resource(material)
|
||||
|
||||
# 添加到新位置
|
||||
target.assign_child_resource(material)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"移动物料失败: {e}")
|
||||
return False
|
||||
|
||||
# ============ 资源转换接口 ============
|
||||
|
||||
def convert_to_unilab_format(self, plr_resource: PLRResource) -> Dict[str, Any]:
|
||||
"""将PyLabRobot资源转换为UniLab格式"""
|
||||
return resource_plr_to_ulab(plr_resource)
|
||||
|
||||
def convert_from_unilab_format(self, unilab_resource: Dict[str, Any]) -> PLRResource:
|
||||
"""将UniLab格式转换为PyLabRobot资源"""
|
||||
return resource_ulab_to_plr(unilab_resource)
|
||||
|
||||
def get_deck_state(self) -> Dict[str, Any]:
|
||||
"""获取Deck状态"""
|
||||
try:
|
||||
return {
|
||||
"deck_info": {
|
||||
"name": self.plr_deck.name,
|
||||
"size": {
|
||||
"x": self.plr_deck.size_x,
|
||||
"y": self.plr_deck.size_y,
|
||||
"z": self.plr_deck.size_z
|
||||
},
|
||||
"children_count": len(self.plr_deck.children)
|
||||
},
|
||||
"resources": {
|
||||
resource_id: self.convert_to_unilab_format(resource)
|
||||
for resource_id, resource in self.plr_resources.items()
|
||||
},
|
||||
"inventory": self.get_material_inventory()
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取Deck状态失败: {e}")
|
||||
return {"error": str(e)}
|
||||
|
||||
# ============ 数据持久化接口 ============
|
||||
|
||||
def save_state_to_file(self, file_path: str) -> bool:
|
||||
"""保存状态到文件"""
|
||||
try:
|
||||
state = self.get_deck_state()
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=2, ensure_ascii=False)
|
||||
logger.info(f"状态已保存到: {file_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"保存状态失败: {e}")
|
||||
return False
|
||||
|
||||
def load_state_from_file(self, file_path: str) -> bool:
|
||||
"""从文件加载状态"""
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
state = json.load(f)
|
||||
|
||||
# 重新创建资源
|
||||
self._recreate_resources_from_state(state)
|
||||
logger.info(f"状态已从文件加载: {file_path}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"加载状态失败: {e}")
|
||||
return False
|
||||
|
||||
def _recreate_resources_from_state(self, state: Dict[str, Any]):
|
||||
"""从状态重新创建资源"""
|
||||
# 清除现有资源
|
||||
self.plr_resources.clear()
|
||||
self.plr_deck.children.clear()
|
||||
|
||||
# 从状态重新创建
|
||||
resources_data = state.get("resources", {})
|
||||
for resource_id, resource_data in resources_data.items():
|
||||
try:
|
||||
plr_resource = self.convert_from_unilab_format(resource_data)
|
||||
self.plr_resources[resource_id] = plr_resource
|
||||
self.plr_deck.assign_child_resource(plr_resource)
|
||||
except Exception as e:
|
||||
logger.error(f"重新创建资源失败 {resource_id}: {e}")
|
||||
|
||||
|
||||
class CoinCellMaterialManagement(MaterialManagementBase):
|
||||
"""纽扣电池物料管理类
|
||||
|
||||
从 button_battery_station 抽取的物料管理功能
|
||||
"""
|
||||
|
||||
def _create_resource_by_type(
|
||||
self,
|
||||
resource_id: str,
|
||||
resource_type: str,
|
||||
config: Dict[str, Any],
|
||||
data: Dict[str, Any],
|
||||
location: PLRCoordinate
|
||||
) -> Optional[PLRResource]:
|
||||
"""根据类型创建纽扣电池相关资源"""
|
||||
|
||||
# 导入纽扣电池资源类
|
||||
from unilabos.device_comms.button_battery_station import (
|
||||
MaterialPlate, PlateSlot, ClipMagazine, BatteryPressSlot,
|
||||
TipBox64, WasteTipBox, BottleRack, Battery, ElectrodeSheet
|
||||
)
|
||||
|
||||
try:
|
||||
if resource_type == "material_plate":
|
||||
return self._create_material_plate(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "plate_slot":
|
||||
return self._create_plate_slot(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "clip_magazine":
|
||||
return self._create_clip_magazine(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "battery_press_slot":
|
||||
return self._create_battery_press_slot(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "tip_box":
|
||||
return self._create_tip_box(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "waste_tip_box":
|
||||
return self._create_waste_tip_box(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "bottle_rack":
|
||||
return self._create_bottle_rack(resource_id, config, data, location)
|
||||
|
||||
elif resource_type == "battery":
|
||||
return self._create_battery(resource_id, config, data, location)
|
||||
|
||||
else:
|
||||
logger.warning(f"未知的资源类型: {resource_type}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"创建资源失败 {resource_id} ({resource_type}): {e}")
|
||||
return None
|
||||
|
||||
def _create_material_plate(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建料板"""
|
||||
from unilabos.device_comms.button_battery_station import MaterialPlate, ElectrodeSheet
|
||||
|
||||
plate = MaterialPlate(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 80.0),
|
||||
size_y=config.get("size_y", 80.0),
|
||||
size_z=config.get("size_z", 10.0),
|
||||
hole_diameter=config.get("hole_diameter", 15.0),
|
||||
hole_depth=config.get("hole_depth", 8.0),
|
||||
hole_spacing_x=config.get("hole_spacing_x", 20.0),
|
||||
hole_spacing_y=config.get("hole_spacing_y", 20.0),
|
||||
number=data.get("number", "")
|
||||
)
|
||||
plate.location = location
|
||||
|
||||
# 如果有预填充的极片数据,创建极片
|
||||
electrode_sheets = data.get("electrode_sheets", [])
|
||||
for i, sheet_data in enumerate(electrode_sheets):
|
||||
if i < len(plate.children): # 确保不超过洞位数量
|
||||
hole = plate.children[i]
|
||||
sheet = ElectrodeSheet(
|
||||
name=f"{resource_id}_sheet_{i}",
|
||||
diameter=sheet_data.get("diameter", 14.0),
|
||||
thickness=sheet_data.get("thickness", 0.1),
|
||||
mass=sheet_data.get("mass", 0.01),
|
||||
material_type=sheet_data.get("material_type", "cathode"),
|
||||
info=sheet_data.get("info", "")
|
||||
)
|
||||
hole.place_electrode_sheet(sheet)
|
||||
|
||||
return plate
|
||||
|
||||
def _create_plate_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建板槽位"""
|
||||
from unilabos.device_comms.button_battery_station import PlateSlot
|
||||
|
||||
slot = PlateSlot(
|
||||
name=resource_id,
|
||||
max_plates=config.get("max_plates", 8)
|
||||
)
|
||||
slot.location = location
|
||||
return slot
|
||||
|
||||
def _create_clip_magazine(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建子弹夹"""
|
||||
from unilabos.device_comms.button_battery_station import ClipMagazine
|
||||
|
||||
magazine = ClipMagazine(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 150.0),
|
||||
size_y=config.get("size_y", 100.0),
|
||||
size_z=config.get("size_z", 50.0),
|
||||
hole_diameter=config.get("hole_diameter", 15.0),
|
||||
hole_depth=config.get("hole_depth", 40.0),
|
||||
hole_spacing=config.get("hole_spacing", 25.0),
|
||||
max_sheets_per_hole=config.get("max_sheets_per_hole", 100)
|
||||
)
|
||||
magazine.location = location
|
||||
return magazine
|
||||
|
||||
def _create_battery_press_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建电池压制槽"""
|
||||
from unilabos.device_comms.button_battery_station import BatteryPressSlot
|
||||
|
||||
slot = BatteryPressSlot(
|
||||
name=resource_id,
|
||||
diameter=config.get("diameter", 20.0),
|
||||
depth=config.get("depth", 15.0)
|
||||
)
|
||||
slot.location = location
|
||||
return slot
|
||||
|
||||
def _create_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建枪头盒"""
|
||||
from unilabos.device_comms.button_battery_station import TipBox64
|
||||
|
||||
tip_box = TipBox64(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 127.8),
|
||||
size_y=config.get("size_y", 85.5),
|
||||
size_z=config.get("size_z", 60.0),
|
||||
with_tips=data.get("with_tips", True)
|
||||
)
|
||||
tip_box.location = location
|
||||
return tip_box
|
||||
|
||||
def _create_waste_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建废枪头盒"""
|
||||
from unilabos.device_comms.button_battery_station import WasteTipBox
|
||||
|
||||
waste_box = WasteTipBox(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 127.8),
|
||||
size_y=config.get("size_y", 85.5),
|
||||
size_z=config.get("size_z", 60.0),
|
||||
max_tips=config.get("max_tips", 100)
|
||||
)
|
||||
waste_box.location = location
|
||||
return waste_box
|
||||
|
||||
def _create_bottle_rack(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建瓶架"""
|
||||
from unilabos.device_comms.button_battery_station import BottleRack
|
||||
|
||||
rack = BottleRack(
|
||||
name=resource_id,
|
||||
size_x=config.get("size_x", 210.0),
|
||||
size_y=config.get("size_y", 140.0),
|
||||
size_z=config.get("size_z", 100.0),
|
||||
bottle_diameter=config.get("bottle_diameter", 30.0),
|
||||
bottle_height=config.get("bottle_height", 100.0),
|
||||
position_spacing=config.get("position_spacing", 35.0)
|
||||
)
|
||||
rack.location = location
|
||||
return rack
|
||||
|
||||
def _create_battery(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate):
|
||||
"""创建电池"""
|
||||
from unilabos.device_comms.button_battery_station import Battery
|
||||
|
||||
battery = Battery(
|
||||
name=resource_id,
|
||||
diameter=config.get("diameter", 20.0),
|
||||
height=config.get("height", 3.2),
|
||||
max_volume=config.get("max_volume", 100.0),
|
||||
barcode=data.get("barcode", "")
|
||||
)
|
||||
battery.location = location
|
||||
return battery
|
||||
|
||||
# ============ 纽扣电池特定查找方法 ============
|
||||
|
||||
def find_material_plates(self):
|
||||
"""查找所有料板"""
|
||||
from unilabos.device_comms.button_battery_station import MaterialPlate
|
||||
return self.resource_tracker.find_by_type(MaterialPlate)
|
||||
|
||||
def find_batteries(self):
|
||||
"""查找所有电池"""
|
||||
from unilabos.device_comms.button_battery_station import Battery
|
||||
return self.resource_tracker.find_by_type(Battery)
|
||||
|
||||
def find_electrode_sheets(self):
|
||||
"""查找所有极片"""
|
||||
found = []
|
||||
plates = self.find_material_plates()
|
||||
for plate in plates:
|
||||
for hole in plate.children:
|
||||
if hasattr(hole, 'has_electrode_sheet') and hole.has_electrode_sheet():
|
||||
found.append(hole._electrode_sheet)
|
||||
return found
|
||||
|
||||
def find_plate_slots(self):
|
||||
"""查找所有板槽位"""
|
||||
from unilabos.device_comms.button_battery_station import PlateSlot
|
||||
return self.resource_tracker.find_by_type(PlateSlot)
|
||||
|
||||
def find_clip_magazines(self):
|
||||
"""查找所有子弹夹"""
|
||||
from unilabos.device_comms.button_battery_station import ClipMagazine
|
||||
return self.resource_tracker.find_by_type(ClipMagazine)
|
||||
|
||||
def find_press_slots(self):
|
||||
"""查找所有压制槽"""
|
||||
from unilabos.device_comms.button_battery_station import BatteryPressSlot
|
||||
return self.resource_tracker.find_by_type(BatteryPressSlot)
|
||||
Reference in New Issue
Block a user