add workstation template and battery example

This commit is contained in:
Junhan Chang
2025-08-19 21:35:27 +08:00
parent 4b7bde6be5
commit 227ff1284a
4 changed files with 2939 additions and 0 deletions

View File

@@ -0,0 +1,454 @@
"""
纽扣电池组装工作站
Coin Cell Assembly Workstation
继承工作站基类,实现纽扣电池特定功能
"""
from typing import Dict, Any, List, Optional, Union
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
from unilabos.device_comms.workstation_base import WorkstationBase, WorkflowInfo
from unilabos.device_comms.workstation_communication import (
WorkstationCommunicationBase, CommunicationConfig, CommunicationProtocol, CoinCellCommunication
)
from unilabos.device_comms.workstation_material_management import (
MaterialManagementBase, CoinCellMaterialManagement
)
from unilabos.utils.log import logger
class CoinCellAssemblyWorkstation(WorkstationBase):
"""纽扣电池组装工作站
基于工作站基类,实现纽扣电池制造的特定功能:
1. 纽扣电池特定的通信协议
2. 纽扣电池物料管理(料板、极片、电池等)
3. 电池制造工作流
4. 质量检查工作流
"""
def __init__(
self,
device_id: str,
children: Dict[str, Dict[str, Any]],
protocol_type: Union[str, List[str]] = "BatteryManufacturingProtocol",
resource_tracker: Optional[DeviceNodeResourceTracker] = None,
modbus_config: Optional[Dict[str, Any]] = None,
deck_config: Optional[Dict[str, Any]] = None,
csv_path: str = "./coin_cell_assembly.csv",
*args,
**kwargs,
):
# 设置通信配置
modbus_config = modbus_config or {"host": "127.0.0.1", "port": 5021}
self.communication_config = CommunicationConfig(
protocol=CommunicationProtocol.MODBUS_TCP,
host=modbus_config["host"],
port=modbus_config["port"],
timeout=modbus_config.get("timeout", 5.0),
retry_count=modbus_config.get("retry_count", 3)
)
# 设置台面配置
self.deck_config = deck_config or {
"size_x": 1620.0,
"size_y": 1270.0,
"size_z": 500.0
}
# CSV地址映射文件路径
self.csv_path = csv_path
# 创建资源跟踪器(如果没有提供)
if resource_tracker is None:
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
resource_tracker = DeviceNodeResourceTracker()
# 初始化基类
super().__init__(
device_id=device_id,
children=children,
protocol_type=protocol_type,
resource_tracker=resource_tracker,
communication_config=self.communication_config,
deck_config=self.deck_config,
*args,
**kwargs
)
logger.info(f"纽扣电池组装工作站 {device_id} 初始化完成")
def _create_communication_module(self) -> WorkstationCommunicationBase:
"""创建纽扣电池通信模块"""
return CoinCellCommunication(
communication_config=self.communication_config,
csv_path=self.csv_path
)
def _create_material_management_module(self) -> MaterialManagementBase:
"""创建纽扣电池物料管理模块"""
return CoinCellMaterialManagement(
device_id=self.device_id,
deck_config=self.deck_config,
resource_tracker=self.resource_tracker,
children_config=self.children
)
def _register_supported_workflows(self):
"""注册纽扣电池工作流"""
# 电池制造工作流
self.supported_workflows["battery_manufacturing"] = WorkflowInfo(
name="battery_manufacturing",
description="纽扣电池制造工作流",
estimated_duration=300.0, # 5分钟
required_materials=["cathode_sheet", "anode_sheet", "separator", "electrolyte"],
output_product="coin_cell_battery",
parameters_schema={
"type": "object",
"properties": {
"electrolyte_num": {
"type": "integer",
"description": "电解液瓶数",
"minimum": 1,
"maximum": 32
},
"electrolyte_volume": {
"type": "number",
"description": "电解液体积 (μL)",
"minimum": 0.1,
"maximum": 100.0
},
"assembly_pressure": {
"type": "number",
"description": "组装压力 (N)",
"minimum": 100.0,
"maximum": 5000.0
},
"cathode_material": {
"type": "string",
"description": "正极材料类型",
"enum": ["LiFePO4", "LiCoO2", "NCM", "LMO"]
},
"anode_material": {
"type": "string",
"description": "负极材料类型",
"enum": ["Graphite", "LTO", "Silicon"]
}
},
"required": ["electrolyte_num", "electrolyte_volume", "assembly_pressure"]
}
)
# 质量检查工作流
self.supported_workflows["quality_inspection"] = WorkflowInfo(
name="quality_inspection",
description="产品质量检查工作流",
estimated_duration=60.0, # 1分钟
required_materials=["finished_battery"],
output_product="quality_report",
parameters_schema={
"type": "object",
"properties": {
"test_voltage": {
"type": "boolean",
"description": "是否测试电压",
"default": True
},
"test_capacity": {
"type": "boolean",
"description": "是否测试容量",
"default": False
},
"voltage_threshold": {
"type": "number",
"description": "电压阈值 (V)",
"minimum": 2.0,
"maximum": 4.5,
"default": 3.0
}
}
}
)
# 设备初始化工作流
self.supported_workflows["device_initialization"] = WorkflowInfo(
name="device_initialization",
description="设备初始化工作流",
estimated_duration=30.0, # 30秒
required_materials=[],
output_product="ready_status",
parameters_schema={
"type": "object",
"properties": {
"auto_mode": {
"type": "boolean",
"description": "是否启用自动模式",
"default": True
}
}
}
)
# ============ 纽扣电池特定方法 ============
def get_electrode_sheet_inventory(self) -> Dict[str, int]:
"""获取极片库存统计"""
try:
sheets = self.material_management.find_electrode_sheets()
inventory = {}
for sheet in sheets:
material_type = getattr(sheet, 'material_type', 'unknown')
inventory[material_type] = inventory.get(material_type, 0) + 1
return inventory
except Exception as e:
logger.error(f"获取极片库存失败: {e}")
return {}
def get_battery_production_statistics(self) -> Dict[str, Any]:
"""获取电池生产统计"""
try:
production_data = self.communication.get_production_data()
# 添加物料统计
electrode_inventory = self.get_electrode_sheet_inventory()
battery_count = len(self.material_management.find_batteries())
return {
**production_data,
"electrode_inventory": electrode_inventory,
"finished_battery_count": battery_count,
"material_plates": len(self.material_management.find_material_plates()),
"press_slots": len(self.material_management.find_press_slots())
}
except Exception as e:
logger.error(f"获取生产统计失败: {e}")
return {"error": str(e)}
def create_new_battery(self, battery_spec: Dict[str, Any]) -> Optional[str]:
"""创建新电池资源"""
try:
from unilabos.device_comms.button_battery_station import Battery
import uuid
battery_id = f"battery_{uuid.uuid4().hex[:8]}"
battery = Battery(
name=battery_id,
diameter=battery_spec.get("diameter", 20.0),
height=battery_spec.get("height", 3.2),
max_volume=battery_spec.get("max_volume", 100.0),
barcode=battery_spec.get("barcode", "")
)
# 添加到物料管理系统
self.material_management.plr_resources[battery_id] = battery
self.material_management.resource_tracker.add_resource(battery)
logger.info(f"创建新电池资源: {battery_id}")
return battery_id
except Exception as e:
logger.error(f"创建电池资源失败: {e}")
return None
def find_available_press_slot(self) -> Optional[str]:
"""查找可用的压制槽"""
try:
press_slots = self.material_management.find_press_slots()
for slot in press_slots:
if hasattr(slot, 'has_battery') and not slot.has_battery():
return slot.name
return None
except Exception as e:
logger.error(f"查找可用压制槽失败: {e}")
return None
def get_glove_box_environment(self) -> Dict[str, Any]:
"""获取手套箱环境数据"""
try:
device_status = self.communication.get_device_status()
environment = device_status.get("environment", {})
return {
"pressure": environment.get("glove_box_pressure", 0.0),
"o2_content": environment.get("o2_content", 0.0),
"water_content": environment.get("water_content", 0.0),
"is_safe": (
environment.get("o2_content", 0.0) < 10.0 and # 氧气含量 < 10ppm
environment.get("water_content", 0.0) < 1.0 # 水分含量 < 1ppm
)
}
except Exception as e:
logger.error(f"获取手套箱环境失败: {e}")
return {"error": str(e)}
def start_data_export(self, file_path: str) -> bool:
"""开始生产数据导出"""
try:
return self.communication.start_data_export(file_path, export_interval=5.0)
except Exception as e:
logger.error(f"启动数据导出失败: {e}")
return False
def stop_data_export(self) -> bool:
"""停止生产数据导出"""
try:
return self.communication.stop_data_export()
except Exception as e:
logger.error(f"停止数据导出失败: {e}")
return False
# ============ 重写基类方法以支持纽扣电池特定功能 ============
def start_workflow(self, workflow_type: str, parameters: Dict[str, Any] = None) -> bool:
"""启动工作流(重写以支持纽扣电池特定预处理)"""
try:
# 进行纽扣电池特定的预检查
if workflow_type == "battery_manufacturing":
# 检查手套箱环境
env = self.get_glove_box_environment()
if not env.get("is_safe", False):
logger.error("手套箱环境不安全,无法启动电池制造工作流")
return False
# 检查是否有可用的压制槽
available_slot = self.find_available_press_slot()
if not available_slot:
logger.error("没有可用的压制槽,无法启动电池制造工作流")
return False
# 检查极片库存
electrode_inventory = self.get_electrode_sheet_inventory()
if not electrode_inventory.get("cathode", 0) > 0 or not electrode_inventory.get("anode", 0) > 0:
logger.error("极片库存不足,无法启动电池制造工作流")
return False
# 调用基类方法
return super().start_workflow(workflow_type, parameters)
except Exception as e:
logger.error(f"启动纽扣电池工作流失败: {e}")
return False
# ============ 纽扣电池特定状态属性 ============
@property
def electrode_sheet_count(self) -> int:
"""极片总数"""
try:
return len(self.material_management.find_electrode_sheets())
except:
return 0
@property
def battery_count(self) -> int:
"""电池总数"""
try:
return len(self.material_management.find_batteries())
except:
return 0
@property
def available_press_slots(self) -> int:
"""可用压制槽数"""
try:
press_slots = self.material_management.find_press_slots()
available = 0
for slot in press_slots:
if hasattr(slot, 'has_battery') and not slot.has_battery():
available += 1
return available
except:
return 0
@property
def environment_status(self) -> Dict[str, Any]:
"""环境状态"""
return self.get_glove_box_environment()
# ============ 工厂函数 ============
def create_coin_cell_workstation(
device_id: str,
config_file: str,
modbus_host: str = "127.0.0.1",
modbus_port: int = 5021,
csv_path: str = "./coin_cell_assembly.csv"
) -> CoinCellAssemblyWorkstation:
"""工厂函数:创建纽扣电池组装工作站
Args:
device_id: 设备ID
config_file: 配置文件路径JSON格式
modbus_host: Modbus主机地址
modbus_port: Modbus端口
csv_path: 地址映射CSV文件路径
Returns:
CoinCellAssemblyWorkstation: 工作站实例
"""
import json
try:
# 加载配置文件
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 提取配置
children = config.get("children", {})
deck_config = config.get("deck_config", {})
# 创建工作站
workstation = CoinCellAssemblyWorkstation(
device_id=device_id,
children=children,
modbus_config={
"host": modbus_host,
"port": modbus_port
},
deck_config=deck_config,
csv_path=csv_path
)
logger.info(f"纽扣电池工作站创建成功: {device_id}")
return workstation
except Exception as e:
logger.error(f"创建纽扣电池工作站失败: {e}")
raise
if __name__ == "__main__":
# 示例用法
workstation = create_coin_cell_workstation(
device_id="coin_cell_station_01",
config_file="./button_battery_workstation.json",
modbus_host="127.0.0.1",
modbus_port=5021
)
# 启动电池制造工作流
success = workstation.start_workflow(
"battery_manufacturing",
{
"electrolyte_num": 16,
"electrolyte_volume": 50.0,
"assembly_pressure": 2000.0,
"cathode_material": "LiFePO4",
"anode_material": "Graphite"
}
)
if success:
print("电池制造工作流启动成功")
else:
print("电池制造工作流启动失败")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,600 @@
"""
工作站通信基类
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,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)