386 lines
16 KiB
Python
386 lines
16 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
|
|||
|
|
"""
|
|||
|
|
试验监控 ViewModel,管理实时机台和工位数据
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
|||
|
|
from typing import Dict, List, Any
|
|||
|
|
from models.system_model import SystemModel
|
|||
|
|
|
|||
|
|
|
|||
|
|
class TestMonitorViewModel(QObject):
|
|||
|
|
"""试验监控 ViewModel - 响应式数据管理"""
|
|||
|
|
|
|||
|
|
# 信号定义
|
|||
|
|
machinesChanged = pyqtSignal() # 机台列表变化
|
|||
|
|
stationsChanged = pyqtSignal() # 工位数据变化(预留)
|
|||
|
|
connectionStatusChanged = pyqtSignal() # WebSocket 连接状态变化
|
|||
|
|
|
|||
|
|
def __init__(self, parent=None):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self._machines = {} # {machine_sn: machine_data}
|
|||
|
|
self._stations = {} # {station_key: station_data}(预留,待后续完善)
|
|||
|
|
self._connected = False
|
|||
|
|
|
|||
|
|
# 启动时从数据库加载机台配置,确保未连接也能显示
|
|||
|
|
self._load_from_db()
|
|||
|
|
|
|||
|
|
# ===================== 响应式属性 =====================
|
|||
|
|
|
|||
|
|
@pyqtProperty('QVariantMap', notify=machinesChanged)
|
|||
|
|
def machines(self):
|
|||
|
|
"""所有机台数据(字典格式:{machine_sn: machine_data})"""
|
|||
|
|
return self._machines
|
|||
|
|
|
|||
|
|
@pyqtProperty('QVariantList', notify=machinesChanged)
|
|||
|
|
def machineList(self):
|
|||
|
|
"""机台列表(数组格式,方便界面遍历)"""
|
|||
|
|
return list(self._machines.values())
|
|||
|
|
|
|||
|
|
@pyqtProperty('QVariantMap', notify=stationsChanged)
|
|||
|
|
def stations(self):
|
|||
|
|
"""所有工位数据(预留)"""
|
|||
|
|
return self._stations
|
|||
|
|
|
|||
|
|
@pyqtProperty(bool, notify=connectionStatusChanged)
|
|||
|
|
def connected(self):
|
|||
|
|
"""WebSocket 连接状态"""
|
|||
|
|
return self._connected
|
|||
|
|
|
|||
|
|
def _load_from_db(self):
|
|||
|
|
"""从数据库加载机台配置,构建初始显示数据"""
|
|||
|
|
try:
|
|||
|
|
model = SystemModel()
|
|||
|
|
machines = model.load_machines() or []
|
|||
|
|
for m in machines:
|
|||
|
|
sn = m.get('SN') or ''
|
|||
|
|
if not sn:
|
|||
|
|
continue
|
|||
|
|
# 构造与 WebSocket 保持一致的数据结构
|
|||
|
|
machine_data = {
|
|||
|
|
'label': m.get('label', ''),
|
|||
|
|
'SN': sn,
|
|||
|
|
'com': m.get('com', ''),
|
|||
|
|
'testType': m.get('testType', ''),
|
|||
|
|
'plcAddress': m.get('plcAddress', ''),
|
|||
|
|
'type': m.get('type', ''),
|
|||
|
|
# stations: 根据 station1~4 生成,'NA' 表示未启用
|
|||
|
|
'stations': []
|
|||
|
|
}
|
|||
|
|
stations = []
|
|||
|
|
for i in range(1, 5):
|
|||
|
|
station_val = m.get(f'station{i}', '')
|
|||
|
|
if station_val and str(station_val).upper() != 'NA':
|
|||
|
|
stations.append({
|
|||
|
|
'dtMachineSN': sn,
|
|||
|
|
'dtMachineLabel': m.get('label', ''),
|
|||
|
|
'label': f'工位{i}',
|
|||
|
|
'SN': str(station_val),
|
|||
|
|
'key': i - 1,
|
|||
|
|
'formal': True,
|
|||
|
|
# 初始状态:未连接时统一显示 idle(后续由 WebSocket 更新)
|
|||
|
|
'status': 'idle',
|
|||
|
|
'index': 0,
|
|||
|
|
'dutStatus': '',
|
|||
|
|
})
|
|||
|
|
machine_data['stations'] = stations
|
|||
|
|
self._machines[sn] = machine_data
|
|||
|
|
# 触发界面构建
|
|||
|
|
self.machinesChanged.emit()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f'加载机台配置失败: {e}')
|
|||
|
|
|
|||
|
|
def updateMachineData(self, message_data: Dict[str, Any]):
|
|||
|
|
"""
|
|||
|
|
更新机台数据(由 WebSocket 消息触发,增量合并)
|
|||
|
|
|
|||
|
|
消息格式示例:
|
|||
|
|
{
|
|||
|
|
'status': 'success',
|
|||
|
|
'command': 'machine_data_change',
|
|||
|
|
'data': {
|
|||
|
|
'machines': [
|
|||
|
|
{
|
|||
|
|
'label': 'Phone试验1#机-com7',
|
|||
|
|
'SN': 'SUN-LAB1-TOP-001',
|
|||
|
|
'com': 'com7',
|
|||
|
|
'testType': 'Phone',
|
|||
|
|
'plcAddress': '01',
|
|||
|
|
'type': 'xinjie',
|
|||
|
|
'comm_config': {...},
|
|||
|
|
'connectState': 1,
|
|||
|
|
'stations': [
|
|||
|
|
{
|
|||
|
|
'dtMachineSN': 'SUN-LAB1-TOP-001',
|
|||
|
|
'dtMachineLabel': 'Phone试验1#机-com7',
|
|||
|
|
'label': '工位1',
|
|||
|
|
'SN': '01',
|
|||
|
|
'status': 'running',
|
|||
|
|
'dutStatus': '试验中',
|
|||
|
|
'dutSN': '543009865',
|
|||
|
|
'dut': {
|
|||
|
|
'testReqList': [ # 跌落参数数据
|
|||
|
|
{'dropCode': 1, 'item': '背面', 'height': 75.0, 'count': 100},
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
},
|
|||
|
|
'machine_sn': 'SUN-LAB1-TOP-001'
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
data = message_data.get('data', {})
|
|||
|
|
machines = data.get('machines', [])
|
|||
|
|
|
|||
|
|
for machine in machines:
|
|||
|
|
sn = machine.get('SN', '')
|
|||
|
|
if not sn:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 增量合并:如果机台已存在,合并字段;否则新建
|
|||
|
|
existing_machine = self._machines.get(sn)
|
|||
|
|
if existing_machine:
|
|||
|
|
# 合并机台级字段(只更新消息中存在的字段)
|
|||
|
|
for key in ('label', 'com', 'testType', 'plcAddress', 'type', 'comm_config', 'tcp_modbus_unit', 'connectState', 'key'):
|
|||
|
|
if key in machine:
|
|||
|
|
existing_machine[key] = machine[key]
|
|||
|
|
|
|||
|
|
# 合并stations字段(增量更新)
|
|||
|
|
if 'stations' in machine:
|
|||
|
|
new_stations = machine['stations']
|
|||
|
|
existing_stations = existing_machine.get('stations', [])
|
|||
|
|
# 按SN索引构建映射
|
|||
|
|
station_map = {st.get('SN'): st for st in existing_stations}
|
|||
|
|
for new_st in new_stations:
|
|||
|
|
st_sn = new_st.get('SN', '')
|
|||
|
|
if st_sn:
|
|||
|
|
if st_sn in station_map:
|
|||
|
|
# 增量合并station字段
|
|||
|
|
self._merge_station_data(station_map[st_sn], new_st)
|
|||
|
|
else:
|
|||
|
|
# 新增station
|
|||
|
|
self._process_station_data(new_st)
|
|||
|
|
station_map[st_sn] = new_st
|
|||
|
|
# 更新回机台
|
|||
|
|
existing_machine['stations'] = list(station_map.values())
|
|||
|
|
else:
|
|||
|
|
# 新机台:处理testReqList -> dropDirections转换
|
|||
|
|
if 'stations' in machine:
|
|||
|
|
for station in machine['stations']:
|
|||
|
|
self._process_station_data(station)
|
|||
|
|
self._machines[sn] = machine
|
|||
|
|
|
|||
|
|
# 触发响应式刷新
|
|||
|
|
self.machinesChanged.emit()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"更新机台数据失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
def _merge_station_data(self, existing_station: Dict[str, Any], new_station: Dict[str, Any]):
|
|||
|
|
"""增量合并station数据(只更新新消息中存在的字段)"""
|
|||
|
|
# 合并所有新字段
|
|||
|
|
for key, value in new_station.items():
|
|||
|
|
if key == 'dut':
|
|||
|
|
# dut字段特殊处理:增量合并
|
|||
|
|
existing_dut = existing_station.get('dut', {})
|
|||
|
|
new_dut = new_station.get('dut', {})
|
|||
|
|
if isinstance(existing_dut, dict) and isinstance(new_dut, dict):
|
|||
|
|
existing_dut.update(new_dut)
|
|||
|
|
existing_station['dut'] = existing_dut
|
|||
|
|
# 处理testReqList -> dropDirections
|
|||
|
|
if 'testReqList' in existing_dut:
|
|||
|
|
existing_station['dropDirections'] = self._convert_test_req_to_drop_directions(existing_dut['testReqList'])
|
|||
|
|
else:
|
|||
|
|
existing_station['dut'] = new_dut
|
|||
|
|
else:
|
|||
|
|
existing_station[key] = value
|
|||
|
|
# 额外处理:若新station中有dut但无dropDirections,从testReqList转换
|
|||
|
|
if 'dut' in existing_station and 'dropDirections' not in existing_station:
|
|||
|
|
dut = existing_station.get('dut', {})
|
|||
|
|
if isinstance(dut, dict) and 'testReqList' in dut:
|
|||
|
|
existing_station['dropDirections'] = self._convert_test_req_to_drop_directions(dut['testReqList'])
|
|||
|
|
|
|||
|
|
def _process_station_data(self, station: Dict[str, Any]):
|
|||
|
|
"""处理station数据:将testReqList转换为dropDirections"""
|
|||
|
|
dut = station.get('dut', {})
|
|||
|
|
if isinstance(dut, dict) and 'testReqList' in dut:
|
|||
|
|
station['dropDirections'] = self._convert_test_req_to_drop_directions(dut['testReqList'])
|
|||
|
|
|
|||
|
|
def _convert_test_req_to_drop_directions(self, test_req_list: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|||
|
|
"""将testReqList转换为dropDirections格式"""
|
|||
|
|
if not test_req_list or not isinstance(test_req_list, list):
|
|||
|
|
return []
|
|||
|
|
drop_directions = []
|
|||
|
|
for req in test_req_list:
|
|||
|
|
drop_directions.append({
|
|||
|
|
'方向': req.get('item', ''),
|
|||
|
|
'高度': req.get('height', 0),
|
|||
|
|
'设定次数': req.get('count', 0),
|
|||
|
|
'当前次数': req.get('itemCurrentCycles',0)
|
|||
|
|
})
|
|||
|
|
return drop_directions
|
|||
|
|
|
|||
|
|
def updateStationData(self, message_data: Dict[str, Any]):
|
|||
|
|
"""
|
|||
|
|
更新工位(通道)详细数据
|
|||
|
|
|
|||
|
|
参数:
|
|||
|
|
message_data: WebSocket 消息数据(包含通道详细信息)
|
|||
|
|
|
|||
|
|
通道数据结构:
|
|||
|
|
{
|
|||
|
|
'dtMachineSN': 'SUN-LAB1-TOP-001',
|
|||
|
|
'label': '工位1',
|
|||
|
|
'SN': '01',
|
|||
|
|
'status': 'idle', // 工位状态
|
|||
|
|
'dutStatus': '待启动', // DUT 状态
|
|||
|
|
'dutName': '电池组成1型',
|
|||
|
|
'dutSN': '202401032245180707',
|
|||
|
|
'dutProject': 'DTM-PRJ-00002',
|
|||
|
|
'dutPhase': 'DTM-PHASE-00002',
|
|||
|
|
'projectName': 'GV9206 后视镜',
|
|||
|
|
'projectPhase': 'B Sample TEST',
|
|||
|
|
'dut': {详细的 DUT 信息...},
|
|||
|
|
...
|
|||
|
|
}
|
|||
|
|
"""
|
|||
|
|
try:
|
|||
|
|
# 通道数据已包含在机台数据的 stations 字段中
|
|||
|
|
# 此方法用于单独更新某个通道的详细数据(如有需要)
|
|||
|
|
|
|||
|
|
machine_sn = message_data.get('dtMachineSN', '')
|
|||
|
|
station_sn = message_data.get('SN', '')
|
|||
|
|
|
|||
|
|
if machine_sn and station_sn:
|
|||
|
|
station_key = self.getStationKey(machine_sn, station_sn)
|
|||
|
|
self._stations[station_key] = message_data
|
|||
|
|
print(f"更新工位数据: {machine_sn}:{station_sn}")
|
|||
|
|
|
|||
|
|
# 同时更新机台数据中的 stations 列表
|
|||
|
|
machine = self._machines.get(machine_sn)
|
|||
|
|
if machine:
|
|||
|
|
stations = machine.get('stations', [])
|
|||
|
|
station_found = False
|
|||
|
|
for i, station in enumerate(stations):
|
|||
|
|
if station.get('SN') == station_sn:
|
|||
|
|
stations[i] = message_data
|
|||
|
|
station_found = True
|
|||
|
|
break
|
|||
|
|
# 如果没有找到对应的站点,则添加新站点
|
|||
|
|
if not station_found:
|
|||
|
|
stations.append(message_data)
|
|||
|
|
# 触发响应式刷新
|
|||
|
|
self.machinesChanged.emit()
|
|||
|
|
|
|||
|
|
self.stationsChanged.emit()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"更新工位数据失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
def updateStationDropResults(self, message_data: Dict[str, Any]):
|
|||
|
|
"""更新工位(通道)跌落结果数据(增量合并)"""
|
|||
|
|
try:
|
|||
|
|
machine_sn = message_data.get('dtMachineSN', '') or message_data.get('machine_sn', '')
|
|||
|
|
station_sn = message_data.get('SN', '') or message_data.get('station_sn', '')
|
|||
|
|
if not machine_sn or not station_sn:
|
|||
|
|
return
|
|||
|
|
machine = self._machines.get(machine_sn)
|
|||
|
|
if not machine:
|
|||
|
|
return
|
|||
|
|
stations = machine.get('stations', [])
|
|||
|
|
for i, station in enumerate(stations):
|
|||
|
|
if station.get('SN') == station_sn:
|
|||
|
|
drop_dirs = message_data.get('dropDirections')
|
|||
|
|
if drop_dirs is not None:
|
|||
|
|
station['dropDirections'] = drop_dirs
|
|||
|
|
for k in ('dropResult', 'currentDirection', 'currentCycles'):
|
|||
|
|
if k in message_data:
|
|||
|
|
station[k] = message_data[k]
|
|||
|
|
stations[i] = station
|
|||
|
|
break
|
|||
|
|
self.stationsChanged.emit()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"更新工位跌落结果失败: {e}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
def setConnectionStatus(self, connected: bool):
|
|||
|
|
"""设置 WebSocket 连接状态"""
|
|||
|
|
if self._connected != connected:
|
|||
|
|
self._connected = connected
|
|||
|
|
self.connectionStatusChanged.emit()
|
|||
|
|
|
|||
|
|
# ===================== 查询方法 =====================
|
|||
|
|
|
|||
|
|
def getMachineByLabel(self, label: str) -> Dict[str, Any]:
|
|||
|
|
"""根据标签获取机台数据"""
|
|||
|
|
for machine in self._machines.values():
|
|||
|
|
if machine.get('label') == label:
|
|||
|
|
return machine
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
def getMachineBySN(self, sn: str) -> Dict[str, Any]:
|
|||
|
|
"""根据 SN 获取机台数据"""
|
|||
|
|
return self._machines.get(sn, {})
|
|||
|
|
|
|||
|
|
def getStationsByMachineSN(self, machine_sn: str) -> List[Dict[str, Any]]:
|
|||
|
|
"""获取指定机台的所有工位"""
|
|||
|
|
machine = self._machines.get(machine_sn, {})
|
|||
|
|
return machine.get('stations', [])
|
|||
|
|
|
|||
|
|
def getStationKey(self, machine_sn: str, station_sn: str) -> str:
|
|||
|
|
"""生成工位唯一标识"""
|
|||
|
|
return f"{machine_sn}:{station_sn}"
|
|||
|
|
|
|||
|
|
def getStationDetailData(self, machine_sn: str, station_sn: str) -> Dict[str, Any]:
|
|||
|
|
"""
|
|||
|
|
获取工位详细数据(预留接口)
|
|||
|
|
|
|||
|
|
参数:
|
|||
|
|
machine_sn: 机台 SN
|
|||
|
|
station_sn: 工位 SN
|
|||
|
|
|
|||
|
|
返回:
|
|||
|
|
工位详细数据字典(待后续完善)
|
|||
|
|
"""
|
|||
|
|
station_key = self.getStationKey(machine_sn, station_sn)
|
|||
|
|
return self._stations.get(station_key, {})
|
|||
|
|
|
|||
|
|
# ===================== 分组方法 =====================
|
|||
|
|
|
|||
|
|
def getMachinesByTestType(self, test_type: str) -> List[Dict[str, Any]]:
|
|||
|
|
"""按测试类型分组获取机台"""
|
|||
|
|
return [
|
|||
|
|
machine for machine in self._machines.values()
|
|||
|
|
if machine.get('testType') == test_type
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def getAllTestTypes(self) -> List[str]:
|
|||
|
|
"""获取所有测试类型"""
|
|||
|
|
test_types = set()
|
|||
|
|
for machine in self._machines.values():
|
|||
|
|
test_type = machine.get('testType', '')
|
|||
|
|
if test_type:
|
|||
|
|
test_types.add(test_type)
|
|||
|
|
return sorted(list(test_types))
|
|||
|
|
|
|||
|
|
# ===================== 清理方法 =====================
|
|||
|
|
|
|||
|
|
def clear(self):
|
|||
|
|
"""清空所有数据"""
|
|||
|
|
self._machines.clear()
|
|||
|
|
self._stations.clear()
|
|||
|
|
self.machinesChanged.emit()
|
|||
|
|
self.stationsChanged.emit()
|