""" 传感器测试设备软件 - 主入口 基于 PyQt5 构建的桌面图形界面应用。 提供从登录、测试配置、自动化执行、实时监控到报告生成的全流程操作界面。 """ import sys import time import threading from datetime import datetime from typing import Optional from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QTextEdit, QComboBox, QTableWidget, QTableWidgetItem, QGroupBox, QFormLayout, QMessageBox, QTabWidget, QSplitter, QStatusBar, QHeaderView, QProgressBar, QFrame, QGridLayout, QStackedWidget, QDialog, QListWidget, QListWidgetItem ) from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject from PyQt5.QtGui import QFont, QPalette, QColor, QPixmap, QPainter, QPen from app.data_structures import ( UserInfo, UserRole, SensorModelConfig, TestContext, JudgmentResult, TestResult, SystemStatus, SystemMode ) from app.services import ( SecurityService, SensorConfigService, TestEngine, CalculationService, ReportService ) # ============================================================================= # 数据采集模拟线程 # ============================================================================= class AcquisitionWorker(QObject): """ 数据采集工作线程 在后台线程中模拟硬件数据采集,避免阻塞UI。 """ data_acquired = pyqtSignal(object) status_changed = pyqtSignal() test_completed = pyqtSignal(object) def __init__(self, engine: TestEngine): """ 初始化采集线程 Args: engine: 测试引擎实例 """ super().__init__() self._engine = engine self._running = False def start_acquisition(self) -> None: """开始采集循环""" self._running = True while self._running: ctx = self._engine.context if ctx.status_flags["is_running"] and not ctx.status_flags["is_paused"]: point = self._engine.simulate_acquisition() self.data_acquired.emit(point) self.status_changed.emit() # 检查测试是否完成 if not ctx.status_flags["is_running"]: self.test_completed.emit( self._engine.context.acquisition_buffer.get_all() ) time.sleep(0.05) # ~20fps else: time.sleep(0.1) def stop(self) -> None: """停止采集循环""" self._running = False # ============================================================================= # 登录对话框 # ============================================================================= class LoginDialog(QDialog): """ 登录对话框 提供双因子认证(用户名/密码 + TOTP动态口令)登录界面。 """ login_success = pyqtSignal(object) def __init__(self, security_service: SecurityService): """ 初始化登录对话框 Args: security_service: 安全认证服务实例 """ super().__init__() self._security = security_service self._init_ui() def _init_ui(self) -> None: """初始化用户界面""" self.setWindowTitle("传感器测试设备 - 登录") self.setFixedSize(400, 300) layout = QVBoxLayout() # 标题 title = QLabel("传感器测试设备软件") title.setAlignment(Qt.AlignCenter) title_font = QFont("Microsoft YaHei", 16, QFont.Bold) title.setFont(title_font) layout.addWidget(title) layout.addSpacing(20) # 表单 form = QFormLayout() self.username_input = QLineEdit() self.username_input.setPlaceholderText("请输入用户名") self.password_input = QLineEdit() self.password_input.setPlaceholderText("请输入密码") self.password_input.setEchoMode(QLineEdit.Password) self.totp_input = QLineEdit() self.totp_input.setPlaceholderText("请输入6位动态口令") self.totp_input.setMaxLength(6) form.addRow("用户名:", self.username_input) form.addRow("密 码:", self.password_input) form.addRow("动态口令:", self.totp_input) layout.addLayout(form) layout.addSpacing(20) # 登录按钮 self.login_btn = QPushButton("登 录") self.login_btn.setMinimumHeight(36) self.login_btn.clicked.connect(self._do_login) layout.addWidget(self.login_btn) # 状态显示 self.status_label = QLabel("") self.status_label.setAlignment(Qt.AlignCenter) self.status_label.setStyleSheet("color: red;") layout.addWidget(self.status_label) self.setLayout(layout) # 默认填充(方便测试) self.username_input.setText("op01") self.password_input.setText("pass123") self.totp_input.setText("123456") def _do_login(self) -> None: """执行登录""" username = self.username_input.text().strip() password = self.password_input.text() totp = self.totp_input.text().strip() if not username or not password or not totp: self.status_label.setText("请填写所有字段") return success, msg = self._security.login(username, password, totp) if success: self.login_success.emit(self._security.get_current_user()) self.accept() else: self.status_label.setText(msg) # ============================================================================= # 主窗口 # ============================================================================= class MainWindow(QMainWindow): """ 传感器测试设备主窗口 提供测试任务配置、自动化执行、实时监控、质量判定和报告生成的核心界面。 """ def __init__(self, user: UserInfo): """ 初始化主窗口 Args: user: 当前登录用户信息 """ super().__init__() self._current_user = user # 初始化服务 self._security = SecurityService() self._security._current_session = user self._sensor_config = SensorConfigService() self._test_engine = TestEngine(self._sensor_config) self._calc_service = CalculationService() self._report_service = ReportService() # 采集线程 self._acq_thread: Optional[QThread] = None self._acq_worker: Optional[AcquisitionWorker] = None # 最后判定结果 self._last_judgment: Optional[JudgmentResult] = None self._init_ui() self._setup_timers() self._update_permissions() def _init_ui(self) -> None: """初始化主界面""" self.setWindowTitle("传感器测试设备软件 - 安全模式") self.setGeometry(100, 100, 1200, 800) # 中央部件 central = QWidget() self.setCentralWidget(central) main_layout = QVBoxLayout() # 顶部工具栏 toolbar = QHBoxLayout() self.user_label = QLabel(f"用户: {self._current_user.user_id} " f"({self._current_user.role.value})") self.user_label.setStyleSheet("font-weight: bold; padding: 5px;") toolbar.addWidget(self.user_label) toolbar.addStretch() self.status_indicator = QLabel("● 系统正常") self.status_indicator.setStyleSheet("color: green; font-weight: bold;") toolbar.addWidget(self.status_indicator) self.logout_btn = QPushButton("注销") self.logout_btn.clicked.connect(self._do_logout) toolbar.addWidget(self.logout_btn) main_layout.addLayout(toolbar) # 主分割器 splitter = QSplitter(Qt.Horizontal) # 左侧面板 left_panel = QWidget() left_layout = QVBoxLayout() # 扫码/条码输入区 barcode_group = QGroupBox("条码识别") barcode_layout = QVBoxLayout() barcode_input_layout = QHBoxLayout() self.barcode_input = QLineEdit() self.barcode_input.setPlaceholderText("扫码或手动输入传感器条码") self.barcode_input.returnPressed.connect(self._on_barcode_input) barcode_input_layout.addWidget(self.barcode_input) self.load_btn = QPushButton("加载测试计划") self.load_btn.clicked.connect(self._on_barcode_input) barcode_input_layout.addWidget(self.load_btn) barcode_layout.addLayout(barcode_input_layout) self.barcode_info = QLabel("等待扫码...") barcode_layout.addWidget(self.barcode_info) barcode_group.setLayout(barcode_layout) left_layout.addWidget(barcode_group) # 控制区 control_group = QGroupBox("测试控制") control_layout = QGridLayout() self.start_btn = QPushButton("▶ 开始") self.start_btn.clicked.connect(self._start_test) control_layout.addWidget(self.start_btn, 0, 0) self.pause_btn = QPushButton("⏸ 暂停") self.pause_btn.clicked.connect(self._pause_test) self.pause_btn.setEnabled(False) control_layout.addWidget(self.pause_btn, 0, 1) self.stop_btn = QPushButton("⏹ 停止") self.stop_btn.clicked.connect(self._stop_test) self.stop_btn.setEnabled(False) control_layout.addWidget(self.stop_btn, 1, 0) self.skip_btn = QPushButton("⏭ 跳步") self.skip_btn.clicked.connect(self._skip_point) self.skip_btn.setEnabled(False) control_layout.addWidget(self.skip_btn, 1, 1) control_group.setLayout(control_layout) left_layout.addWidget(control_group) # 进度区 progress_group = QGroupBox("测试进度") progress_layout = QVBoxLayout() progress_layout.addWidget(QLabel("当前循环:")) self.cycle_label = QLabel("0 / 0") progress_layout.addWidget(self.cycle_label) progress_layout.addWidget(QLabel("测试点进度:")) self.point_progress = QProgressBar() self.point_progress.setRange(0, 100) self.point_progress.setValue(0) progress_layout.addWidget(self.point_progress) progress_layout.addWidget(QLabel("总体进度:")) self.total_progress = QProgressBar() self.total_progress.setRange(0, 100) self.total_progress.setValue(0) progress_layout.addWidget(self.total_progress) progress_group.setLayout(progress_layout) left_layout.addWidget(progress_group) left_layout.addStretch() left_panel.setLayout(left_layout) splitter.addWidget(left_panel) # 中间面板 - 实时数据与绘图 center_panel = QWidget() center_layout = QVBoxLayout() # 实时数值显示 value_group = QGroupBox("实时数据") value_layout = QGridLayout() value_layout.addWidget(QLabel("目标压力:"), 0, 0) self.target_pressure_label = QLabel("-- MPa") self.target_pressure_label.setStyleSheet("font-size: 18px; font-weight: bold;") value_layout.addWidget(self.target_pressure_label, 0, 1) value_layout.addWidget(QLabel("实际压力:"), 1, 0) self.actual_pressure_label = QLabel("-- MPa") self.actual_pressure_label.setStyleSheet("font-size: 18px; font-weight: bold;") value_layout.addWidget(self.actual_pressure_label, 1, 1) value_layout.addWidget(QLabel("传感器输出:"), 2, 0) self.output_label = QLabel("--") self.output_label.setStyleSheet("font-size: 18px; font-weight: bold;") value_layout.addWidget(self.output_label, 2, 1) value_layout.addWidget(QLabel("报警状态:"), 3, 0) self.alarm_label = QLabel("● NORMAL") self.alarm_label.setStyleSheet("color: green; font-size: 16px; font-weight: bold;") value_layout.addWidget(self.alarm_label, 3, 1) value_group.setLayout(value_layout) center_layout.addWidget(value_group) # 实时绘图区域(简化:使用文本模拟) plot_group = QGroupBox("实时曲线图 (压力-输出)") plot_layout = QVBoxLayout() self.plot_widget = QTextEdit() self.plot_widget.setReadOnly(True) self.plot_widget.setMaximumHeight(200) self.plot_widget.setPlaceholderText("实时曲线数据将在测试过程中显示...") plot_layout.addWidget(self.plot_widget) plot_group.setLayout(plot_layout) center_layout.addWidget(plot_group) # 数据表 data_group = QGroupBox("采集数据表") data_layout = QVBoxLayout() self.data_table = QTableWidget(0, 3) self.data_table.setHorizontalHeaderLabels(["压力 (MPa)", "输出值", "时间戳"]) self.data_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) data_layout.addWidget(self.data_table) data_group.setLayout(data_layout) center_layout.addWidget(data_group) center_panel.setLayout(center_layout) splitter.addWidget(center_panel) # 右侧面板 right_panel = QWidget() right_layout = QVBoxLayout() # 判定结果 result_group = QGroupBox("判定结果") result_layout = QVBoxLayout() self.result_label = QLabel("--") self.result_label.setAlignment(Qt.AlignCenter) self.result_label.setStyleSheet( "font-size: 24px; font-weight: bold; padding: 10px;" ) result_layout.addWidget(self.result_label) result_layout.addWidget(QLabel("非线性误差:")) self.nonlinearity_label = QLabel("-- %") result_layout.addWidget(self.nonlinearity_label) result_layout.addWidget(QLabel("迟滞误差:")) self.hysteresis_label = QLabel("-- %") result_layout.addWidget(self.hysteresis_label) result_layout.addWidget(QLabel("重复性误差:")) self.repeatability_label = QLabel("-- %") result_layout.addWidget(self.repeatability_label) result_layout.addWidget(QLabel("超标详情:")) self.details_text = QTextEdit() self.details_text.setReadOnly(True) self.details_text.setMaximumHeight(60) result_layout.addWidget(self.details_text) result_group.setLayout(result_layout) right_layout.addWidget(result_group) # 报告生成 report_group = QGroupBox("报告导出") report_layout = QVBoxLayout() self.report_btn = QPushButton("📄 生成报告") self.report_btn.clicked.connect(self._generate_report) self.report_btn.setEnabled(False) report_layout.addWidget(self.report_btn) self.report_status = QLabel("就绪") report_layout.addWidget(self.report_status) report_group.setLayout(report_layout) right_layout.addWidget(report_group) right_layout.addStretch() right_panel.setLayout(right_layout) splitter.addWidget(right_panel) main_layout.addWidget(splitter) central.setLayout(main_layout) # 状态栏 self.statusBar().showMessage("系统就绪 | 安全模式已启用") def _setup_timers(self) -> None: """设置定时器""" # 状态刷新定时器 (100ms) self.status_timer = QTimer() self.status_timer.timeout.connect(self._refresh_status) self.status_timer.start(100) def _update_permissions(self) -> None: """根据用户权限更新界面""" role = self._current_user.role # 工艺员和管理员可以管理配置(简化为界面不可见功能) # 操作员只能执行测试 def _on_barcode_input(self) -> None: """处理条码输入""" barcode = self.barcode_input.text().strip() if not barcode: return # 查找配置 config = self._sensor_config.get_config(barcode) if config: self.barcode_info.setText( f"已识别: {config.model_id}\n" f"量程: {config.range_min}-{config.range_max} MPa\n" f"测试点数: {config.test_points} | 循环: {config.cycles}" ) # 加载测试计划 if self._test_engine.load_test_plan(barcode): self.statusBar().showMessage(f"测试计划已加载: {barcode}") self.start_btn.setEnabled(True) else: self.barcode_info.setText("加载测试计划失败") else: self.barcode_info.setText(f"未找到型号配置: {barcode}") QMessageBox.warning(self, "未识别的条码", f"传感器条码 '{barcode}' 未在配置表中找到。") def _start_test(self) -> None: """启动测试""" if not self._test_engine.start_test(): QMessageBox.warning(self, "启动失败", "无法启动测试,请先加载测试计划。") return # 启动采集线程 self._acq_thread = QThread() self._acq_worker = AcquisitionWorker(self._test_engine) self._acq_worker.moveToThread(self._acq_thread) self._acq_worker.data_acquired.connect(self._on_data_acquired) self._acq_worker.status_changed.connect(self._refresh_ui) self._acq_worker.test_completed.connect(self._on_test_completed) self._acq_thread.started.connect(self._acq_worker.start_acquisition) self._acq_thread.start() # 更新按钮状态 self.start_btn.setEnabled(False) self.pause_btn.setEnabled(True) self.stop_btn.setEnabled(True) self.skip_btn.setEnabled(True) self.load_btn.setEnabled(False) self.statusBar().showMessage("测试进行中...") def _pause_test(self) -> None: """暂停测试""" if self._test_engine.pause_test(): self.pause_btn.setText("▶ 继续") self.statusBar().showMessage("测试已暂停") else: # 如果当前是暂停状态,点击继续 if self._test_engine.resume_test(): self.pause_btn.setText("⏸ 暂停") self.statusBar().showMessage("测试已继续") def _stop_test(self) -> None: """停止测试""" self._test_engine.stop_test() if self._acq_worker: self._acq_worker.stop() if self._acq_thread: self._acq_thread.quit() self._acq_thread.wait() self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.skip_btn.setEnabled(False) self.load_btn.setEnabled(True) self.pause_btn.setText("⏸ 暂停") self.statusBar().showMessage("测试已停止") def _skip_point(self) -> None: """跳步到下一个测试点""" ctx = self._test_engine.context new_idx = ctx.current_point_index + 1 if self._test_engine.skip_to_point(new_idx): self.statusBar().showMessage(f"跳转到第 {new_idx + 1} 个测试点") else: self.statusBar().showMessage("跳步失败") def _on_data_acquired(self, point) -> None: """处理采集到的数据点""" # 更新实时数值 self.target_pressure_label.setText(f"{point.pressure:.4f} MPa") self.actual_pressure_label.setText(f"{point.pressure:.4f} MPa") self.output_label.setText(f"{point.output:.6f}") # 更新报警状态 ctx = self._test_engine.context if ctx.status_flags["alarm_active"]: self.alarm_label.setText("● ALARM") self.alarm_label.setStyleSheet("color: red; font-size: 16px; font-weight: bold;") else: self.alarm_label.setText("● NORMAL") self.alarm_label.setStyleSheet("color: green; font-size: 16px; font-weight: bold;") def _refresh_ui(self) -> None: """刷新UI状态""" ctx = self._test_engine.context config = self._sensor_config.get_config(ctx.current_model) # 更新进度 total_points = len(ctx.pressure_sequence) if total_points > 0: progress = int(ctx.current_point_index / total_points * 100) self.point_progress.setValue(progress) if config: total_cycles = config.cycles total = total_points * total_cycles current = ctx.current_point_index + (ctx.current_cycle - 1) * total_points if total > 0: overall = int(current / total * 100) self.total_progress.setValue(overall) self.cycle_label.setText(f"{ctx.current_cycle} / {total_cycles}") # 更新数据表(仅显示最近10条) all_data = ctx.acquisition_buffer.get_all() recent = all_data[-20:] if len(all_data) > 20 else all_data self.data_table.setRowCount(len(recent)) for i, dp in enumerate(recent): self.data_table.setItem(i, 0, QTableWidgetItem(f"{dp.pressure:.4f}")) self.data_table.setItem(i, 1, QTableWidgetItem(f"{dp.output:.6f}")) self.data_table.setItem(i, 2, QTableWidgetItem(f"{dp.timestamp:.3f}")) # 更新曲线文本 if len(all_data) > 0: display_data = all_data[-50:] if len(all_data) > 50 else all_data text = "压力-输出曲线数据点:\n" for dp in display_data: bar_len = int(abs(dp.output) * 10) if dp.output else 1 bar = "█" * min(bar_len, 40) text += f"{dp.pressure:8.4f} MPa | {bar} {dp.output:.4f}\n" self.plot_widget.setText(text) def _refresh_status(self) -> None: """定时刷新系统状态""" status = self._test_engine.system_status if status.system_mode == SystemMode.NA: self.status_indicator.setText("● 离线模式") self.status_indicator.setStyleSheet("color: red; font-weight: bold;") elif status.system_mode == SystemMode.NORMAL: self.status_indicator.setText("● 系统正常") self.status_indicator.setStyleSheet("color: green; font-weight: bold;") else: self.status_indicator.setText("● 维护模式") self.status_indicator.setStyleSheet("color: orange; font-weight: bold;") def _on_test_completed(self, data_points) -> None: """测试完成回调""" self.statusBar().showMessage("测试完成,正在计算判定结果...") # 停止采集线程 if self._acq_worker: self._acq_worker.stop() if self._acq_thread: self._acq_thread.quit() self._acq_thread.wait() # 计算判定结果 config = self._sensor_config.get_config( self._test_engine.context.current_model ) if config: self._last_judgment = self._calc_service.calculate( data_points, config ) self._display_judgment(self._last_judgment) self.report_btn.setEnabled(True) # 重置按钮 self.start_btn.setEnabled(True) self.pause_btn.setEnabled(False) self.stop_btn.setEnabled(False) self.skip_btn.setEnabled(False) self.load_btn.setEnabled(True) self.pause_btn.setText("⏸ 暂停") def _display_judgment(self, judgment: JudgmentResult) -> None: """ 显示判定结果 Args: judgment: 判定结果对象 """ if judgment.result == TestResult.PASS: self.result_label.setText("✓ PASS") self.result_label.setStyleSheet( "font-size: 24px; font-weight: bold; padding: 10px; " "color: green; background-color: #e8f5e8;" ) else: self.result_label.setText("✗ FAIL") self.result_label.setStyleSheet( "font-size: 24px; font-weight: bold; padding: 10px; " "color: red; background-color: #fde8e8;" ) self.nonlinearity_label.setText(f"{judgment.non_linearity:.4f} %") self.hysteresis_label.setText(f"{judgment.hysteresis:.4f} %") self.repeatability_label.setText(f"{judgment.repeatability:.4f} %") self.details_text.setText(judgment.details if judgment.details else "无异常") def _generate_report(self) -> None: """生成测试报告""" if not self._last_judgment: QMessageBox.warning(self, "无测试数据", "请先完成一次测试。") return self.report_status.setText("正在生成报告...") self.report_btn.setEnabled(False) try: # 生成报告 report = self._report_service.generate_report( self._test_engine.context, self._last_judgment, self._current_user.user_id ) # 生成PDF pdf_data = self._report_service.generate_pdf_report(report) # 生成CSV csv_data = self._report_service.generate_csv_data(report) self.report_status.setText( f"报告已生成: {report.test_id}\n" f"PDF: {len(pdf_data)} 字节 | CSV: {len(csv_data)} 字节\n" f"SM3: {report.sm3_hash[:16]}..." ) QMessageBox.information( self, "报告生成成功", f"测试报告已生成\n" f"测试编号: {report.test_id}\n" f"SM3哈希: {report.sm3_hash}\n" f"数字水印已嵌入\n\n" f"注:在实际系统中,报告将通过认证U盘导出。" ) except Exception as e: self.report_status.setText(f"报告生成失败: {str(e)}") QMessageBox.critical(self, "报告生成失败", str(e)) finally: self.report_btn.setEnabled(True) def _do_logout(self) -> None: """注销当前用户""" self._test_engine.shutdown() self._security.logout() self.close() # ============================================================================= # 应用入口 # ============================================================================= def main(): """ 应用主入口函数 启动传感器测试设备软件,显示登录界面。 """ app = QApplication(sys.argv) app.setApplicationName("传感器测试设备软件") app.setApplicationVersion("1.0.0") # 设置全局样式 app.setStyle("Fusion") palette = QPalette() palette.setColor(QPalette.Window, QColor(240, 240, 240)) app.setPalette(palette) # 初始化安全服务 security = SecurityService() # 显示登录对话框 login = LoginDialog(security) if login.exec_() == QDialog.Accepted: user = security.get_current_user() if user: window = MainWindow(user) window.show() sys.exit(app.exec_()) else: sys.exit(0) if __name__ == "__main__": main()