#!/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()