diff --git a/UI/Main/Main.py b/UI/Main/Main.py index 86d497e..dbfed97 100644 --- a/UI/Main/Main.py +++ b/UI/Main/Main.py @@ -207,7 +207,7 @@ class MainWindow(QMainWindow): def reFreshTrendWidget(self): if Globals.getValue('currentPro') == -1: return - self.trendWidget.getMems() + # self.trendWidget.getMems() self.exButtonClicked(2) def initProcedureDB(self): diff --git a/UI/TrendManage/ActualTrendWidget.py b/UI/TrendManage/ActualTrendWidget.py index 8d16d56..a1b02b3 100644 --- a/UI/TrendManage/ActualTrendWidget.py +++ b/UI/TrendManage/ActualTrendWidget.py @@ -9,88 +9,77 @@ from matplotlib.figure import Figure from protocol.Celery.MBTCPMaster import app as MBTCPMApp from celery.result import AsyncResult -from utils.Queue import Queue from model.ProjectModel.VarManage import ModbusVarManage +from utils import Globals class ActualTrend(QtWidgets.QMainWindow): def __init__(self, parent=None, varName = None): super(ActualTrend, self).__init__(parent) self.varName = varName + self.protocolManage = Globals.getValue('protocolManage') _main = QtWidgets.QWidget() self.setCentralWidget(_main) layout = QtWidgets.QVBoxLayout(_main) - dynamic_canvas = FigureCanvas(Figure(figsize=(5, 3))) layout.addWidget(dynamic_canvas) self.addToolBar(QtCore.Qt.BottomToolBarArea, NavigationToolbar(dynamic_canvas, self)) - self.x = [] #建立空的x轴数组和y轴数组 + self.x = [] # 建立空的x轴数组和y轴数组 self.y = [] - - self.xQueue = Queue(length = 20) - self.yQueue = Queue(length = 20) - # self.n = 0 + self.maxPoints = 200 self._dynamic_ax = dynamic_canvas.figure.subplots() self._timer = dynamic_canvas.new_timer( - 1000, [(self._update_canvas, (), {})]) + 500, [(self._update_canvas, (), {})]) self._timer.start() - - def _update_canvas(self): - # self.n += 1 + # 防止canvas已被销毁时报错 + if not hasattr(self, '_dynamic_ax') or not hasattr(self._dynamic_ax, 'figure') or not hasattr(self._dynamic_ax.figure, 'canvas'): + return varName = self.varName - varMM = ModbusVarManage.getByName(varName) - - - self._dynamic_ax.clear() + if not varName or not self.protocolManage: + return + # 获取实时值 + yValue = self.protocolManage.readVariableValue(varName) acttime = time.strftime('%H:%M:%S', time.localtime(time.time())) - # print(acttime) - if varName != '': - if MBTCPMApp.backend.get('ModBus') is not None: - uid = MBTCPMApp.backend.get('ModBus').decode('utf-8') - else: - return - # print(uid) - res = AsyncResult(uid) # 参数为task id - if res.result: - # print(res.result, res.date_done) - try: - yValue = (res.result[varName]) - self.yQueue.enqueue(yValue) - self.xQueue.enqueue(acttime) - except Exception as e: - print(e) - - if not varMM[-2] and not varMM[-1]: - minY = min(self.yQueue.list) - 5 - maxY = max(self.yQueue.list) + 5 - else: - minY = float(varMM[-2]) - float(5) - maxY = float(varMM[-1]) + float(5) - - name_list = self.xQueue.list - value_list = np.array(self.yQueue.list) - pos_list = np.arange(len(name_list)) - + if yValue is not None: + self.x.append(acttime) + self.y.append(yValue) + # 只保留最新maxPoints个点 + if len(self.x) > self.maxPoints: + self.x = self.x[-self.maxPoints:] + self.y = self.y[-self.maxPoints:] + self._dynamic_ax.clear() + pos_list = np.arange(len(self.x)) self._dynamic_ax.set_xticks(pos_list) - self._dynamic_ax.set_xticklabels(name_list) - - self._dynamic_ax.plot(pos_list, value_list, 'bo-', linewidth=2) - # self._dynamic_ax.set_xlim(0,10) - self._dynamic_ax.set_ylim(minY, maxY) - # print(id(self._dynamic_ax)) + # 限制最多显示16个主刻度,自动旋转,字体更大更明显 + import matplotlib.ticker as ticker + max_xticks = 12 + if len(self.x) > max_xticks: + step = max(1, len(self.x) // max_xticks) + display_labels = [label if i % step == 0 else '' for i, label in enumerate(self.x)] + else: + display_labels = self.x + self._dynamic_ax.set_xticklabels(display_labels, rotation=30, fontsize=13, fontweight='bold') + self._dynamic_ax.plot(pos_list, self.y, 'bo-', linewidth=2) + if self.y: + minY = min(self.y) - 5 + maxY = max(self.y) + 5 + self._dynamic_ax.set_ylim(minY, maxY) + from matplotlib.ticker import MaxNLocator + self._dynamic_ax.yaxis.set_major_locator(MaxNLocator(nbins=12, prune=None)) self._dynamic_ax.figure.canvas.draw() def closeEvent(self, event): - # 判断是否点击了窗口的关闭按钮 - if event.type() == event.Close: + # 停止定时器,防止回调访问已销毁的canvas + if hasattr(self, '_timer'): self._timer.stop() - self.hide() + super().closeEvent(event) + self.hide() def showEvent(self, event): self._timer.start() diff --git a/UI/TrendManage/TrendWidget.py b/UI/TrendManage/TrendWidget.py index c4e1502..97070d4 100644 --- a/UI/TrendManage/TrendWidget.py +++ b/UI/TrendManage/TrendWidget.py @@ -1,161 +1,701 @@ from PyQt5 import QtCore, QtGui, QtWidgets -from PyQt5.QtCore import QSize, Qt -from PyQt5.Qt import * +from PyQt5.QtCore import QSize, Qt, QTimer, QEvent from PyQt5.QtGui import QPixmap, QIcon -from PyQt5.QtWidgets import QHBoxLayout, QAbstractItemView, QTableView, QVBoxLayout, QSplitter, \ - QApplication, QGroupBox, QLabel, QGridLayout, QLineEdit, QComboBox, QTextEdit, QCheckBox,QVBoxLayout,QListView, QMainWindow -from PyQt5.QtWidgets import QListWidget, QStackedWidget, QFrame -from PyQt5.QtWidgets import QListWidgetItem, QSizePolicy -from PyQt5.QtWidgets import QWidget, QSpacerItem, QHeaderView - -from PyQt5.QtWebEngineWidgets import QWebEngineView -from PyQt5.QtWebEngineWidgets import QWebEnginePage - -from utils.DBModels.ProtocolModel import InfluxMem +from PyQt5.QtWidgets import (QApplication, QGridLayout, QListWidget, QListWidgetItem, + QToolBar, QAction, QLabel, QWidget, QVBoxLayout, QHBoxLayout, + QSplitter, QGroupBox, QLineEdit, QComboBox, QTextEdit, + QCheckBox, QFrame, QSpacerItem, QSizePolicy, QHeaderView, QToolButton) +from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage from utils import Globals -from model.HistoryDBModel.HistoryDBManage import HistoryDBManage - -import datetime - -import pyecharts.options as opts -from pyecharts.charts import Line - +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt import NavigationToolbar2QT as NavigationToolbar +from matplotlib.figure import Figure import numpy as np -from Static import static - -class HistoryTrend(object): - def __init__(self, view): - self.line = Line(init_opts= opts.InitOpts(width = str(view.size().width() - 20) + 'px', height = str(view.size().height() - 30) + 'px')) - self.setOpts() - - def addXAxis(self, xAxis): - self.line.add_xaxis(xaxis_data=xAxis) - - def addYAxis(self, lineName, yAxis): - self.line.add_yaxis( - series_name=lineName, - y_axis=yAxis, - markpoint_opts=opts.MarkPointOpts( - data=[ - opts.MarkPointItem(type_="max", name="最大值"), - opts.MarkPointItem(type_="min", name="最小值"), - ] - ), - markline_opts=opts.MarkLineOpts( - data=[opts.MarkLineItem(type_="average", name="平均值")] - ), - ) - - def setOpts(self): - self.line.set_global_opts( - title_opts=opts.TitleOpts(title="趋势图"), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - toolbox_opts=opts.ToolboxOpts(is_show=True), - xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False), - datazoom_opts=[ - opts.DataZoomOpts(xaxis_index=0, range_start=0, range_end=100), - ], - ) - - @property - def html(self): - return self.line.render_embed() - +import datetime +import sys +import pandas as pd +import bisect +from matplotlib.widgets import RectangleSelector +import matplotlib.patches as patches +from matplotlib.widgets import SpanSelector +from matplotlib.gridspec import GridSpec + +# 配置中文字体支持 +plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans'] +plt.rcParams['axes.unicode_minus'] = False class TrendWidgets(QWidget): - def __init__(self, parent=None): - super(TrendWidgets, self).__init__(parent) - # self.setAttribute(Qt.WA_StyledBackground, True) # 移除无效属性 - self.gridLayout = QtWidgets.QGridLayout(self) - self.gridLayout.setObjectName("gridLayout") - + def __init__(self): + super().__init__() + self.setWindowTitle("历史趋势浏览器") + self.setMinimumSize(120, 120) + # 设置窗口标志,禁用窗口拖拽 + self.setWindowFlags(QtCore.Qt.WindowType.Window) + # 初始化数据 + self.historyDB = None + self.currentVarName = None + self.xData = [] + self.yData = [] + # 多变量支持 + self.multiVarData = {} # 存储多个变量的数据 {varName: {'x': [], 'y': []}} + self.selectedVars = [] # 当前选中的变量列表 + self.varColors = {} # 存储变量颜色 {varName: color} + # 初始化历史缩放记录 + self.history = [] + self.historyIndex = 0 + # 主布局 + mainLayout = QHBoxLayout(self) + mainLayout.setSpacing(10) + mainLayout.setContentsMargins(10, 10, 10, 10) + # 左侧变量列表区域 + self.createVariableListPanel() + mainLayout.addWidget(self.variableListGroup, 1) # 1份宽度 + # 右侧趋势图区域 + self.trendViewerGroup = QWidget() + self.createTrendViewerPanel() + mainLayout.addWidget(self.trendViewerGroup, 6) # 6份宽度 + # 连接信号 + self.connectSignals() + Globals.setValue('HistoryWidget', self) + + def createVariableListPanel(self): + # 创建变量列表面板 + self.variableListGroup = QGroupBox("变量列表") + layout = QVBoxLayout(self.variableListGroup) + # 搜索框单独一行 + searchInputLayout = QHBoxLayout() + self.searchInput = QLineEdit() + self.searchInput.setPlaceholderText("搜索变量...") + self.searchInput.textChanged.connect(self.filterVarList) + self.searchInput.setMinimumWidth(220) + searchInputLayout.addWidget(self.searchInput) + layout.addLayout(searchInputLayout) # 变量列表 self.varListWidget = QListWidget() - self.varListWidget.setObjectName("varListWidget") self.varListWidget.itemDoubleClicked.connect(self.showVarTrend) - self.gridLayout.addWidget(self.varListWidget, 0, 0, 15, 4) - - # 趋势图WebView - self.trendWebView = QWebEngineView() - self.trendWebView.setObjectName("trendWebView") - self.gridLayout.addWidget(self.trendWebView, 0, 4, 15, 22) - - self.gridLayout.setSpacing(10) - self.gridLayout.setContentsMargins(20, 20, 20, 20) - Globals.setValue('HistoryWidget', self) - + self.varListWidget.itemSelectionChanged.connect(self.onVarSelected) + layout.addWidget(self.varListWidget) + # 时间选择+便捷查询区域 + searchLayout = QHBoxLayout() + from PyQt5.QtCore import QDateTime + self.startTimeEdit = QtWidgets.QDateTimeEdit() + self.startTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss") + self.startTimeEdit.setCalendarPopup(True) + self.endTimeEdit = QtWidgets.QDateTimeEdit() + self.endTimeEdit.setDisplayFormat("yyyy-MM-dd HH:mm:ss") + self.endTimeEdit.setCalendarPopup(True) + now = QDateTime.currentDateTime() + self.startTimeEdit.setDateTime(now.addDays(-1)) + self.endTimeEdit.setDateTime(now) + searchLayout.addWidget(self.startTimeEdit) + searchLayout.addWidget(self.endTimeEdit) + self.quickRangeCombo = QtWidgets.QComboBox() + self.quickRangeCombo.addItems(["最近一天", "最近6小时", "最近1小时", "最近30分钟", "自定义"]) + self.quickRangeCombo.currentIndexChanged.connect(self.onQuickRangeChanged) + searchLayout.addWidget(self.quickRangeCombo) + self.queryBtn = QToolButton() + self.queryBtn.setText("查询") + self.queryBtn.setToolTip("按时间范围查询变量数据") + self.queryBtn.clicked.connect(self.onTimeRangeQuery) + searchLayout.addWidget(self.queryBtn) + layout.addLayout(searchLayout) + # 按钮区域 + buttonLayout = QHBoxLayout() + # 刷新按钮 + refreshBtn = QToolButton() + refreshBtn.setText("刷新列表") + refreshBtn.clicked.connect(self.refreshVarList) + buttonLayout.addWidget(refreshBtn) + # 添加到趋势图按钮 + addToTrendBtn = QToolButton() + addToTrendBtn.setText("添加到趋势图") + addToTrendBtn.setToolTip("将选中的变量添加到趋势图") + addToTrendBtn.clicked.connect(self.addSelectedVarsToTrend) + buttonLayout.addWidget(addToTrendBtn) + # 清除所有变量按钮 + clearAllBtn = QToolButton() + clearAllBtn.setText("清除所有") + clearAllBtn.setToolTip("清除趋势图中的所有变量") + clearAllBtn.clicked.connect(self.clearAllVars) + buttonLayout.addWidget(clearAllBtn) + layout.addLayout(buttonLayout) + self.variableListGroup.setLayout(layout) + # 记录当前时间范围 + self._query_time_range = (self.startTimeEdit.dateTime().toPyDateTime(), self.endTimeEdit.dateTime().toPyDateTime()) + + def createTrendViewerPanel(self): + # 创建趋势图查看器面板 + layout = QVBoxLayout(self.trendViewerGroup) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + # 创建包含图表的组框 + chartGroup = QGroupBox() + chartLayout = QVBoxLayout(chartGroup) + chartLayout.setContentsMargins(5, 5, 5, 5) + # 创建matplotlib图表,设置主图和缩略图比例为6:1 + self.figure = Figure(figsize=(12, 8), dpi=100) + gs = GridSpec(7, 1, figure=self.figure) + self.ax_main = self.figure.add_subplot(gs[:6, 0]) # 主图,占6份 + self.ax_overview = self.figure.add_subplot(gs[6, 0]) # 缩略图,占1份 + self.canvas = FigureCanvas(self.figure) + chartLayout.addWidget(self.canvas, 20) + # 底部信息栏(含状态显示) + infoLayout = QHBoxLayout() + infoLayout.setContentsMargins(5, 5, 5, 5) + self.varNameLabel = QLabel("当前变量: 无") + self.dataCountLabel = QLabel("数据点: 0") + self.timeRangeLabel = QLabel("时间范围: 无数据") + self.statusLabel = QLabel("就绪") + self.statusLabel.setFont(QtGui.QFont("Arial", 9)) + self.statusLabel.setStyleSheet("color: #555; padding: 2px;") + infoLayout.addWidget(self.varNameLabel) + infoLayout.addStretch() + infoLayout.addWidget(self.dataCountLabel) + infoLayout.addStretch() + infoLayout.addWidget(self.timeRangeLabel) + infoLayout.addStretch() + infoLayout.addWidget(self.statusLabel) + chartLayout.addLayout(infoLayout, 1) + layout.addWidget(chartGroup) + # 悬浮信息气泡label + self.infoBubble = QLabel(self.trendViewerGroup) + self.infoBubble.setStyleSheet("background:rgba(255,255,220,0.95); border:1px solid #aaa; border-radius:4px; padding:4px; color:#222;") + self.infoBubble.setFont(QtGui.QFont("Consolas", 10)) + self.infoBubble.setVisible(False) + # 连接鼠标事件,支持拖拽平移 + self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move) + self.canvas.mpl_connect('axes_leave_event', self.on_mouse_leave) + self.canvas.mpl_connect('scroll_event', self.on_mouse_wheel) + self.canvas.mpl_connect('button_press_event', self.on_mouse_press) + self.canvas.mpl_connect('button_release_event', self.on_mouse_release) + # 拖拽状态 + self._is_panning = False + self._pan_start_x = None + self._pan_start_xlim = None + # 初始化选择器 + self.selector = None + self.is_selecting = False + self.zoom_start = None + # 添加SpanSelector用于区间选择 + self.span = SpanSelector( + self.ax_overview, self.on_select, 'horizontal', + useblit=True, interactive=True, props=dict(alpha=0.3, facecolor='orange') + ) + # 绑定双击事件恢复全局视图 + self.canvas.mpl_connect('button_press_event', self.on_overview_double_click) + self._selected_range = None # 记录选中区间 + # 设置下方缩略图x轴显示更多时间内容 + import matplotlib.dates as mdates + from matplotlib.ticker import MaxNLocator + self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator()) + self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M')) + + def connectSignals(self): + # 连接所有信号 + self.ax_overview.callbacks.connect('xlim_changed', self.on_overview_xlim_changed) def exchangeProject(self): + # 项目切换时调用 self.historyDB = Globals.getValue('historyDBManage') self.refreshVarList() - - def deleteMem(self): - pass # 历史mem相关功能已废弃 - - def itemCheckstate(self): - pass # 历史mem相关功能已废弃 - - @QtCore.pyqtSlot(str) - def on_lineEdit_textChanged(self, text): - pass # 历史mem相关功能已废弃 - - def listContext(self, position): - pass # 历史mem相关功能已废弃 - - def delVarRecard(self): - pass # 历史mem相关功能已废弃 - - - def getMems(self): - pass # 历史mem相关功能已废弃 - - def refreshList(self, index): - pass # 历史mem相关功能已废弃 - - def createHtml(self, varNames): - pass # 历史mem相关功能已废弃 - + def refreshVarList(self): - # 显示进度条 + # 刷新变量列表 self.varListWidget.clear() - varNames = [] - try: - sql = f'SELECT DISTINCT("varName") FROM "{self.historyDB.table}"' - df = self.historyDB.client.query(sql, mode="pandas") - import pandas as pd - if isinstance(df, pd.DataFrame) and 'varName' in df.columns: - varNames = df['varName'].tolist() - except Exception as e: - print(f"获取变量名失败: {e}") - varNames = [] + self.statusLabel.setText("正在加载变量列表...") + if not self.historyDB or not hasattr(self.historyDB, 'getAllVarNames'): + self.statusLabel.setText("未连接历史数据库,无法获取变量列表") + return + varNames = self.historyDB.getAllVarNames() + # print(varNames) + # 对变量名称进行排序 + varNames.sort(key=lambda x: x.lower()) + # 添加排序后的变量到列表 for name in varNames: - item = QListWidgetItem(str(name)) + item = QListWidgetItem(name) self.varListWidget.addItem(item) - - - def showVarTrend(self, item): - varName = item.text() - # 查询该变量历史数据 - data, timeList = self.historyDB.queryVarHistory(varName) - if not data or not timeList: - self.trendWebView.setHtml('

