commit 31a6a0b255b9d0a943296ce2b61824685b405d97 Author: leimingsheng Date: Thu Aug 21 18:19:04 2025 +0800 init repo diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e585a7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +tmp/ +src/__pycache__/ +log.tar.gz diff --git a/resource/MainWindow.ui b/resource/MainWindow.ui new file mode 100644 index 0000000..aa6a83d --- /dev/null +++ b/resource/MainWindow.ui @@ -0,0 +1,184 @@ + + + MainWindow + + + + 0 + 0 + 1212 + 693 + + + + OnekeyLogDiagnose + + + + + + + + + + 0 + + + + 控制台 + + + + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'SimSun'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">通过文件上传一键日志压缩包来开始</p></body></html> + + + + + + + + 基本信息 + + + + + + + + + + 关键告警 + + + + + 温度信息 + + + + + + false + + + + ALL_Temp + + + + + CPU_Temp + + + + + FPGA_Temp + + + + + DIMM_Temp + + + + + Inlet_CPU_Temp + + + + + Inlet_FPGA_Temp + + + + + M2_Temp + + + + + Outlet_CPU_Temp + + + + + Outlet_FPGA_Temp + + + + + VR_CPU_Temp + + + + + VR_FPGA_Temp + + + + + + + + + + + + + + + + + 0 + 0 + 1212 + 23 + + + + + 文件 + + + + + + + + + 打开 + + + 上传日志文件到程序中 + + + Ctrl+O + + + + + + + + 10 + + + 10 + + + true + + + true + + + true + + + diff --git a/src/MainWindow_ui.py b/src/MainWindow_ui.py new file mode 100644 index 0000000..ae6df24 --- /dev/null +++ b/src/MainWindow_ui.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'resource\Mainwindow.ui' +# +# Created by: PyQt5 UI code generator 5.15.9 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(1212, 693) + MainWindow.setStatusTip("") + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.centralwidget) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) + self.tabWidget.setObjectName("tabWidget") + self.tab_console = QtWidgets.QWidget() + self.tab_console.setObjectName("tab_console") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.tab_console) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.textBrowser_console = QtWidgets.QTextBrowser(self.tab_console) + self.textBrowser_console.setObjectName("textBrowser_console") + self.horizontalLayout_4.addWidget(self.textBrowser_console) + self.tabWidget.addTab(self.tab_console, "") + self.tab_baseinfo = QtWidgets.QWidget() + self.tab_baseinfo.setObjectName("tab_baseinfo") + self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.tab_baseinfo) + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.textBrowser_info = QtWidgets.QTextBrowser(self.tab_baseinfo) + self.textBrowser_info.setObjectName("textBrowser_info") + self.horizontalLayout_3.addWidget(self.textBrowser_info) + self.tabWidget.addTab(self.tab_baseinfo, "") + self.tab_alert = QtWidgets.QWidget() + self.tab_alert.setObjectName("tab_alert") + self.tabWidget.addTab(self.tab_alert, "") + self.tab_temp_all = QtWidgets.QWidget() + self.tab_temp_all.setObjectName("tab_temp_all") + self.verticalLayout = QtWidgets.QVBoxLayout(self.tab_temp_all) + self.verticalLayout.setObjectName("verticalLayout") + self.comboBox = QtWidgets.QComboBox(self.tab_temp_all) + self.comboBox.setEditable(False) + self.comboBox.setObjectName("comboBox") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.comboBox.addItem("") + self.verticalLayout.addWidget(self.comboBox) + self.graphicsView = QtWidgets.QGraphicsView(self.tab_temp_all) + self.graphicsView.setObjectName("graphicsView") + self.verticalLayout.addWidget(self.graphicsView) + self.tabWidget.addTab(self.tab_temp_all, "") + self.horizontalLayout_2.addWidget(self.tabWidget) + MainWindow.setCentralWidget(self.centralwidget) + self.menubar = QtWidgets.QMenuBar(MainWindow) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1212, 23)) + self.menubar.setObjectName("menubar") + self.menu = QtWidgets.QMenu(self.menubar) + self.menu.setObjectName("menu") + MainWindow.setMenuBar(self.menubar) + self.statusbar = QtWidgets.QStatusBar(MainWindow) + self.statusbar.setObjectName("statusbar") + MainWindow.setStatusBar(self.statusbar) + self.actionUpload_log = QtWidgets.QAction(MainWindow) + self.actionUpload_log.setObjectName("actionUpload_log") + self.menu.addAction(self.actionUpload_log) + self.menubar.addAction(self.menu.menuAction()) + + self.retranslateUi(MainWindow) + self.tabWidget.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "OnekeyLogDiagnose")) + self.textBrowser_console.setHtml(_translate("MainWindow", "\n" +"\n" +"

