Compare commits
10 Commits
7d71a4774f
...
1a36aa217b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a36aa217b | ||
|
|
7c09dde2d1 | ||
|
|
844aa4a78a | ||
|
|
8f5a65405d | ||
|
|
ab56316930 | ||
|
|
2a7e8d641f | ||
|
|
8ad4fdd355 | ||
|
|
b6b7de2fcb | ||
|
|
0587ca1a06 | ||
|
|
40af0afda8 |
@ -1,16 +1,16 @@
|
||||
{
|
||||
"dist_open_start_1" : 0.005,
|
||||
"dist_open_start_2" : 0.005,
|
||||
"dist_open_after_1" : 0.006,
|
||||
"dist_open_after_2" : 0.006,
|
||||
"dist_open_end_1" : 0.010,
|
||||
"dist_open_end_2" : 0.050,
|
||||
"dist_close_end_1" : 0.005,
|
||||
"dist_close_end_2" : 0.005,
|
||||
"time_wielding" : 1,
|
||||
"time_command" : 0.060,
|
||||
"time_robot_movement" : 0.2,
|
||||
"object_thickness" : 4.5e-3,
|
||||
"force_target" : 5000,
|
||||
"force_capture" : 500
|
||||
"dist_open_start_1": 0.005,
|
||||
"dist_open_start_2": 0.005,
|
||||
"dist_open_after_1": 0.006,
|
||||
"dist_open_after_2": 0.006,
|
||||
"dist_open_end_1": 0.01,
|
||||
"dist_open_end_2": 0.05,
|
||||
"dist_close_end_1": 0.005,
|
||||
"dist_close_end_2": 0.005,
|
||||
"time_wielding": 1,
|
||||
"time_command": 0.06,
|
||||
"time_robot_movement": 0.2,
|
||||
"object_thickness": 0.0045,
|
||||
"force_target": 5000,
|
||||
"force_capture": 500
|
||||
}
|
||||
@ -15,5 +15,9 @@
|
||||
"position_start_2": 0.08,
|
||||
"k_prop": 0.05,
|
||||
"time_capture": 100000,
|
||||
"UML_time_scaler": 1000
|
||||
}
|
||||
"UML_time_scaler": 1000,
|
||||
"Closure_signal": "Closing",
|
||||
"Squeeze_signal": "Squeeze",
|
||||
"Welding_signal": "Welding",
|
||||
"Release_signal": "Relief"
|
||||
}
|
||||
|
||||
Binary file not shown.
@ -1,3 +1,3 @@
|
||||
from .plot_window import PlotWindow
|
||||
from .plot_window import Plotter
|
||||
from .settings_window import settingsWindow
|
||||
from .app import app
|
||||
from .app import tabWidgetGenerator
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
115
src/gui/app.py
115
src/gui/app.py
@ -3,77 +3,74 @@ 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 PlotWindow, settingsWindow
|
||||
from src.gui import Plotter, settingsWindow
|
||||
|
||||
|
||||
class app:
|
||||
def __init__(self):
|
||||
|
||||
self.parser = DiagramParser()
|
||||
self.request_generator = Request(server_url='http://www.plantuml.com/plantuml/svg/')
|
||||
|
||||
self.operSettings = settingsWindow("params\operator_params.json", 'Operator', self._update_settings)
|
||||
self.sysSettings = settingsWindow("params\system_params.json", 'System', self._update_settings)
|
||||
|
||||
self._load_preset()
|
||||
|
||||
|
||||
def _get_ideal_timings(self, opt: OptAlgorithm) -> list[float]:
|
||||
data = opt.Ts
|
||||
ideal_time = [data['tclose'], data['tgrow'], opt.getMarkOpen()]
|
||||
return ideal_time
|
||||
|
||||
def _load_preset(self):
|
||||
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.opt_algorithm = OptAlgorithm(operator_config=self.operator_params, system_config=self.system_params)
|
||||
self.ideal_times = self._get_ideal_timings(self.opt_algorithm)
|
||||
self.paths = []
|
||||
self.plotters = []
|
||||
self.bool_dicts = []
|
||||
self.float_dicts = []
|
||||
self.timings_dicts = []
|
||||
self.modes = []
|
||||
self.names = []
|
||||
|
||||
self.parser.setData("trace_samples/2024_11_08-19_30_49.csv")
|
||||
self.bool_dict = self.parser.getBoolDict()
|
||||
self.float_dict = self.parser.getFloatDict()
|
||||
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
|
||||
|
||||
self.plotter = PlotWindow(operator_config=self.operator_params,
|
||||
system_config=self.system_params,
|
||||
opt=self.opt_algorithm,
|
||||
bool_dict=self.bool_dict,
|
||||
float_dict=self.float_dict,
|
||||
show_settings_func=self._show_settings)
|
||||
|
||||
self.uml_creator = UMLCreator(operator_config=self.operator_params,
|
||||
system_config=self.system_params,
|
||||
request_generator=self.request_generator,
|
||||
ideal_time=self.ideal_times,
|
||||
bool_dict=self.bool_dict,
|
||||
float_dict=self.float_dict)
|
||||
|
||||
self.uml_creator.update_uml(operator_config=self.operator_params,
|
||||
system_config=self.system_params,
|
||||
request_generator=self.request_generator,
|
||||
ideal_time=self.ideal_times,
|
||||
bool_dict=self.bool_dict,
|
||||
float_dict=self.float_dict)
|
||||
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_settings(self, _):
|
||||
def _update(self, _ = None):
|
||||
self.operator_params = self.operSettings.getParams()
|
||||
self.system_params = self.sysSettings.getParams()
|
||||
|
||||
self.opt_algorithm = OptAlgorithm(operator_config=self.operator_params, system_config=self.system_params)
|
||||
self.ideal_times = self._get_ideal_timings(self.opt_algorithm)
|
||||
|
||||
self.uml_creator.update_uml(system_config=self.system_params,
|
||||
request_generator=self.request_generator,
|
||||
ideal_time=self.ideal_times,
|
||||
bool_dict=self.bool_dict,
|
||||
float_dict=self.float_dict)
|
||||
opt_algorithm = OptAlgorithm(operator_config=self.operator_params, system_config=self.system_params)
|
||||
ideal_times = self._get_ideal_timings(opt_algorithm)
|
||||
|
||||
self.plotter.update_data(operator_config=self.operator_params,
|
||||
for i in range (len(self.plotters)):
|
||||
self.plotters[i].update_data(operator_config=self.operator_params,
|
||||
system_config=self.system_params,
|
||||
opt=self.opt_algorithm,
|
||||
bool_dict=self.bool_dict,
|
||||
float_dict=self.float_dict)
|
||||
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()
|
||||
@ -82,5 +79,7 @@ class app:
|
||||
|
||||
if __name__ == '__main__':
|
||||
pg.mkQApp("Plotting")
|
||||
temp = app()
|
||||
temp = tabWidgetGenerator()
|
||||
widget = temp.get_widget("trace_samples/2024_11_08-19_30_49.csv")
|
||||
widget.show()
|
||||
pg.exec()
|
||||
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -7,22 +8,10 @@ from src.gui import qt_settings as qts
|
||||
from src.OptAlgorithm import OptAlgorithm
|
||||
|
||||
|
||||
class PlotWindow:
|
||||
def __init__(self,
|
||||
system_config : dict,
|
||||
operator_config: dict,
|
||||
opt: OptAlgorithm,
|
||||
bool_dict: dict,
|
||||
float_dict: dict, show_settings_func):
|
||||
class Plotter:
|
||||
def __init__(self, show_settings_func):
|
||||
pg.setConfigOptions(antialias=True)
|
||||
self.opt = opt
|
||||
self.bool_dict = bool_dict
|
||||
self.float_dict = float_dict
|
||||
self.scaler = int(system_config['UML_time_scaler'])
|
||||
self.WeldTime = operator_config['time_wielding'] #[sec]
|
||||
self.alpha = 100 #[0-255 прозрачность фона]
|
||||
|
||||
self._getIdealTimings()
|
||||
self._init_ui()
|
||||
self.settings_button.clicked.connect(show_settings_func)
|
||||
|
||||
@ -31,15 +20,22 @@ class PlotWindow:
|
||||
system_config : dict,
|
||||
operator_config: dict,
|
||||
opt: OptAlgorithm,
|
||||
ideal_time: list[float],
|
||||
bool_dict: dict,
|
||||
float_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._getIdealTimings()
|
||||
self.WeldData = self.opt.calcPhaseGrow(self.idealTime[1])
|
||||
self._updatePlots()
|
||||
|
||||
|
||||
def _init_ui(self):
|
||||
self.widget = QtWidgets.QWidget()
|
||||
@ -53,7 +49,15 @@ class PlotWindow:
|
||||
layout.addWidget(self.win)
|
||||
self.settings_button = QtWidgets.QPushButton("Show settings")
|
||||
self.settings_button.setFixedWidth(160)
|
||||
layout.addWidget(self.settings_button)
|
||||
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')
|
||||
@ -74,9 +78,7 @@ class PlotWindow:
|
||||
self.p11.setAutoVisible(x=False, y=True)
|
||||
self.p12.setAutoVisible(x=False, y=True)
|
||||
self.p13.setAutoVisible(x=False, y=True)
|
||||
self._updatePlots()
|
||||
self.widget.setStyleSheet(qts.dark_style)
|
||||
self.widget.show()
|
||||
|
||||
def _init_graph(self, title, Yname, Yunits, Xname, Xunits):
|
||||
plot = self.win.addPlot(title = title)
|
||||
@ -95,40 +97,125 @@ class PlotWindow:
|
||||
self.p13.clear()
|
||||
self.l13.clear()
|
||||
|
||||
self._plotRealData()
|
||||
times = np.arange(20000)/10
|
||||
self._plotIdealData(times)
|
||||
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 _getIdealTimings(self):
|
||||
data = self.opt.Ts
|
||||
self.idealTime = [data['tclose'], data['tgrow'], self.opt.getMarkOpen(), data["tmovement"]]
|
||||
|
||||
def _form_idealdatGraph(self, times):
|
||||
self.idealPhase0 = (self.idealTime[0])*self.scaler #Подъезд
|
||||
self.idealPhase1 = (self.idealTime[0]+ self.idealTime[1])*self.scaler #Сжатие
|
||||
self.idealPhase2 = (self.idealTime[0]+ self.idealTime[1] + self.WeldTime)*self.scaler #Сварка
|
||||
self.idealPhase3 = (self.idealTime[0]+ self.idealTime[1] + self.idealTime[2] + self.WeldTime)*self.scaler #Разъезд
|
||||
self.idealPhase4 = (sum(self.idealTime[:4]) + self.WeldTime)*self.scaler #Последнее смыкание #TODO добавить идеальное время вместо 0.25
|
||||
self.x_splitter = np.array([[0, self.idealPhase0],
|
||||
[self.idealPhase0, self.idealPhase1],
|
||||
[self.idealPhase1, self.idealPhase2],
|
||||
[self.idealPhase2, self.idealPhase3],
|
||||
[self.idealPhase3, self.idealPhase4]])
|
||||
data = []
|
||||
for time in times:
|
||||
if time <= self.idealPhase0:
|
||||
x_fe, x_me, v_fe, v_me, f = self.opt.calcPhaseClose(time/self.scaler)
|
||||
elif time <= self.idealPhase1:
|
||||
x_fe, x_me, v_fe, v_me, f = self.opt.calcPhaseGrow(time/self.scaler-self.idealTime[0])
|
||||
elif time <= self.idealPhase2:
|
||||
x_fe, x_me, v_fe, v_me, f = data[-1]
|
||||
elif time <= self.idealPhase3:
|
||||
x_fe, x_me, v_fe, v_me, f = self.opt.calcPhaseOpen(time/self.scaler-self.idealTime[0]-self.idealTime[1]-self.WeldTime)
|
||||
else:
|
||||
x_fe, x_me, v_fe, v_me, f = self.opt.calcPhaseMovement(time/self.scaler-sum(self.idealTime[:3])-self.WeldTime)
|
||||
data.append([x_fe, x_me, v_fe, v_me, f])
|
||||
data = np.array(data).T
|
||||
return data
|
||||
|
||||
def _plotRealData(self):
|
||||
for i, (key, dat) in enumerate(self.float_dict.items()):
|
||||
@ -146,62 +233,46 @@ class PlotWindow:
|
||||
self.l13.addItem(curve, key)
|
||||
return dat[0]
|
||||
|
||||
def _plotIdealData(self, times):
|
||||
data = self._form_idealdatGraph(times)
|
||||
|
||||
x_fe = pg.PlotDataItem(times, data[0]*1000, pen=pg.mkPen(color=qts.colors[8], width=2), name='x_fe', autoDownsample=True, downsample=True)
|
||||
x_me = pg.PlotDataItem(times, data[1]*1000, pen=pg.mkPen(color=qts.colors[9], width=2), name='x_me', autoDownsample=True, downsample=True)
|
||||
v_fe = pg.PlotDataItem(times, data[2]*1000, pen=pg.mkPen(color=qts.colors[8], width=2), name='v_fe', autoDownsample=True, downsample=True)
|
||||
v_me = pg.PlotDataItem(times, data[3]*1000, pen=pg.mkPen(color=qts.colors[9], width=2), name='v_me', autoDownsample=True, downsample=True)
|
||||
f = pg.PlotDataItem(times, data[4], pen=pg.mkPen(color=qts.colors[8], width=2), name='f', autoDownsample=True, downsample=True)
|
||||
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.l11.addItem(f, 'Ideal force')
|
||||
|
||||
self.p12.addItem(x_fe)
|
||||
self.l12.addItem(x_fe, 'FE POS')
|
||||
#self.l12.addItem(x_fe, 'FE POS')
|
||||
self.p12.addItem(x_me)
|
||||
self.l12.addItem(x_me, 'ME POS')
|
||||
#self.l12.addItem(x_me, 'ME POS')
|
||||
|
||||
self.p13.addItem(v_fe)
|
||||
self.l13.addItem(v_fe, 'FE VEL')
|
||||
#self.l13.addItem(v_fe, 'FE VEL')
|
||||
self.p13.addItem(v_me)
|
||||
self.l13.addItem(v_me, 'ME VEL')
|
||||
self._addBackgroundSplitter()
|
||||
self._addEquidistances(times, data)
|
||||
#self.l13.addItem(v_me, 'ME VEL')
|
||||
#self._addBackgroundSplitter()
|
||||
#self._addEquidistances(time, data)
|
||||
|
||||
def _addBackgroundSplitter(self):
|
||||
def _addBackgroundSplitter(self, x, color):
|
||||
alpha = self.alpha
|
||||
y01 = np.array([10000, 10000])
|
||||
y0_1 = np.array([-10000, -10000])
|
||||
for i, _ in enumerate(self.x_splitter):
|
||||
a01 = pg.PlotDataItem(_, y01, pen=pg.mkPen(color=qts.colors[8], width=2), name=' ')
|
||||
a0_1 = pg.PlotDataItem(_, y0_1, pen=pg.mkPen(color=qts.colors[8], width=2), name=' ')
|
||||
bg1 = pg.FillBetweenItem(a01, a0_1, qts.RGBA[i]+(alpha,))
|
||||
bg2 = pg.FillBetweenItem(a01, a0_1, qts.RGBA[i]+(alpha,))
|
||||
bg3 = pg.FillBetweenItem(a01, a0_1, qts.RGBA[i]+(alpha,))
|
||||
self.p11.addItem(bg1)
|
||||
self.p12.addItem(bg2)
|
||||
self.p13.addItem(bg3)
|
||||
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 _addEquidistances(self, times, data):
|
||||
a1, b1, c1 = self._calculate_equidistant('fill', max(data[4]), 2.5, self.x_splitter[1], 3)
|
||||
self.p11.addItem(a1)
|
||||
self.p11.addItem(b1)
|
||||
self.p11.addItem(c1)
|
||||
|
||||
a1, b1, c1 = self._calculate_equidistant(times, data[4], 0.75, self.x_splitter[2], 3)
|
||||
self.p11.addItem(a1)
|
||||
self.p11.addItem(b1)
|
||||
self.p11.addItem(c1)
|
||||
|
||||
|
||||
|
||||
|
||||
def _makeFiller(self, x1, y1, x2, y2, color):
|
||||
alpha = self.alpha
|
||||
eq1 = pg.PlotDataItem(x1, y1, pen=pg.mkPen(color='#000000', width=1))
|
||||
@ -210,11 +281,7 @@ class PlotWindow:
|
||||
return eq1, eq2, bg
|
||||
|
||||
|
||||
def _calculate_equidistant(self, x, y, percent, splitter, color):
|
||||
if str(x) == 'fill':
|
||||
x = [splitter[0]+0.001, splitter[0]+0.002, splitter[1]-0.002, splitter[1]-0.001]
|
||||
y = [y, y, y, y]
|
||||
|
||||
def _calculate_equidistant(self, x, y, percent, color):
|
||||
if len(x) != len(y):
|
||||
raise ValueError("x и y должны быть одного размера")
|
||||
distance = max(y)/100*percent
|
||||
@ -224,19 +291,17 @@ class PlotWindow:
|
||||
y_eq2 = []
|
||||
|
||||
for i in range(0, len(x) - 1):
|
||||
if splitter[0]<= x[i] and x[i] <= splitter[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)
|
||||
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)
|
||||
@ -233,7 +233,13 @@ QPushButton:disabled {
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
BigSuccessLabel = """
|
||||
QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
|
||||
colors = [
|
||||
'#FF6F61', # яркий коралловый
|
||||
|
||||
115
src/main.py
115
src/main.py
@ -1,12 +1,115 @@
|
||||
import pyqtgraph as pg
|
||||
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
|
||||
|
||||
# Импортируйте ваш класс `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 import app
|
||||
|
||||
def main():
|
||||
pg.mkQApp("Plotting")
|
||||
temp = app()
|
||||
pg.exec()
|
||||
directory_to_watch = "D:/downloads/a22"
|
||||
|
||||
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_())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -2,40 +2,49 @@ from src.uml.request_generator import Request
|
||||
|
||||
|
||||
class UMLCreator:
|
||||
def __init__(self,
|
||||
system_config: dict,
|
||||
operator_config: dict,
|
||||
request_generator: Request,
|
||||
ideal_time: list[float],
|
||||
bool_dict: dict,
|
||||
float_dict: dict):
|
||||
def __init__(self, request_generator: Request, path_to_save: str):
|
||||
self._request_generator = request_generator
|
||||
self._ideal_time = ideal_time
|
||||
self.bool_dict = bool_dict
|
||||
self.float_dict = float_dict
|
||||
self.scaler = int(system_config['UML_time_scaler'])
|
||||
self.WeldTime = operator_config['time_wielding']
|
||||
self.path_to_save = path_to_save
|
||||
|
||||
|
||||
def _build_data(self):
|
||||
sig = [
|
||||
"Closing",
|
||||
"Relief",
|
||||
"Squeeze",
|
||||
"Welding"
|
||||
]
|
||||
closure = [self.bool_dict[sig[0]][1][0], self.bool_dict[sig[0]][2][0]]
|
||||
compression = [self.bool_dict[sig[2]][1][0], self.bool_dict[sig[2]][2][0]]
|
||||
opening = [self.bool_dict[sig[1]][1][0], self.bool_dict[sig[1]][-1][0]]
|
||||
real_data = []
|
||||
ideal_data = []
|
||||
if not self.theor_mode:
|
||||
|
||||
for key, items in self.timings_dict.items():
|
||||
if key == 'closure': ideal_time = self._ideal_time[0]
|
||||
elif key == 'compression': ideal_time = self._ideal_time[1]
|
||||
elif key == 'welding': ideal_time = self.WeldTime
|
||||
elif key == 'opening': ideal_time = self._ideal_time[2]
|
||||
|
||||
for item in items:
|
||||
real_data.append([item[0]*self.scaler+0.0001, str(key) + '#green'])
|
||||
real_data.append([item[1]*self.scaler, '{-}'])
|
||||
|
||||
if key == 'compression':
|
||||
ideal_data.append([(item[1]-ideal_time)*self.scaler, str(key) + '#yellow'])
|
||||
ideal_data.append([(item[1]-0.0001)*self.scaler, '{-}'])
|
||||
else:
|
||||
ideal_data.append([item[0]*self.scaler+0.0001, str(key) + '#yellow'])
|
||||
ideal_data.append([(item[0]+ideal_time)*self.scaler, '{-}'])
|
||||
if key == 'opening':
|
||||
ideal_data.append([item[1]*self.scaler+0.0001, 'coming #yellow'])
|
||||
ideal_data.append([(item[1]+self._ideal_time[3])*self.scaler, '{-}'])
|
||||
|
||||
real_data = [
|
||||
[closure[0]*self.scaler, 'closure #green'],
|
||||
[closure[1]*self.scaler, '{-}'],
|
||||
[compression[0]*self.scaler+0.0001, 'compression #green'],
|
||||
[compression[1]*self.scaler, 'welding #green'],
|
||||
[opening[0]*self.scaler+0.0001, 'opening #green'],
|
||||
[opening[1]*self.scaler, '{-}'],
|
||||
]
|
||||
else:
|
||||
real_data = []
|
||||
ideal_data = [
|
||||
[0.0, 'closure #yellow'],
|
||||
[self._ideal_time[0] * self.scaler, '{-}'],
|
||||
[(self._ideal_time[0] + 0.0001) * self.scaler, 'compression #yellow'],
|
||||
[sum(self._ideal_time[:2]) * self.scaler, '{-}'],
|
||||
[(sum(self._ideal_time[:2])+ 0.0001) * self.scaler, 'welding #yellow'],
|
||||
[(sum(self._ideal_time[:2]) + self.WeldTime)*self.scaler, '{-}'],
|
||||
[(sum(self._ideal_time[:2]) + 0.0001 + self.WeldTime) * self.scaler, 'opening #yellow'],
|
||||
[(sum(self._ideal_time[:3]) + self.WeldTime) * self.scaler, 'coming #yellow'],
|
||||
[(sum(self._ideal_time[:4]) + self.WeldTime) * self.scaler, '{-}'],
|
||||
]
|
||||
|
||||
client_data = [
|
||||
[0.0 * self.scaler, 'closure'],
|
||||
@ -48,31 +57,15 @@ class UMLCreator:
|
||||
[0.300 * self.scaler, '{-}'],
|
||||
]
|
||||
|
||||
ideal_data = [
|
||||
[0.0 * self.scaler, 'closure #yellow'],
|
||||
[self._ideal_time[0] * self.scaler, '{-}'],
|
||||
[(self._ideal_time[0] + 0.0001) * self.scaler, 'compression #yellow'],
|
||||
[(self._ideal_time[0] + self._ideal_time[1]) * self.scaler, '{-}'],
|
||||
[(self._ideal_time[0] + self._ideal_time[1]+ 0.0001) * self.scaler, 'welding #yellow'],
|
||||
[(self._ideal_time[0] + self._ideal_time[1] + self.WeldTime)*self.scaler, '{-}'],
|
||||
[(self._ideal_time[0] + self._ideal_time[1] + 0.0001 + self.WeldTime) * self.scaler, 'opening #yellow'],
|
||||
[(self._ideal_time[0] + self._ideal_time[1] + self._ideal_time[2] + self.WeldTime) * self.scaler, '{-}'],
|
||||
]
|
||||
|
||||
return real_data, client_data, ideal_data, self.bool_dict
|
||||
|
||||
def _get_time(self, signals = '', states = ''):
|
||||
res = []
|
||||
for i, sig in enumerate(signals):
|
||||
if states: state = states[i]
|
||||
else: state = 'high'
|
||||
index1 = next ((i for i, _ in enumerate(self.bool_dict[sig]) if _[1]==state), -1)
|
||||
time = self.bool_dict[sig][index1][0]
|
||||
res.append(time)
|
||||
return res
|
||||
|
||||
def _generate_svg(self, real_data, client_data, ideal_data, bool_data) -> None:
|
||||
try:
|
||||
self._request_generator.clear()
|
||||
|
||||
self._request_generator.clear()
|
||||
self._request_generator.appendStr('scale 25 as 50 pixels')
|
||||
|
||||
if not self.theor_mode:
|
||||
self._request_generator.addLineStyle('biba', 'red', 2)
|
||||
|
||||
for i, [signal, changes] in enumerate(bool_data.items()):
|
||||
@ -85,28 +78,33 @@ class UMLCreator:
|
||||
|
||||
self._request_generator.addConcise('RD', 'Real data')
|
||||
self._request_generator.setTimestamps('RD', real_data)
|
||||
self._request_generator.addConcise('CD', 'Client data')
|
||||
self._request_generator.setTimestamps('CD', client_data)
|
||||
self._request_generator.addConcise('ID', 'Ideal data')
|
||||
self._request_generator.setTimestamps('ID', ideal_data)
|
||||
|
||||
self._request_generator.generateSVG()
|
||||
else: self.name = self.path_to_save + '/Ideal'
|
||||
|
||||
self._request_generator.addConcise('CD', 'Client data')
|
||||
self._request_generator.setTimestamps('CD', client_data)
|
||||
self._request_generator.addConcise('ID', 'Ideal data')
|
||||
self._request_generator.setTimestamps('ID', ideal_data)
|
||||
try:
|
||||
self._request_generator.generateSVG(self.name)
|
||||
except Exception as e:
|
||||
print(f"SVG generate error: {e}")
|
||||
|
||||
def update_uml(self,
|
||||
operator_config: dict,
|
||||
system_config : dict,
|
||||
request_generator: Request,
|
||||
ideal_time: list[float],
|
||||
bool_dict: dict,
|
||||
float_dict: dict):
|
||||
float_dict: dict,
|
||||
timings_dict: dict,
|
||||
mode: bool,
|
||||
name:str):
|
||||
|
||||
self._request_generator = request_generator
|
||||
self._ideal_time = ideal_time
|
||||
self.bool_dict = bool_dict
|
||||
self.float_dict = float_dict
|
||||
self.timings_dict = timings_dict
|
||||
self.theor_mode = mode
|
||||
self.name = name
|
||||
self.scaler = int(system_config['UML_time_scaler'])
|
||||
self.WeldTime = operator_config['time_wielding']
|
||||
|
||||
|
||||
@ -48,10 +48,10 @@ class Request:
|
||||
def addLineStyle(self, name, color = 'green', thicknes = 1):
|
||||
self.lineStyles[name] = [f'LineColor {color}', f'LineThickness {thicknes}']
|
||||
|
||||
def generateSVG(self):
|
||||
self._compileUML()
|
||||
filename = abspath('UML.txt')
|
||||
self.server.processes_file(filename, outfile='UML.svg')
|
||||
def generateSVG(self, name):
|
||||
self._compileUML(name)
|
||||
filename = abspath(f'{name}.txt')
|
||||
self.server.processes_file(filename, outfile=f'{name}.svg')
|
||||
#result = self.server.processes(self.stringUML)
|
||||
#return result
|
||||
|
||||
@ -78,12 +78,11 @@ class Request:
|
||||
self.reqArr.append('}')
|
||||
|
||||
self.reqArr.append('</style>')
|
||||
self.reqArr.append('scale 10 as 50 pixels')
|
||||
|
||||
def _addVariables(self):
|
||||
for var in self.variables: self.reqArr.append(str(var))
|
||||
|
||||
def _compileUML(self):
|
||||
def _compileUML(self, name):
|
||||
self._addHeader()
|
||||
self._addVariables()
|
||||
|
||||
@ -93,7 +92,7 @@ class Request:
|
||||
|
||||
self._endUML()
|
||||
self.stringUML = [line + '\n' for line in self.reqArr]
|
||||
with open('UML.txt', 'w', encoding='utf-8') as file:
|
||||
with open(f'{name}.txt', 'w', encoding='utf-8') as file:
|
||||
file.writelines(self.stringUML)
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@ -2,28 +2,60 @@ import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
class DiagramParser:
|
||||
def __init__(self):
|
||||
def __init__(self, system_config: dict):
|
||||
self.data = pd.DataFrame({})
|
||||
self.signals = [system_config["Closure_signal"],
|
||||
system_config["Squeeze_signal"],
|
||||
system_config["Welding_signal"],
|
||||
system_config["Release_signal"]]
|
||||
|
||||
self.boolDict = {}
|
||||
self.floatDict = {}
|
||||
self.timingsDict = {}
|
||||
self.theor_mode = False
|
||||
|
||||
def setData(self, path):
|
||||
self.data = pd.read_csv(path)
|
||||
if not path:
|
||||
self.theor_mode = True
|
||||
else:
|
||||
self.data = pd.read_csv(path)
|
||||
|
||||
for signalName in self.data.columns:
|
||||
if type (self.data[signalName].iloc[0]) == np.bool:
|
||||
self.boolDict[signalName] = self._getBoolChanges(signalName)
|
||||
|
||||
def getBoolDict (self):
|
||||
boolDict = {}
|
||||
for signalName in self.data.columns:
|
||||
if type (self.data[signalName].iloc[0]) == np.bool:
|
||||
boolDict[signalName] = self._getBoolChanges(signalName)
|
||||
return boolDict
|
||||
|
||||
def getFloatDict (self):
|
||||
floatDict = {}
|
||||
for signalName in self.data.columns:
|
||||
if type (self.data[signalName].iloc[0]) == np.float64:
|
||||
floatDict[signalName] = self._getFloatChanges(signalName)
|
||||
return floatDict
|
||||
for signalName in self.data.columns:
|
||||
if type (self.data[signalName].iloc[0]) == np.float64:
|
||||
self.floatDict[signalName] = self._getFloatChanges(signalName)
|
||||
|
||||
for key, items in self.boolDict.items():
|
||||
if key == self.signals[0]: name = "closure"
|
||||
elif key == self.signals[1]: name = "compression"
|
||||
elif key == self.signals[2]: name = "welding"
|
||||
elif key == self.signals[3]: name = "opening"
|
||||
|
||||
self.timingsDict[name] = []
|
||||
len_items = len(items)
|
||||
for i, item in enumerate(items):
|
||||
if item[1] == 'high':
|
||||
if i < len_items-1:
|
||||
self.timingsDict[name].append([items[i][0], items[i+1][0]])
|
||||
else:
|
||||
self.timingsDict[name].append([items[i][0], items[i][0]+0.01])
|
||||
|
||||
def getBoolDict (self) -> dict:
|
||||
return self.boolDict
|
||||
|
||||
def _getBoolChanges(self, signalName):
|
||||
def getFloatDict (self) -> dict:
|
||||
return self.floatDict
|
||||
|
||||
def getRealTimings(self) -> dict:
|
||||
return self.timingsDict
|
||||
|
||||
def getMode(self) -> bool:
|
||||
return self.theor_mode
|
||||
|
||||
def _getBoolChanges(self, signalName) -> list:
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
@ -38,7 +70,7 @@ class DiagramParser:
|
||||
return changes
|
||||
|
||||
|
||||
def _getFloatChanges(self, signalName):
|
||||
def _getFloatChanges(self, signalName) -> list:
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user