refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

This commit is contained in:
Junhan Chang
2025-08-22 06:43:43 +08:00
parent 14bc2e6cda
commit ae3c1100ae
7 changed files with 561 additions and 2637 deletions

File diff suppressed because it is too large Load Diff

View File

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

View 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

View File

@@ -947,6 +947,12 @@ class ROS2DeviceNode:
# 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:
# 先对pylabrobot的子资源进行加载不然subclass无法认出
# 在下方对于加载Deck等Resource要手动import
@@ -956,10 +962,18 @@ class ROS2DeviceNode:
)
else:
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)
else:
self._use_protocol_node_ros = False
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
if driver_is_ros:
@@ -973,6 +987,35 @@ class ROS2DeviceNode:
# 创建ROS2节点
if driver_is_ros:
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:
self._ros_node = BaseROS2DeviceNode(
driver_instance=self._driver_instance,

View File

@@ -2,7 +2,7 @@ import json
import time
import traceback
from pprint import pprint, saferepr, pformat
from typing import Union
from typing import Union, Dict, Any
import rclpy
from rosidl_runtime_py import message_to_ordereddict
@@ -53,6 +53,10 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
self.children = children
self.workstation_config = workstation_config or {} # 新增:保存工作站配置
self.communication_interfaces = self.workstation_config.get('communication_interfaces', {}) # 从工作站配置获取通信接口
# 新增:获取工作站实例(如果存在)
self.workstation_instance = self.workstation_config.get('workstation_instance')
self._busy = False
self.sub_devices = {}
self._goals = {}
@@ -60,8 +64,11 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
self._action_clients = {}
# 初始化基类,让基类处理常规动作
# 如果有工作站实例使用工作站实例作为driver_instance
driver_instance = self.workstation_instance if self.workstation_instance else self
super().__init__(
driver_instance=self,
driver_instance=driver_instance,
device_id=device_id,
status_types={},
action_value_mappings=self.protocol_action_mappings,
@@ -77,8 +84,56 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
# 设置硬件接口代理
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}")
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):
"""初始化子设备 - 重构为更清晰的方法"""
# 设备分类字典 - 统一管理