diff --git a/params/system_params.json b/params/system_params.json index 017e7fb..58e2c7e 100644 --- a/params/system_params.json +++ b/params/system_params.json @@ -1,4 +1,6 @@ { + "trace_storage_path": "D:/downloads/a22", + "monitor_update_period": 100, "a_max_1": 5.41, "v_max_1": 0.108, "a_max_2": 35.81, diff --git a/src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc b/src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc index d27e969..68246f1 100644 Binary files a/src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc differ diff --git a/src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc b/src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc index 3946d0f..22ef919 100644 Binary files a/src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc differ diff --git a/src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc b/src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc index 1cde38a..4983639 100644 Binary files a/src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc differ diff --git a/src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc b/src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc index a50de4f..05c8dd5 100644 Binary files a/src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc differ diff --git a/src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc b/src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc index 137f017..8223991 100644 Binary files a/src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc differ diff --git a/src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc b/src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc index a874aab..898300f 100644 Binary files a/src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc and b/src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/__pycache__/__init__.cpython-310.pyc b/src/__pycache__/__init__.cpython-310.pyc index 64de15d..beb5899 100644 Binary files a/src/__pycache__/__init__.cpython-310.pyc and b/src/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/__pycache__/main.cpython-310.pyc b/src/__pycache__/main.cpython-310.pyc index 2f36987..5c5e522 100644 Binary files a/src/__pycache__/main.cpython-310.pyc and b/src/__pycache__/main.cpython-310.pyc differ diff --git a/src/controller/__pycache__/controller.cpython-310.pyc b/src/controller/__pycache__/controller.cpython-310.pyc new file mode 100644 index 0000000..f731778 Binary files /dev/null and b/src/controller/__pycache__/controller.cpython-310.pyc differ diff --git a/src/controller/__pycache__/converter.cpython-310.pyc b/src/controller/__pycache__/converter.cpython-310.pyc new file mode 100644 index 0000000..05a9938 Binary files /dev/null and b/src/controller/__pycache__/converter.cpython-310.pyc differ diff --git a/src/controller/__pycache__/ideal_data_builder.cpython-310.pyc b/src/controller/__pycache__/ideal_data_builder.cpython-310.pyc new file mode 100644 index 0000000..df2b5ff Binary files /dev/null and b/src/controller/__pycache__/ideal_data_builder.cpython-310.pyc differ diff --git a/src/controller/__pycache__/mediator.cpython-310.pyc b/src/controller/__pycache__/mediator.cpython-310.pyc new file mode 100644 index 0000000..639c4a9 Binary files /dev/null and b/src/controller/__pycache__/mediator.cpython-310.pyc differ diff --git a/src/controller/__pycache__/monitor.cpython-310.pyc b/src/controller/__pycache__/monitor.cpython-310.pyc new file mode 100644 index 0000000..c3e89e8 Binary files /dev/null and b/src/controller/__pycache__/monitor.cpython-310.pyc differ diff --git a/src/controller/__pycache__/monitor.cpython-311.pyc b/src/controller/__pycache__/monitor.cpython-311.pyc new file mode 100644 index 0000000..529ca51 Binary files /dev/null and b/src/controller/__pycache__/monitor.cpython-311.pyc differ diff --git a/src/controller/controller.py b/src/controller/controller.py new file mode 100644 index 0000000..bafe0ff --- /dev/null +++ b/src/controller/controller.py @@ -0,0 +1,12 @@ +from PyQt5.QtWidgets import QWidget +from PyQt5.QtCore import pyqtSignal + +from src.utils.base.base import BaseController + + +class Controller(BaseController): + + signal_widgets = pyqtSignal(list) + + def send_widgets(self, widgets: list[QWidget]) -> None: + self.signal_widgets.emit(widgets) diff --git a/src/controller/converter.py b/src/controller/converter.py new file mode 100644 index 0000000..f281298 --- /dev/null +++ b/src/controller/converter.py @@ -0,0 +1,20 @@ +import pandas as pd + +#FIXME: костыль для выключения предупреждения "replace deprecated". Потом надо поправить. +pd.set_option('future.no_silent_downcasting', True) + +from src.utils.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) diff --git a/src/controller/ideal_data_builder.py b/src/controller/ideal_data_builder.py new file mode 100644 index 0000000..9a25ec0 --- /dev/null +++ b/src/controller/ideal_data_builder.py @@ -0,0 +1,8 @@ +from src.utils.base.base import BaseIdealDataBuilder +import pandas as pd + + +class idealDataBuilder(BaseIdealDataBuilder): + def __init__(self,operator_config: dict, system_config: dict): + super().__init__(operator_config, system_config) + self.mul = 10000 diff --git a/src/controller/mediator.py b/src/controller/mediator.py new file mode 100644 index 0000000..c564840 --- /dev/null +++ b/src/controller/mediator.py @@ -0,0 +1,21 @@ +import pandas as pd + +from typing import Union +from PyQt5.QtWidgets import QWidget + +from src.utils.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) diff --git a/src/controller/monitor.py b/src/controller/monitor.py new file mode 100644 index 0000000..a5229af --- /dev/null +++ b/src/controller/monitor.py @@ -0,0 +1,28 @@ +from time import sleep +import os + +from loguru import logger + +from src.utils.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 = [] + diff --git a/src/gui/__init__.py b/src/gui/__init__.py index 58fa804..03363c8 100644 --- a/src/gui/__init__.py +++ b/src/gui/__init__.py @@ -1,3 +1,2 @@ -from .plot_window import Plotter +from .plotter import PlotWidget from .settings_window import settingsWindow -from .app import tabWidgetGenerator diff --git a/src/gui/__pycache__/__init__.cpython-310.pyc b/src/gui/__pycache__/__init__.cpython-310.pyc index ccbb522..4d2a802 100644 Binary files a/src/gui/__pycache__/__init__.cpython-310.pyc and b/src/gui/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/gui/__pycache__/app.cpython-310.pyc b/src/gui/__pycache__/app.cpython-310.pyc index c7b68f3..9dd09c6 100644 Binary files a/src/gui/__pycache__/app.cpython-310.pyc and b/src/gui/__pycache__/app.cpython-310.pyc differ diff --git a/src/gui/__pycache__/mainGui.cpython-310.pyc b/src/gui/__pycache__/mainGui.cpython-310.pyc new file mode 100644 index 0000000..913dfe6 Binary files /dev/null and b/src/gui/__pycache__/mainGui.cpython-310.pyc differ diff --git a/src/gui/__pycache__/plot_window.cpython-310.pyc b/src/gui/__pycache__/plot_window.cpython-310.pyc index 51afe18..f592268 100644 Binary files a/src/gui/__pycache__/plot_window.cpython-310.pyc and b/src/gui/__pycache__/plot_window.cpython-310.pyc differ diff --git a/src/gui/__pycache__/plotter.cpython-310.pyc b/src/gui/__pycache__/plotter.cpython-310.pyc new file mode 100644 index 0000000..5df53bd Binary files /dev/null and b/src/gui/__pycache__/plotter.cpython-310.pyc differ diff --git a/src/gui/__pycache__/qt_settings.cpython-310.pyc b/src/gui/__pycache__/qt_settings.cpython-310.pyc index 0a6353d..c37cdc5 100644 Binary files a/src/gui/__pycache__/qt_settings.cpython-310.pyc and b/src/gui/__pycache__/qt_settings.cpython-310.pyc differ diff --git a/src/gui/__pycache__/settings_window.cpython-310.pyc b/src/gui/__pycache__/settings_window.cpython-310.pyc index 15e1b4b..aca61fa 100644 Binary files a/src/gui/__pycache__/settings_window.cpython-310.pyc and b/src/gui/__pycache__/settings_window.cpython-310.pyc differ diff --git a/src/gui/app.py b/src/gui/app.py deleted file mode 100644 index d29b0ed..0000000 --- a/src/gui/app.py +++ /dev/null @@ -1,85 +0,0 @@ -import pyqtgraph as pg - -from src.utils import read_json, DiagramParser -from src.uml import Request, UMLCreator -from src.OptAlgorithm import OptAlgorithm -from src.gui import Plotter, settingsWindow - - -class tabWidgetGenerator: - def __init__(self, directory_to_save): - self.UMLgenerator = Request(server_url='http://www.plantuml.com/plantuml/svg/') - self.uml_creator = UMLCreator(request_generator=self.UMLgenerator, path_to_save=directory_to_save) - self.operator_params = read_json("params/operator_params.json") - self.system_params = read_json("params/system_params.json") - self.operSettings = settingsWindow("params\operator_params.json", 'Operator', self._update) - self.sysSettings = settingsWindow("params\system_params.json", 'System', self._update) - - self.paths = [] - self.plotters = [] - self.bool_dicts = [] - self.float_dicts = [] - self.timings_dicts = [] - self.modes = [] - self.names = [] - - def get_widget(self, path): - self.paths.append(path) - self.plotters.append(Plotter(show_settings_func=self._show_settings)) - self._getParsedData(path) - self._update() - return self.plotters[-1].widget - - def _get_ideal_timings(self, opt: OptAlgorithm) -> list[float]: - data = opt.Ts - ideal_time = [data['tclose'], data['tgrow'], opt.getMarkOpen(), data["tmovement"]] - return ideal_time - - def _getParsedData(self, path): - self.names.append(path) - parser = DiagramParser(system_config=self.system_params) - parser.setData(path) - self.bool_dicts.append(parser.getBoolDict()) - self.float_dicts.append(parser.getFloatDict()) - self.timings_dicts.append(parser.getRealTimings()) - self.modes.append(parser.getMode()) - - - def _update(self, _ = None): - self.operator_params = self.operSettings.getParams() - self.system_params = self.sysSettings.getParams() - - opt_algorithm = OptAlgorithm(operator_config=self.operator_params, system_config=self.system_params) - ideal_times = self._get_ideal_timings(opt_algorithm) - - for i in range (len(self.plotters)): - self.plotters[i].update_data(operator_config=self.operator_params, - system_config=self.system_params, - opt=opt_algorithm, - bool_dict=self.bool_dicts[i], - ideal_time=ideal_times, - float_dict=self.float_dicts[i], - mode = self.modes[i], - timings_dict=self.timings_dicts[i]) - - self.uml_creator.update_uml(operator_config=self.operator_params, - system_config=self.system_params, - ideal_time=ideal_times, - bool_dict=self.bool_dicts[i], - float_dict=self.float_dicts[i], - mode = self.modes[i], - timings_dict=self.timings_dicts[i], - name = self.names[i]) - - - def _show_settings(self): - self.operSettings.show() - self.sysSettings.show() - - -if __name__ == '__main__': - pg.mkQApp("Plotting") - temp = tabWidgetGenerator() - widget = temp.get_widget("trace_samples/2024_11_08-19_30_49.csv") - widget.show() - pg.exec() \ No newline at end of file diff --git a/src/gui/mainGui.py b/src/gui/mainGui.py new file mode 100644 index 0000000..35c68f2 --- /dev/null +++ b/src/gui/mainGui.py @@ -0,0 +1,32 @@ +from datetime import datetime as dt + +from PyQt5 import QtWidgets +from PyQt5.QtCore import Qt +from src.utils.base.base_widgets import BaseMainWindow + +class MainWindow(BaseMainWindow): + def __init__(self): + super().__init__() + self.initUI() + self.set_style(self) + + def initUI(self) -> None: + self.tabWidget = QtWidgets.QTabWidget() + layout = QtWidgets.QVBoxLayout() + layout.addWidget(self.tabWidget) + self.setLayout(layout) + + 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.clear() + + diff --git a/src/gui/plot_window.py b/src/gui/plot_window.py deleted file mode 100644 index 3d5c0dc..0000000 --- a/src/gui/plot_window.py +++ /dev/null @@ -1,307 +0,0 @@ -import pyqtgraph as pg -from pyqtgraph.Qt import QtWidgets -from PyQt5.QtCore import Qt -import numpy as np - -from src.gui import qt_settings as qts - -from src.OptAlgorithm import OptAlgorithm - - -class Plotter: - def __init__(self, show_settings_func): - pg.setConfigOptions(antialias=True) - self.alpha = 100 #[0-255 прозрачность фона] - self._init_ui() - self.settings_button.clicked.connect(show_settings_func) - - - def update_data(self, - system_config : dict, - operator_config: dict, - opt: OptAlgorithm, - ideal_time: list[float], - bool_dict: dict, - float_dict: dict, - timings_dict: dict, - mode: bool): - self.opt = opt - self.bool_dict = bool_dict - self.float_dict = float_dict - self.timings_dict = timings_dict - self.idealTime = ideal_time - self.theor_mode = mode - self.scaler = int(system_config['UML_time_scaler']) - self.WeldTime = operator_config['time_wielding'] #[sec] - self.WeldData = self.opt.calcPhaseGrow(self.idealTime[1]) - self._updatePlots() - - - def _init_ui(self): - self.widget = QtWidgets.QWidget() - layout = QtWidgets.QVBoxLayout() - self.widget.setLayout(layout) - - self.win = pg.GraphicsLayoutWidget(show=True, title="") - self.win.resize(1000,600) - self.win.setWindowTitle('') - - layout.addWidget(self.win) - self.settings_button = QtWidgets.QPushButton("Show settings") - self.settings_button.setFixedWidth(160) - info_layout = QtWidgets.QHBoxLayout() - layout.addLayout(info_layout) - info_layout.addWidget(self.settings_button, alignment = Qt.AlignLeft) - info_layout.setSpacing(20) - self.efficiency = QtWidgets.QLabel() - info_layout.addWidget(self.efficiency, alignment = Qt.AlignRight) - - - - - self.p11, self.l11 = self._init_graph('Electrode force, closure', 'Force', 'N', 'Time', 'ms') - #self.p21, _ = self._init_graph('Electrode force, compression', 'Force', 'N', 'Time', 'ms') - #self.p31, _ = self._init_graph('Electrode force, compression', 'Force', 'N', 'Time', 'ms') - self.win.nextRow() - self.p12, self.l12 = self._init_graph('Rotor Position, closure', 'Posicion', 'mm', 'Time', 'ms') - #self.p22, _ = self._init_graph('Rotor Position, compression', 'Posicion', 'mm', 'Time', 'ms') - #self.p32, _ = self._init_graph('Rotor Position, compression', 'Posicion', 'mm', 'Time', 'ms') - self.win.nextRow() - self.p13, self.l13 = self._init_graph('Rotor Speed, closure', 'Speed', 'mm/s', 'Time', 'ms') - #self.p23, _ = self._init_graph('Rotor Speed, compression', 'Speed', 'mm/s', 'Time', 'ms') - #self.p33, _ = self._init_graph('Rotor Speed, compression', 'Speed', 'mm/s', 'Time', 'ms') - self.win.nextRow() - - self.p12.setXLink(self.p11) - self.p13.setXLink(self.p11) - - self.p11.setAutoVisible(x=False, y=True) - self.p12.setAutoVisible(x=False, y=True) - self.p13.setAutoVisible(x=False, y=True) - self.widget.setStyleSheet(qts.dark_style) - - def _init_graph(self, title, Yname, Yunits, Xname, Xunits): - plot = self.win.addPlot(title = title) - plot.showGrid(x=True, y=True) - plot.setLabel('left', Yname, units=Yunits) - plot.setLabel('bottom', Xname, units=Xunits) - legend1 = pg.LegendItem((80,60), offset=(70,20)) - legend1.setParentItem(plot) - return plot, legend1 - - def _updatePlots(self): - self.p11.clear() - self.l11.clear() - self.p12.clear() - self.l12.clear() - self.p13.clear() - self.l13.clear() - - if not self.theor_mode: - self._plotRealData() - self._form_idealdatGraph() - self._calcOurScore() - - def _calcOurScore (self): - success = [] - start = np.array(self.timings_dict["closure"])[:, 0] - end = np.array(self.timings_dict["opening"])[:, 1] - points_timings = end-start - - ideal_time = sum(self.idealTime[:3])+self.WeldTime - for point in points_timings: - success.append(int(ideal_time/point * 100)) - if len(success) > 1: - maxS = max(success) - minS = min(success) - average = int(sum(success[:])/len(success)) - self.efficiency.setText(f'Efficiency Maximum: {maxS}%, Average: {average}%, Minimum: {minS}%' ) - else: - self.efficiency.setText(f'Efficiency Maximum: {success[0]}' ) - self.efficiency.setStyleSheet(qts.BigSuccessLabel) - - - def _form_idealdatGraph(self): - - if self.theor_mode: - self.timings_dict["closure"] = [[0, self.idealTime[0]]] - self.timings_dict["compression"] = [[self.idealTime[0], sum(self.idealTime[:2])]] - self.timings_dict["welding"] = [[sum(self.idealTime[:2]), sum(self.idealTime[:2])+self.WeldTime]] - self.timings_dict["opening"] = [[sum(self.idealTime[:2])+self.WeldTime, sum(self.idealTime[:3])+self.WeldTime]] - - delta = 10 #points_per_ms - for key, items in self.timings_dict.items(): - for item in items: - item_data = [] - time_data = [] - - if key == 'closure': - ideal_time = self.idealTime[0] - calc = self.opt.calcPhaseClose - color = qts.RGBA[0] - for i in range(0, int(ideal_time*self.scaler)*delta): - time = i/delta - item_data.append(calc(time/self.scaler)) - time_data.append(time+item[0]*self.scaler) - #print (item_data[-1], time_data[-1]) - self._plotIdealData(np.array(time_data), np.array(item_data).T) - self._addBackgroundSplitter([item[0]*self.scaler,item[0]*self.scaler + time], color) - - elif key == 'compression': - ideal_time = self.idealTime[1] - calc = self.opt.calcPhaseGrow - color = qts.RGBA[1] - - for i in range(int(ideal_time*self.scaler)*delta, 0, -1): - time = i/delta - item_data.append(calc(time/self.scaler)) - time_data.append(item[1]*self.scaler-(ideal_time*self.scaler-time)) - #print (item_data[-1], time_data[-1]) - self._plotIdealData(np.array(time_data), np.array(item_data).T) - self._addBackgroundSplitter([(item[1]-ideal_time)*self.scaler, item[1]*self.scaler], color) - - temp = item_data[0][4] - x = [time_data[0], time_data[-1], time_data[-1]-0.0001] - y = [temp, temp, temp] - a1, b1, c1 = self._calculate_equidistant(x, y, 2.5, 3) - self.p11.addItem(a1) - self.p11.addItem(b1) - self.p11.addItem(c1) - - elif key == 'welding': - ideal_time = self.WeldTime - calc = self._returnWeldData - color = qts.RGBA[2] - - for i in range(0, int(ideal_time*self.scaler)*delta): - time = i/delta - item_data.append(calc(time/self.scaler)) - time_data.append(time+item[0]*self.scaler) - #print (item_data[-1], time_data[-1]) - self._plotIdealData(np.array(time_data), np.array(item_data).T) - self._addBackgroundSplitter([item[0]*self.scaler,item[0]*self.scaler + time], color) - - x = [time_data[0], time_data[-1], time_data[-1]+0.0001] - y = [item_data[0][4], item_data[0][4], item_data[0][4]] - a1, b1, c1 = self._calculate_equidistant(x, y, 0.75, 3) - self.p11.addItem(a1) - self.p11.addItem(b1) - self.p11.addItem(c1) - - elif key == 'opening': - calc = self.opt.calcPhaseOpen - ideal_time = self.idealTime[2] - ideal_closure = self.idealTime[3] - color = qts.RGBA[3] - color_closure = qts.RGBA[4] - - for i in range(0, int(ideal_time*self.scaler)*delta): - time = i/delta - item_data.append(calc(time/self.scaler)) - time_data.append(time+item[0]*self.scaler) - #print (item_data[-1], time_data[-1]) - self._plotIdealData(np.array(time_data), np.array(item_data).T) - self._addBackgroundSplitter([item[0]*self.scaler,item[0]*self.scaler + time], color) - - item_data = [] - time_data = [] - for i in range(0, int(ideal_closure*self.scaler)*delta): - time = i/delta - item_data.append(self.opt.calcPhaseMovement(time/self.scaler)) - time_data.append(time+item[1]*self.scaler) - self._plotIdealData(np.array(time_data), np.array(item_data).T) - self._addBackgroundSplitter([item[1]*self.scaler,item[1]*self.scaler + time], color_closure) - - def _returnWeldData(self, _): - return self.WeldData - - - - def _plotRealData(self): - for i, (key, dat) in enumerate(self.float_dict.items()): - dat = np.array(dat).T - dat[0] = dat[0]*self.scaler - curve = pg.PlotDataItem(dat[0], dat[1], pen=pg.mkPen(color=qts.colors[i], width=2), name=key, autoDownsample=True, downsample=True) - if 'Electrode Force' in key: - self.p11.addItem(curve) - self.l11.addItem(curve, key) - elif 'Rotor Position' in key: - self.p12.addItem(curve) - self.l12.addItem(curve, key) - elif 'Rotor Speed' in key: - self.p13.addItem(curve) - self.l13.addItem(curve, key) - return dat[0] - - def _plotIdealData(self, time, data): - x_fe = pg.PlotDataItem(time, data[0]*1000, pen=pg.mkPen(color=qts.colors[8], width=2), name='x_fe', autoDownsample=True, downsample=True) - x_me = pg.PlotDataItem(time, data[1]*1000, pen=pg.mkPen(color=qts.colors[9], width=2), name='x_me', autoDownsample=True, downsample=True) - v_fe = pg.PlotDataItem(time, data[2]*1000, pen=pg.mkPen(color=qts.colors[8], width=2), name='v_fe', autoDownsample=True, downsample=True) - v_me = pg.PlotDataItem(time, data[3]*1000, pen=pg.mkPen(color=qts.colors[9], width=2), name='v_me', autoDownsample=True, downsample=True) - f = pg.PlotDataItem(time, data[4], pen=pg.mkPen(color=qts.colors[8], width=2), name='f', autoDownsample=True, downsample=True) - - self.p11.addItem(f) - #self.l11.addItem(f, 'Ideal force') - - self.p12.addItem(x_fe) - #self.l12.addItem(x_fe, 'FE POS') - self.p12.addItem(x_me) - #self.l12.addItem(x_me, 'ME POS') - - self.p13.addItem(v_fe) - #self.l13.addItem(v_fe, 'FE VEL') - self.p13.addItem(v_me) - #self.l13.addItem(v_me, 'ME VEL') - #self._addBackgroundSplitter() - #self._addEquidistances(time, data) - - def _addBackgroundSplitter(self, x, color): - alpha = self.alpha - y01 = np.array([10000, 10000]) - y0_1 = np.array([-10000, -10000]) - a01 = pg.PlotDataItem(x, y01, pen=pg.mkPen(color=qts.colors[8], width=2), name=' ') - a0_1 = pg.PlotDataItem(x, y0_1, pen=pg.mkPen(color=qts.colors[8], width=2), name=' ') - bg1 = pg.FillBetweenItem(a01, a0_1, color+(alpha,)) - bg2 = pg.FillBetweenItem(a01, a0_1, color+(alpha,)) - bg3 = pg.FillBetweenItem(a01, a0_1, color+(alpha,)) - self.p11.addItem(bg1) - self.p12.addItem(bg2) - self.p13.addItem(bg3) - - self.p11.setYRange(-1000, 5000) - self.p12.setYRange(-50, 250) - self.p13.setYRange(-400, 400) - - - def _makeFiller(self, x1, y1, x2, y2, color): - alpha = self.alpha - eq1 = pg.PlotDataItem(x1, y1, pen=pg.mkPen(color='#000000', width=1)) - eq2 = pg.PlotDataItem(x2, y2, pen=pg.mkPen(color='#000000', width=1)) - bg = pg.FillBetweenItem(eq1, eq2, qts.RGBA[color]+(alpha,)) - return eq1, eq2, bg - - - def _calculate_equidistant(self, x, y, percent, color): - if len(x) != len(y): - raise ValueError("x и y должны быть одного размера") - distance = max(y)/100*percent - x_eq1 = [] - y_eq1 = [] - x_eq2 = [] - y_eq2 = [] - - for i in range(0, len(x) - 1): - dx = x[i + 1] - x[i] - dy = y[i + 1] - y[i] - length = np.sqrt(dx ** 2 + dy ** 2) - sinA = dy/length - sinB = dx/length - - nx = -sinA*distance - ny = sinB*distance - x_eq1.append(x[i] + nx) - y_eq1.append(y[i] + ny) - x_eq2.append(x[i] - nx) - y_eq2.append(y[i] - ny) - - return self._makeFiller(np.array(x_eq1), np.array(y_eq1), np.array(x_eq2), np.array(y_eq2), color) \ No newline at end of file diff --git a/src/gui/plotter.py b/src/gui/plotter.py new file mode 100644 index 0000000..4b44628 --- /dev/null +++ b/src/gui/plotter.py @@ -0,0 +1,158 @@ +import pandas as pd +from PyQt5.QtWidgets import QWidget, QVBoxLayout +import pyqtgraph as pg +import numpy as np +from numpy import floating +from typing import Optional, Any, NamedTuple + +from src.utils.base.base import BasePlotWidget + + +class ProcessStage(NamedTuple): + mean_value: floating[Any] + start_index: int + finish_index: int + +class PlotWidget(BasePlotWidget): + + def _create_stage_ideal(self, + stage: str, + signal: 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] + data = self._stage_ideals[stage]() + + if start_index.size: + start_timestamp = times[start_index[0]] + finish_timestamp = times[finish_index[0]] if finish_index.size else times[len(times) - 1] + plot = pg.PlotDataItem(x=start_timestamp+data["time"], y=data[signal["name"]], pen=signal["pen"]) + return plot + return None + + + 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.size: + start_timestamp = times[start_index[0]] + finish_timestamp = times[finish_index[0]] if finish_index.size 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 + + @staticmethod + def _init_plot_widget(title: str) -> (pg.PlotWidget, pg.LegendItem): + plot_widget = pg.PlotWidget(title=title) + plot_widget.showGrid(x=True, y=True) + legend = pg.LegendItem((80, 60), offset=(70, 20)) + legend.setParentItem(plot_widget.graphicsItem()) + return plot_widget, legend + + def get_stage_info(self, + stage: str, + dataframe: pd.DataFrame, + signal_name: str) -> Optional[ProcessStage]: + if stage in self._stages: + stage_diff = np.diff(dataframe[stage]) + start_index = np.where(stage_diff == 1)[0] + finish_index = np.where(stage_diff == -1)[0] + + data = dataframe[signal_name] if signal_name in dataframe.columns.tolist() else [] + + if data.size and start_index.size: + start = start_index[0] + finish = finish_index[0] if finish_index.size else (len(data) - 1) + data_slice = data[start:finish] + mean = np.mean(data_slice) + return ProcessStage(mean_value=mean, start_index=int(start), finish_index=int(finish)) + 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, legend = self._init_plot_widget(title=channel) + + 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) + for signal in description["Ideal_signals"]: + ideal_plot = self._create_stage_ideal(stage, signal ,time_axis, dataframe) + if ideal_plot: + plot_widget.addItem(ideal_plot) + + if region: + plot_widget.addItem(region) + + + if settings["zoom"]: + if max(time_axis) < 5.0: + stages = [self.get_stage_info("Welding", + dataframe, + signal["name"]) for signal in description["Real_signals"]] + if stages: + means_raw = [stage.mean_value for stage in stages] + mean = max(means_raw) + start = time_axis[stages[0].start_index] + finish = time_axis[stages[0].finish_index] + + overshoot = pg.BarGraphItem(x0=0, + y0=mean - mean * 0.05, + height=mean * 0.05 * 2, + width=start, + brush=pg.mkBrush([0, 250, 0, 100])) + plot_widget.addItem(overshoot) + + stable = pg.BarGraphItem(x0=start, + y0=mean - mean * 0.015, + height=mean * 0.015 * 2, + width=finish - start, + brush=pg.mkBrush([0, 250, 0, 100])) + plot_widget.addItem(stable) + + plot_widget.setYRange(mean - 260, mean + 260) + plot_widget.setInteractive(False) + else: + max_value = min([max(dataframe[signal["name"]]) for signal in description["Real_signals"]]) + region = pg.LinearRegionItem([max_value - max_value * 0.015, + max_value + max_value * 0.015], + movable=False, + orientation="horizontal") + + region.setBrush(pg.mkBrush([0, 250, 0, 100])) + plot_widget.setYRange(max_value - 200, max_value + 200) + plot_widget.setXRange(3.5, 4.5) + plot_widget.addItem(region) + plot_widget.setInteractive(False) + + for signal in description["Real_signals"]: + if signal["name"] in dataframe_headers: + plot = plot_widget.plot(time_axis, dataframe[signal["name"]], pen=signal["pen"]) + legend.addItem(plot, signal["name"]) + + + + 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) + + diff --git a/src/main.py b/src/main.py index a0ae44a..02bc55d 100644 --- a/src/main.py +++ b/src/main.py @@ -1,115 +1,38 @@ import sys -import os -import time -from PyQt5.QtWidgets import ( - QApplication, - QMainWindow, - QTabWidget, - QWidget, - QVBoxLayout, - QLabel, -) -from PyQt5.QtCore import Qt, QThread, pyqtSignal, QObject, QFileSystemWatcher -from PyQt5.QtGui import QIcon -from src.gui import qt_settings as qts +from PyQt5 import QtWidgets +import json +from os import path -# Импортируйте ваш класс `app` здесь -# Предполагается, что класс `app` предоставляет интерфейс для отображения данных -# Если `app` не предоставляет виджет, возможно, потребуется его модифицировать -from src.gui.app import tabWidgetGenerator - - -class DirectoryWatcher(QObject): - - file_created = pyqtSignal(str) - - def __init__(self, directory_path): - super().__init__() - self.directory_path = directory_path - self.watcher = QFileSystemWatcher() - self.watcher.addPath(self.directory_path) - self.existing_files = set(os.listdir(self.directory_path)) - self.watcher.directoryChanged.connect(self.on_directory_changed) - - - def on_directory_changed(self, _): - try: - current_files = set(os.listdir(self.directory_path)) - new_files = current_files - self.existing_files - self.existing_files = current_files - for file in new_files: - if file.lower().endswith(".csv"): - full_path = os.path.join(self.directory_path, file) - self.file_created.emit(full_path) - except Exception as e: - print(f"Ошибка при обработке изменений директории: {e}") - - -class MainWindow(QMainWindow): - def __init__(self, directory_to_watch): - super().__init__() - self.setWindowTitle("Мониторинг CSV-файлов") - self.setGeometry(100, 100, 800, 600) - - self.tabs = QTabWidget() - self.tabs.setStyleSheet(qts.dark_style) - self.tabGen = tabWidgetGenerator(directory_to_watch) - self.handle_new_file() - self.setCentralWidget(self.tabs) - - # self.setWindowIcon(QIcon("path_to_icon.png")) - self.start_directory_watcher(directory_to_watch) - - - def start_directory_watcher(self, directory_path): - self.watcher_thread = QThread() - self.watcher = DirectoryWatcher(directory_path) - self.watcher.moveToThread(self.watcher_thread) - - self.watcher.file_created.connect(self.handle_new_file) - self.watcher_thread.started.connect(self.watcher.on_directory_changed) - - self.watcher_thread.start() - - def handle_new_file(self, file_path = None): - time.sleep(0.2) - if file_path: - file_name = os.path.basename(file_path) - else: - file_path = None - file_name = 'Ideal' - - tab_widget = self.tabGen.get_widget(path=file_path) - - tab = QWidget() - layout = QVBoxLayout() - layout.addWidget(tab_widget) - - label = QLabel(f"{file_name}") - label.setAlignment(Qt.AlignCenter) - layout.addWidget(label) - tab.setLayout(layout) - - self.tabs.addTab(tab, file_name) - - def closeEvent(self, event): - self.watcher_thread.quit() - self.watcher_thread.wait() - event.accept() +from src.gui.mainGui import MainWindow +from src.controller.monitor import DirectoryMonitor +from src.controller.mediator import Mediator +from src.controller.converter import DataConverter +from src.controller.ideal_data_builder import idealDataBuilder +from src.gui.plotter import PlotWidget +from src.controller.controller import Controller +from src.utils.json_tools import read_json def main(): - directory_to_watch = "D:/downloads/a22" + app = QtWidgets.QApplication(sys.argv) + operator_params = read_json("params/operator_params.json") + system_params = read_json("params/system_params.json") + monitor = DirectoryMonitor(system_params['trace_storage_path'], system_params['monitor_update_period']) + data_converter = DataConverter() + ideal_data_builder = idealDataBuilder(operator_params, system_params) + plot_widget_builder = PlotWidget(idealDataBuilder=ideal_data_builder) + controller = Controller() + mediator = Mediator(monitor, data_converter, plot_widget_builder, controller) + monitor.start() + window = MainWindow() - if not os.path.isdir(directory_to_watch): - print(f"Директория не найдена: {directory_to_watch}") - sys.exit(1) - - app_instance = QApplication(sys.argv) - window = MainWindow(directory_to_watch) window.show() - sys.exit(app_instance.exec_()) + + controller.signal_widgets.connect(window.show_plot_tabs) -if __name__ == "__main__": + sys.exit(app.exec_()) + + +if __name__ == '__main__': main() diff --git a/src/uml/__pycache__/__init__.cpython-310.pyc b/src/uml/__pycache__/__init__.cpython-310.pyc index d8643a5..8a313df 100644 Binary files a/src/uml/__pycache__/__init__.cpython-310.pyc and b/src/uml/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/uml/__pycache__/creator.cpython-310.pyc b/src/uml/__pycache__/creator.cpython-310.pyc index 4dc2d1b..1d533ad 100644 Binary files a/src/uml/__pycache__/creator.cpython-310.pyc and b/src/uml/__pycache__/creator.cpython-310.pyc differ diff --git a/src/uml/__pycache__/request_generator.cpython-310.pyc b/src/uml/__pycache__/request_generator.cpython-310.pyc index c1f681f..f932a8a 100644 Binary files a/src/uml/__pycache__/request_generator.cpython-310.pyc and b/src/uml/__pycache__/request_generator.cpython-310.pyc differ diff --git a/src/utils/__pycache__/__init__.cpython-310.pyc b/src/utils/__pycache__/__init__.cpython-310.pyc index d1fbc01..0957cd8 100644 Binary files a/src/utils/__pycache__/__init__.cpython-310.pyc and b/src/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/src/utils/__pycache__/base.cpython-310.pyc b/src/utils/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..e6fe4f4 Binary files /dev/null and b/src/utils/__pycache__/base.cpython-310.pyc differ diff --git a/src/utils/__pycache__/base_widgets.cpython-310.pyc b/src/utils/__pycache__/base_widgets.cpython-310.pyc new file mode 100644 index 0000000..7cad539 Binary files /dev/null and b/src/utils/__pycache__/base_widgets.cpython-310.pyc differ diff --git a/src/utils/__pycache__/diagram_parser.cpython-310.pyc b/src/utils/__pycache__/diagram_parser.cpython-310.pyc index 6ee7ca1..098b2f8 100644 Binary files a/src/utils/__pycache__/diagram_parser.cpython-310.pyc and b/src/utils/__pycache__/diagram_parser.cpython-310.pyc differ diff --git a/src/utils/__pycache__/json_tools.cpython-310.pyc b/src/utils/__pycache__/json_tools.cpython-310.pyc index 6def110..45c90a3 100644 Binary files a/src/utils/__pycache__/json_tools.cpython-310.pyc and b/src/utils/__pycache__/json_tools.cpython-310.pyc differ diff --git a/src/utils/base/__pycache__/base.cpython-310.pyc b/src/utils/base/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..2766c43 Binary files /dev/null and b/src/utils/base/__pycache__/base.cpython-310.pyc differ diff --git a/src/utils/base/__pycache__/base_widgets.cpython-310.pyc b/src/utils/base/__pycache__/base_widgets.cpython-310.pyc new file mode 100644 index 0000000..0d4e21d Binary files /dev/null and b/src/utils/base/__pycache__/base_widgets.cpython-310.pyc differ diff --git a/src/utils/base/base.py b/src/utils/base/base.py new file mode 100644 index 0000000..1f22ba3 --- /dev/null +++ b/src/utils/base/base.py @@ -0,0 +1,255 @@ +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 +from src.OptAlgorithm import OptAlgorithm +import pandas as pd + + +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, + idealDataBuilder: Optional[BaseIdealDataBuilder] = None): + super().__init__() + self._mediator = mediator + self._opt = idealDataBuilder + + 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._stage_ideals = { + "Closing": self._opt.get_closingDF, + "Squeeze": self._opt.get_compressionDF, + "Welding": self._opt.get_weldingDF, + "Relief": self._opt.get_openingDF + } + + self._plt_channels = { + "Electrode Force, N & Welding Current, kA": { + "Settings": { + "zoom": False, + "stages": True + }, + "Real_signals": [ + { + "name": "Electrode Force, N ME", + "pen": 'r', + }, + { + "name": "Electrode Force, N FE", + "pen": 'w', + }, + { + "name": "Welding Current ME", + "pen": "y", + } + ], + "Ideal_signals": [ + { + "name": "Force", + "pen": {'color': 'g', 'width':3}, + } + ] + }, + "Electrode Force, N": { + "Settings": { + "zoom": True, + "stages": False + }, + "Real_signals": [ + { + "name": "Electrode Force, N ME", + "pen": 'r', + }, + { + "name": "Electrode Force, N FE", + "pen": 'w', + } + ], + "Ideal_signals": [ + { + "name": "Force", + "pen": {'color': 'r', 'width':3}, + } + ] + }, + "Electrode Speed, mm/s": { + "Settings": { + "zoom": False, + "stages": True + }, + "Real_signals": [ + { + "name": "Rotor Speed, mm/s ME", + "pen": 'r', + "zoom": False + }, + { + "name": "Rotor Speed, mm/s FE", + "pen": 'w', + "zoom": False + } + ], + "Ideal_signals": [ + { + "name": "Rotor Speed ME", + "pen": {'color': 'y', 'width':3}, + "zoom": False + }, + { + "name": "Rotor Speed FE", + "pen": {'color': 'g', 'width':3}, + "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: + ... + + +# FIXME: WeldingDF показывает только 1 секунду +class BaseIdealDataBuilder(OptAlgorithm): + + def _get_data(self, end_timestamp:float, func:function) -> pd.DataFrame: + data = [] + for i in range (0, int(end_timestamp*self.mul)): + time = i/self.mul + X1, X2, V1, V2, F = func(time) + data.append({"time":time, "Posicion FE":X1*1000,"Posicion ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F}) + return pd.DataFrame(data) + + def get_closingDF(self) -> pd.DataFrame: + return self._get_data(self.Ts['tclose'], self.calcPhaseClose) + + def get_compressionDF(self) -> pd.DataFrame: + return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow) + + def get_openingDF(self) -> pd.DataFrame: + return self._get_data(self.getMarkOpen(), self.calcPhaseOpen) + + def get_tmovementDF(self) -> pd.DataFrame: + return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement) + + def get_weldingDF(self) -> pd.DataFrame: + data = [] + X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow']) + data.append({"time":0, "Posicion FE":X1,"Posicion ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F}) + data.append({"time":1, "Posicion FE":X1,"Posicion ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F}) + return pd.DataFrame(data) + + def get_ideal_timings(self) -> list[float, float, float, float]: + data = self.Ts + ideal_timings = [data['tclose'], data['tgrow'], self.getMarkOpen(), data["tmovement"]] + return ideal_timings + diff --git a/src/utils/base/base_widgets.py b/src/utils/base/base_widgets.py new file mode 100644 index 0000000..5faf97d --- /dev/null +++ b/src/utils/base/base_widgets.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +import os +from typing import Optional, Union + +import pandas as pd +from PyQt5.QtWidgets import QTabWidget, QWidget, QVBoxLayout + +class BaseMainWindow(QWidget): + + def set_style(self, object: Union[QTabWidget, QWidget]) -> None: + object.setStyleSheet(""" + QWidget { + background-color: #0D1117; + font-family: "Segoe UI", sans-serif; + font-size: 14px; + } + QMessageBox { + background-color: #161B22; + font-family: "Segoe UI", sans-serif; + font-size: 14px; + } + QPushButton { + background-color: #FFCC00; + color: #0D1117; + padding: 12px 25px; + border: 2px solid #E6B800; + border-radius: 8px; + font-family: "Segoe UI", sans-serif; + font-size: 16px; + font-weight: bold; + } + QPushButton:hover:!disabled { + background-color: #FFD700; + } + QPushButton:disabled { + background-color: #555555; + color: #cccccc; + border: none; + } + QLabel { + color: #ffffff; + font-size: 16px; + font-weight: bold; + font-family: "Segoe UI", sans-serif; + } + """) \ No newline at end of file