无历史数据

') + self.statusLabel.setText(f"已加载 {len(varNames)} 个变量(已排序)") + + def sortVarList(self): + # 手动排序变量列表 + try: + varNames = [] + for i in range(self.varListWidget.count()): + item = self.varListWidget.item(i) + if item and not item.isHidden(): + varNames.append(item.text()) + varNames.sort(key=lambda x: x.lower()) + self.varListWidget.clear() + for name in varNames: + item = QListWidgetItem(name) + self.varListWidget.addItem(item) + self.statusLabel.setText(f"已排序 {len(varNames)} 个变量") + except Exception as e: + self.statusLabel.setText(f"排序失败: {str(e)}") + + def addSelectedVarsToTrend(self): + # 将选中的变量添加到趋势图 + selectedItems = self.varListWidget.selectedItems() + if not selectedItems: + self.statusLabel.setText("请先选择要添加的变量") return - # 构建趋势图 - line = Line(init_opts=opts.InitOpts(width='900px', height='500px')) - line.add_xaxis(timeList) - line.add_yaxis(varName, data) - line.set_global_opts( - title_opts=opts.TitleOpts(title=f"{varName} 历史趋势"), - tooltip_opts=opts.TooltipOpts(trigger="axis"), - xaxis_opts=opts.AxisOpts(type_="category", boundary_gap=False), - datazoom_opts=[opts.DataZoomOpts(xaxis_index=0, range_start=0, range_end=100)], + + addedCount = 0 + for item in selectedItems: + varName = item.text() + if varName not in self.selectedVars: + self.selectedVars.append(varName) + self.loadVarData(varName) + addedCount += 1 + if addedCount > 0: + self.updateMultiVarChart() + self.statusLabel.setText(f"已添加 {addedCount} 个变量到趋势图") + else: + self.statusLabel.setText("选中的变量已在趋势图中") + + def onTimeRangeQuery(self): + # 查询按钮点击,记录时间范围 + self._query_time_range = ( + self.startTimeEdit.dateTime().toPyDateTime(), + self.endTimeEdit.dateTime().toPyDateTime() ) - html = line.render_embed() - page = QWebEnginePage(self.trendWebView) - page.setHtml('\n\n' + html) - self.trendWebView.setPage(page) - self.trendWebView.reload() - - - - + import datetime + # 重新加载所有已选变量的数据 + for varName in self.selectedVars: + start_time, end_time = self._query_time_range + if isinstance(start_time, datetime.datetime): + start_time = start_time.isoformat() + if isinstance(end_time, datetime.datetime): + end_time = end_time.isoformat() + if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'): + data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time) + xData = [] + for t in timeList: + dt = None + if isinstance(t, datetime.datetime): + dt = t + elif hasattr(t, 'to_pydatetime'): + dt = t.to_pydatetime() + elif isinstance(t, str): + try: + dt = datetime.datetime.fromisoformat(t) + except: + continue + if dt: + xData.append(dt) + self.multiVarData[varName] = {'x': xData, 'y': data} + self.updateMultiVarChart() + self.statusLabel.setText(f"已设置时间范围: {self._query_time_range[0].strftime('%Y-%m-%d %H:%M:%S')} ~ {self._query_time_range[1].strftime('%Y-%m-%d %H:%M:%S')}") + + def loadVarData(self, varName): + # 加载单个变量的数据,按选定时间范围 + try: + self.statusLabel.setText(f"加载 {varName} 数据...") + import datetime + if hasattr(self, '_query_time_range') and self._query_time_range: + start_time, end_time = self._query_time_range + else: + start_time = None + end_time = None + # 从数据库获取数据 + if self.historyDB and hasattr(self.historyDB, 'queryVarHistory'): + print(start_time, end_time) + data, timeList = self.historyDB.queryVarHistory(varName, start_time, end_time) + # 直接使用数据库返回的时间,不做任何时区转换 + xData = [] + for t in timeList: + if isinstance(t, datetime.datetime): + xData.append(t) + elif isinstance(t, pd.Timestamp): + xData.append(t.to_pydatetime()) + elif isinstance(t, str): + try: + # 直接按本地时间字符串解析 + xData.append(datetime.datetime.fromisoformat(t)) + except: + xData.append(datetime.datetime.now()) + else: + xData.append(datetime.datetime.now()) + # 存储数据 + self.multiVarData[varName] = {'x': xData, 'y': data} + # 分配颜色 + if varName not in self.varColors: + colors = ['blue', 'red', 'green', 'cyan', 'magenta', 'yellow', 'black'] + colorIndex = len(self.varColors) % len(colors) + self.varColors[varName] = colors[colorIndex] + except Exception as e: + self.statusLabel.setText(f"加载 {varName} 数据失败: {str(e)}") + + def updateMultiVarChart(self): + import matplotlib.dates as mdates + # 更新多变量图表 + self.ax_main.clear() + self.ax_overview.clear() + # 设置网格 + self.ax_main.grid(True, alpha=0.3) + self.ax_overview.grid(True, alpha=0.3) + # 复用数据处理逻辑,提升性能 + for varName in self.selectedVars: + if varName in self.multiVarData: + data = self.multiVarData[varName] + if data['x'] and data['y']: + # 处理断线数据(复用逻辑) + x_processed, y_processed = self._process_break_line_data(data['x'], data['y']) + + # 缩略图:显示全量数据 + self.ax_overview.plot(x_processed, y_processed, color=self.varColors[varName], linewidth=1, alpha=0.7) + + # 主图:根据选中区间显示数据 + if self._selected_range is not None: + xlim = self._selected_range + # 筛选区间数据 + x_num = [mdates.date2num(xx) for xx in x_processed if xx is not None] + x_filtered, y_filtered = [], [] + for xx, yy in zip(x_processed, y_processed): + if xx is not None: + xn = mdates.date2num(xx) + if xlim[0] <= xn <= xlim[1]: + x_filtered.append(xx) + y_filtered.append(yy) + x_main, y_main = x_filtered, y_filtered + else: + # 没有选择范围时,主图显示全部数据 + x_main, y_main = x_processed, y_processed + + self.ax_main.plot(x_main, y_main, color=self.varColors[varName], linewidth=2, marker='o', markersize=4, label=varName) + # 设置时间格式 + self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + # 增加主图和缩略图的坐标轴刻度数量,并显示完整日期时间 + from matplotlib.ticker import MaxNLocator + self.ax_main.xaxis.set_major_locator(mdates.AutoDateLocator()) + self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S')) + self.ax_main.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None)) + self.ax_overview.xaxis.set_major_locator(mdates.AutoDateLocator()) + self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d %H:%M:%S')) + self.ax_overview.yaxis.set_major_locator(MaxNLocator(nbins=16, prune=None)) + # 刻度数量更多 + self.ax_main.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None)) + self.ax_overview.xaxis.set_major_locator(MaxNLocator(nbins=16, prune=None)) + # 设置标签 + self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold') + self.ax_main.set_ylabel('数值') + self.ax_overview.set_xlabel('时间') + self.ax_overview.set_ylabel('数值') + # 添加图例 + if self.selectedVars: + self.ax_main.legend(loc='upper right') + # 自动调整布局 + self.figure.tight_layout() + # 刷新画布 + self.canvas.draw() + + def _process_break_line_data(self, x_raw, y_raw): + """处理断线数据:间隔大于2分钟的点不连线""" + x, y = [], [] + prev_time = None + for xi, yi in zip(x_raw, y_raw): + if prev_time is not None and (xi - prev_time).total_seconds() > 60: + # 断线时只在y中插入None,x继续插入当前时间戳 + x.append(xi) + y.append(None) + x.append(xi) + y.append(yi) + prev_time = xi + return x, y + + def filterVarList(self, text): + # 本地过滤变量列表 + for i in range(self.varListWidget.count()): + item = self.varListWidget.item(i) + if item is not None: + item.setHidden(text.lower() not in item.text().lower()) + + def onVarSelected(self): + # 当选中一个变量时(非双击) + selectedItems = self.varListWidget.selectedItems() + if selectedItems: + self.statusLabel.setText(f"已选择: {selectedItems[0].text()}") + + def showVarTrend(self, item): + # 显示选中变量的趋势图(与多变量流程一致) + varName = item.text() + self.currentVarName = varName + self.statusLabel.setText(f"加载 {varName} 历史趋势...") + # 清空多变量数据,只保留当前变量 + self.selectedVars = [varName] + self.multiVarData = {} + self.varColors = {} + self.loadVarData(varName) + # 更新UI信息 + if varName in self.multiVarData: + data = self.multiVarData[varName]['y'] + xdata = self.multiVarData[varName]['x'] + else: + data = [] + xdata = [] + self.varNameLabel.setText(f"当前变量: {varName}") + self.dataCountLabel.setText(f"数据点: {len(data)}") + if xdata: + import datetime + import pandas as pd + def to_dt(t): + if isinstance(t, datetime.datetime): + return t + elif isinstance(t, pd.Timestamp): + return t.to_pydatetime() + elif isinstance(t, (int, float)): + return datetime.datetime.fromtimestamp(t) + else: + return datetime.datetime.now() + start_time = to_dt(xdata[0]).strftime('%Y-%m-%d %H:%M') + end_time = to_dt(xdata[-1]).strftime('%Y-%m-%d %H:%M') + self.timeRangeLabel.setText(f"时间范围: {start_time} - {end_time}") + else: + self.timeRangeLabel.setText("时间范围: 无数据") + # 刷新多变量趋势图 + self.updateMultiVarChart() + self.resetZoom() + self.statusLabel.setText(f"{varName} 趋势已显示") + + def updateChart(self): + # 用当前数据更新图表 + self.ax_main.clear() + self.ax_overview.clear() + + # 设置网格 + self.ax_main.grid(True, alpha=0.3) + self.ax_overview.grid(True, alpha=0.3) + + # 绘制数据 + if self.xData and self.yData: + self.ax_main.plot(self.xData, self.yData, + color='blue', + linewidth=2, + marker='o', + markersize=4, + label=self.currentVarName) + + # 概览图(降采样) + step = max(1, len(self.xData) // 10) + x_overview = self.xData[::step] + y_overview = self.yData[::step] + self.ax_overview.plot(x_overview, y_overview, + color='blue', + linewidth=1, + alpha=0.7) + + # 设置时间格式 + self.ax_main.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + self.ax_overview.xaxis.set_major_formatter(mdates.DateFormatter('%H:%M:%S')) + + # 设置标签 + self.ax_main.set_title('历史趋势图', fontsize=12, fontweight='bold') + self.ax_main.set_ylabel('数值') + self.ax_overview.set_xlabel('时间') + self.ax_overview.set_ylabel('数值') + + # 添加图例 + if self.currentVarName: + self.ax_main.legend(loc='upper right') + + # 自动调整布局 + self.figure.tight_layout() + + # 刷新画布 + self.canvas.draw() + + def on_mouse_press(self, event): + # 鼠标左键按下,准备平移 + if event.inaxes == self.ax_main and event.button == 1 and event.xdata is not None: + self._is_panning = True + self._pan_start_x = event.xdata + self._pan_start_xlim = self.ax_main.get_xlim() + + def on_mouse_release(self, event): + # 鼠标左键松开,结束平移 + if self._is_panning: + self._is_panning = False + self._pan_start_x = None + self._pan_start_xlim = None + # 拖拽结束后再重绘 + self.canvas.draw_idle() + + def on_mouse_move(self, event): + # 鼠标移动事件,显示悬浮气泡+平移主图 + if self._is_panning and event.inaxes == self.ax_main and event.xdata is not None and self._pan_start_x is not None: + dx = event.xdata - self._pan_start_x + xlim0, xlim1 = self._pan_start_xlim + self.ax_main.set_xlim(xlim0 - dx, xlim1 - dx) + # 拖拽时不重绘,提高流畅度 + # self.canvas.draw_idle() + # 悬浮气泡逻辑(不变) + if event.inaxes == self.ax_main and event.xdata is not None and event.ydata is not None: + info_text = f"时间: {mdates.num2date(event.xdata).strftime('%Y-%m-%d %H:%M:%S')}
" + if self.selectedVars: + for varName in self.selectedVars: + if varName in self.multiVarData: + data = self.multiVarData[varName] + if data['x'] and data['y']: + closest_idx = self.find_closest_point(data['x'], event.xdata) + if closest_idx is not None and closest_idx < len(data['y']): + value = data['y'][closest_idx] + info_text += f"{varName}: {value:.3f}
" + self.infoBubble.setText(f"{info_text}") + x = int(event.guiEvent.x()) + y = int(event.guiEvent.y()) + self.infoBubble.move(x + 16, y + 8) + self.infoBubble.adjustSize() + self.infoBubble.setVisible(True) + else: + self.infoBubble.setVisible(False) + # self.canvas.draw() # 性能优化:去除重绘 + + def on_mouse_leave(self, event): + # 鼠标离开主图时隐藏气泡 + self.infoBubble.setVisible(False) + + def find_closest_point(self, x_data, target_x): + # 找到最接近目标X值的数据点索引 + if not x_data: + return None + + # 将datetime转换为matplotlib日期 + if isinstance(x_data[0], datetime.datetime): + x_nums = mdates.date2num(x_data) + else: + x_nums = x_data + + # 使用二分查找 + left = 0 + right = len(x_nums) - 1 + while left <= right: + mid = (left + right) // 2 + if x_nums[mid] == target_x: + return mid + elif x_nums[mid] < target_x: + left = mid + 1 + else: + right = mid - 1 + # 找到最接近的点 + if left >= len(x_nums): + return len(x_nums) - 1 + elif right < 0: + return 0 + else: + if abs(x_nums[left] - target_x) < abs(x_nums[right] - target_x): + return left + else: + return right + + def on_overview_xlim_changed(self, event_ax): + # 当概览图范围变化时更新主图 + if event_ax == self.ax_overview: + x1, x2 = self.ax_overview.get_xlim() + self.ax_main.set_xlim(x1) + self.canvas.draw() + + def resetZoom(self): + # 重置到完整视图 + if self.xData: + self.ax_main.set_xlim(min(self.xData), max(self.xData)) + self.ax_overview.set_xlim(min(self.xData), max(self.xData)) + self.canvas.draw() + + def zoomBack(self): + # 退到上一个视图 + if self.history and self.historyIndex > 0: + self.historyIndex -= 1 + self.applyZoom(self.history[self.historyIndex]) + self.backBtn.setEnabled(self.historyIndex > 0) + self.forwardBtn.setEnabled(True) + + def zoomForward(self): + # 进到下一个视图 + if self.history and self.historyIndex < len(self.history) - 1: + self.historyIndex += 1 + self.applyZoom(self.history[self.historyIndex]) + self.forwardBtn.setEnabled(self.historyIndex < len(self.history) - 1) + self.backBtn.setEnabled(True) + def applyZoom(self, zoom_range): + # 应用指定的缩放范围 + x1, x2 = zoom_range + self.ax_main.set_xlim(x1, x2) + self.canvas.draw() + + def enableRectZoom(self): + # 启用框选缩放模式 + self.statusLabel.setText("模式: 框选缩放 - 在概览图中拖动选择区域") + + def enablePanMode(self): + # 启用平移模式 + self.statusLabel.setText("模式: 平移视图 - 在主图中拖动移动视图") + def onProjectChanged(self): + # 项目更改时处理 + self.exchangeProject() + def clearAllVars(self): + # 清除趋势图中的所有变量 + self.selectedVars = [] + self.multiVarData = {} + self.varColors = {} + self.ax_main.clear() + self.ax_overview.clear() + self.ax_main.grid(True, alpha=0.3) + self.ax_overview.grid(True, alpha=0.3) + self.canvas.draw() + self.statusLabel.setText("趋势图已清除") + self.varNameLabel.setText("当前变量: 无") + self.dataCountLabel.setText("数据点: 0") + self.timeRangeLabel.setText("时间范围: 无数据") + self.resetZoom() + + def on_select(self, xmin, xmax): + # SpanSelector回调,记录选中区间并更新主趋势图 + self._selected_range = (xmin, xmax) + self.updateMultiVarChart() + + def on_overview_double_click(self, event): + # 双击下方缩略图恢复全局视图 + if event.dblclick and event.inaxes == self.ax_overview: + self.ax_main.set_xlim(auto=True) + self.canvas.draw_idle() + + def on_mouse_wheel(self, event): + # 鼠标滚轮缩放主趋势图X轴 + if event.inaxes == self.ax_main and event.xdata is not None: + xlim = self.ax_main.get_xlim() + x_center = event.xdata + # 缩放因子,滚轮向上放大,向下缩小 + scale_factor = 0.8 if event.button == 'up' else 1.25 + x_left = x_center - (x_center - xlim[0]) * scale_factor + x_right = x_center + (xlim[1] - x_center) * scale_factor + self.ax_main.set_xlim(x_left, x_right) + self.canvas.draw_idle() + + def onQuickRangeChanged(self, idx): + from PyQt5.QtCore import QDateTime + now = QDateTime.currentDateTime() + if idx == 0: # 最近一天 + self.startTimeEdit.setDateTime(now.addDays(-1)) + self.endTimeEdit.setDateTime(now) + elif idx == 1: # 最近6小时 + self.startTimeEdit.setDateTime(now.addSecs(-6*3600)) + self.endTimeEdit.setDateTime(now) + elif idx == 2: # 最近1小时 + self.startTimeEdit.setDateTime(now.addSecs(-3600)) + self.endTimeEdit.setDateTime(now) + elif idx == 3: # 最近30分钟 + self.startTimeEdit.setDateTime(now.addSecs(-1800)) + self.endTimeEdit.setDateTime(now) + # idx==4为自定义,不做处理 + # 自动触发查询 + if idx != 4: + self.onTimeRangeQuery() + +# 使用示例 +if __name__ == "__main__": + app = QApplication(sys.argv) + window = TrendWidgets() + window.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/UI/VarManages/AnalogModel.py b/UI/VarManages/AnalogModel.py index 07c0cb8..5a60b87 100644 --- a/UI/VarManages/AnalogModel.py +++ b/UI/VarManages/AnalogModel.py @@ -107,6 +107,18 @@ class AnalogButtonDelegate(TcRtdButtonDelegate): def __init__(self, parent=None): super(AnalogButtonDelegate, self).__init__(parent) + self.trendWindows = {} # 保存所有打开的趋势窗口 + + def trend_action(self): + sender = self.sender() + model = self.parent().model + varName = model.datas[sender.index[0]][3] + # 以变量名为key,避免重复打开 + if varName not in self.trendWindows or self.trendWindows[varName] is None: + self.trendWindows[varName] = ActualTrend(varName=varName) + self.trendWindows[varName].show() + self.trendWindows[varName].raise_() + self.trendWindows[varName].activateWindow() def edit_action(self): sender = self.sender() diff --git a/UI/VarManages/TCRTDModel.py b/UI/VarManages/TCRTDModel.py index 956ae94..55b5137 100644 --- a/UI/VarManages/TCRTDModel.py +++ b/UI/VarManages/TCRTDModel.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QHBoxLayout, QWidget, QMessageBox, QComboBox from protocol.TCP.TemToMv import temToMv from model.ProjectModel.VarManage import * from UI.VarManages.ModbusModel import * +from UI.TrendManage.ActualTrendWidget import ActualTrend from utils import Globals import re @@ -109,6 +110,7 @@ class TcRtdButtonDelegate(VarButtonDelegate): def __init__(self, parent=None): super(TcRtdButtonDelegate, self).__init__(parent) + self.trendWindows = {} # 保存所有打开的趋势窗口 def paint(self, painter, option, index): if not self.parent().indexWidget(index): @@ -127,15 +129,24 @@ class TcRtdButtonDelegate(VarButtonDelegate): clicked=self.edit_action ) + button3 = QPushButton( + qtawesome.icon('fa.line-chart', color='#393c4e'), + "", + self.parent(), + clicked=self.trend_action + ) + button1.clicked.connect(self.start_action) button2.clicked.connect(self.edit_action) + button3.clicked.connect(self.trend_action) button2.oldName = False button2.isEdit = True button1.index = [index.row(), index.column()] button2.index = [index.row(), index.column()] + button3.index = [index.row(), index.column()] data = self.parent().model.datas[index.row()] @@ -149,6 +160,7 @@ class TcRtdButtonDelegate(VarButtonDelegate): h_box_layout = QHBoxLayout() h_box_layout.addWidget(button1) h_box_layout.addWidget(button2) + h_box_layout.addWidget(button3) h_box_layout.setContentsMargins(2, 0, 0, 2) h_box_layout.setAlignment(Qt.AlignCenter) widget = QWidget() @@ -262,6 +274,17 @@ class TcRtdButtonDelegate(VarButtonDelegate): varMes.append('') model.insert_data(varMes, rowIndex) model.remove_row(rowIndex + 1) + + def trend_action(self): + sender = self.sender() + model = self.parent().model + varName = model.datas[sender.index[0]][3] + # 以变量名为key,避免重复打开 + if varName not in self.trendWindows or self.trendWindows[varName] is None: + self.trendWindows[varName] = ActualTrend(varName=varName) + self.trendWindows[varName].show() + self.trendWindows[varName].raise_() + self.trendWindows[varName].activateWindow() class TcRtdTypeDelegate(TcRtdButtonDelegate): def __init__(self, parent=None): diff --git a/model/HistoryDBModel/HistoryDBManage.py b/model/HistoryDBModel/HistoryDBManage.py index 888d11c..c1c3b8b 100644 --- a/model/HistoryDBModel/HistoryDBManage.py +++ b/model/HistoryDBModel/HistoryDBManage.py @@ -88,9 +88,8 @@ class HistoryDBManage: pass def writeVarValue(self, varName, value): - # 入队时记录当前时间 - enqueue_time = datetime.now(timezone(timedelta(hours=8))) - # print(enqueue_time) + # 入队时记录中国时区时间 + enqueue_time = datetime.now() self.writeQueue.put((varName, value, enqueue_time)) def _writeWorker(self): @@ -105,39 +104,62 @@ class HistoryDBManage: point = Point(self.table).tag("varName", varName).field("value", float(value)).time(enqueue_time) self.client.write(point) - def queryVarHistory(self, varName, startTime=None, endTime=None, limit=1000): + def queryVarHistory(self, varName, startTime=None, endTime=None): where = f'"varName" = \'{varName}\'' if startTime: where += f" AND time >= '{startTime}'" if endTime: where += f" AND time <= '{endTime}'" - sql = f'SELECT time, value FROM "{self.table}" WHERE {where} ORDER BY time LIMIT {limit}' + # 关键:用tz()函数将time字段转为中国时区 + sql = f'SELECT tz(time, \'Asia/Shanghai\') AS time, value FROM "{self.table}" WHERE {where} ORDER BY time ASC LIMIT 100000' + # print(f"执行SQL查询: {sql}") try: df = self.client.query(sql, mode="pandas") import pandas as pd if isinstance(df, pd.DataFrame): data = df["value"].tolist() if "value" in df.columns else [] timeList = df["time"].tolist() if "time" in df.columns else [] + print(f"查询到 {len(data)} 个数据点") else: + # print(f"查询结果不是DataFrame: {type(df)}") data, timeList = [], [] except Exception as e: print(f"查询结果处理失败: {e}") data, timeList = [], [] return data, timeList + def getAllVarNames(self): + """获取所有去重后的变量名列表""" + sql = f'SELECT DISTINCT("varName") FROM "{self.table}"' + try: + df = self.client.query(sql, mode="pandas") + import pandas as pd + if isinstance(df, pd.DataFrame) and 'varName' in df.columns: + return df['varName'].tolist() + else: + return [] + except Exception as e: + print(f"获取变量名失败: {e}") + return [] + @classmethod def deleteTable(cls, table): token = cls.getAPIToken() host = 'http://localhost:8181' - url = f"{host.rstrip('/')}/api/v3/configure/table?db={'dcs'}&table={table}" + import datetime + # 获取当前UTC时间+1分钟,去除微秒,Z结尾 + now = datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc, microsecond=0) + hardDeleteAt = (now + datetime.timedelta(minutes=1)).isoformat().replace('+00:00', 'Z') + url = f"{host.rstrip('/')}/api/v3/configure/table?db={'dcs'}&table={table}&hard_delete_at={hardDeleteAt}" headers = { 'Authorization': f'Bearer {token}' } response = requests.delete(url, headers=headers) + print(hardDeleteAt) if response.status_code != 200: print(f"删除失败: {response.status_code} {response.text}") else: - print(f"已删除表 {table} 的所有历史数据。") + print(f"已硬删除表 {table} 的所有历史数据。") def stopWriteThread(self): self._stopWriteThread.set() diff --git a/model/ProjectModel/ProjectManage.py b/model/ProjectModel/ProjectManage.py index 257b405..8434602 100644 --- a/model/ProjectModel/ProjectManage.py +++ b/model/ProjectModel/ProjectManage.py @@ -107,8 +107,9 @@ class ProjectManage(object): try: if name == Globals.getValue('currentPro'): self.closePopen() + Globals.getValue('protocolManage').shutdown() Globals.getValue('currentProDB').close() - Globals.setValue('currentPro', None) + Globals.setValue('currentPro', None) QTimer.singleShot(1000, lambda:shutil.rmtree(os.path.join('project', name))) except OSError as e: print(e) diff --git a/model/ProjectModel/VarManage.py b/model/ProjectModel/VarManage.py index 1b2fa08..bb96090 100644 --- a/model/ProjectModel/VarManage.py +++ b/model/ProjectModel/VarManage.py @@ -17,11 +17,10 @@ from protocol.ProtocolManage import ProtocolManage # 获取ProtocolManage单例或全局对象(假设为全局唯一,实际项目可根据实际情况调整) -def getProtocolManager(): +def refreshCache(): protocolManagerInstance = Globals.getValue('protocolManage') - if protocolManagerInstance is None: - protocolManagerInstance = ProtocolManage() - return protocolManagerInstance + if protocolManagerInstance: + protocolManagerInstance.refreshVarCache() class ModbusVarManage(object): @@ -55,7 +54,7 @@ class ModbusVarManage(object): modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address, slaveID = slaveID, min = min, max = max, order = order, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() # def importModbusVar(self, path): @@ -162,7 +161,7 @@ class ModbusVarManage(object): modbusVarType.createVar(varName = varName, varType = varType, des = des, address = address, slaveID = slaveID, min = min, max = max, order = order, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -172,7 +171,7 @@ class ModbusVarManage(object): # print(name) self.getVarClass(modbusType).deleteVar(name = name) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -201,7 +200,7 @@ class ModbusVarManage(object): self.getVarClass(modbusType).update(varName = Nname, description = des, varType = varType, address = address, slaveID = slaveID, min = min, max = max, order = order, varModel = varModel).where(self.getVarClass(modbusType).varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editOrder(self, name, order, modbusType): @@ -209,7 +208,7 @@ class ModbusVarManage(object): order = str(order) self.getVarClass(modbusType).update(order = order).where(self.getVarClass(modbusType).varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editVarModel(self, name, varModel, modbusType): @@ -217,7 +216,7 @@ class ModbusVarManage(object): varModel = str(varModel) self.getVarClass(modbusType).update(varModel = varModel).where(self.getVarClass(modbusType).varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getAllVar(self, modbusType): @@ -255,7 +254,7 @@ class HartVarManage(object): hartVarType = HartVar() hartVarType.createVar(varName = varName, des = des, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getAllVar(self): @@ -276,7 +275,7 @@ class HartVarManage(object): # print(name) HartVar.deleteVar(name = name) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -298,7 +297,7 @@ class HartVarManage(object): else: HartVar.update(varName = Nname, description = des, varModel = varModel).where(HartVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editVarModel(self, name, varModel): @@ -306,7 +305,7 @@ class HartVarManage(object): print('修改变量模型',name) HartVar.update(varModel = str(varModel)).where(HartVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -332,7 +331,7 @@ class HartVarManage(object): hartVarType = HartVar() hartVarType.createVar(varName = varName, des = des, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() class TcRtdManage(object): @@ -352,7 +351,7 @@ class TcRtdManage(object): tcRtdVarType = TcRtdVar() tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getAllVar(self): @@ -373,7 +372,7 @@ class TcRtdManage(object): # print(name) TcRtdVar.deleteVar(name = name) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -399,7 +398,7 @@ class TcRtdManage(object): else: TcRtdVar.update(varName=Nname, channelNumber = channelNumber, description=des, varType=varType, min=min, max=max, compensationVar = compensationVar).where(TcRtdVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getByName(self, name): @@ -425,14 +424,14 @@ class TcRtdManage(object): name = str(name) TcRtdVar.update(varType = str(varType)).where(TcRtdVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editVarModel(self, name, varModel): name = str(name) TcRtdVar.update(varModel = str(varModel)).where(TcRtdVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def importVarForm(self, varName, channelNumber, varType, des, min, max, compensationVar, varModel): @@ -443,7 +442,7 @@ class TcRtdManage(object): tcRtdVarType = TcRtdVar() tcRtdVarType.createVar(varName = varName, channelNumber=channelNumber, des = des, varType = varType, min = min, max = max, compensationVar = compensationVar, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() class AnalogManage(object): def __init__(self): @@ -462,7 +461,7 @@ class AnalogManage(object): analogVarType = AnalogVar() analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getAllVar(self): @@ -483,7 +482,7 @@ class AnalogManage(object): # print(name) AnalogVar.deleteVar(name = name) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -507,14 +506,14 @@ class AnalogManage(object): else: AnalogVar.update(varName=Nname, channelNumber =channelNumber, description=des, varType=varType, min=min, max=max).where(AnalogVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editVarModel(self, name, varModel): name = str(name) AnalogVar.update(varModel = str(varModel)).where(AnalogVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getByName(self, name): # 查询指定变量信息 @@ -565,7 +564,7 @@ class AnalogManage(object): analogVarType = AnalogVar() analogVarType.createVar(varName = varName, channelNumber = channelNumber, des = des, varType = varType, min = min, max = max, varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @@ -585,7 +584,7 @@ class HartSimulateVarManage(object): hartSimulateVarType = HartSimulateVar() hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getAllVar(self): @@ -606,7 +605,7 @@ class HartSimulateVarManage(object): # print(name) HartSimulateVar.deleteVar(name = name) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod @@ -627,14 +626,14 @@ class HartSimulateVarManage(object): else: HartSimulateVar.update(varName = Nname, description = des, varModel = varModel).where(HartSimulateVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def editVarModel(self, name, varModel): name = str(name) HartSimulateVar.update(varModel = str(varModel)).where(HartSimulateVar.varName == name).execute() # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() @classmethod def getByName(self, name): # 查询指定变量信息 @@ -658,7 +657,7 @@ class HartSimulateVarManage(object): hartSimulateVarType = HartSimulateVar() hartSimulateVarType.createVar(varName = varName, des = des,varModel = varModel) # 操作后刷新缓存 - getProtocolManager().refreshVarCache() + refreshCache() class GlobalVarManager(object): diff --git a/protocol/ProtocolManage.py b/protocol/ProtocolManage.py index 9fe16ad..da94164 100644 --- a/protocol/ProtocolManage.py +++ b/protocol/ProtocolManage.py @@ -229,6 +229,7 @@ class ProtocolManage(object): def _readVariableValueOriginal(self, variableName): # 完全保留原有读取逻辑 + # print(12) varInfo = self.lookupVariable(variableName) value = None if not varInfo: @@ -315,6 +316,10 @@ class ProtocolManage(object): def shutdown(self): self.tcpVarManager.shutdown() + # 关闭后台读取线程 + if hasattr(self, 'readThreadStop') and hasattr(self, 'readThread'): + self.readThreadStop.set() + self.readThread.join(timeout=1)