Compare commits

...

10 Commits

Author SHA1 Message Date
34b8b49624 fix: исправил падение приложения в случае отсутствия меток начала и конца этапа 2024-11-15 11:36:05 +03:00
7683e4395c feat: добавил возможность очистки области отображения графиков (по клавише F5) 2024-11-15 09:49:04 +03:00
a04c114370 chore: поменял имя открываемой вкладки с графиками 2024-11-14 23:12:27 +03:00
0c9ce475fa feat: добавил закрашивание этапов 2024-11-14 22:49:48 +03:00
ed9cda81cb chore: изменил структуру описания виджетов осциллографа 2024-11-14 21:39:44 +03:00
3eb030cb2d feat: добавил обработку признака технологического графика и проверку наличия сигнала в датафрейме 2024-11-14 20:59:21 +03:00
b2458bec15 feat: заменил график позиций на увеличенное усилие 2024-11-14 16:56:45 +03:00
dba7e3c235 feat: добавил отображение графиков 2024-11-14 13:45:57 +03:00
ac8c128484 feat: добавил конвертер данных из csv в pandas.DataFrame 2024-11-14 09:37:54 +03:00
a38d949712 feat: добавил смотрителя директории с графиками 2024-11-14 00:56:31 +03:00
10 changed files with 362 additions and 3 deletions

View File

@ -1,3 +1,4 @@
loguru==0.7.2
numpy==2.1.3 numpy==2.1.3
pandas==2.2.3 pandas==2.2.3
PyQt5==5.15.11 PyQt5==5.15.11

183
src/base/base.py Normal file
View File

@ -0,0 +1,183 @@
from __future__ import annotations
import os
from typing import Optional, Union
import pandas as pd
from PyQt5.QtCore import QThread, QObject, QTimer
from PyQt5.QtWidgets import QWidget
class BaseMediator:
def __init__(self,
monitor: BaseDirectoryMonitor,
converter: BaseDataConverter,
plot: BasePlotWidget,
controller: BaseController):
self._monitor = monitor
self._monitor.mediator = self
self._converter = converter
self._converter.mediator = self
self._plot = plot
self._plot.mediator = self
self._controller = controller
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget],
data: Union[list[str], list[pd.DataFrame], list[QWidget]]):
...
class BaseDirectoryMonitor:
update_timer = QTimer()
def __init__(self,
directory_path: str,
update_time: int,
mediator: Optional[BaseMediator] = None):
super().__init__()
self._directory_path = directory_path
self._update_time = update_time
self._mediator = mediator
self._files: list[str] = []
self._init_state()
@property
def directory_path(self) -> str:
return self._directory_path
@property
def update_time(self) -> int:
return self._update_time
@property
def files(self) -> list[str]:
return self._files
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
def _init_state(self):
files = os.listdir(self._directory_path)
self._files = files
def start(self):
self.update_timer.start(self._update_time)
class BaseDataConverter:
def __init__(self, mediator: Optional[BaseMediator] = None):
self._mediator = mediator
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
def convert_data(self, files: list[str]) -> None:
...
class BasePlotWidget:
def __init__(self, mediator: Optional[BaseMediator] = None):
super().__init__()
self._mediator = mediator
self._stages = [
"Relief",
"Closing",
"Squeeze",
"Welding"
]
self._stage_colors = {
"Closing": [208, 28, 31, 100],
"Squeeze": [45, 51, 89, 150],
"Welding": [247, 183, 24, 100],
"Relief": [0, 134, 88, 100]
}
self._plt_channels = {
"Electrode Force, N & Welding Current, kA": {
"Settings": {
"zoom": False,
"stages": True
},
"Signals": [
{
"name": "Electrode Force, N ME",
"pen": 'r',
},
{
"name": "Electrode Force, N FE",
"pen": 'w',
},
{
"name": "Welding Current ME",
"pen": "y",
}
]
},
"Electrode Force, N": {
"Settings": {
"zoom": True,
"stages": False
},
"Signals": [
{
"name": "Electrode Force, N ME",
"pen": 'r',
},
{
"name": "Electrode Force, N FE",
"pen": 'w',
}
]
},
"Electrode Speed, mm/s": {
"Settings": {
"zoom": False,
"stages": True
},
"Signals": [
{
"name": "Rotor Speed, mm/s ME",
"pen": 'r',
"zoom": False
},
{
"name": "Rotor Speed, mm/s FE",
"pen": 'w',
"zoom": False
}
]
},
}
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
...
class BaseController(QObject):
def send_widgets(self, widgets: list[QWidget]) -> None:
...

View File

@ -3,4 +3,4 @@ from typing import NamedTuple
class ConfigSchema(NamedTuple): class ConfigSchema(NamedTuple):
trace_storage_path: str trace_storage_path: str
monitor_update_period: int monitor_update_period: int | float

View File

@ -0,0 +1,12 @@
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal
from base.base import BaseController
class Controller(BaseController):
signal_widgets = pyqtSignal(list)
def send_widgets(self, widgets: list[QWidget]) -> None:
self.signal_widgets.emit(widgets)

View File

@ -0,0 +1,20 @@
import pandas as pd
#FIXME: костыль для выключения предупреждения "replace deprecated". Потом надо поправить.
pd.set_option('future.no_silent_downcasting', True)
from base.base import BaseDataConverter
class DataConverter(BaseDataConverter):
@staticmethod
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
bool_columns = dataframe.columns[dataframe.isin([True, False]).any()]
dataframe[bool_columns] = dataframe[bool_columns].replace({True: 1, False: 0})
return dataframe
def convert_data(self, files: list[str]) -> None:
dataframes = [pd.read_csv(file) for file in files]
converted_dataframes = list(map(self._replace_bool, dataframes))
self._mediator.notify(self, converted_dataframes)