通过文件上传一键日志压缩包来开始

")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_console), _translate("MainWindow", "控制台")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_baseinfo), _translate("MainWindow", "基本信息")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_alert), _translate("MainWindow", "关键告警")) + self.comboBox.setItemText(0, _translate("MainWindow", "ALL_Temp")) + self.comboBox.setItemText(1, _translate("MainWindow", "CPU_Temp")) + self.comboBox.setItemText(2, _translate("MainWindow", "FPGA_Temp")) + self.comboBox.setItemText(3, _translate("MainWindow", "DIMM_Temp")) + self.comboBox.setItemText(4, _translate("MainWindow", "Inlet_CPU_Temp")) + self.comboBox.setItemText(5, _translate("MainWindow", "Inlet_FPGA_Temp")) + self.comboBox.setItemText(6, _translate("MainWindow", "M2_Temp")) + self.comboBox.setItemText(7, _translate("MainWindow", "Outlet_CPU_Temp")) + self.comboBox.setItemText(8, _translate("MainWindow", "Outlet_FPGA_Temp")) + self.comboBox.setItemText(9, _translate("MainWindow", "VR_CPU_Temp")) + self.comboBox.setItemText(10, _translate("MainWindow", "VR_FPGA_Temp")) + self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_temp_all), _translate("MainWindow", "温度信息")) + self.menu.setTitle(_translate("MainWindow", "文件")) + self.actionUpload_log.setText(_translate("MainWindow", "打开")) + self.actionUpload_log.setStatusTip(_translate("MainWindow", "上传日志文件到程序中")) + self.actionUpload_log.setShortcut(_translate("MainWindow", "Ctrl+O")) diff --git a/src/ZiJin_parse_baseinfo.py b/src/ZiJin_parse_baseinfo.py new file mode 100644 index 0000000..a37820c --- /dev/null +++ b/src/ZiJin_parse_baseinfo.py @@ -0,0 +1,79 @@ +import os +import utils +import json + +project_root = utils.get_project_root() +cache_dir = os.path.join(project_root, "tmp") +log_dir = os.path.join(cache_dir, "onekeylog") + +component_dir = os.path.join(log_dir, "component") +component_file = os.path.join(component_dir, "component.log") +baseinfo_file = os.path.join(cache_dir, "baseinfo.txt") + +def get_fw_version_info(): + fw_version_line = 3 + if not os.path.exists(component_file): + print("no component.log found") + return + + fw_version_info = utils.read_specific_line(component_file, fw_version_line) + json_fw_info = json.loads(fw_version_info) + + version_str = "\nDPU Firmware Version Info:\n" + version_str += f"BMC Version : {json_fw_info[0]['dev_version']}\n" + version_str += f"Bios Version : {json_fw_info[2]['dev_version']}\n" + version_str += f"ME Version : {json_fw_info[3]['dev_version']}\n" + version_str += f"Soc_CPLD Version : {json_fw_info[4]['dev_version']}\n" + version_str += f"Fpga_CPLD Version : {json_fw_info[5]['dev_version']}\n" + version_str += f"FPGA Version : {json_fw_info[6]['dev_version']}\n" + + print(version_str) + utils.append_to_file(baseinfo_file, version_str) + +def get_fru_info(): + fru_line = 19 + if not os.path.exists(component_file): + print("no component.log found") + return + fru_info = utils.read_specific_line(component_file, fru_line) + json_fru_info = json.loads(fru_info) + + fru_str = "\nDPU FRU Infomation:\n" + fru_str += f"Board Product Name : {json_fru_info[0]['board']['product_name']}\n" + fru_str += f"Board SN : {json_fru_info[0]['board']['serial_number']}\n" + fru_str += f"Board PartNumber : {json_fru_info[0]['board']['part_number']}\n" + fru_str += f"Server Product Name : {json_fru_info[0]['product']['product_name']}\n" + fru_str += f"Server Product Ver : {json_fru_info[0]['product']['product_version']}\n" + fru_str += f"Server Product SN : {json_fru_info[0]['product']['serial_number']}\n" + fru_str += f"Server Custom Field : {json_fru_info[0]['product']['custom_fields-1']}\n" + + print(fru_str) + utils.append_to_file(baseinfo_file, fru_str) + +# def get_hardware_info(): +# hw_cpu_info_line = 5 +# if not os.path.exists(component_file): +# print("no component.log found") +# return +# cpu_info = read_specific_line(component_file, hw_cpu_info_line) +# json_cpu_info = json.loads(cpu_info) + +# hw_info_str = "\nDPU Hardware Infomation:\n" + + +def program_main(): + get_fw_version_info() + get_fru_info() + # get_hardware_info() + + return True + +def get_all_infostring(): + return utils.read_file_to_string(baseinfo_file) + +if __name__ == "__main__": + get_fw_version_info() + + + + \ No newline at end of file diff --git a/src/ZiJin_parse_idl.py b/src/ZiJin_parse_idl.py new file mode 100644 index 0000000..ac92f20 --- /dev/null +++ b/src/ZiJin_parse_idl.py @@ -0,0 +1 @@ +import os diff --git a/src/ZiJin_parse_sensorhistory.py b/src/ZiJin_parse_sensorhistory.py new file mode 100644 index 0000000..de5ba35 --- /dev/null +++ b/src/ZiJin_parse_sensorhistory.py @@ -0,0 +1,293 @@ +import pandas as pd +import matplotlib.pyplot as plt +import re +from datetime import datetime +import os +import argparse +import utils + +project_root = utils.get_project_root() +cache_dir = os.path.join(project_root, "tmp") +default_chart_dir = os.path.join(cache_dir, "chart") + +def parse_temperature_data(file_path): + """ + 解析温度数据文件,返回一个包含时间戳和各温度点数据的DataFrame + 处理温度值为'disable'或255的情况,将其视为无效值 + + 参数: + file_path: 数据文件路径 + + 返回: + DataFrame: 包含时间戳和各温度点数据的DataFrame + """ + # 初始化数据列表 + timestamps = [] + temperature_data = {} + + try: + with open(file_path, 'r') as file: + for line_num, line in enumerate(file, 1): + line = line.strip() + if not line: + continue + + # 分割时间戳和温度数据 + parts = line.split('|') + if len(parts) != 2: + print(f"警告: 第 {line_num} 行格式不正确,已跳过") + continue + + timestamp_str, temps_str = parts + + # 解析时间戳 + try: + # 尝试多种常见时间格式 + timestamp_formats = [ + '%Y-%m-%dT%H:%M:%S', + ] + + timestamp = None + for fmt in timestamp_formats: + try: + timestamp = datetime.strptime(timestamp_str, fmt) + break + except ValueError: + continue + + if timestamp is None: + print(f"警告: 第 {line_num} 行时间戳格式无法识别,已跳过") + continue + + timestamps.append(timestamp) + + except Exception as e: + print(f"警告: 第 {line_num} 行时间戳解析错误 - {str(e)},已跳过") + continue + + # 解析温度数据 + temp_entries = temps_str.split(',') + for entry in temp_entries: + entry = entry.strip() + if not entry: + continue + + try: + name, temp_value = entry.split(':') + name = name.strip() + temp_value = temp_value.strip() + + # 检查是否为无效值 + if temp_value.lower() == 'disable' or temp_value == '255': + temp = None # 用None表示无效值 + else: + temp = float(temp_value) + + # 初始化温度点列表(如果不存在) + if name not in temperature_data: + temperature_data[name] = [] + + # 填充数据(确保所有列表长度相同) + while len(temperature_data[name]) < len(timestamps) - 1: + temperature_data[name].append(None) + + temperature_data[name].append(temp) + + except Exception as e: + print(f"警告: 第 {line_num} 行温度数据 '{entry}' 解析错误 - {str(e)},已跳过") + continue + + # 确保所有温度点列表长度相同 + for name in temperature_data: + while len(temperature_data[name]) < len(timestamps): + temperature_data[name].append(None) + + # 创建DataFrame + data = {'timestamp': timestamps} + data.update(temperature_data) + df = pd.DataFrame(data) + df.set_index('timestamp', inplace=True) + + # 统计每个温度点的无效值数量 + for column in df.columns: + invalid_count = df[column].isna().sum() + if invalid_count > 0: + print(f"注意: 温度点 '{column}' 包含 {invalid_count} 个无效值,已排除在分析之外") + + return df + + except FileNotFoundError: + print(f"错误: 文件 '{file_path}' 不存在") + return None + except Exception as e: + print(f"解析文件时发生错误: {str(e)}") + return None + +def generate_temperature_charts(df, output_dir=default_chart_dir): + """ + 根据温度数据生成图表,自动忽略无效值 + + 参数: + df: 包含温度数据的DataFrame + output_dir: 图表输出目录 + """ + if df is None or df.empty: + print("没有数据可生成图表") + return + + # 创建输出目录 + os.makedirs(output_dir, exist_ok=True) + + # 设置中文字体支持 + plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"] + plt.rcParams["axes.unicode_minus"] = False # 正确显示负号 + + # 1. 所有温度点的趋势图 + plt.figure(figsize=(12, 6)) + for column in df.columns: + # 过滤掉无效值(None)再绘图 + valid_data = df[column].dropna() + plt.plot(valid_data.index, valid_data, label=column, alpha=0.7) + + plt.title('各温度点随时间变化趋势') + plt.xlabel('时间') + plt.ylabel('温度 (°C)') + plt.legend() + plt.grid(True, linestyle='--', alpha=0.7) + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig(os.path.join(output_dir, 'temperature_trends.png'), dpi=300) + plt.close() + + # 2. 每个温度点的单独图表 + for column in df.columns: + plt.figure(figsize=(12, 4)) + # 过滤掉无效值(None)再绘图 + valid_data = df[column].dropna() + plt.plot(valid_data.index, valid_data, label=column, color='blue') + + # 计算并绘制平均值线(仅基于有效值) + if not valid_data.empty: + mean_temp = valid_data.mean() + plt.axhline(y=mean_temp, color='r', linestyle='--', label=f'平均值: {mean_temp:.2f}°C') + + plt.title(f'{column} 温度随时间变化') + plt.xlabel('时间') + plt.ylabel('温度 (°C)') + plt.legend() + plt.grid(True, linestyle='--', alpha=0.7) + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig(os.path.join(output_dir, f'{column}_temperature.png'), dpi=300) + plt.close() + + # 3. 温度分布箱线图(自动排除NaN值) + plt.figure(figsize=(10, 6)) + df.boxplot() + plt.title('各温度点分布情况') + plt.ylabel('温度 (°C)') + plt.grid(True, linestyle='--', alpha=0.7) + plt.tight_layout() + plt.savefig(os.path.join(output_dir, 'temperature_distribution.png'), dpi=300) + plt.close() + + print(f"图表已成功生成并保存到 '{output_dir}' 目录") + +def get_sensor_all_image_path(type): + match type: + case "all": + file_path = os.path.join(default_chart_dir, "temperature_trends.png") + case "cpu": + file_path = os.path.join(default_chart_dir, "CPU_temperature.png") + case "fpga": + file_path = os.path.join(default_chart_dir, "FPGA_temperature.png") + case "dimm": + file_path = os.path.join(default_chart_dir, "DIMM_temperature.png") + case "inlet_cpu": + file_path = os.path.join(default_chart_dir, "Inlet_CPU_temperature.png") + case "inlet_fpga": + file_path = os.path.join(default_chart_dir, "Inlet_FPGA_temperature.png") + case "m2": + file_path = os.path.join(default_chart_dir, "M2_Temp_temperature.png") + case "outlet_cpu": + file_path = os.path.join(default_chart_dir, "Outlet_CPU_temperature.png") + case "outlet_fpga": + file_path = os.path.join(default_chart_dir, "Outlet_FPGA_temperature.png") + case "vr_cpu": + file_path = os.path.join(default_chart_dir, "VR_CPU_temperature.png") + case "vr_fpga": + file_path = os.path.join(default_chart_dir, "VR_FPGA_temperature.png") + case _: + file_path = os.path.join(default_chart_dir, "temperature_distribution.png") + return file_path + +def program_main(): + # 寻找日志并合并 + onekeylog_root = os.path.join(cache_dir, "onekeylog") + log_dir = os.path.join(onekeylog_root, "log") + sensor_file0 = os.path.join(log_dir, "sensorhistory.log") + sensor_file1 = os.path.join(log_dir, "sensorhistory.log.1") + sensor_merge = os.path.join(cache_dir, "sensorhistory_merge.log") + + if not os.path.exists(sensor_file0): + print("无温度数据日志文件") + return False + + try: + # 合并文件 + with open(sensor_merge, 'w', encoding='utf-8') as out_f: + # 写入第一个文件内容 + with open(sensor_file0, 'r', encoding='utf-8') as f1: + out_f.write(f1.read()) + out_f.write('\n') # 在两个文件内容之间添加空行分隔 + + if os.path.exists(sensor_file1): + # 写入第二个文件内容 + with open(sensor_file1, 'r', encoding='utf-8') as f2: + out_f.write(f2.read()) + + print(f"成功合并文件到: {sensor_merge}") + + except Exception as e: + print(f"合并文件失败: {str(e)}") + return False + + # 解析数据 + print(f"正在解析数据文件: {sensor_merge}") + df = parse_temperature_data(sensor_merge) + + if df is not None and not df.empty: + print("数据解析成功,包含以下温度点:", ", ".join(df.columns)) + print(f"时间范围: {df.index.min()} 至 {df.index.max()}") + print(f"记录总数: {len(df)}") + + # 生成图表 + generate_temperature_charts(df, default_chart_dir) + else: + print("数据解析失败") + return False + + return True + +def main(): + # 设置命令行参数 + parser = argparse.ArgumentParser(description='分析温度数据文件并生成可视化图表') + parser.add_argument('file_path', help='温度数据文件路径') + parser.add_argument('-o', '--output', default='charts', help='图表输出目录,默认为 "charts"') + args = parser.parse_args() + + # 解析数据 + print(f"正在解析数据文件: {args.file_path}") + df = parse_temperature_data(args.file_path) + + if df is not None and not df.empty: + print("数据解析成功,包含以下温度点:", ", ".join(df.columns)) + print(f"时间范围: {df.index.min()} 至 {df.index.max()}") + print(f"记录总数: {len(df)}") + + # 生成图表 + generate_temperature_charts(df, args.output) + +if __name__ == "__main__": + main() + \ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..d63b1bc --- /dev/null +++ b/src/main.py @@ -0,0 +1,196 @@ +import sys +import os +from PyQt5.QtWidgets import (QApplication, QMainWindow, QFileDialog, QTextBrowser, + QMessageBox, QTextEdit, QGraphicsScene, QGraphicsPixmapItem) +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QPixmap +from PyQt5.QtGui import QPainter # 单独导入QPainter用于抗锯齿设置 +from MainWindow_ui import Ui_MainWindow # 导入转换后的UI类 +import service + + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self): + super().__init__() # 初始化父类 + self.setupUi(self) # 调用UI类的方法初始化界面 + self.init_events() # 绑定事件(自定义方法) + self.parseStatus = service.ServiceStatus() + self.init_graphic_views() + self.init_textbrowser_style() + + self.comboBoxTextDict = { + "ALL_Temp": "all", + "CPU_Temp": "cpu", + "FPGA_Temp": "fpga", + "DIMM_Temp": "dimm", + "Inlet_CPU_Temp": "inlet_cpu", + "Inlet_FPGA_Temp": "inlet_fpga", + "M2_Temp": "m2", + "Outlet_CPU_Temp": "outlet_cpu", + "Outlet_FPGA_Temp": "outlet_fpga", + "VR_CPU_Temp": "vr_cpu", + "VR_FPGA_Temp": "vr_fpga" + } + + def init_textbrowser_style(self): + # 设置样式表(全局文本样式) + self.textBrowser_info.setStyleSheet(""" + QTextBrowser { + font-family: 'SimHei'; + font-size: 14px; + color: #2c3e50; + background-color: #f8f9fa; + border: 1px solid #ddd; + border-radius: 4px; + padding: 8px; + } + """) + + def init_events(self): + """绑定界面元素的事件(如按钮点击、输入框变化等)""" + # 示例:给按钮添加点击事件 + # self.pushButton.clicked.connect(self.on_button_click) # pushButton是UI中定义的按钮对象名 + + # 绑定上传菜单项事件 + self.actionUpload_log.triggered.connect(self.upload_file) + self.comboBox.currentTextChanged.connect(self.on_combo_changed) + + def init_graphic_views(self): + # 初始化图形场景和视图 + self.scene = QGraphicsScene(self) # 创建场景 + self.graphicsView.setScene(self.scene) # 将场景绑定到视图 + self.graphicsView.setRenderHint(QPainter.Antialiasing) # 抗锯齿 + self.graphicsView.setRenderHint(QPainter.SmoothPixmapTransform) # 平滑缩放 + + # 初始化图形场景和视图 + # self.scene_temp_cpu = QGraphicsScene(self) # 创建场景 + # self.graphicsView_temp_cpu.setScene(self.scene_temp_cpu) # 将场景绑定到视图 + # self.graphicsView_temp_cpu.setRenderHint(QPainter.Antialiasing) # 抗锯齿 + # self.graphicsView_temp_cpu.setRenderHint(QPainter.SmoothPixmapTransform) # 平滑缩放 + + # 存储当前显示的图片项 + self.pixmap_item = None + self.current_image_path = None + + def upload_file(self): + """打开文件选择对话框并处理选中的文件""" + # 打开文件选择对话框 + # 参数说明: + # - self:父窗口 + # - "选择文件":对话框标题 + # - os.getcwd():默认打开路径(当前工作目录) + # - "所有文件 (*);;文本文件 (*.txt);;图片文件 (*.png *.jpg)":文件筛选器 + file_path, file_type = QFileDialog.getOpenFileName( + self, + "选择文件", + os.getcwd(), + "所有文件 (*);;日志压缩文件 (*.gz)" + ) + + # 判断用户是否选择了文件(取消选择时file_path为空) + if file_path: + self.process_uploaded_file(file_path) + # else: + # self.text_edit.append("已取消文件选择") + + def process_uploaded_file(self, file_path): + """处理上传的文件(这里仅展示文件信息,可根据需求扩展)""" + try: + # 获取文件信息 + file_name = os.path.basename(file_path) + file_size = os.path.getsize(file_path) + file_size_kb = file_size / 1024 # 转换为KB + + # 显示文件信息 + info = f"已上传文件:\n" + info += f"文件名:{file_name}\n" + info += f"路径:{file_path}\n" + info += f"大小:{file_size_kb:.2f} KB\n" + info += "-" * 50 + "\n" + + self.textBrowser_console.insertPlainText(info) + + # 这里可以添加实际的文件处理逻辑: + service.send_log_to_cache(file_path) + service.start_diagnose(self.parseStatus) + + if self.parseStatus.baseinfo_status: + baseinfo_str = service.get_baseinfo_str() + self.textBrowser_info.insertPlainText(baseinfo_str) + + if self.parseStatus.sensorhistory_status: + self.display_pic(service.get_sensorhistory_path("all")) + + self.textBrowser_console.insertPlainText("完成文件解析\n") + + except Exception as e: + QMessageBox.critical(self, "处理失败", f"文件处理出错:{str(e)}") + + def on_combo_changed(self, selected_text): + """下拉列表选项变化时切换图片""" + self.display_pic(service.get_sensorhistory_path(self.comboBoxTextDict[selected_text])) + + + def display_pic(self, image_path): + """在QGraphicsView中显示图片""" + try: + # 清空场景中已有的内容 + self.scene.clear() + + # 加载图片并创建图形项 + pixmap = QPixmap(image_path) + if pixmap.isNull(): + raise Exception("无法加载图片文件") + + # 创建图片项并添加到场景 + self.pixmap_item = QGraphicsPixmapItem(pixmap) + self.scene.addItem(self.pixmap_item) + + # 初始显示时让图片居中 + self.graphicsView.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) + + except Exception as e: + QMessageBox.critical(self, "错误", f"显示图片失败:{str(e)}") + + def zoom_with_mouse_wheel(self, event): + """鼠标滚轮缩放图片""" + if self.pixmap_item: # 只有存在图片时才允许缩放 + # 获取当前鼠标位置作为缩放中心 + mouse_pos = event.pos() + scene_pos = self.graphicsView.mapToScene(mouse_pos) + + # 缩放因子(滚轮向前放大,向后缩小) + scale_factor = 1.1 if event.angleDelta().y() > 0 else 0.9 + + # 缩放视图 + self.graphicsView.scale(scale_factor, scale_factor) + + # 缩放后将鼠标位置保持在原场景位置(避免缩放时画面跳动) + new_mouse_pos = self.graphicsView.mapFromScene(scene_pos) + delta = new_mouse_pos - mouse_pos + self.graphicsView.horizontalScrollBar().setValue( + self.graphicsView.horizontalScrollBar().value() - delta.x() + ) + self.graphicsView.verticalScrollBar().setValue( + self.graphicsView.verticalScrollBar().value() - delta.y() + ) + else: + # 若无图片,忽略滚轮事件 + super(type(self.graphicsView), self.graphicsView).wheelEvent(event) + + def resizeEvent(self, event): + """窗口大小改变时,自适应调整图片显示""" + if self.pixmap_item and not self.graphicsView.transform().isIdentity(): + # 仅在未手动缩放时自动适应窗口 + self.graphicsView.fitInView(self.scene.sceneRect(), Qt.KeepAspectRatio) + super().resizeEvent(event) + +if __name__ == "__main__": + service.app_cache_init() + # 创建应用实例 + app = QApplication(sys.argv) + # 创建主窗口并显示 + window = MainWindow() + window.show() + # 进入应用主循环 + sys.exit(app.exec_()) \ No newline at end of file diff --git a/src/service.py b/src/service.py new file mode 100644 index 0000000..407fa8c --- /dev/null +++ b/src/service.py @@ -0,0 +1,48 @@ +import sys +import os +import utils +import ZiJin_parse_sensorhistory as sensorparse +import ZiJin_parse_baseinfo as baseinfo + +class ServiceStatus(): + def __init__(self): + self.sensorhistory_status = False + self.baseinfo_status = False + + def set_sensorhistory_status(self, status): + self.sensorhistory_status = status + + def get_sensorhistory_status(self): + return self.sensorhistory_status + + def set_baseinfo_status(self, status): + self.baseinfo_status = status + + def get_baseinfo_status(self): + return self.baseinfo_status + +def app_cache_init(): + project_root = utils.get_project_root() + cache_dir = os.path.join(project_root, "tmp") + utils.clean_log_data(cache_dir) + + os.mkdir(cache_dir) + pass + +def send_log_to_cache(filepath): + project_root = utils.get_project_root() + cache_dir = os.path.join(project_root, "tmp") + utils.unzip_log(filepath, cache_dir) + +def get_sensorhistory_path(type): + return sensorparse.get_sensor_all_image_path(type) + +def get_baseinfo_str(): + return baseinfo.get_all_infostring() + +def start_diagnose(parseStatus): + result_sensorhistory = sensorparse.program_main() + parseStatus.set_sensorhistory_status(result_sensorhistory) + + result_baseinfo = baseinfo.program_main() + parseStatus.set_baseinfo_status(result_baseinfo) \ No newline at end of file diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..1d224cd --- /dev/null +++ b/src/utils.py @@ -0,0 +1,136 @@ +import os +import tarfile +import shutil +import stat + +def get_project_root(): + current_file = os.path.abspath(__file__) + project_root = os.path.dirname(os.path.dirname(current_file)) + return project_root + +def unzip_log(tar_path, extract_path='.'): + """ + 解压 tar.gz 文件到指定目录 + + 参数: + tar_path (str): tar.gz 文件的路径 + extract_path (str): 解压目标目录,默认为当前目录 + """ + try: + # 检查文件是否存在 + if not os.path.exists(tar_path): + raise FileNotFoundError(f"文件不存在: {tar_path}") + + # 创建解压目录(如果不存在) + os.makedirs(extract_path, exist_ok=True) + + # 打开 tar.gz 文件并解压 + with tarfile.open(tar_path, "r:gz") as tar: + # 列出所有文件(可选) + print(f"解压文件列表:") + for member in tar.getmembers(): + print(f"- {member.name}") + + # 解压所有文件到目标目录 + tar.extractall(path=extract_path) + print(f"\n成功解压到: {os.path.abspath(extract_path)}") + + except tarfile.TarError as e: + print(f"tar 文件处理错误: {e}") + except Exception as e: + print(f"解压失败: {e}") + +def remove_readonly(func, path, excinfo): + """用于处理删除只读文件的错误回调函数""" + # 尝试修改文件权限 + os.chmod(path, stat.S_IWRITE) + # 再次尝试删除 + func(path) + +def clean_log_data(path): + """删除目录,处理权限问题""" + if not os.path.exists(path): + print(f"目录不存在: {path}") + return + + try: + # 方法1: 使用onerror回调处理权限问题 + shutil.rmtree(path, onerror=remove_readonly) + print(f"成功删除目录: {path}") + + except Exception as e: + print(f"删除目录时出错: {e}") + # 方法2: 先递归修改权限再删除(备选方案) + try: + # 递归修改目录权限 + for root, dirs, files in os.walk(path): + for dir in dirs: + dir_path = os.path.join(root, dir) + os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + for file in files: + file_path = os.path.join(root, file) + os.chmod(file_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) + + # 修改权限后再次尝试删除 + shutil.rmtree(path) + print(f"通过修改权限成功删除目录: {path}") + except Exception as e2: + print(f"修改权限后仍无法删除目录: {e2}") + +def read_specific_line(file_path, line_number): + """ + 读取文件中指定行的内容(行号从1开始) + """ + try: + with open(file_path, 'r', encoding='utf-8') as file: + for current_line, content in enumerate(file, 1): # 从1开始计数 + if current_line == line_number: + return content.strip() # strip() 去除换行符和空格 + # 若行号超出文件总行数,返回None + return None + except FileNotFoundError: + print(f"错误:文件 '{file_path}' 不存在") + return None + +def append_to_file(file_path, content): + """ + 以追加模式将字符串写入文件 + + 参数: + file_path (str): 文件路径 + content (str): 要写入的字符串内容 + """ + try: + # 打开文件,模式为 'a'(追加),编码指定为 utf-8 以支持中文 + with open(file_path, 'a', encoding='utf-8') as file: + # 写入内容(可根据需要添加换行符 '\n') + file.write(content + '\n') # 加 '\n' 使每次写入占一行 + print(f"内容已成功追加到文件:{file_path}") + except Exception as e: + print(f"写入文件失败:{str(e)}") + +def read_file_to_string(file_path): + """ + 打开文件并将全部内容读取到一个字符串中 + + 参数: + file_path (str): 要读取的文件路径 + 返回: + str: 文件内容字符串;若读取失败则返回 None + """ + try: + # 使用 with 语句打开文件(自动处理关闭) + # 'r' 表示只读模式,encoding='utf-8' 确保中文正常读取 + with open(file_path, 'r', encoding='utf-8') as file: + # read() 方法读取全部内容并返回字符串 + content = file.read() + return content + except FileNotFoundError: + print(f"错误:文件 '{file_path}' 不存在") + except PermissionError: + print(f"错误:没有权限读取文件 '{file_path}'") + except UnicodeDecodeError: + print(f"错误:文件 '{file_path}' 不是 UTF-8 编码,无法读取") + except Exception as e: + print(f"读取文件失败:{str(e)}") + return None \ No newline at end of file