mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 21:11:12 +00:00
Merge branch 'workstation_dev' into dev
# Conflicts: # .conda/recipe.yaml # recipes/msgs/recipe.yaml # recipes/unilabos/recipe.yaml # setup.py # unilabos/registry/devices/work_station.yaml # unilabos/ros/nodes/base_device_node.py # unilabos/ros/nodes/presets/protocol_node.py # unilabos_msgs/package.xml
This commit is contained in:
184
unilabos/devices/workstation/README.md
Normal file
184
unilabos/devices/workstation/README.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# 工作站抽象基类物料系统架构说明
|
||||
|
||||
## 设计理念
|
||||
|
||||
基于用户需求"请你帮我系统思考一下,工作站抽象基类的物料系统基类该如何构建",我们最终确定了一个**PyLabRobot Deck为中心**的简化架构。
|
||||
|
||||
### 核心原则
|
||||
|
||||
1. **PyLabRobot为物料管理核心**:使用PyLabRobot的Deck系统作为物料管理的基础,利用其成熟的Resource体系
|
||||
2. **Graphio转换函数集成**:使用graphio中的`resource_ulab_to_plr`等转换函数实现UniLab与PLR格式的无缝转换
|
||||
3. **关注点分离**:基类专注核心物料系统,HTTP服务等功能在子类中实现
|
||||
4. **外部系统集成模式**:通过ResourceSynchronizer抽象类提供外部物料系统对接模式
|
||||
|
||||
## 架构组成
|
||||
|
||||
### 1. WorkstationBase(基类)
|
||||
**文件**: `workstation_base.py`
|
||||
|
||||
**核心功能**:
|
||||
- 使用deck_config和children通过`resource_ulab_to_plr`转换为PLR物料self.deck
|
||||
- 基础的资源查找和管理功能
|
||||
- 抽象的工作流执行接口
|
||||
- ResourceSynchronizer集成点
|
||||
|
||||
**关键代码**:
|
||||
```python
|
||||
def _initialize_material_system(self, deck_config: Dict[str, Any], children_config: Dict[str, Any] = None):
|
||||
"""初始化基于PLR的物料系统"""
|
||||
# 合并deck_config和children
|
||||
complete_config = self._merge_deck_and_children_config(deck_config, children_config)
|
||||
|
||||
# 使用graphio转换函数转换为PLR资源
|
||||
self.deck = resource_ulab_to_plr(complete_config)
|
||||
```
|
||||
|
||||
### 2. ResourceSynchronizer(外部系统集成抽象类)
|
||||
**定义在**: `workstation_base.py`
|
||||
|
||||
**设计目的**:
|
||||
- 提供外部物料系统(如Bioyong、LIMS等)集成的标准接口
|
||||
- 双向同步:从外部系统同步到本地deck,以及将本地变更同步到外部系统
|
||||
- 处理外部系统的变更通知
|
||||
|
||||
**核心方法**:
|
||||
```python
|
||||
async def sync_from_external(self) -> bool:
|
||||
"""从外部系统同步物料到本地deck"""
|
||||
|
||||
async def sync_to_external(self, plr_resource) -> bool:
|
||||
"""将本地物料同步到外部系统"""
|
||||
|
||||
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
"""处理外部系统的变更通知"""
|
||||
```
|
||||
|
||||
### 3. WorkstationWithHTTP(子类示例)
|
||||
**文件**: `workstation_with_http_example.py`
|
||||
|
||||
**扩展功能**:
|
||||
- HTTP报送接收服务集成
|
||||
- 具体工作流实现(液体转移、板洗等)
|
||||
- Bioyong物料系统同步器示例
|
||||
- 外部报送处理方法
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 核心依赖
|
||||
- **PyLabRobot**: 物料资源管理核心(Deck, Resource, Coordinate)
|
||||
- **GraphIO转换函数**: UniLab ↔ PLR格式转换
|
||||
- `resource_ulab_to_plr`: UniLab格式转PLR格式
|
||||
- `resource_plr_to_ulab`: PLR格式转UniLab格式
|
||||
- `convert_resources_to_type`: 通用资源类型转换
|
||||
- **ROS2**: 基础设备节点通信(BaseROS2DeviceNode)
|
||||
|
||||
### 可选依赖
|
||||
- **HTTP服务**: 仅在需要外部报送接收的子类中使用
|
||||
- **外部系统API**: 根据具体集成需求添加
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 简单工作站(仅PLR物料系统)
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_base import WorkstationBase
|
||||
|
||||
# Deck配置
|
||||
deck_config = {
|
||||
"size_x": 1200.0,
|
||||
"size_y": 800.0,
|
||||
"size_z": 100.0
|
||||
}
|
||||
|
||||
# 子资源配置
|
||||
children_config = {
|
||||
"source_plate": {
|
||||
"name": "source_plate",
|
||||
"type": "plate",
|
||||
"position": {"x": 100, "y": 100, "z": 10},
|
||||
"config": {"size_x": 127.8, "size_y": 85.5, "size_z": 14.4}
|
||||
}
|
||||
}
|
||||
|
||||
# 创建工作站
|
||||
workstation = WorkstationBase(
|
||||
device_id="simple_workstation",
|
||||
deck_config=deck_config,
|
||||
children_config=children_config
|
||||
)
|
||||
|
||||
# 查找资源
|
||||
plate = workstation.find_resource_by_name("source_plate")
|
||||
```
|
||||
|
||||
### 2. 带HTTP服务的工作站
|
||||
|
||||
```python
|
||||
from unilabos.devices.workstation.workstation_with_http_example import WorkstationWithHTTP
|
||||
|
||||
# HTTP服务配置
|
||||
http_service_config = {
|
||||
"enabled": True,
|
||||
"host": "127.0.0.1",
|
||||
"port": 8081
|
||||
}
|
||||
|
||||
# 创建带HTTP服务的工作站
|
||||
workstation = WorkstationWithHTTP(
|
||||
device_id="http_workstation",
|
||||
deck_config=deck_config,
|
||||
children_config=children_config,
|
||||
http_service_config=http_service_config
|
||||
)
|
||||
|
||||
# 执行工作流
|
||||
success = workstation.execute_workflow("liquid_transfer", {
|
||||
"volume": 100.0,
|
||||
"source_wells": ["A1", "A2"],
|
||||
"dest_wells": ["B1", "B2"]
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 外部系统集成
|
||||
|
||||
```python
|
||||
class BioyongResourceSynchronizer(ResourceSynchronizer):
|
||||
"""Bioyong系统同步器"""
|
||||
|
||||
async def sync_from_external(self) -> bool:
|
||||
# 从Bioyong API获取物料
|
||||
external_materials = await self._fetch_bioyong_materials()
|
||||
|
||||
# 转换并添加到本地deck
|
||||
for material in external_materials:
|
||||
await self._add_material_to_deck(material)
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## 设计优势
|
||||
|
||||
### 1. **简洁性**
|
||||
- 基类只专注核心物料管理,没有冗余功能
|
||||
- 使用成熟的PyLabRobot作为物料管理基础
|
||||
|
||||
### 2. **可扩展性**
|
||||
- 通过子类添加HTTP服务、特定工作流等功能
|
||||
- ResourceSynchronizer模式支持任意外部系统集成
|
||||
|
||||
### 3. **标准化**
|
||||
- PLR Deck提供标准的资源管理接口
|
||||
- Graphio转换函数确保格式一致性
|
||||
|
||||
### 4. **灵活性**
|
||||
- 可选择性使用HTTP服务和外部系统集成
|
||||
- 支持不同类型的工作站需求
|
||||
|
||||
## 发展历程
|
||||
|
||||
1. **初始设计**: 复杂的统一物料系统,包含HTTP服务和多种功能
|
||||
2. **PyLabRobot集成**: 引入PLR Deck管理,但保留了ResourceTracker复杂性
|
||||
3. **Graphio转换**: 使用graphio转换函数简化初始化
|
||||
4. **最终简化**: 专注核心PLR物料系统,HTTP服务移至子类
|
||||
|
||||
这个架构体现了"用PyLabRobot Deck来管理物料会更好;但是要做好和外部物料系统的对接"的设计理念,以及"现在我只需要在工作站创建的时候,整体使用deck_config和children,一起通过resource_ulab_to_plr转换为plr物料self.deck即可"的简化要求。
|
||||
0
unilabos/devices/workstation/__init__.py
Normal file
0
unilabos/devices/workstation/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Any, Dict, Optional
|
||||
from pylabrobot.resources import Resource as PLRResource
|
||||
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
|
||||
from unilabos.devices.workstation.workstation_base import ResourceSynchronizer, WorkstationBase
|
||||
|
||||
|
||||
class CoinCellAssemblyWorkstation(WorkstationBase):
|
||||
def __init__(
|
||||
self,
|
||||
device_id: str,
|
||||
deck_config: Dict[str, Any],
|
||||
children: Optional[Dict[str, Any]] = None,
|
||||
resource_synchronizer: Optional[ResourceSynchronizer] = None,
|
||||
host: str = "192.168.0.0",
|
||||
port: str = "",
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
device_id=device_id,
|
||||
deck_config=deck_config,
|
||||
children=children,
|
||||
resource_synchronizer=resource_synchronizer,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.hardware_interface = ModbusTcpClient(host=host, port=port)
|
||||
|
||||
def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"):
|
||||
"""启动工作流"""
|
||||
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||
logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}")
|
||||
|
||||
# TODO: 实现工作流逻辑
|
||||
|
||||
anode_sheet = self.deck.get_resource("anode_sheet")
|
||||
|
||||
|
||||
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._workstation_node:
|
||||
return self.workstation._workstation_node.call_device_method(
|
||||
self.device_id, 'execute_workflow', workflow_name, parameters
|
||||
)
|
||||
else:
|
||||
logger.error("代理模式需要workstation_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._workstation_node:
|
||||
return self.workstation._workstation_node.call_device_method(
|
||||
self.device_id, 'stop_workflow', emergency
|
||||
)
|
||||
else:
|
||||
logger.error("代理模式需要workstation_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"
|
||||
489
unilabos/devices/workstation/workstation_base.py
Normal file
489
unilabos/devices/workstation/workstation_base.py
Normal file
@@ -0,0 +1,489 @@
|
||||
"""
|
||||
工作站基类
|
||||
Workstation Base Class - 简化版
|
||||
|
||||
基于PLR Deck的简化工作站架构
|
||||
专注于核心物料系统和工作流管理
|
||||
"""
|
||||
|
||||
import collections
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional, Union
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from pylabrobot.resources import Deck, Plate, Resource as PLRResource
|
||||
|
||||
from pylabrobot.resources.coordinate import Coordinate
|
||||
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
|
||||
|
||||
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 WorkStationContainer(Plate):
|
||||
"""
|
||||
WorkStation 专用 Container 类,继承自 Plate和TipRack
|
||||
注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
size_x: float,
|
||||
size_y: float,
|
||||
size_z: float,
|
||||
category: str,
|
||||
ordering: collections.OrderedDict,
|
||||
model: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
这里的初始化入参要和plr的保持一致
|
||||
"""
|
||||
super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model)
|
||||
self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的
|
||||
|
||||
def load_state(self, state: Dict[str, Any]) -> None:
|
||||
"""从给定的状态加载工作台信息。"""
|
||||
super().load_state(state)
|
||||
self._unilabos_state = state
|
||||
|
||||
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
|
||||
data = super().serialize_state()
|
||||
data.update(
|
||||
self._unilabos_state
|
||||
) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等)
|
||||
return data
|
||||
|
||||
|
||||
def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法
|
||||
"""
|
||||
用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml
|
||||
可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错
|
||||
:param name: 资源名称
|
||||
:return: Resource对象
|
||||
"""
|
||||
plate = WorkStationContainer(
|
||||
name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict()
|
||||
)
|
||||
tip_rack = WorkStationContainer(
|
||||
"tip_rack_inside_plate",
|
||||
size_x=50,
|
||||
size_y=50,
|
||||
size_z=10,
|
||||
category="tip_rack",
|
||||
ordering=collections.OrderedDict(),
|
||||
)
|
||||
plate.assign_child_resource(tip_rack, Coordinate.zero())
|
||||
return plate
|
||||
|
||||
|
||||
class ResourceSynchronizer(ABC):
|
||||
"""资源同步器基类
|
||||
|
||||
负责与外部物料系统的同步,并对 self.deck 做修改
|
||||
"""
|
||||
|
||||
def __init__(self, workstation: "WorkstationBase"):
|
||||
self.workstation = workstation
|
||||
|
||||
@abstractmethod
|
||||
async def sync_from_external(self) -> bool:
|
||||
"""从外部系统同步物料到本地deck"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def sync_to_external(self, plr_resource: PLRResource) -> bool:
|
||||
"""将本地物料同步到外部系统"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
|
||||
"""处理外部系统的变更通知"""
|
||||
pass
|
||||
|
||||
|
||||
class WorkstationBase(ABC):
|
||||
"""工作站基类 - 简化版
|
||||
|
||||
核心功能:
|
||||
1. 基于 PLR Deck 的物料系统,支持格式转换
|
||||
2. 可选的资源同步器支持外部物料系统
|
||||
3. 简化的工作流管理
|
||||
"""
|
||||
|
||||
_ros_node: ROS2WorkstationNode
|
||||
|
||||
@property
|
||||
def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别
|
||||
return self._ros_node.children
|
||||
|
||||
async def update_resource_example(self):
|
||||
return await self._ros_node.update_resource([get_workstation_plate_resource("test")])
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
station_resource: PLRResource,
|
||||
*args,
|
||||
**kwargs, # 必须有kwargs
|
||||
):
|
||||
# 基本配置
|
||||
print(station_resource)
|
||||
self.deck_config = station_resource
|
||||
|
||||
# PLR 物料系统
|
||||
self.deck: Optional[Deck] = None
|
||||
self.plr_resources: Dict[str, PLRResource] = {}
|
||||
|
||||
# 资源同步器(可选)
|
||||
self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用
|
||||
|
||||
# 硬件接口
|
||||
self.hardware_interface: Union[Any, str] = None
|
||||
|
||||
# 工作流状态
|
||||
self.current_workflow_status = WorkflowStatus.IDLE
|
||||
self.current_workflow_info = None
|
||||
self.workflow_start_time = None
|
||||
self.workflow_parameters = {}
|
||||
|
||||
# 支持的工作流(静态预定义)
|
||||
self.supported_workflows: Dict[str, WorkflowInfo] = {}
|
||||
|
||||
# 初始化物料系统
|
||||
self._initialize_material_system()
|
||||
|
||||
# 注册支持的工作流
|
||||
self._register_supported_workflows()
|
||||
|
||||
logger.info(f"工作站 {device_id} 初始化完成(简化版)")
|
||||
|
||||
def _initialize_material_system(self):
|
||||
"""初始化物料系统 - 使用 graphio 转换"""
|
||||
try:
|
||||
from unilabos.resources.graphio import resource_ulab_to_plr
|
||||
|
||||
# 1. 合并 deck_config 和 children 创建完整的资源树
|
||||
complete_resource_config = self._create_complete_resource_config()
|
||||
|
||||
# 2. 使用 graphio 转换为 PLR 资源
|
||||
self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True)
|
||||
|
||||
# 3. 建立资源映射
|
||||
self._build_resource_mappings(self.deck)
|
||||
|
||||
# 4. 如果有资源同步器,执行初始同步
|
||||
if self.resource_synchronizer:
|
||||
# 这里可以异步执行,暂时跳过
|
||||
pass
|
||||
|
||||
logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def _create_complete_resource_config(self) -> Dict[str, Any]:
|
||||
"""创建完整的资源配置 - 合并 deck_config 和 children"""
|
||||
# 创建主 deck 配置
|
||||
deck_resource = {
|
||||
"id": f"{self.device_id}_deck",
|
||||
"name": f"{self.device_id}_deck",
|
||||
"type": "deck",
|
||||
"position": {"x": 0, "y": 0, "z": 0},
|
||||
"config": {
|
||||
"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", 100.0),
|
||||
**{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]},
|
||||
},
|
||||
"data": {},
|
||||
"children": [],
|
||||
"parent": None,
|
||||
}
|
||||
|
||||
# 添加子资源
|
||||
if self._children:
|
||||
children_list = []
|
||||
for child_id, child_config in self._children.items():
|
||||
child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"])
|
||||
children_list.append(child_resource)
|
||||
deck_resource["children"] = children_list
|
||||
|
||||
return deck_resource
|
||||
|
||||
def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]:
|
||||
"""标准化子资源配置"""
|
||||
return {
|
||||
"id": resource_id,
|
||||
"name": config.get("name", resource_id),
|
||||
"type": config.get("type", "container"),
|
||||
"position": self._normalize_position(config.get("position", {})),
|
||||
"config": config.get("config", {}),
|
||||
"data": config.get("data", {}),
|
||||
"children": [], # 简化版本:只支持一层子资源
|
||||
"parent": parent_id,
|
||||
}
|
||||
|
||||
def _normalize_position(self, position: Any) -> Dict[str, float]:
|
||||
"""标准化位置信息"""
|
||||
if isinstance(position, dict):
|
||||
return {
|
||||
"x": float(position.get("x", 0)),
|
||||
"y": float(position.get("y", 0)),
|
||||
"z": float(position.get("z", 0)),
|
||||
}
|
||||
elif isinstance(position, (list, tuple)) and len(position) >= 2:
|
||||
return {
|
||||
"x": float(position[0]),
|
||||
"y": float(position[1]),
|
||||
"z": float(position[2]) if len(position) > 2 else 0.0,
|
||||
}
|
||||
else:
|
||||
return {"x": 0.0, "y": 0.0, "z": 0.0}
|
||||
|
||||
def _build_resource_mappings(self, deck: Deck):
|
||||
"""递归构建资源映射"""
|
||||
|
||||
def add_resource_recursive(resource: PLRResource):
|
||||
if hasattr(resource, "name"):
|
||||
self.plr_resources[resource.name] = resource
|
||||
|
||||
if hasattr(resource, "children"):
|
||||
for child in resource.children:
|
||||
add_resource_recursive(child)
|
||||
|
||||
add_resource_recursive(deck)
|
||||
|
||||
# ============ 硬件接口管理 ============
|
||||
|
||||
def set_hardware_interface(self, hardware_interface: Union[Any, str]):
|
||||
"""设置硬件接口"""
|
||||
self.hardware_interface = hardware_interface
|
||||
logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}")
|
||||
|
||||
def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"):
|
||||
"""设置协议节点引用(用于代理模式)"""
|
||||
self._ros_node = workstation_node
|
||||
logger.info(f"工作站 {self.device_id} 关联协议节点")
|
||||
|
||||
# ============ 设备操作接口 ============
|
||||
|
||||
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._ros_node:
|
||||
raise RuntimeError("代理模式需要设置workstation_node")
|
||||
|
||||
device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀
|
||||
return self._ros_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 get_deck(self) -> Deck:
|
||||
"""获取主 Deck"""
|
||||
return self.deck
|
||||
|
||||
def get_all_resources(self) -> Dict[str, PLRResource]:
|
||||
"""获取所有 PLR 资源"""
|
||||
return self.plr_resources.copy()
|
||||
|
||||
def find_resource_by_name(self, name: str) -> Optional[PLRResource]:
|
||||
"""按名称查找资源"""
|
||||
return self.plr_resources.get(name)
|
||||
|
||||
def find_resources_by_type(self, resource_type: type) -> List[PLRResource]:
|
||||
"""按类型查找资源"""
|
||||
return [res for res in self.plr_resources.values() if isinstance(res, resource_type)]
|
||||
|
||||
async def sync_with_external_system(self) -> bool:
|
||||
"""与外部物料系统同步"""
|
||||
if not self.resource_synchronizer:
|
||||
logger.info(f"工作站 {self.device_id} 没有配置资源同步器")
|
||||
return True
|
||||
|
||||
try:
|
||||
success = await self.resource_synchronizer.sync_from_external()
|
||||
if success:
|
||||
logger.info(f"工作站 {self.device_id} 外部同步成功")
|
||||
else:
|
||||
logger.warning(f"工作站 {self.device_id} 外部同步失败")
|
||||
return success
|
||||
except Exception as e:
|
||||
logger.error(f"工作站 {self.device_id} 外部同步异常: {e}")
|
||||
return False
|
||||
|
||||
# ============ 简化的工作流控制 ============
|
||||
|
||||
def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行工作流"""
|
||||
try:
|
||||
# 设置工作流状态
|
||||
self.current_workflow_status = WorkflowStatus.INITIALIZING
|
||||
self.workflow_parameters = parameters
|
||||
self.workflow_start_time = time.time()
|
||||
|
||||
# 委托给子类实现
|
||||
success = self._execute_workflow_impl(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 stop_workflow(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流"""
|
||||
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._stop_workflow_impl(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
|
||||
|
||||
# ============ 抽象方法 - 子类必须实现 ============
|
||||
|
||||
@abstractmethod
|
||||
def _register_supported_workflows(self):
|
||||
"""注册支持的工作流 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行工作流的具体实现 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流的具体实现 - 子类必须实现"""
|
||||
pass
|
||||
|
||||
class WorkstationExample(WorkstationBase):
|
||||
"""工作站示例实现"""
|
||||
|
||||
def _register_supported_workflows(self):
|
||||
"""注册支持的工作流"""
|
||||
self.supported_workflows["example_workflow"] = WorkflowInfo(
|
||||
name="example_workflow",
|
||||
description="这是一个示例工作流",
|
||||
estimated_duration=300.0,
|
||||
required_materials=["sample_plate"],
|
||||
output_product="processed_plate",
|
||||
parameters_schema={"param1": "string", "param2": "integer"},
|
||||
)
|
||||
|
||||
def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool:
|
||||
"""执行工作流的具体实现"""
|
||||
if workflow_name not in self.supported_workflows:
|
||||
logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}")
|
||||
return False
|
||||
|
||||
# 这里添加实际的工作流逻辑
|
||||
logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}")
|
||||
return True
|
||||
|
||||
def _stop_workflow_impl(self, emergency: bool = False) -> bool:
|
||||
"""停止工作流的具体实现"""
|
||||
# 这里添加实际的停止逻辑
|
||||
logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})")
|
||||
return True
|
||||
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