mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-17 13:01:12 +00:00
refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,600 +0,0 @@
|
|||||||
"""
|
|
||||||
工作站通信基类
|
|
||||||
Workstation Communication Base Class
|
|
||||||
|
|
||||||
从具体设备驱动中抽取通用通信模式
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
from typing import Dict, Any, Optional, Callable, Union, List
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
from unilabos.device_comms.modbus_plc.client import TCPClient as ModbusTCPClient
|
|
||||||
from unilabos.device_comms.modbus_plc.node.modbus import DataType, WorderOrder
|
|
||||||
from unilabos.utils.log import logger
|
|
||||||
|
|
||||||
|
|
||||||
class CommunicationProtocol(Enum):
|
|
||||||
"""通信协议类型"""
|
|
||||||
MODBUS_TCP = "modbus_tcp"
|
|
||||||
MODBUS_RTU = "modbus_rtu"
|
|
||||||
SERIAL = "serial"
|
|
||||||
ETHERNET = "ethernet"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class CommunicationConfig:
|
|
||||||
"""通信配置"""
|
|
||||||
protocol: CommunicationProtocol
|
|
||||||
host: str
|
|
||||||
port: int
|
|
||||||
timeout: float = 5.0
|
|
||||||
retry_count: int = 3
|
|
||||||
extra_params: Dict[str, Any] = None
|
|
||||||
|
|
||||||
|
|
||||||
class WorkstationCommunicationBase(ABC):
|
|
||||||
"""工作站通信基类
|
|
||||||
|
|
||||||
定义工作站通信的标准接口:
|
|
||||||
1. 状态查询 - 定期获取设备状态
|
|
||||||
2. 命令下发 - 发送控制指令
|
|
||||||
3. 数据采集 - 收集生产数据
|
|
||||||
4. 紧急控制 - 单点调试控制
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, communication_config: CommunicationConfig):
|
|
||||||
self.config = communication_config
|
|
||||||
self.client = None
|
|
||||||
self.is_connected = False
|
|
||||||
self.last_status = {}
|
|
||||||
self.data_export_thread = None
|
|
||||||
self.data_export_running = False
|
|
||||||
|
|
||||||
# 状态缓存
|
|
||||||
self._status_cache = {}
|
|
||||||
self._last_update_time = 0
|
|
||||||
self._cache_timeout = 1.0 # 缓存1秒
|
|
||||||
|
|
||||||
self._initialize_communication()
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _initialize_communication(self):
|
|
||||||
"""初始化通信连接"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _load_address_mapping(self) -> Dict[str, Any]:
|
|
||||||
"""加载地址映射表"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def connect(self) -> bool:
|
|
||||||
"""建立连接"""
|
|
||||||
try:
|
|
||||||
if self.config.protocol == CommunicationProtocol.MODBUS_TCP:
|
|
||||||
self.client = ModbusTCPClient(
|
|
||||||
addr=self.config.host,
|
|
||||||
port=self.config.port
|
|
||||||
)
|
|
||||||
self.client.client.connect()
|
|
||||||
|
|
||||||
# 等待连接建立
|
|
||||||
count = 100
|
|
||||||
while count > 0:
|
|
||||||
count -= 1
|
|
||||||
if self.client.client.is_socket_open():
|
|
||||||
self.is_connected = True
|
|
||||||
logger.info(f"工作站通信连接成功: {self.config.host}:{self.config.port}")
|
|
||||||
return True
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
if not self.client.client.is_socket_open():
|
|
||||||
raise ConnectionError(f"无法连接到工作站: {self.config.host}:{self.config.port}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(f"协议 {self.config.protocol} 暂未实现")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"工作站通信连接失败: {e}")
|
|
||||||
self.is_connected = False
|
|
||||||
return False
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
"""断开连接"""
|
|
||||||
try:
|
|
||||||
if self.client and hasattr(self.client, 'client'):
|
|
||||||
self.client.client.close()
|
|
||||||
self.is_connected = False
|
|
||||||
logger.info("工作站通信连接已断开")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"断开连接时出错: {e}")
|
|
||||||
|
|
||||||
# ============ 标准工作流接口 ============
|
|
||||||
|
|
||||||
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
|
||||||
"""启动工作流"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
logger.error("通信未连接,无法启动工作流")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"启动工作流: {workflow_type}, 参数: {parameters}")
|
|
||||||
return self._execute_start_workflow(workflow_type, parameters or {})
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stop_workflow(self, emergency: bool = False) -> bool:
|
|
||||||
"""停止工作流"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
logger.error("通信未连接,无法停止工作流")
|
|
||||||
return False
|
|
||||||
|
|
||||||
logger.info(f"停止工作流 (紧急: {emergency})")
|
|
||||||
return self._execute_stop_workflow(emergency)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"停止工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_workflow_status(self) -> Dict[str, Any]:
|
|
||||||
"""获取工作流状态"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
return {"error": "通信未连接"}
|
|
||||||
|
|
||||||
return self._query_workflow_status()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询工作流状态失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
# ============ 设备状态查询接口 ============
|
|
||||||
|
|
||||||
def get_device_status(self, force_refresh: bool = False) -> Dict[str, Any]:
|
|
||||||
"""获取设备状态(带缓存)"""
|
|
||||||
current_time = time.time()
|
|
||||||
|
|
||||||
if not force_refresh and (current_time - self._last_update_time) < self._cache_timeout:
|
|
||||||
return self._status_cache
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
return {"error": "通信未连接"}
|
|
||||||
|
|
||||||
status = self._query_device_status()
|
|
||||||
self._status_cache = status
|
|
||||||
self._last_update_time = current_time
|
|
||||||
return status
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询设备状态失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
def get_production_data(self) -> Dict[str, Any]:
|
|
||||||
"""获取生产数据"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
return {"error": "通信未连接"}
|
|
||||||
|
|
||||||
return self._query_production_data()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询生产数据失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
# ============ 单点控制接口(调试用)============
|
|
||||||
|
|
||||||
def write_register(self, register_name: str, value: Any, data_type: DataType = None, word_order: WorderOrder = None) -> bool:
|
|
||||||
"""写寄存器(单点控制)"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
logger.error("通信未连接,无法写寄存器")
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self._write_single_register(register_name, value, data_type, word_order)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"写寄存器失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def read_register(self, register_name: str, count: int = 1, data_type: DataType = None, word_order: WorderOrder = None) -> tuple:
|
|
||||||
"""读寄存器(单点控制)"""
|
|
||||||
try:
|
|
||||||
if not self.is_connected:
|
|
||||||
logger.error("通信未连接,无法读寄存器")
|
|
||||||
return None, True
|
|
||||||
|
|
||||||
return self._read_single_register(register_name, count, data_type, word_order)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"读寄存器失败: {e}")
|
|
||||||
return None, True
|
|
||||||
|
|
||||||
# ============ 数据导出功能 ============
|
|
||||||
|
|
||||||
def start_data_export(self, file_path: str, export_interval: float = 1.0) -> bool:
|
|
||||||
"""开始数据导出"""
|
|
||||||
try:
|
|
||||||
if self.data_export_running:
|
|
||||||
logger.warning("数据导出已在运行")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.data_export_file = file_path
|
|
||||||
self.data_export_interval = export_interval
|
|
||||||
self.data_export_running = True
|
|
||||||
|
|
||||||
# 创建CSV文件并写入表头
|
|
||||||
self._initialize_export_file(file_path)
|
|
||||||
|
|
||||||
# 启动数据收集线程
|
|
||||||
self.data_export_thread = threading.Thread(target=self._data_export_worker)
|
|
||||||
self.data_export_thread.daemon = True
|
|
||||||
self.data_export_thread.start()
|
|
||||||
|
|
||||||
logger.info(f"数据导出已启动: {file_path}")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动数据导出失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def stop_data_export(self) -> bool:
|
|
||||||
"""停止数据导出"""
|
|
||||||
try:
|
|
||||||
if not self.data_export_running:
|
|
||||||
logger.warning("数据导出未运行")
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.data_export_running = False
|
|
||||||
|
|
||||||
if self.data_export_thread and self.data_export_thread.is_alive():
|
|
||||||
self.data_export_thread.join(timeout=5.0)
|
|
||||||
|
|
||||||
logger.info("数据导出已停止")
|
|
||||||
return True
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"停止数据导出失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _data_export_worker(self):
|
|
||||||
"""数据导出工作线程"""
|
|
||||||
while self.data_export_running:
|
|
||||||
try:
|
|
||||||
data = self.get_production_data()
|
|
||||||
self._append_to_export_file(data)
|
|
||||||
time.sleep(self.data_export_interval)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"数据导出工作线程错误: {e}")
|
|
||||||
|
|
||||||
# ============ 抽象方法 - 子类必须实现 ============
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _execute_start_workflow(self, workflow_type: str, parameters: Dict[str, Any]) -> bool:
|
|
||||||
"""执行启动工作流命令"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _execute_stop_workflow(self, emergency: bool) -> bool:
|
|
||||||
"""执行停止工作流命令"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _query_workflow_status(self) -> Dict[str, Any]:
|
|
||||||
"""查询工作流状态"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _query_device_status(self) -> Dict[str, Any]:
|
|
||||||
"""查询设备状态"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _query_production_data(self) -> Dict[str, Any]:
|
|
||||||
"""查询生产数据"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _write_single_register(self, register_name: str, value: Any, data_type: DataType, word_order: WorderOrder) -> bool:
|
|
||||||
"""写单个寄存器"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _read_single_register(self, register_name: str, count: int, data_type: DataType, word_order: WorderOrder) -> tuple:
|
|
||||||
"""读单个寄存器"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _initialize_export_file(self, file_path: str):
|
|
||||||
"""初始化导出文件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def _append_to_export_file(self, data: Dict[str, Any]):
|
|
||||||
"""追加数据到导出文件"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CoinCellCommunication(WorkstationCommunicationBase):
|
|
||||||
"""纽扣电池组装系统通信类
|
|
||||||
|
|
||||||
从 coin_cell_assembly_system 抽取的通信功能
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, communication_config: CommunicationConfig, csv_path: str = "./coin_cell_assembly.csv"):
|
|
||||||
self.csv_path = csv_path
|
|
||||||
super().__init__(communication_config)
|
|
||||||
|
|
||||||
def _initialize_communication(self):
|
|
||||||
"""初始化通信连接"""
|
|
||||||
# 加载节点映射
|
|
||||||
try:
|
|
||||||
nodes = self.client.load_csv(self.csv_path) if self.client else []
|
|
||||||
if self.client:
|
|
||||||
self.client.register_node_list(nodes)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"加载节点映射失败: {e}")
|
|
||||||
|
|
||||||
def _load_address_mapping(self) -> Dict[str, Any]:
|
|
||||||
"""加载地址映射表"""
|
|
||||||
# 从CSV文件加载地址映射
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def _execute_start_workflow(self, workflow_type: str, parameters: Dict[str, Any]) -> bool:
|
|
||||||
"""执行启动工作流命令"""
|
|
||||||
if workflow_type == "battery_manufacturing":
|
|
||||||
# 发送电池制造启动命令
|
|
||||||
return self._start_battery_manufacturing(parameters)
|
|
||||||
else:
|
|
||||||
logger.error(f"不支持的工作流类型: {workflow_type}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _start_battery_manufacturing(self, parameters: Dict[str, Any]) -> bool:
|
|
||||||
"""启动电池制造工作流"""
|
|
||||||
try:
|
|
||||||
# 1. 设置参数
|
|
||||||
if "electrolyte_num" in parameters:
|
|
||||||
self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(parameters["electrolyte_num"])
|
|
||||||
|
|
||||||
if "electrolyte_volume" in parameters:
|
|
||||||
self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').write(
|
|
||||||
parameters["electrolyte_volume"],
|
|
||||||
data_type=DataType.FLOAT32,
|
|
||||||
word_order=WorderOrder.LITTLE
|
|
||||||
)
|
|
||||||
|
|
||||||
if "assembly_pressure" in parameters:
|
|
||||||
self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(
|
|
||||||
parameters["assembly_pressure"],
|
|
||||||
data_type=DataType.FLOAT32,
|
|
||||||
word_order=WorderOrder.LITTLE
|
|
||||||
)
|
|
||||||
|
|
||||||
# 2. 发送启动命令
|
|
||||||
self.client.use_node('COIL_SYS_START_CMD').write(True)
|
|
||||||
|
|
||||||
# 3. 确认启动成功
|
|
||||||
time.sleep(0.5)
|
|
||||||
status, read_err = self.client.use_node('COIL_SYS_START_STATUS').read(1)
|
|
||||||
return not read_err and status[0]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"启动电池制造工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _execute_stop_workflow(self, emergency: bool) -> bool:
|
|
||||||
"""执行停止工作流命令"""
|
|
||||||
try:
|
|
||||||
if emergency:
|
|
||||||
# 紧急停止
|
|
||||||
self.client.use_node('COIL_SYS_RESET_CMD').write(True)
|
|
||||||
else:
|
|
||||||
# 正常停止
|
|
||||||
self.client.use_node('COIL_SYS_STOP_CMD').write(True)
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
|
||||||
status, read_err = self.client.use_node('COIL_SYS_STOP_STATUS').read(1)
|
|
||||||
return not read_err and status[0]
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"停止工作流失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _query_workflow_status(self) -> Dict[str, Any]:
|
|
||||||
"""查询工作流状态"""
|
|
||||||
try:
|
|
||||||
status = {}
|
|
||||||
|
|
||||||
# 读取系统状态
|
|
||||||
start_status, _ = self.client.use_node('COIL_SYS_START_STATUS').read(1)
|
|
||||||
stop_status, _ = self.client.use_node('COIL_SYS_STOP_STATUS').read(1)
|
|
||||||
auto_status, _ = self.client.use_node('COIL_SYS_AUTO_STATUS').read(1)
|
|
||||||
init_status, _ = self.client.use_node('COIL_SYS_INIT_STATUS').read(1)
|
|
||||||
|
|
||||||
status.update({
|
|
||||||
"is_running": start_status[0] if start_status else False,
|
|
||||||
"is_stopped": stop_status[0] if stop_status else False,
|
|
||||||
"is_auto_mode": auto_status[0] if auto_status else False,
|
|
||||||
"is_initialized": init_status[0] if init_status else False,
|
|
||||||
})
|
|
||||||
|
|
||||||
return status
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询工作流状态失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
def _query_device_status(self) -> Dict[str, Any]:
|
|
||||||
"""查询设备状态"""
|
|
||||||
try:
|
|
||||||
status = {}
|
|
||||||
|
|
||||||
# 读取位置信息
|
|
||||||
x_pos, _ = self.client.use_node('REG_DATA_AXIS_X_POS').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
y_pos, _ = self.client.use_node('REG_DATA_AXIS_Y_POS').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
z_pos, _ = self.client.use_node('REG_DATA_AXIS_Z_POS').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
|
|
||||||
# 读取环境数据
|
|
||||||
pressure, _ = self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
o2_content, _ = self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
water_content, _ = self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
|
|
||||||
status.update({
|
|
||||||
"axis_position": {
|
|
||||||
"x": x_pos[0] if x_pos else 0.0,
|
|
||||||
"y": y_pos[0] if y_pos else 0.0,
|
|
||||||
"z": z_pos[0] if z_pos else 0.0,
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"glove_box_pressure": pressure[0] if pressure else 0.0,
|
|
||||||
"o2_content": o2_content[0] if o2_content else 0.0,
|
|
||||||
"water_content": water_content[0] if water_content else 0.0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return status
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询设备状态失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
def _query_production_data(self) -> Dict[str, Any]:
|
|
||||||
"""查询生产数据"""
|
|
||||||
try:
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# 读取生产统计
|
|
||||||
coin_cell_num, _ = self.client.use_node('REG_DATA_ASSEMBLY_COIN_CELL_NUM').read(1)
|
|
||||||
assembly_time, _ = self.client.use_node('REG_DATA_ASSEMBLY_TIME').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
voltage, _ = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(2, word_order=WorderOrder.LITTLE)
|
|
||||||
|
|
||||||
# 读取当前产品信息
|
|
||||||
coin_cell_code, _ = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(20) # 假设是字符串
|
|
||||||
electrolyte_code, _ = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(20)
|
|
||||||
|
|
||||||
data.update({
|
|
||||||
"production_count": coin_cell_num[0] if coin_cell_num else 0,
|
|
||||||
"assembly_time": assembly_time[0] if assembly_time else 0.0,
|
|
||||||
"open_circuit_voltage": voltage[0] if voltage else 0.0,
|
|
||||||
"current_battery_code": self._decode_string(coin_cell_code) if coin_cell_code else "",
|
|
||||||
"current_electrolyte_code": self._decode_string(electrolyte_code) if electrolyte_code else "",
|
|
||||||
"timestamp": time.time(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"查询生产数据失败: {e}")
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
def _write_single_register(self, register_name: str, value: Any, data_type: DataType = None, word_order: WorderOrder = None) -> bool:
|
|
||||||
"""写单个寄存器"""
|
|
||||||
try:
|
|
||||||
kwargs = {"value": value}
|
|
||||||
if data_type:
|
|
||||||
kwargs["data_type"] = data_type
|
|
||||||
if word_order:
|
|
||||||
kwargs["word_order"] = word_order
|
|
||||||
|
|
||||||
result = self.client.use_node(register_name).write(**kwargs)
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"写寄存器 {register_name} 失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _read_single_register(self, register_name: str, count: int = 1, data_type: DataType = None, word_order: WorderOrder = None) -> tuple:
|
|
||||||
"""读单个寄存器"""
|
|
||||||
try:
|
|
||||||
kwargs = {"count": count}
|
|
||||||
if data_type:
|
|
||||||
kwargs["data_type"] = data_type
|
|
||||||
if word_order:
|
|
||||||
kwargs["word_order"] = word_order
|
|
||||||
|
|
||||||
value, error = self.client.use_node(register_name).read(**kwargs)
|
|
||||||
return value, error
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"读寄存器 {register_name} 失败: {e}")
|
|
||||||
return None, True
|
|
||||||
|
|
||||||
def _initialize_export_file(self, file_path: str):
|
|
||||||
"""初始化导出文件"""
|
|
||||||
import csv
|
|
||||||
try:
|
|
||||||
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
|
|
||||||
fieldnames = [
|
|
||||||
'timestamp', 'production_count', 'assembly_time',
|
|
||||||
'open_circuit_voltage', 'battery_code', 'electrolyte_code',
|
|
||||||
'axis_x', 'axis_y', 'axis_z', 'glove_box_pressure',
|
|
||||||
'o2_content', 'water_content'
|
|
||||||
]
|
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
||||||
writer.writeheader()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"初始化导出文件失败: {e}")
|
|
||||||
|
|
||||||
def _append_to_export_file(self, data: Dict[str, Any]):
|
|
||||||
"""追加数据到导出文件"""
|
|
||||||
import csv
|
|
||||||
try:
|
|
||||||
with open(self.data_export_file, 'a', newline='', encoding='utf-8') as csvfile:
|
|
||||||
fieldnames = [
|
|
||||||
'timestamp', 'production_count', 'assembly_time',
|
|
||||||
'open_circuit_voltage', 'battery_code', 'electrolyte_code',
|
|
||||||
'axis_x', 'axis_y', 'axis_z', 'glove_box_pressure',
|
|
||||||
'o2_content', 'water_content'
|
|
||||||
]
|
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
||||||
|
|
||||||
row = {
|
|
||||||
'timestamp': data.get('timestamp', time.time()),
|
|
||||||
'production_count': data.get('production_count', 0),
|
|
||||||
'assembly_time': data.get('assembly_time', 0.0),
|
|
||||||
'open_circuit_voltage': data.get('open_circuit_voltage', 0.0),
|
|
||||||
'battery_code': data.get('current_battery_code', ''),
|
|
||||||
'electrolyte_code': data.get('current_electrolyte_code', ''),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 添加位置数据
|
|
||||||
axis_pos = data.get('axis_position', {})
|
|
||||||
row.update({
|
|
||||||
'axis_x': axis_pos.get('x', 0.0),
|
|
||||||
'axis_y': axis_pos.get('y', 0.0),
|
|
||||||
'axis_z': axis_pos.get('z', 0.0),
|
|
||||||
})
|
|
||||||
|
|
||||||
# 添加环境数据
|
|
||||||
env = data.get('environment', {})
|
|
||||||
row.update({
|
|
||||||
'glove_box_pressure': env.get('glove_box_pressure', 0.0),
|
|
||||||
'o2_content': env.get('o2_content', 0.0),
|
|
||||||
'water_content': env.get('water_content', 0.0),
|
|
||||||
})
|
|
||||||
|
|
||||||
writer.writerow(row)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"追加数据到导出文件失败: {e}")
|
|
||||||
|
|
||||||
def _decode_string(self, data_list: List[int]) -> str:
|
|
||||||
"""将寄存器数据解码为字符串"""
|
|
||||||
try:
|
|
||||||
# 假设每个寄存器包含2个字符(16位)
|
|
||||||
chars = []
|
|
||||||
for value in data_list:
|
|
||||||
if value == 0:
|
|
||||||
break
|
|
||||||
chars.append(chr(value & 0xFF))
|
|
||||||
if (value >> 8) & 0xFF != 0:
|
|
||||||
chars.append(chr((value >> 8) & 0xFF))
|
|
||||||
return ''.join(chars).rstrip('\x00')
|
|
||||||
except:
|
|
||||||
return ""
|
|
||||||
460
unilabos/devices/work_station/workstation_base.py
Normal file
460
unilabos/devices/work_station/workstation_base.py
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
"""
|
||||||
|
工作站基类
|
||||||
|
Workstation Base Class
|
||||||
|
|
||||||
|
集成通信、物料管理和工作流的工作站基类
|
||||||
|
融合子设备管理、动态工作流注册等高级功能
|
||||||
|
"""
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from typing import Dict, Any, List, Optional, Union, Callable
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from rclpy.action import ActionServer, ActionClient
|
||||||
|
from rclpy.action.server import ServerGoalHandle
|
||||||
|
from rclpy.callback_groups import ReentrantCallbackGroup
|
||||||
|
from rclpy.service import Service
|
||||||
|
from unilabos_msgs.srv import SerialCommand
|
||||||
|
from unilabos_msgs.msg import Resource
|
||||||
|
|
||||||
|
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
|
||||||
|
from unilabos.device_comms.workstation_material_management import MaterialManagementBase
|
||||||
|
from unilabos.device_comms.workstation_http_service import (
|
||||||
|
WorkstationHTTPService, WorkstationReportRequest, MaterialUsage
|
||||||
|
)
|
||||||
|
from unilabos.ros.msgs.message_converter import convert_to_ros_msg, convert_from_ros_msg
|
||||||
|
from unilabos.utils.log import logger
|
||||||
|
from unilabos.utils.type_check import serialize_result_info
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceType(Enum):
|
||||||
|
"""设备类型枚举"""
|
||||||
|
LOGICAL = "logical" # 逻辑设备
|
||||||
|
COMMUNICATION = "communication" # 通信设备 (modbus/opcua/serial)
|
||||||
|
PROTOCOL = "protocol" # 协议设备
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommunicationInterface:
|
||||||
|
"""通信接口配置"""
|
||||||
|
device_id: str # 通信设备ID
|
||||||
|
read_method: str # 读取方法名
|
||||||
|
write_method: str # 写入方法名
|
||||||
|
protocol_type: str # 协议类型 (modbus/opcua/serial)
|
||||||
|
config: Dict[str, Any] # 协议特定配置
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkflowStep:
|
||||||
|
"""工作流步骤定义"""
|
||||||
|
device_id: str
|
||||||
|
action_name: str
|
||||||
|
action_kwargs: Dict[str, Any]
|
||||||
|
depends_on: Optional[List[str]] = None # 依赖的步骤ID
|
||||||
|
step_id: Optional[str] = None
|
||||||
|
timeout: Optional[float] = None
|
||||||
|
retry_count: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WorkflowDefinition:
|
||||||
|
"""工作流定义"""
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
|
steps: List[WorkflowStep]
|
||||||
|
input_schema: Dict[str, Any]
|
||||||
|
output_schema: Dict[str, Any]
|
||||||
|
metadata: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
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] # 参数架构
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommunicationConfig:
|
||||||
|
"""通信配置"""
|
||||||
|
protocol: str
|
||||||
|
host: str
|
||||||
|
port: int
|
||||||
|
timeout: float = 5.0
|
||||||
|
retry_count: int = 3
|
||||||
|
extra_params: Dict[str, Any] = None
|
||||||
|
|
||||||
|
|
||||||
|
class WorkstationBase(ABC):
|
||||||
|
"""工作站基类
|
||||||
|
|
||||||
|
提供工作站的核心功能:
|
||||||
|
1. 物料管理 - 基于PyLabRobot的物料系统
|
||||||
|
2. 工作流控制 - 支持动态注册和静态预定义工作流
|
||||||
|
3. 状态监控 - 设备状态和生产数据监控
|
||||||
|
4. HTTP服务 - 接收外部报送和状态查询
|
||||||
|
|
||||||
|
注意:子设备管理和通信转发功能已移入ROS2ProtocolNode
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
device_id: str,
|
||||||
|
deck_config: Optional[Dict[str, Any]] = None,
|
||||||
|
http_service_config: Optional[Dict[str, Any]] = None, # HTTP服务配置
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
# 保存工作站基本配置
|
||||||
|
self.device_id = device_id
|
||||||
|
self.deck_config = deck_config or {"size_x": 1000.0, "size_y": 1000.0, "size_z": 500.0}
|
||||||
|
|
||||||
|
# HTTP服务配置 - 现在专门用于报送接收
|
||||||
|
self.http_service_config = http_service_config or {
|
||||||
|
"enabled": True,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 8081 # 默认使用8081端口作为报送接收服务
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误处理和动作追踪
|
||||||
|
self.current_action_context = None # 当前正在执行的动作上下文
|
||||||
|
self.error_history = [] # 错误历史记录
|
||||||
|
self.action_results = {} # 动作结果缓存
|
||||||
|
|
||||||
|
# 工作流状态 - 支持静态和动态工作流
|
||||||
|
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.registered_workflows: Dict[str, WorkflowDefinition] = {}
|
||||||
|
|
||||||
|
# 初始化工作站模块
|
||||||
|
self.material_management: MaterialManagementBase = self._create_material_management_module()
|
||||||
|
|
||||||
|
# 注册支持的工作流
|
||||||
|
self._register_supported_workflows()
|
||||||
|
|
||||||
|
# 启动HTTP报送接收服务
|
||||||
|
self.http_service = None
|
||||||
|
self._start_http_service()
|
||||||
|
|
||||||
|
logger.info(f"工作站基类 {device_id} 初始化完成")
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _create_material_management_module(self) -> MaterialManagementBase:
|
||||||
|
"""创建物料管理模块 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _register_supported_workflows(self):
|
||||||
|
"""注册支持的工作流 - 子类必须实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _create_workstation_services(self):
|
||||||
|
"""创建工作站ROS服务"""
|
||||||
|
def _start_http_service(self):
|
||||||
|
"""启动HTTP报送接收服务"""
|
||||||
|
if self.http_service_config.get("enabled", True):
|
||||||
|
try:
|
||||||
|
self.http_service = WorkstationHTTPService(
|
||||||
|
host=self.http_service_config.get("host", "127.0.0.1"),
|
||||||
|
port=self.http_service_config.get("port", 8081),
|
||||||
|
workstation_handler=self
|
||||||
|
)
|
||||||
|
logger.info(f"HTTP报送接收服务已启动: {self.http_service_config['host']}:{self.http_service_config['port']}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动HTTP报送接收服务失败: {e}")
|
||||||
|
else:
|
||||||
|
logger.info("HTTP报送接收服务已禁用")
|
||||||
|
|
||||||
|
def _stop_http_service(self):
|
||||||
|
"""停止HTTP报送接收服务"""
|
||||||
|
if self.http_service:
|
||||||
|
try:
|
||||||
|
self.http_service.stop()
|
||||||
|
logger.info("HTTP报送接收服务已停止")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||||
|
|
||||||
|
# ============ 核心业务方法 ============
|
||||||
|
|
||||||
|
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
|
||||||
|
"""启动工作流 - 业务逻辑层"""
|
||||||
|
try:
|
||||||
|
if self.current_workflow_status != WorkflowStatus.IDLE:
|
||||||
|
logger.warning(f"工作流 {workflow_type} 启动失败:当前状态为 {self.current_workflow_status}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 设置工作流状态
|
||||||
|
self.current_workflow_status = WorkflowStatus.INITIALIZING
|
||||||
|
self.workflow_parameters = parameters or {}
|
||||||
|
self.workflow_start_time = time.time()
|
||||||
|
|
||||||
|
# 执行具体的工作流启动逻辑
|
||||||
|
success = self._execute_start_workflow(workflow_type, parameters or {})
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.current_workflow_status = WorkflowStatus.RUNNING
|
||||||
|
logger.info(f"工作流 {workflow_type} 启动成功")
|
||||||
|
else:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作流 {workflow_type} 启动失败")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"启动工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def stop_workflow(self, emergency: bool = False) -> bool:
|
||||||
|
"""停止工作流 - 业务逻辑层"""
|
||||||
|
try:
|
||||||
|
if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]:
|
||||||
|
logger.warning("没有正在运行的工作流")
|
||||||
|
return True
|
||||||
|
|
||||||
|
self.current_workflow_status = WorkflowStatus.STOPPING
|
||||||
|
|
||||||
|
# 执行具体的工作流停止逻辑
|
||||||
|
success = self._execute_stop_workflow(emergency)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
self.current_workflow_status = WorkflowStatus.STOPPED
|
||||||
|
logger.info(f"工作流停止成功 (紧急: {emergency})")
|
||||||
|
else:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"工作流停止失败")
|
||||||
|
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.current_workflow_status = WorkflowStatus.ERROR
|
||||||
|
logger.error(f"停止工作流失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ============ 抽象方法 - 子类必须实现具体的工作流控制 ============
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _execute_start_workflow(self, workflow_type: str, parameters: Dict[str, Any]) -> bool:
|
||||||
|
"""执行启动工作流的具体逻辑 - 子类实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _execute_stop_workflow(self, emergency: bool) -> bool:
|
||||||
|
"""执行停止工作流的具体逻辑 - 子类实现"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ============ 状态属性 ============
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_status(self) -> WorkflowStatus:
|
||||||
|
"""获取当前工作流状态"""
|
||||||
|
return self.current_workflow_status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_busy(self) -> bool:
|
||||||
|
"""检查工作站是否忙碌"""
|
||||||
|
return self.current_workflow_status in [
|
||||||
|
WorkflowStatus.INITIALIZING,
|
||||||
|
WorkflowStatus.RUNNING,
|
||||||
|
WorkflowStatus.STOPPING
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def workflow_runtime(self) -> float:
|
||||||
|
"""获取工作流运行时间(秒)"""
|
||||||
|
if self.workflow_start_time is None:
|
||||||
|
return 0.0
|
||||||
|
return time.time() - self.workflow_start_time
|
||||||
|
|
||||||
|
@property
|
||||||
|
def error_count(self) -> int:
|
||||||
|
"""获取错误计数"""
|
||||||
|
return len(self.error_history)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_error(self) -> Optional[Dict[str, Any]]:
|
||||||
|
"""获取最后一个错误"""
|
||||||
|
return self.error_history[-1] if self.error_history else None
|
||||||
|
|
||||||
|
def _start_http_service(self):
|
||||||
|
"""启动HTTP报送接收服务"""
|
||||||
|
try:
|
||||||
|
if not self.http_service_config.get("enabled", True):
|
||||||
|
logger.info("HTTP报送接收服务已禁用")
|
||||||
|
return
|
||||||
|
|
||||||
|
host = self.http_service_config.get("host", "127.0.0.1")
|
||||||
|
port = self.http_service_config.get("port", 8081)
|
||||||
|
|
||||||
|
self.http_service = WorkstationHTTPService(
|
||||||
|
workstation_handler=self,
|
||||||
|
host=host,
|
||||||
|
port=port
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"工作站 {self.device_id} HTTP报送接收服务启动成功: {host}:{port}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"启动HTTP报送接收服务失败: {e}")
|
||||||
|
self.http_service = None
|
||||||
|
|
||||||
|
def _stop_http_service(self):
|
||||||
|
"""停止HTTP报送接收服务"""
|
||||||
|
try:
|
||||||
|
if self.http_service:
|
||||||
|
self.http_service.stop()
|
||||||
|
self.http_service = None
|
||||||
|
logger.info("HTTP报送接收服务已停止")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||||
|
logger.error(f"停止HTTP报送接收服务失败: {e}")
|
||||||
|
|
||||||
|
# ============ 报送处理方法 ============
|
||||||
|
|
||||||
|
# ============ 报送处理方法 ============
|
||||||
|
|
||||||
|
def process_material_change_report(self, report) -> Dict[str, Any]:
|
||||||
|
"""处理物料变更报送"""
|
||||||
|
try:
|
||||||
|
logger.info(f"处理物料变更报送: {report.workstation_id} -> {report.resource_id} ({report.change_type})")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'processed': True,
|
||||||
|
'resource_id': report.resource_id,
|
||||||
|
'change_type': report.change_type,
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 更新本地物料管理系统
|
||||||
|
if hasattr(self, 'material_management'):
|
||||||
|
try:
|
||||||
|
self.material_management.sync_external_material_change(report)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"同步物料变更到本地管理系统失败: {e}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理物料变更报送失败: {e}")
|
||||||
|
return {'processed': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def process_step_finish_report(self, request: WorkstationReportRequest) -> Dict[str, Any]:
|
||||||
|
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||||||
|
try:
|
||||||
|
data = request.data
|
||||||
|
logger.info(f"处理步骤完成报送: {data['orderCode']} - {data['stepName']}")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'processed': True,
|
||||||
|
'order_code': data['orderCode'],
|
||||||
|
'step_id': data['stepId'],
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理步骤完成报送失败: {e}")
|
||||||
|
return {'processed': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def process_sample_finish_report(self, request: WorkstationReportRequest) -> Dict[str, Any]:
|
||||||
|
"""处理样品完成报送"""
|
||||||
|
try:
|
||||||
|
data = request.data
|
||||||
|
logger.info(f"处理样品完成报送: {data['sampleId']}")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'processed': True,
|
||||||
|
'sample_id': data['sampleId'],
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理样品完成报送失败: {e}")
|
||||||
|
return {'processed': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def process_order_finish_report(self, request: WorkstationReportRequest, used_materials: List[MaterialUsage]) -> Dict[str, Any]:
|
||||||
|
"""处理订单完成报送"""
|
||||||
|
try:
|
||||||
|
data = request.data
|
||||||
|
logger.info(f"处理订单完成报送: {data['orderCode']}")
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'processed': True,
|
||||||
|
'order_code': data['orderCode'],
|
||||||
|
'used_materials': len(used_materials),
|
||||||
|
'timestamp': time.time()
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理订单完成报送失败: {e}")
|
||||||
|
return {'processed': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def handle_external_error(self, error_request):
|
||||||
|
"""处理外部错误报告"""
|
||||||
|
try:
|
||||||
|
logger.error(f"收到外部错误报告: {error_request}")
|
||||||
|
|
||||||
|
# 记录错误
|
||||||
|
error_record = {
|
||||||
|
'timestamp': time.time(),
|
||||||
|
'error_type': error_request.get('error_type', 'unknown'),
|
||||||
|
'error_message': error_request.get('message', ''),
|
||||||
|
'source': error_request.get('source', 'external'),
|
||||||
|
'context': error_request.get('context', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
self.error_history.append(error_record)
|
||||||
|
|
||||||
|
# 处理紧急停止情况
|
||||||
|
if error_request.get('emergency_stop', False):
|
||||||
|
self._trigger_emergency_stop(error_record['error_message'])
|
||||||
|
|
||||||
|
return {'processed': True, 'error_id': len(self.error_history)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"处理外部错误失败: {e}")
|
||||||
|
return {'processed': False, 'error': str(e)}
|
||||||
|
|
||||||
|
def _trigger_emergency_stop(self, reason: str):
|
||||||
|
"""触发紧急停止"""
|
||||||
|
logger.critical(f"触发紧急停止: {reason}")
|
||||||
|
self.stop_workflow(emergency=True)
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
"""清理资源"""
|
||||||
|
try:
|
||||||
|
self._stop_http_service()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
@@ -947,6 +947,12 @@ class ROS2DeviceNode:
|
|||||||
|
|
||||||
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
# TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建
|
||||||
# 创建设备类实例
|
# 创建设备类实例
|
||||||
|
# 判断是否包含设备子节点,决定是否使用ROS2ProtocolNode
|
||||||
|
has_device_children = any(
|
||||||
|
child_config.get("type", "device") == "device"
|
||||||
|
for child_config in children.values()
|
||||||
|
)
|
||||||
|
|
||||||
if use_pylabrobot_creator:
|
if use_pylabrobot_creator:
|
||||||
# 先对pylabrobot的子资源进行加载,不然subclass无法认出
|
# 先对pylabrobot的子资源进行加载,不然subclass无法认出
|
||||||
# 在下方对于加载Deck等Resource要手动import
|
# 在下方对于加载Deck等Resource要手动import
|
||||||
@@ -956,10 +962,18 @@ class ROS2DeviceNode:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||||
|
from unilabos.device_comms.workstation_base import WorkstationBase
|
||||||
|
|
||||||
if issubclass(self._driver_class, ROS2ProtocolNode): # 是ProtocolNode的子节点,就要调用ProtocolNodeCreator
|
# 检查是否是WorkstationBase的子类且包含设备子节点
|
||||||
|
if issubclass(self._driver_class, WorkstationBase) and has_device_children:
|
||||||
|
# WorkstationBase + 设备子节点 -> 使用ProtocolNode作为ros_instance
|
||||||
|
self._use_protocol_node_ros = True
|
||||||
|
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
|
elif issubclass(self._driver_class, ROS2ProtocolNode): # 是ProtocolNode的子节点,就要调用ProtocolNodeCreator
|
||||||
|
self._use_protocol_node_ros = False
|
||||||
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
else:
|
else:
|
||||||
|
self._use_protocol_node_ros = False
|
||||||
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
|
||||||
|
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
@@ -973,6 +987,35 @@ class ROS2DeviceNode:
|
|||||||
# 创建ROS2节点
|
# 创建ROS2节点
|
||||||
if driver_is_ros:
|
if driver_is_ros:
|
||||||
self._ros_node = self._driver_instance # type: ignore
|
self._ros_node = self._driver_instance # type: ignore
|
||||||
|
elif hasattr(self, '_use_protocol_node_ros') and self._use_protocol_node_ros:
|
||||||
|
# WorkstationBase + 设备子节点 -> 创建ROS2ProtocolNode作为ros_instance
|
||||||
|
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
|
||||||
|
|
||||||
|
# 从children提取设备协议类型
|
||||||
|
protocol_types = set()
|
||||||
|
for child_id, child_config in children.items():
|
||||||
|
if child_config.get("type", "device") == "device":
|
||||||
|
# 检查设备配置中的协议类型
|
||||||
|
if "protocol_type" in child_config:
|
||||||
|
if isinstance(child_config["protocol_type"], list):
|
||||||
|
protocol_types.update(child_config["protocol_type"])
|
||||||
|
else:
|
||||||
|
protocol_types.add(child_config["protocol_type"])
|
||||||
|
|
||||||
|
# 如果没有明确的协议类型,使用默认值
|
||||||
|
if not protocol_types:
|
||||||
|
protocol_types = ["default_protocol"]
|
||||||
|
|
||||||
|
self._ros_node = ROS2ProtocolNode(
|
||||||
|
device_id=device_id,
|
||||||
|
children=children,
|
||||||
|
protocol_type=list(protocol_types),
|
||||||
|
resource_tracker=self.resource_tracker,
|
||||||
|
workstation_config={
|
||||||
|
'workstation_instance': self._driver_instance,
|
||||||
|
'deck_config': getattr(self._driver_instance, 'deck_config', {}),
|
||||||
|
}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._ros_node = BaseROS2DeviceNode(
|
self._ros_node = BaseROS2DeviceNode(
|
||||||
driver_instance=self._driver_instance,
|
driver_instance=self._driver_instance,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import json
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from pprint import pprint, saferepr, pformat
|
from pprint import pprint, saferepr, pformat
|
||||||
from typing import Union
|
from typing import Union, Dict, Any
|
||||||
|
|
||||||
import rclpy
|
import rclpy
|
||||||
from rosidl_runtime_py import message_to_ordereddict
|
from rosidl_runtime_py import message_to_ordereddict
|
||||||
@@ -53,6 +53,10 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
|||||||
self.children = children
|
self.children = children
|
||||||
self.workstation_config = workstation_config or {} # 新增:保存工作站配置
|
self.workstation_config = workstation_config or {} # 新增:保存工作站配置
|
||||||
self.communication_interfaces = self.workstation_config.get('communication_interfaces', {}) # 从工作站配置获取通信接口
|
self.communication_interfaces = self.workstation_config.get('communication_interfaces', {}) # 从工作站配置获取通信接口
|
||||||
|
|
||||||
|
# 新增:获取工作站实例(如果存在)
|
||||||
|
self.workstation_instance = self.workstation_config.get('workstation_instance')
|
||||||
|
|
||||||
self._busy = False
|
self._busy = False
|
||||||
self.sub_devices = {}
|
self.sub_devices = {}
|
||||||
self._goals = {}
|
self._goals = {}
|
||||||
@@ -60,8 +64,11 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
|||||||
self._action_clients = {}
|
self._action_clients = {}
|
||||||
|
|
||||||
# 初始化基类,让基类处理常规动作
|
# 初始化基类,让基类处理常规动作
|
||||||
|
# 如果有工作站实例,使用工作站实例作为driver_instance
|
||||||
|
driver_instance = self.workstation_instance if self.workstation_instance else self
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
driver_instance=self,
|
driver_instance=driver_instance,
|
||||||
device_id=device_id,
|
device_id=device_id,
|
||||||
status_types={},
|
status_types={},
|
||||||
action_value_mappings=self.protocol_action_mappings,
|
action_value_mappings=self.protocol_action_mappings,
|
||||||
@@ -77,8 +84,56 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
|
|||||||
# 设置硬件接口代理
|
# 设置硬件接口代理
|
||||||
self._setup_hardware_proxies()
|
self._setup_hardware_proxies()
|
||||||
|
|
||||||
|
# 新增:如果有工作站实例,建立双向引用
|
||||||
|
if self.workstation_instance:
|
||||||
|
self.workstation_instance._protocol_node = self
|
||||||
|
self._setup_workstation_method_proxies()
|
||||||
|
self.lab_logger().info(f"ROS2ProtocolNode {device_id} 与工作站实例 {type(self.workstation_instance).__name__} 关联")
|
||||||
|
|
||||||
self.lab_logger().info(f"ROS2ProtocolNode {device_id} initialized with protocols: {self.protocol_names}")
|
self.lab_logger().info(f"ROS2ProtocolNode {device_id} initialized with protocols: {self.protocol_names}")
|
||||||
|
|
||||||
|
def _setup_workstation_method_proxies(self):
|
||||||
|
"""设置工作站方法代理"""
|
||||||
|
if not self.workstation_instance:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 代理工作站的核心方法
|
||||||
|
workstation_methods = [
|
||||||
|
'start_workflow', 'stop_workflow', 'workflow_status', 'is_busy',
|
||||||
|
'process_material_change_report', 'process_step_finish_report',
|
||||||
|
'process_sample_finish_report', 'process_order_finish_report',
|
||||||
|
'handle_external_error'
|
||||||
|
]
|
||||||
|
|
||||||
|
for method_name in workstation_methods:
|
||||||
|
if hasattr(self.workstation_instance, method_name):
|
||||||
|
# 创建代理方法
|
||||||
|
setattr(self, method_name, getattr(self.workstation_instance, method_name))
|
||||||
|
self.lab_logger().debug(f"代理工作站方法: {method_name}")
|
||||||
|
|
||||||
|
# ============ 工作站方法代理 ============
|
||||||
|
|
||||||
|
def get_workstation_status(self) -> Dict[str, Any]:
|
||||||
|
"""获取工作站状态"""
|
||||||
|
if self.workstation_instance:
|
||||||
|
return {
|
||||||
|
'workflow_status': str(self.workstation_instance.workflow_status.value),
|
||||||
|
'is_busy': self.workstation_instance.is_busy,
|
||||||
|
'workflow_runtime': self.workstation_instance.workflow_runtime,
|
||||||
|
'error_count': self.workstation_instance.error_count,
|
||||||
|
'last_error': self.workstation_instance.last_error
|
||||||
|
}
|
||||||
|
return {'status': 'no_workstation_instance'}
|
||||||
|
|
||||||
|
def delegate_to_workstation(self, method_name: str, *args, **kwargs):
|
||||||
|
"""委托方法调用给工作站实例"""
|
||||||
|
if self.workstation_instance and hasattr(self.workstation_instance, method_name):
|
||||||
|
method = getattr(self.workstation_instance, method_name)
|
||||||
|
return method(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
self.lab_logger().warning(f"工作站实例不存在或没有方法: {method_name}")
|
||||||
|
return None
|
||||||
|
|
||||||
def _initialize_child_devices(self):
|
def _initialize_child_devices(self):
|
||||||
"""初始化子设备 - 重构为更清晰的方法"""
|
"""初始化子设备 - 重构为更清晰的方法"""
|
||||||
# 设备分类字典 - 统一管理
|
# 设备分类字典 - 统一管理
|
||||||
|
|||||||
Reference in New Issue
Block a user