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:
Xuwznln
2025-09-19 19:52:53 +08:00
35 changed files with 5734 additions and 110 deletions

View 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即可"的简化要求。

View File

View 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")

View 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"

View 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进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
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

View 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 # 物料IdGUID
locationId: str # 库位IdGUID
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'
]

View 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)