这是使用PyQT5作为UI的首次提交,将后端和UI合并到1个工程中,统一使用了Python,没有使用JS和HTML
This commit is contained in:
385
UI/viewmodels/test_monitor_viewmodel.py
Normal file
385
UI/viewmodels/test_monitor_viewmodel.py
Normal file
@@ -0,0 +1,385 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user