View File

@ -0,0 +1,21 @@
import pandas as pd
from typing import Union
from PyQt5.QtWidgets import QWidget
from base.base import BaseMediator, BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget
class Mediator(BaseMediator):
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget],
data: Union[list[str], list[pd.DataFrame], list[QWidget]]):
if issubclass(source.__class__, BaseDirectoryMonitor):
self._converter.convert_data(data)
if issubclass(source.__class__, BaseDataConverter):
self._plot.build(data)
if issubclass(source.__class__, BasePlotWidget):
self._controller.send_widgets(data)

28
src/controller/monitor.py Normal file
View File

@ -0,0 +1,28 @@
from time import sleep
import os
from loguru import logger
from base.base import BaseDirectoryMonitor
class DirectoryMonitor(BaseDirectoryMonitor):
def _init_state(self):
files = os.listdir(self._directory_path)
self._files = files
self.update_timer.timeout.connect(self._monitor)
logger.info("Monitor initiated!")
def _monitor(self):
files = os.listdir(self._directory_path)
new_files = sorted(list(map(lambda x: os.path.join(self._directory_path, x),
filter(lambda x: x not in self._files, files))))
if new_files:
logger.info(f"New files detected: {new_files}")
self._mediator.notify(self, new_files)
self._files = files
if not files:
self._files = []

View File

@ -1,4 +1,7 @@
from datetime import datetime as dt
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from gui.widgets import mainForm from gui.widgets import mainForm
@ -7,3 +10,16 @@ class MainWindow(QtWidgets.QWidget, mainForm.Ui_Form):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.setupUi(self) self.setupUi(self)
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
for plot_widget in plot_widgets:
tab = QtWidgets.QWidget()
grid = QtWidgets.QGridLayout()
grid.addWidget(plot_widget)
tab.setLayout(grid)
self.tabWidget.addTab(tab, "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S'))
self.tabWidget.setCurrentWidget(tab)
def keyPressEvent(self, a0):
if a0.key() == Qt.Key_F5:
self.tabWidget.clear()

64
src/gui/widgets/plot.py Normal file
View File

@ -0,0 +1,64 @@
from typing import Optional
import pandas as pd
from PyQt5.QtWidgets import QWidget, QVBoxLayout
import pyqtgraph as pg
import numpy as np
from base.base import BasePlotWidget
class PlotWidget(BasePlotWidget):
def _create_stage_region(self,
stage: str,
times: pd.Series,
dataframe: pd.DataFrame) -> Optional[pg.LinearRegionItem]:
stage_diff = np.diff(dataframe[stage])
start_index = np.where(stage_diff == 1)[0]
finish_index = np.where(stage_diff == -1)[0]
if start_index:
start_timestamp = times[start_index[0]]
finish_timestamp = times[finish_index[0]] if finish_index else times[len(times) - 1]
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
region.setBrush(pg.mkBrush(self._stage_colors[stage]))
return region
return None
def _build_widget(self, dataframe: pd.DataFrame) -> QWidget:
widget = QWidget()
layout = QVBoxLayout()
time_axis = dataframe["time"]
dataframe_headers = dataframe.columns.tolist()
for channel, description in self._plt_channels.items():
plot_widget = pg.PlotWidget(title=channel)
plot_widget.showGrid(x=True, y=True)
legend = pg.LegendItem((80, 60), offset=(70, 20))
legend.setParentItem(plot_widget.graphicsItem())
settings = description["Settings"]
if settings["stages"] and all([stage in dataframe_headers for stage in self._stages]):
for stage in self._stages:
region = self._create_stage_region(stage, time_axis, dataframe)
if region:
plot_widget.addItem(region)
for signal in description["Signals"]:
if signal["name"] in dataframe_headers:
plot = plot_widget.plot(time_axis, dataframe[signal["name"]], pen=signal["pen"])
legend.addItem(plot, signal["name"])
if settings["zoom"] and max(time_axis) < 5.0:
max_value = max(dataframe[signal["name"]])
plot_widget.setYRange(max_value - 200, max_value)
plot_widget.setInteractive(False)
layout.addWidget(plot_widget)
widget.setLayout(layout)
return widget
def build(self, data: list[pd.DataFrame]) -> None:
widgets = [self._build_widget(data_sample) for data_sample in data]
self._mediator.notify(self, widgets)

View File

@ -5,6 +5,11 @@ from os import path
from gui.mainGui import MainWindow from gui.mainGui import MainWindow
from cfg.schema import ConfigSchema from cfg.schema import ConfigSchema
from controller.monitor import DirectoryMonitor
from controller.mediator import Mediator
from controller.converter import DataConverter
from gui.widgets import plot
from controller.controller import Controller
def read_json(filepath: str) -> dict: def read_json(filepath: str) -> dict:
@ -19,10 +24,19 @@ def read_json(filepath: str) -> dict:
def main(): def main():
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
config = ConfigSchema(**read_json("config/config.json")) config = ConfigSchema(**read_json("config/config.json"))
monitor = DirectoryMonitor(config.trace_storage_path, config.monitor_update_period)
data_converter = DataConverter()
plot_widget_builder = plot.PlotWidget()
controller = Controller()
mediator = Mediator(monitor, data_converter, plot_widget_builder, controller)
monitor.start()
window = MainWindow() window = MainWindow()
window.show() window.show()
controller.signal_widgets.connect(window.show_plot_tabs)
sys.exit(app.exec_()) sys.exit(app.exec_())