Compare commits

..

No commits in common. "master" and "feature-positioning" have entirely different histories.

107 changed files with 2123 additions and 122565 deletions

3
.gitignore vendored
View File

@ -1,5 +1,4 @@
**/__pycache__/
*.pyc
/__pycache__
/tck_venv
/.venv
/.vscode

View File

@ -1,32 +0,0 @@
{
"act_pos_decrease": [
5.0
],
"act_vel_min": [
1900.0
],
"act_vel_close": [
1700.0
],
"act_vel_thresh": [
10.0
],
"act_vel_negative": [
0.0
],
"rob_vel_thresh": [
4.0
],
"act_force_close": [
700.0
],
"act_force_weld": [
1300.0
],
"force_increase": [
200.0
],
"force_decrease": [
80.0
]
}

View File

@ -64,36 +64,36 @@
0.001
],
"distance_l_1": [
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02,
0.02
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005,
0.005
],
"distance_l_2": [
0.0275,
0.0255,
0.0242,
0.0245,
0.0228,
0.0236,
0.0229,
0.0248,
0.024,
0.0235,
0.025,
0.0276,
0.0234,
0.0215
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625,
0.0625
],
"distance_h_end1": [
0.003,
@ -127,6 +127,22 @@
0.005,
0.005
],
"object_position": [
0.02743,
0.0249,
0.024,
0.02425,
0.0225,
0.0233,
0.0226,
0.02442,
0.02357,
0.02321,
0.02464,
0.02745,
0.02321,
0.021348
],
"time_wielding": [
1.332,
1.644,
@ -160,20 +176,20 @@
0.0
],
"time_robot_movement": [
0.516,
0.492,
0.636,
0.492,
0.42,
0.54,
0.444,
0.66,
0.521,
0.557,
0.51,
0.51,
0.534,
0.0
0.314,
0.331,
0.356,
0.428,
0.418,
0.454,
0.458,
0.44,
0.49,
0.47,
0.44,
0.425,
0.464,
0.5
],
"object_thickness": [
0.0045,
@ -222,5 +238,85 @@
500.0,
500.0,
500.0
],
"Tesla closing": [
0.216,
0.228,
0.252,
0.216,
0.228,
0.216,
0.228,
0.228,
0.228,
0.216,
0.228,
0.216,
0.216,
0.216
],
"Tesla squeeze": [
0.276,
0.288,
0.264,
0.264,
0.276,
0.276,
0.312,
0.276,
0.24,
0.24,
0.24,
0.24,
0.24,
0.24
],
"Tesla welding": [
1.332,
1.644,
1.644,
1.428,
1.284,
1.308,
1.272,
1.38,
1.416,
1.392,
1.38,
1.404,
1.452,
1.452
],
"Tesla oncomming_relief": [
0.528,
0.528,
0.54,
0.636,
0.504,
0.468,
0.492,
0.54,
0.563,
0.588,
0.541,
0.564,
0.576,
0.507
],
"Tesla summary time": [
2.34,
2.652,
2.796,
2.4,
2.208,
2.34,
2.256,
2.544,
2.405,
2.405,
2.358,
2.37,
2.442,
1.908
]
}

View File

@ -1,325 +0,0 @@
{
"Online Path Scanner": {
"Electrode Force, N & Welding Current, kA": {
"Settings": {
"zoom": false,
"stages": true,
"performance": true,
"ideals": true,
"mirror ME": false,
"workpiece": false,
"force compensation FE": false,
"force accuracy":true
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": "r"
},
{
"name": "Electrode Force, N FE",
"pen": "w"
}
],
"Ideal_signals": [
{
"name": "Force",
"pen": {"color": "g", "width":3}
}
]
},
"Electrode Position, mm": {
"Settings": {
"zoom": false,
"stages": true,
"performance": false,
"ideals": true,
"mirror ME": true,
"workpiece": true,
"force compensation FE": true,
"force accuracy":false
},
"Real_signals": [
{
"name": "Rotor Position, mm ME",
"pen": {"color": "r", "width":2}
},
{
"name": "Rotor Position, mm FE",
"pen": {"color": "w", "width":2}
}
],
"Ideal_signals": [
{
"name": "Position ME",
"pen": {"color": "g", "width":4}
},
{
"name": "Position FE",
"pen": {"color": "b", "width":4}
}
]
},
"Electrode Speed, mm/s": {
"Settings": {
"zoom": false,
"stages": true,
"performance": false,
"ideals": true,
"mirror ME": false,
"workpiece": false,
"force compensation FE": false,
"force accuracy":false
},
"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": "g", "width":3},
"zoom": false
},
{
"name": "Rotor Speed FE",
"pen": {"color": "b", "width":3},
"zoom": false
}
]
}
},
"Report Editor": {
"Electrode Force, N & Welding Current, kA": {
"Settings": {
"zoom": false,
"stages": true,
"performance": true,
"ideals": true,
"mirror ME": false,
"workpiece": false,
"force compensation FE": false,
"force accuracy":true
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": "r"
},
{
"name": "Electrode Force, N FE",
"pen": "w"
}
],
"Ideal_signals": [
{
"name": "Force",
"pen": {"color": "g", "width":3}
}
]
},
"Electrode Position, mm": {
"Settings": {
"zoom": false,
"stages": true,
"performance": false,
"ideals": true,
"mirror ME": true,
"workpiece": true,
"force compensation FE": true,
"force accuracy":false
},
"Real_signals": [
{
"name": "Rotor Position, mm ME",
"pen": {"color": "r", "width":2}
},
{
"name": "Rotor Position, mm FE",
"pen": {"color": "w", "width":2}
}
],
"Ideal_signals": [
{
"name": "Position ME",
"pen": {"color": "g", "width":4}
},
{
"name": "Position FE",
"pen": {"color": "b", "width":4}
}
]
},
"Electrode Speed, mm/s": {
"Settings": {
"zoom": false,
"stages": true,
"performance": false,
"ideals": true,
"mirror ME": false,
"workpiece": false,
"force compensation FE": false,
"force accuracy":false
},
"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": "g", "width":3},
"zoom": false
},
{
"name": "Rotor Speed FE",
"pen": {"color": "b", "width":3},
"zoom": false
}
]
}
},
"Client Trace Watcher":{
"Client Trace": {
"Settings": {
"zoom": false,
"stages": true
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": "r"
},
{
"name": "Electrode Position, mm ME",
"pen": "g"
},
{
"name": "Rotor Speed, deg/s ME",
"pen": "b"
},
{
"name": "Cartesian Tool Speed, mm/s ME",
"pen": "w"
}
],
"Ideal_signals": []
},
"TCW Trace": {
"Settings": {
"stages": true,
"ideals": true,
"mirror ME": true,
"workpiece": true,
"force compensation FE": true
},
"Real_signals": [],
"Ideal_signals": [
{
"name": "Force",
"pen": {"color": "r", "width":4}
},
{
"name": "Position ME",
"pen": {"color": "g", "width":4}
},
{
"name": "Position FE",
"pen": {"color": "b", "width":4}
},
{
"name": "Rotor Speed ME",
"pen": {"color": "w", "width":3},
"zoom": false
},
{
"name": "Rotor Speed FE",
"pen": {"color": "w", "width":3},
"zoom": false
}
]
},
"Combined Trace": {
"Settings": {
"stages": true,
"performance": false,
"ideals": true,
"workpiece": false
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": "r"
},
{
"name": "Electrode Position, mm ME",
"pen": "g"
},
{
"name": "Rotor Speed, deg/s ME",
"pen": "b"
},
{
"name": "Cartesian Tool Speed, mm/s ME",
"pen": "w"
}
],
"Ideal_signals": [
{
"name": "Force",
"pen": {"color": "r", "width":4}
},
{
"name": "Position ME",
"pen": {"color": "g", "width":4}
},
{
"name": "Position FE",
"pen": {"color": "b", "width":4}
},
{
"name": "Rotor Speed ME",
"pen": {"color": "w", "width":3},
"zoom": false
},
{
"name": "Rotor Speed FE",
"pen": {"color": "w", "width":3},
"zoom": false
}
]
}
}
}

View File

@ -1,6 +1,6 @@
{
"trace_storage_path": [
"/home/leonid/PycharmProjects/WeldingSpotPerformance/trace_samples"
"D:/downloads/a22"
],
"monitor_update_period": [
1000.0
@ -21,7 +21,7 @@
270.0
],
"mass_2": [
1.0
270.0
],
"k_hardness_1": [
2148570.0
@ -33,22 +33,22 @@
13.8
],
"torque_max_2": [
0.0
13.8
],
"transmission_ratio_1": [
0.00125
],
"transmission_ratio_2": [
1.0
0.00125
],
"contact_distance_1": [
0.02
0.0
],
"contact_distance_2": [
0.081
0.0
],
"k_prop": [
0.075
0.005
],
"time_capture": [
1000.0
@ -56,19 +56,7 @@
"UML_time_scaler": [
1000.0
],
"Range ME, mm": [
115.0
],
"client_time": [
35.488
],
"performance_mode": [
"client"
],
"time_before_start": [
0.924
],
"time_after_end": [
1.14
"gun_range": [
112.5
]
}

View File

@ -4,7 +4,7 @@ cycler==0.12.1
fonttools==4.54.1
httplib2==0.22.0
kiwisolver==1.4.7
loguru==0.7.3
loguru==0.7.2
matplotlib==3.9.2
numpy==2.1.2
packaging==24.1
@ -21,3 +21,4 @@ pytz==2024.2
scipy==1.14.1
six==1.16.0
tzdata==2024.2
watchdog==6.0.0

View File

@ -16,12 +16,6 @@ class OptAlgorithm(AutoConfigClass):
calc = OptTimeCalculator(operator_config, system_config)
if self.distance_l_1 < 0:
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода FE")
if self.distance_l_2 < 0:
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода ME")
self.Ts = calc.T(self.distance_h_start_1,
self.distance_h_start_2,
self.distance_s_1,
@ -227,8 +221,7 @@ class OptAlgorithm(AutoConfigClass):
def FMovement(self, t: float):
x1 = self.X1Movement(t)
x2 = self.X2Movement(t)
#its not true, but theres no need to track collisions at this point
F = 0#self.k_hardness_1 * max(0, (x1 - self.x1_contact))
F = self.k_hardness_1 * max(0, (x1 - self.x1_contact))
return F
def X1Movement(self, t: float):

View File

@ -1,5 +1,4 @@
import numpy as np
from numpy import sqrt, arcsin, arccos, cos, sin, array
from numpy import sqrt, arcsin, arccos, cos, sin
from OptAlgorithm.AutoConfigClass import AutoConfigClass
from OptAlgorithm.ConstantCalculator import ConstantCalculator
@ -22,8 +21,8 @@ class OptTimeCalculator(AutoConfigClass):
v0q = min(sqrt(2 * self.a_max_1 * h1), self.v_max_1)
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
t1 = v0 / self.a_max_1
hleft = max(0, h1 - (self.a_max_1 * t1 * t1 / 2))
t3 = min(max(0, v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
hleft = (h1 - (self.a_max_1 * t1 * t1 / 2))
t3 = min((v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
v1 = (t1 + t3) * self.a_max_1
hleft2 = h1 - (self.a_max_1 * (t1+t3) * (t1+t3) / 2) - v1 * t3 + t3**2 * self.a_max_1 / 2
t2t = max(0, hleft2 / v1)
@ -87,7 +86,7 @@ class OptTimeCalculator(AutoConfigClass):
v1 = topen_1_acc * self.a_max_1
if s1 > topen_1_speed * v1:
s1 -= topen_1_speed * v1
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(max(0,topen_1_acc ** 2 - 2 * s1 / self.a_max_1))
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(topen_1_acc ** 2 - 2 * s1 / self.a_max_1)
else:
topen_1_mark = topen_1_acc + s1 / v1
@ -97,7 +96,7 @@ class OptTimeCalculator(AutoConfigClass):
v2 = topen_2_acc * self.a_max_2
if s2 > topen_2_speed * v2:
s2 -= topen_2_speed * v2
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(max(0,topen_2_acc ** 2 - 2 * s2 / self.a_max_2))
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(topen_2_acc ** 2 - 2 * s2 / self.a_max_2)
else:
topen_2_mark = topen_2_acc + s2 / v2
@ -109,7 +108,7 @@ class OptTimeCalculator(AutoConfigClass):
v0 = self.allTimes["tclose_1_acc"] * self.a_max_1
vF0 = v0 * self.k_hardness_1
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / self.mass_1) * self.Ftogrow)
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / (self.mass_1)) * self.Ftogrow)
L = sqrt(self.k_hardness_1 / self.mass_1 * self.eff_control ** 2 + vF0 * vF0)
tspeed = sqrt(self.mass_1 / self.k_hardness_1) * (
@ -160,78 +159,56 @@ class OptTimeCalculator(AutoConfigClass):
Tfull = self.time_robot_movement
L = self.__dict__["distance_l_" + str(i)]
maxL = L + x0
maxL = contact[i - 1] - L - x0
self.Tmovementi(i, x, Tfull, v0, maxL)
return pos0s, v0s
def Tmovementi(self, i, Sfull, Tfull, v0, maxL) -> None:
v0 = abs(v0)
vmax = self.__dict__["v_max_" + str(i)]
if vmax < v0 - self.check_eps:
raise Exception("Ошибка - скорость в начале перемещения больше макимальной - проверьте скорость")
a = self.__dict__["a_max_" + str(i)]
Sfull = -Sfull
t3 = (Tfull + v0 / a) / 2
time_eps = 1e-8
T = 0
if Sfull > 0:
S1_min = max(Sfull,v0*v0 / (2*a)) + 1e-7
else:
S1_min = v0*v0 / (2*a) + 1e-7
S1_max = maxL
if S1_min > S1_max:
raise Exception("Ошибка - невозможно перемещение робота из-за слишком большого оффсета следующей заготовки - не хватает максимального раскрытия")
# calc for S1
def calc_Tmovement_for_S1(S1):
S2 = -Sfull + S1
if S2 < 0:
raise Exception("Ошибка - прохождение второй части перемещения - отрицательно, неизвестное поведение")
sqrtval = a ** 2 * (
a ** 2 * (Tfull + 2 * t3) ** 2 - 8 * a * Sfull + 2 * a * v0 * (Tfull + 2 * t3) - 3 * v0 ** 2)
if sqrtval < 0:
raise Exception("""Невозможно с S_{i} добраться но H*_{i} за указанное время,
проверьте distance_s_{i}, distance_h_end{i}, time_command, time_robot_movement""")
t1max = ((Tfull + 2 * t3) + v0 / a) / (2) - sqrt(sqrtval) * sqrt(2) / (4 * a ** 2)
t1 = min(t1max, (vmax - abs(v0)) / a)
t1Llimit = -v0 / a + sqrt(v0 ** 2 / (a ** 2) + (abs(maxL) - v0 * v0 / a) / a)
t1 = max(0, min(t1, t1Llimit))
t31 = v0 / a + t1
v1 = v0 + a * t1
t1_theory = -v0 / a + sqrt(v0 * v0 / (2 * a * a) + S1 / a)
t1 = min(t1_theory, max(0, vmax - v0) / a)
v2 = v0 + t1 * a
t31 = v2 / a
S1_fact = v0 * t1 + a * t1 ** 2 / 2 + v2 * t31 - a * t31 ** 2 / 2
t2 = max(0, (S1 - S1_fact) / v2)
T1 = t1 + t31 + t2
#try to push t2 to limit S3_2-5
maxiter = 10
t2 = 0
for j in range(maxiter):
S1 = v0 * t1 + a * t1 * t1 / 2 + v1 * t31 - a * t31 * t31 / 2 + t2 * v1
S2max = Sfull + S1
t5max = sqrt(S2max / a)
t5 = min(t5max, (vmax) / a)
t32 = t5
t3 = t31 + t32
t32_theory = sqrt(S2 / a)
t32 = min(t32_theory, (vmax) / a)
v4 = t32 * a
t5 = t32
S2_fact = a * t32 ** 2 / 2 + v4 * t5 - a * t5 ** 2 / 2
t4 = max(0, (S2 - S2_fact) / v4)
T2 = t32 + t4 + t5
T = T1 + T2
return T, (t1, t2, t31, t32, t4, t5)
T_min, _ = calc_Tmovement_for_S1(S1_min)
if T_min > Tfull:
raise Exception(f"""Ошибка - время перемещения слишком мало, чтобы хотя бы закончить раскрытие, проверьте скорость, ускорение, время перемещения робота """)
T_max, _ = calc_Tmovement_for_S1(S1_max)
if T_max < Tfull:
S1 = S1_max
else:
maxiter = 20
cur_iter = 0
while abs(T - Tfull) > time_eps and S1_max > S1_min and cur_iter < maxiter:
S1_cur = (S1_min + S1_max)/2
T, _ = calc_Tmovement_for_S1(S1_cur)
if T > Tfull:
S1_max = S1_cur
else:
S1_min = S1_cur
cur_iter += 1
S1 = S1_min
T, tarray = calc_Tmovement_for_S1(S1)
tstay = max(0, Tfull - T)
v1 = abs(v0 + t1 * a)
v3 = abs(v0 + t1 * a - t3 * a)
timeleft = Tfull - t1 - t5 - t3
sq = -v0 * t1 - a * t1 ** 2 / 2 - v1 * t3 + a * t3 ** 2 / 2 + v3 * t5 - a * t5 ** 2 / 2
Sleft = Sfull - sq
t2max = (timeleft - Sleft / v3) / (1 + v1 / v3)
Smovement = -v0 * t1 - a / 2 * t1 ** 2 - v1 * t31 + a / 2 * t31 ** 2
if v1 == 0:
t2 = 0
else:
t2 = max(0, min(t2max, (abs(maxL) - abs(Smovement)) / v1))
t4 = max(0, Sleft / v3 + v1 / v3 * t2)
tstay = max(0, Tfull - t1 - t2 - t3 - t4 - t5)
t1, t2, t31, t32, t4, t5 = tarray
for j, t in enumerate(tarray):
if t < 0:
raise Exception(f"""Ошибка - время перехода во время фазы {j + 1} {i}-го электрода отрицательно - переход невозможен с такими параметрами,
проверьте скорость, ускорение, смещение """)
self.allTimes["tmovement_" + str(i) + "_acc"] = t1
self.allTimes["tmovement_" + str(i) + "_speed"] = t2
self.allTimes["tmovement_" + str(i) + "_slow"] = t31
@ -249,7 +226,7 @@ class OptTimeCalculator(AutoConfigClass):
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
t2 = T - sqrt(max(0, T ** 2 - 2 * s / self.a_max_1))
if t2 * self.a_max_1 < v0:
#we should wait to end with max speed but dont do it
#we should wait to end with max speed
t2 = v0 / self.a_max_1
t3 = max(0, (s - self.a_max_1 * t2 ** 2 / 2) / (self.a_max_1 * t2))
t1 = T - t2 - t3

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,751 +0,0 @@
from __future__ import annotations
import os
from typing import Optional, Union, List, Tuple, Dict, Any
from dataclasses import dataclass, field
from cachetools import LRUCache
import pandas as pd
from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
from OptAlgorithm import OptAlgorithm
from utils.qt_settings import dark_style
# ========= Дата-классы для хранения данных =========
@dataclass
class KukaTXT:
"""
Класс для представления текстовых данных Кука.
"""
time: float = 0
endtime: float = 0
func: str = ""
type_: str = ""
signal: str = ""
# Дополнительные поля (закомментированы) можно добавить при необходимости
# module: str
# line: int = 0
# point_name: str = ""
# ...
@dataclass
class KukaDataHead:
"""
Класс, описывающий заголовок данных Кука.
"""
rob_ID: int = 0
filename: str = ""
channels: dict = field(default_factory=dict)
@dataclass
class PlotItems:
"""
Класс для хранения элементов графика: регионы, кривые и QT-виджеты.
"""
regions: dict = field(default_factory=dict)
curves: dict = field(default_factory=dict)
qt_items: dict = field(default_factory=dict)
@dataclass
class PointPassport:
"""
Паспорт точки, содержащий временной интервал, события,
идеальные данные и полезную информацию для этой точки.
"""
timeframe: List[Any] = field(default_factory=list)
events: Dict = field(default_factory=dict)
ideal_data: Dict = field(default_factory=dict)
useful_data: Dict = field(default_factory=dict)
@dataclass
class PerformanceData:
client_to_TWC: float = 0
client_to_ideal: float = 0
TWC_to_ideal: float = 0
@dataclass
class UsefulGraphData:
"""
Полезные данные для графика.
:param client_time: Время, рассчитанное для клиента.
:param range_ME: Диапазон для ME.
:param k_hardness: Коэффициент твердости.
"""
performance: PerformanceData = field(default_factory=PerformanceData)
range_ME: float = 0
k_hardness: float = 0
@dataclass
class GraphicPassport:
"""
Графический паспорт, объединяющий DataFrame с данными,
список паспортов точек и полезные данные для построения графика.
"""
dataframe: pd.DataFrame = field(default_factory=pd.DataFrame)
points_pocket: List[PointPassport] = field(default_factory=list)
useful_data: UsefulGraphData = field(default_factory=UsefulGraphData)
@dataclass
class Settings:
"""
Настройки приложения, разделённые на категории:
- operator: настройки, связанные с оператором,
- system: системные настройки,
- filter: настройки фильтрации трейса клиента для определения событий.
"""
operator: Dict = field(default_factory=dict)
system: Dict = field(default_factory=dict)
filter: Dict = field(default_factory=dict)
# ========= Базовые классы для взаимодействия компонентов =========
class BaseMediator:
"""
Базовый класс медиатора для организации взаимодействия между компонентами приложения.
Медиатор связывает такие модули, как конвертер данных, формирователь паспортов,
виджет построения графиков, контроллер, файловый менеджер и процессор трассировки.
"""
def __init__(self,
converter: BaseDataConverter,
passport_former: BasePointPassportFormer,
plot: BasePlotWidget,
controller: BaseController,
file_manager: BaseFileManager,
trace_processor: BaseRawTraceProcessor):
self._converter = converter
self._converter.mediator = self
self._passport_former = passport_former
self._passport_former.mediator = self
self._plot = plot
self._plot.mediator = self
self._controller = controller
self._controller.mediator = self
self._file_manager = file_manager
self._file_manager.mediator = self
self._trace_processor = trace_processor
self._trace_processor.mediator = self
def notify(self,
source: Union[
BaseDirectoryMonitor,
BaseDataConverter,
BasePointPassportFormer,
BasePlotWidget,
BaseRawTraceProcessor
],
data: Union[
List[str],
List[pd.DataFrame],
List[list],
List[QWidget],
pd.DataFrame
]) -> None:
"""
Получает уведомление от компонента-источника и распределяет данные по соответствующим модулям.
"""
...
def prerender_TCW(self,
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data: Union[str, List[str], List[pd.DataFrame, dict]]) -> None:
"""
Выполняет операции для отображения предварительных данных клиента.
"""
...
def render_TCW(self,
source: Union[BaseController, BaseRawTraceProcessor],
data: Union[str, List[pd.DataFrame, dict]]) -> None:
"""
Выполняет операции для отображения обработанных данных клиента и ТСК.
"""
...
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим работы.
"""
...
class BaseDirectoryMonitor:
"""
Базовый класс для мониторинга директории.
Использует QTimer для периодической проверки содержимого директории.
"""
update_timer = QTimer()
def __init__(self, file_manager: Optional[BaseFileManager] = None):
self._directory_path: Optional[str] = None
self._update_time: Optional[float] = None
self.isActive: bool = False
self._files: List[str] = []
self._file_manager = file_manager
@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 file_manager(self) -> BaseFileManager:
return self._file_manager
@file_manager.setter
def file_manager(self, file_manager: BaseFileManager) -> None:
self._file_manager = file_manager
def init_state(self) -> None:
"""
Инициализирует состояние мониторинга, считывая список файлов из директории.
"""
files = os.listdir(self._directory_path)
self._files = files
def start(self) -> None:
"""
Запускает мониторинг директории, устанавливая флаг активности и запуская таймер.
"""
self.isActive = True
self.update_timer.start(int(self._update_time))
def stop(self) -> None:
"""
Останавливает мониторинг директории.
"""
self.isActive = False
self.update_timer.stop()
def pause(self) -> None:
"""
Приостанавливает мониторинг директории (останавливает таймер).
"""
self.update_timer.stop()
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, controller: BaseController = None):
self._mediator: BaseMediator = mediator
self._controller: BaseController = controller
self._datalen: int = 0
self._datastep: int = 0
self._stage_colors = {
"Closing": [220, 20, 60, 100], # Crimson
"Squeeze": [30, 144, 255, 100], # Dodger Blue
"Welding": [128, 128, 128, 100], # Gray
"Relief": [34, 139, 34, 100], # Forest Green
"Oncomming": [255, 165, 0, 100] # Orange
}
self._plt_channels = None
@staticmethod
def set_style(obj: Union[QTabWidget, QWidget]) -> None:
"""
Устанавливает стиль для переданного объекта.
"""
obj.setStyleSheet(
"""QLabel {
color: #ffffff;
font-size: 26px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}"""
)
@property
def controller(self) -> BaseController:
return self._controller
@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]:
"""
Строит графики на основе списка DataFrame.
"""
...
def build_raw_trace(self, data: pd.DataFrame) -> None:
"""
Строит график трейсов клиента с предварительным разбиением на этапы.
"""
...
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим отображения графиков.
"""
...
class BaseController(QObject):
"""
Базовый класс контроллера, отвечающий за обновление интерфейса и взаимодействие с медиатором.
"""
def __init__(self, mediator: Optional[BaseMediator] = None, file_manager: Optional[BaseFileManager] = None):
super().__init__()
self._mediator = mediator
self._file_manager = file_manager
def send_widgets(self, widgets: List[QWidget]) -> None:
"""
Отправляет список виджетов для дальнейшей обработки.
"""
...
def update_settings(self, settings: List[Dict]) -> None:
"""
Обновляет настройки приложения.
"""
...
def set_working_mode(self, mode: int) -> None:
"""
Устанавливает рабочий режим приложения.
"""
...
def open_file(self, filepath: str) -> None:
"""
Открывает файл по заданному пути.
"""
...
def open_dir(self, dirpath: str) -> None:
"""
Открывает директорию.
"""
...
def update_plots(self) -> None:
"""
Обновляет графики.
"""
...
def update_status(self, msg: str) -> None:
"""
Обновляет статусное сообщение в интерфейсе.
"""
...
def update_progress(self, progress: int) -> None:
"""
Обновляет значение прогресс-бара.
"""
...
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
@property
def file_manager(self) -> BaseFileManager:
return self._file_manager
@file_manager.setter
def file_manager(self, file_manager: BaseFileManager) -> None:
self._file_manager = file_manager
class BaseFileManager:
"""
Базовый класс файлового менеджера.
"""
def __init__(self, mediator: Optional[BaseMediator] = None, monitor: Optional[BaseDirectoryMonitor] = None):
self._mediator = mediator
self._monitor = monitor
self._paths_library: set = set()
self._mode: int = 0
@property
def paths_library(self) -> set:
return self._paths_library
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
@property
def monitor(self) -> BaseDirectoryMonitor:
return self._monitor
@monitor.setter
def monitor(self, monitor: BaseDirectoryMonitor) -> None:
self._monitor = monitor
def replot_all(self) -> None:
"""
Переотрисовывает все данные, используя текущую библиотеку путей.
"""
...
def open_custom_file(self, path: str) -> None:
"""
Открывает пользовательский файл по заданному пути.
"""
...
def open_raw_traces_dir(self, path: str) -> None:
"""
Открывает директорию с сырыми трассировочными данными.
"""
...
def set_mode(self, num: int) -> None:
"""
Устанавливает режим работы файлового менеджера.
"""
...
def update_monitor_settings(self, settings: List[Dict]) -> None:
"""
Обновляет настройки мониторинга директории.
"""
...
def add_new_paths(self, paths: List[str]) -> None:
"""
Добавляет новые пути в библиотеку, если их ранее не было.
"""
...
class BaseIdealDataBuilder(OptAlgorithm):
"""
Базовый класс для построения идеальных данных, расширяющий функциональность OptAlgorithm.
При инициализации на основе настроек устанавливаются множитель времени и время сварки.
"""
def __init__(self, settings: Settings):
self.mul = settings.system['time_capture']
self.welding_time = settings.operator['time_wielding']
self._settings = settings
super().__init__(settings.system, settings.operator)
def get_closingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа закрытия.
"""
...
def get_compressionDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сжатия.
"""
...
def get_openingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа открытия.
"""
...
def get_tmovementDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения.
"""
...
def get_weldingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сварки.
"""
...
def get_oncomingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения (oncomming).
"""
...
def get_ideal_timings(self) -> Tuple[float, float, float, float]:
"""
Получает идеальные временные интервалы для этапов.
"""
...
def get_cycle_time(self) -> float:
"""
Вычисляет общее время цикла как сумму идеальных временных интервалов.
"""
result = sum(self.get_ideal_timings())
return result
class BaseMainWindow(QMainWindow):
"""
Базовое главное окно приложения.
"""
def __init__(self):
super().__init__()
self.resize(200, 200)
# Создаем центральный виджет и устанавливаем его
self._central_widget = QWidget()
self.setCentralWidget(self._central_widget)
# Устанавливаем основной вертикальный макет для центрального виджета
self._central_layout = QVBoxLayout()
self._central_widget.setLayout(self._central_layout)
self.set_style(self)
...
def set_style(self, obj: Union[QTabWidget, QWidget, QMainWindow]) -> None:
"""
Устанавливает стиль для переданного объекта.
"""
obj.setStyleSheet(dark_style)
class BasePointPassportFormer:
"""
Базовый класс для формирования паспортов точек.
Содержит базовые настройки, список этапов, кэш для идеальных данных и набор параметров для алгоритма.
"""
def __init__(self, mediator: Optional[BaseMediator] = None):
self._mediator = mediator
self._clear_stage: str = "Welding"
self._settings = Settings()
self._stages: List[str] = [
"Closing",
"Squeeze",
"Welding",
"Relief",
"Oncomming"
]
self._tesla_stages: List[str] = [
"Tesla squeeze",
"Tesla closing",
"Tesla welding",
"Tesla oncomming_relief"
]
self._ideal_data_cache = LRUCache(maxsize=1000)
self._OptAlgorithm_operator_params = [
"dist_open_start_1",
"dist_open_start_2",
"dist_open_after_1",
"dist_open_after_2",
"dist_open_end_1",
"dist_open_end_2",
"dist_close_end_1",
"dist_close_end_2",
"time_command",
"time_robot_movement",
"object_thickness",
"force_target",
"force_capture",
"time_wielding"
]
self._OptAlgorithm_system_params = [
"a_max_1",
"v_max_1",
"a_max_2",
"v_max_2",
"mass_1",
"mass_2",
"k_hardness_1",
"k_hardness_2",
"torque_max_1",
"torque_max_2",
"transmission_ratio_1",
"transmission_ratio_2",
"position_start_1",
"position_start_2",
"k_prop",
"time_capture"
]
def form_passports(self, data: List[pd.DataFrame]) -> None:
"""
Формирует паспорта для набора данных.
"""
...
def form_customer_passport(self, data: List[Union[pd.DataFrame, dict]]) -> None:
"""
Формирует паспорт заказчика на основе DataFrame и событий.
"""
...
def update_settings(self, params: List) -> None:
"""
Обновляет настройки для формирования паспортов.
"""
...
@property
def opt_algorithm(self) -> BaseIdealDataBuilder:
return self._opt_algorithm
@opt_algorithm.setter
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder) -> None:
self._opt_algorithm = opt_algorithm
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
class BaseRawTraceProcessor:
"""
Базовый класс для обработки сырых трассировочных данных.
"""
def __init__(self,
dataparser: BaseKukaDataParser,
textparser: BaseKukaTextParser,
data_detector: BaseTraceStageDetector,
text_detector: BaseTextStageDetector,
mediator: Optional[BaseMediator] = None):
self._mediator = mediator
self._dataparser = dataparser
self._textparser = textparser
self._data_detector = data_detector
self._text_detector = text_detector
self._trace_df: Optional[pd.DataFrame] = None
self._text_data: Optional[Any] = None
def prerender(self, data: List[str]) -> None:
"""
Выполняет предварительный рендеринг данных.
"""
...
def final_render(self) -> None:
"""
Выполняет финальный рендеринг данных.
"""
...
def update_settings(self, data: Settings) -> None:
"""
Обновляет настройки обработки трассировочных данных.
"""
...
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
class BaseKukaDataParser:
"""
Базовый класс для парсинга данных Кука.
"""
def __init__(self):
self._ch_name: Optional[str] = None
def parse(self, head_path: str) -> pd.DataFrame:
"""
Парсит файл заголовка и возвращает DataFrame с данными.
"""
...
class BaseKukaTextParser:
"""
Базовый класс для парсинга текстовых данных Кука.
"""
def __init__(self):
self._in_msg = None
self._datapacks = None
def parse(self, path: str) -> List[KukaTXT]:
"""
Парсит текстовый файл и возвращает список объектов KukaTXT.
"""
...
class BaseTraceStageDetector:
"""
Базовый класс для детекции стадий на основе данных трассировки.
"""
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
self._parent = parent
def detect_stages(self, df: pd.DataFrame) -> List:
"""
Детектирует стадии процесса по данным DataFrame.
"""
...
class BaseTextStageDetector:
"""
Базовый класс для детекции сварочных стадий на основе текстовых данных.
"""
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
self._parent = parent
def detect_welding(self, data: List[KukaTXT]) -> List:
"""
Детектирует этап сварки по текстовым данным.
"""
...

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,102 +1,43 @@
from typing import Tuple
from PyQt5.QtWidgets import QWidget, QTabWidget
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import pyqtSignal
from typing import Union
from base.base import BaseController, Settings
from utils.base.base import BaseController
class Controller(BaseController):
"""
Контроллер приложения.
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
медиатор и т.д.
"""
signal_widgets = pyqtSignal(list)
signal_progress_bar = pyqtSignal(int)
signal_status_text = pyqtSignal(str)
def set_working_mode(self, mode: int) -> None:
"""
Устанавливает рабочий режим приложения через медиатор.
:param mode: Режим работы (целое число).
"""
self._mediator.set_mode(mode)
def update_plots(self) -> None:
"""
Инициирует переотрисовку графиков через файловый менеджер.
"""
self._file_manager.replot_all()
signal_settings = pyqtSignal(list)
signal_open_file = pyqtSignal(str)
signal_statusBar = pyqtSignal(int)
signal_statusText = pyqtSignal(str)
signal_raport_mode = pyqtSignal()
signal_seeking_mode = pyqtSignal()
signal_update_plots = pyqtSignal()
def send_widgets(self, widgets: list[QWidget]) -> None:
"""
Отправляет список виджетов для дальнейшего использования.
:param widgets: Список виджетов.
"""
self.signal_widgets.emit(widgets)
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки приложения, передавая их через медиатор.
def update_settings(self, settings: list[dict]) -> None:
self.signal_settings.emit(settings)
:param settings: Объект настроек.
"""
self._mediator.notify(self, settings)
def raport_mode(self) -> None:
self.signal_raport_mode.emit()
def update_status(self, msg: str) -> None:
"""
Обновляет статусное сообщение в интерфейсе.
:param msg: Текст сообщения.
"""
self.signal_status_text.emit(msg)
def update_progress(self, progress: int) -> None:
"""
Обновляет значение прогресс-бара.
:param progress: Значение прогресса (от 0 до 100).
"""
self.signal_progress_bar.emit(progress)
def seeking_mode(self) -> None:
self.signal_seeking_mode.emit()
def open_file(self, filepath: str) -> None:
"""
Открывает файл по указанному пути через файловый менеджер.
self.signal_open_file.emit(filepath)
:param filepath: Путь к файлу.
"""
self._file_manager.open_custom_file(filepath)
def update_plots(self) -> None:
self.signal_update_plots.emit()
def open_dir(self, dirpath: str) -> None:
"""
Инициирует предварительный рендеринг данных из директории.
def update_status(self, msg: Union[str, float, int]) -> None:
if type(msg) == float or type(msg) == int:
self.signal_statusBar.emit(int(msg))
else:
self.signal_statusText.emit(msg)
:param dirpath: Путь к директории.
"""
self._mediator.prerender_TCW(self, dirpath)
def build_TCW_for_client(self) -> None:
"""
Выполняет финальный рендеринг TCW (Trace Control Widget) для клиента.
"""
self._mediator.render_TCW(self)
def save_file(self, data: Tuple[str, QTabWidget]) -> None:
"""
Сохраняет снимок содержимого вкладки в указанный файл.
:param data: Кортеж, содержащий путь к файлу и QTabWidget для сохранения.
"""
filepath, tab = data
# Создаем QPixmap с размером, равным размеру вкладки
pixmap = QPixmap(tab.size())
# Рендерим содержимое вкладки в QPixmap
tab.render(pixmap)
# Сохраняем изображение по указанному пути
pixmap.save(filepath)

View File

@ -1,104 +1,35 @@
import pandas as pd
import traceback
import sys
from loguru import logger
from base.base import BaseDataConverter
# FIXME: костыль для выключения предупреждения "replace deprecated"
pd.set_option('future.no_silent_downcasting', True)
from utils.base.base import BaseDataConverter
class DataConverter(BaseDataConverter):
"""
Класс для преобразования данных из CSV-файлов.
Основные задачи:
- Исправление заголовков столбцов по заданному шаблону.
- Преобразование булевых значений в целочисленные.
- Обработка ошибок при конвертации и уведомление через медиатора.
"""
@staticmethod
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
"""
Преобразует столбцы, содержащие булевы значения, в целочисленные.
:param df: DataFrame с исходными данными.
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
"""
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
try:
# Находим столбцы, в которых все значения являются True или False
bool_columns = df.columns[df.isin([True, False]).all()]
# Приводим найденные столбцы к типу int
df = df.astype({col: int for col in bool_columns})
return df
except AttributeError as e:
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
return None
except (TypeError, ValueError) as e:
logger.error(f"_replace_bool - Ошибка типа или значения: {e}")
return None
except Exception as e:
logger.error(f"_replace_bool - Непредвиденная ошибка: {e}")
return None
@staticmethod
def _fix_headers(df: pd.DataFrame) -> pd.DataFrame:
"""
Приводит заголовки столбцов DataFrame к корректным именам согласно заранее заданному списку.
:param df: DataFrame с исходными заголовками.
:return: DataFrame с исправленными заголовками или None, если произошла ошибка.
"""
correct_columns = [
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
"Position Control FE", "Position Control ME", "Relief",
"Rotor Position, mm FE", "Rotor Position, mm ME",
"Rotor Speed, mm/s FE", "Rotor Speed, mm/s ME",
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
]
try:
# Создаем словарь соответствий: ключ - имя в нижнем регистре, значение - корректное имя
correct_mapping = {name.lower(): name for name in correct_columns}
new_columns = []
# Для каждого столбца заменяем имя, если оно присутствует в корректном отображении
for col in df.columns:
fixed_col = correct_mapping.get(col.lower(), col)
new_columns.append(fixed_col)
df.columns = new_columns
# Удаляем повторяющиеся столбцы, оставляя первое вхождение
df = df.loc[:, ~df.columns.duplicated()]
return df
except AttributeError as e:
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
return None
except Exception as e:
logger.error(f"_fix_headers - Непредвиденная ошибка: {e}")
bool_columns = dataframe.columns[dataframe.isin([True, False]).any()]
dataframe[bool_columns] = dataframe[bool_columns].replace({True: 1, False: 0})
return dataframe
except:
return None
def convert_data(self, files: list[str]) -> None:
"""
Считывает данные из списка CSV-файлов, исправляет заголовки и преобразует булевые столбцы,
после чего уведомляет медиатора о завершении преобразования.
:param files: Список путей к CSV-файлам.
"""
try:
# Считываем данные из файлов (если путь не пустой, иначе None)
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
# Применяем исправление заголовков к каждому DataFrame
renamed_dataframes = list(map(self._fix_headers, dataframes))
# Преобразуем булевые значения для каждого DataFrame
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
except FileNotFoundError as e:
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
converted_dataframes = [None]
except pd.errors.EmptyDataError as e:
logger.error(f"convert_data - EmptyDataError: Файл пустой. {e}")
converted_dataframes = [None]
except Exception as e:
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
converted_dataframes = list(map(self._replace_bool, dataframes))
except:
# Get the traceback object
tb = sys.exc_info()[2]
tbinfo = traceback.format_tb(tb)[0]
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
logger.error(pymsg)
converted_dataframes = [None]
finally:
# Передаем результат медиатору
self._mediator.notify(self, converted_dataframes)

View File

@ -1,189 +0,0 @@
import os
from loguru import logger
from base.base import BaseDirectoryMonitor, BaseFileManager, Settings
class FileManager(BaseFileManager):
"""
Менеджер файлов для работы с библиотекой путей.
Основные функции:
- Переотправка (replot) списка найденных файлов.
- Добавление нового пути (например, выбранного пользователем).
- Переключение режимов работы (создание отчёта, онлайн-мониторинг, работа с трейсами клиента).
- Обновление настроек мониторинга.
- Поиск файлов в директории с трейсами: поиск файлов .dat и .txt.
"""
def replot_all(self) -> None:
"""
Переотправляет все пути из библиотеки.
Если текущий режим не равен 3, уведомляет медиатора со списком путей.
Если режим равен 3, открывает директорию с трейсами.
"""
if self._paths_library is not None:
if self._mode != 3:
self._mediator.notify(self, list(self._paths_library))
else:
self.open_raw_traces_dir(list(self._paths_library)[0])
def open_custom_file(self, path: str) -> None:
"""
Добавляет указанный путь в библиотеку и уведомляет медиатора.
:param path: Путь к файлу, выбранному пользователем.
"""
self._paths_library.add(path)
# Передаем уведомление с указанным путем (если путь пустой, передаем пустую строку)
self._mediator.notify(self, [path if path else ''])
def set_mode(self, num: int) -> None:
"""
Устанавливает режим работы менеджера файлов.
Режимы:
1 - Режим создания отчёта.
2 - Режим онлайн-мониторинга папки.
3 - Режим работы с трейсами клиента.
:param num: Целое число, определяющее режим работы.
"""
match num:
case 1:
# Режим создания отчёта
self._monitor.stop()
self._paths_library.clear()
self._paths_library.add('')
self._mediator.notify(self.monitor, list(self._paths_library))
self._mode = 1
case 2:
# Режим онлайн-мониторинга папки
self._monitor.init_state()
self._monitor.start()
self._mode = 2
case 3:
# Режим работы с трейсами клиента
self._monitor.stop()
self._paths_library.clear()
self._mode = 3
def update_monitor_settings(self, settings: Settings) -> None:
"""
Обновляет настройки мониторинга директории.
Из объекта настроек извлекается путь к директории и интервал обновления.
Выполняется проверка существования директории и корректности интервала обновления.
При необходимости приостанавливается или возобновляется монитор.
:param settings: Объект Settings с системными настройками.
"""
directory_path = settings.system['trace_storage_path'][0]
update_time = settings.system['monitor_update_period'][0]
if not os.path.exists(directory_path):
logger.warning(f"Путь {directory_path} не существует.")
# Можно раскомментировать raise, если требуется прерывание работы
# raise FileNotFoundError(f"Путь {directory_path} не существует.")
if update_time <= 0.01:
logger.warning(f"Интервал между проверками папки слишком мал: {update_time}")
if self._monitor.isActive:
self._monitor.pause()
self._monitor._directory_path = directory_path
self._monitor._update_time = update_time
if self._monitor.isActive:
self._monitor.start()
def add_new_paths(self, paths: list) -> None:
"""
Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали,
и уведомляет медиатора о новых путях.
:param paths: Список новых путей.
"""
paths_set = set(paths)
new_paths = self._paths_library.difference(paths_set)
self._paths_library.update(new_paths)
self._mediator.notify(self, list(new_paths))
def open_raw_traces_dir(self, path: str) -> None:
"""
Открывает указанную директорию, ищет в ней файлы с расширениями .dat и .txt,
и при их наличии инициирует предварительный рендеринг через медиатора.
:param path: Путь к директории с трейсами.
"""
self._paths_library.clear()
self._paths_library.add(path)
dat_file, txt_file = None, None
# Перебираем содержимое директории
for entry in os.listdir(path):
full_path = os.path.join(path, entry)
if os.path.isfile(full_path):
_, ext = os.path.splitext(entry)
ext = ext.lower()
if ext == '.dat' and dat_file is None:
dat_file = full_path
elif ext == '.txt' and txt_file is None:
txt_file = full_path
if dat_file and txt_file:
break
if dat_file and txt_file:
# Уведомляем медиатора для запуска предварительного рендеринга трейс-контента
self._mediator.prerender_TCW(self, [dat_file, txt_file])
class DirectoryMonitor(BaseDirectoryMonitor):
"""
Мониторинг директории для поиска CSV-файлов.
Инициализирует состояние, подключает таймер обновления и отслеживает появление новых файлов.
При обнаружении новых файлов уведомляет менеджер файлов.
"""
def init_state(self):
"""
Инициализирует состояние мониторинга директории.
Проверяет, существует ли директория. Если директория не существует, генерирует ошибку.
Собирает список файлов с расширением .csv и подключает метод мониторинга к таймеру.
"""
if not os.path.exists(self._directory_path):
logger.error(f"Путь {self._directory_path} не существует.")
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
self._files = [
os.path.join(self._directory_path, file)
for file in os.listdir(self._directory_path)
if file.lower().endswith('.csv')
]
self.update_timer.timeout.connect(self._monitor)
logger.info("Monitor initiated!")
def _monitor(self):
"""
Метод, вызываемый по таймеру, для проверки появления новых CSV-файлов.
Если обнаружены новые файлы, уведомляет менеджер файлов и обновляет список файлов.
Если в директории отсутствуют файлы, сбрасывает список.
"""
current_files = [
os.path.join(self._directory_path, file)
for file in os.listdir(self._directory_path)
if file.lower().endswith('.csv')
]
# Определяем файлы, которых еще не было в предыдущем списке
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
if new_files:
logger.info(f"New files detected: {new_files}")
self._file_manager.add_new_paths(new_files)
self._files = current_files
if not current_files:
self._files = []

View File

@ -1,127 +1,41 @@
from typing import Union
import pandas as pd
import time
from typing import Union
from PyQt5.QtWidgets import QWidget
from base.base import (
BaseMediator,
BaseFileManager,
BaseDataConverter,
BasePlotWidget,
BasePointPassportFormer,
BaseController,
GraphicPassport,
Settings,
BaseRawTraceProcessor
)
from utils.base.base import (BaseMediator, BaseDirectoryMonitor,
BaseDataConverter, BasePlotWidget,
BasePointPassportFormer)
class Mediator(BaseMediator):
"""
Медиатор для организации взаимодействия между различными компонентами приложения.
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
"""
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
def notify(
self,
source: Union[
BaseFileManager,
BaseDataConverter,
BasePointPassportFormer,
BasePlotWidget,
BaseController,
BaseRawTraceProcessor
],
data: Union[
list[str],
list[pd.DataFrame],
list[GraphicPassport],
list[QWidget],
Settings,
pd.DataFrame
]
) -> None:
"""
Принимает уведомление от компонента-источника и направляет данные в соответствующий модуль.
:param source: Компонент, вызвавший уведомление.
:param data: Передаваемые данные (могут быть строками, DataFrame, графическими паспортами, виджетами, Settings).
"""
# Если источник менеджер файлов: обновляем статус и запускаем конвертацию CSV
if isinstance(source, BaseFileManager):
self._controller.update_status("CSV found! Calculating...")
if issubclass(source.__class__, BaseDirectoryMonitor):
self.update_status("CSV found! Calculating...")
self._converter.convert_data(data)
# Если источник конвертер данных: обновляем прогресс и формируем паспорта
if isinstance(source, BaseDataConverter):
self._controller.update_progress(1)
self._passport_former.form_passports(data)
if issubclass(source.__class__, BaseDataConverter):
self.update_status(0.5)
self._passportFormer.form_passports(data)
# Если источник формирователь паспортов точек: обновляем прогресс и строим графический паспорт
if isinstance(source, BasePointPassportFormer):
self._controller.update_progress(10)
if issubclass(source.__class__, BasePointPassportFormer):
self.update_status(1)
self._plot.build(data)
# Если источник виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
if isinstance(source, BasePlotWidget):
self._controller.update_progress(100)
if issubclass(source.__class__, BasePlotWidget):
self.update_status(100)
self._controller.send_widgets(data)
# Если источник контроллер: обновляем настройки у различных модулей
if isinstance(source, BaseController):
self._file_manager.update_monitor_settings(data)
self._passport_former.update_settings(data)
self._trace_processor.update_settings(data)
def update_settings(self, settings: list[dict]):
self._monitor.update_settings(settings)
self._passportFormer.update_settings(settings)
def prerender_TCW(
self,
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data: Union[str, list[str], list[pd.DataFrame], dict]
) -> None:
"""
Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления.
def update_status(self, msg: Union[str, float]) -> None:
self._controller.update_status(msg)
:param source: Компонент, инициирующий предварительный рендеринг.
:param data: Передаваемые данные, могут быть строкой или списком путей/DataFrame и словарём.
"""
if isinstance(source, BaseController):
self._controller.update_progress(5)
self._file_manager.open_raw_traces_dir(data)
if isinstance(source, BaseFileManager):
self._controller.update_progress(15)
self._trace_processor.prerender(data)
if isinstance(source, BaseRawTraceProcessor):
self._controller.update_progress(40)
self._plot.build_raw_trace(data)
def render_TCW(
self,
source: Union[BaseController, BaseRawTraceProcessor],
data: Union[str, list[pd.DataFrame], dict] = None
) -> None:
"""
Выполняет финальный рендеринг TCW.
:param source: Компонент, инициирующий финальный рендеринг.
:param data: Передаваемые данные (опционально).
"""
if isinstance(source, BaseController):
self._controller.update_progress(5)
self._trace_processor.final_render()
if isinstance(source, BaseRawTraceProcessor):
self._controller.update_progress(5)
self._passport_former.form_customer_passport(data)
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим работы для графиков и файлового менеджера.
:param mode: Целое число, определяющее режим работы.
"""
self._plot.set_mode(mode)
self._file_manager.set_mode(mode)

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

@ -0,0 +1,72 @@
import os
from loguru import logger
from utils.base.base import BaseDirectoryMonitor
class DirectoryMonitor(BaseDirectoryMonitor):
def _init_state(self):
self._files = [
os.path.join(self._directory_path, file)
for file in os.listdir(self._directory_path)
if file.lower().endswith('.csv')
]
self.update_timer.timeout.connect(self._monitor)
logger.info("Monitor initiated!")
def _monitor(self):
current_files = [
os.path.join(self._directory_path, file)
for file in os.listdir(self._directory_path)
if file.lower().endswith('.csv')
]
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
if new_files:
logger.info(f"New files detected: {new_files}")
self._mediator.notify(self, new_files)
self._files = current_files
if not current_files:
self._files = []
def update_settings(self, data: list[dict]) -> None:
if self.isActive:
self.stop()
_, system_params = data
self._directory_path = system_params['trace_storage_path'][0]
self._update_time = system_params['monitor_update_period'][0]
if not os.path.exists(self._directory_path):
logger.error(f"Путь {self._directory_path} не существует.")
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
else:
self._init_state()
self.start()
else:
_, system_params = data
self._directory_path = system_params['trace_storage_path'][0]
self._update_time = system_params['monitor_update_period'][0]
def update_plots(self):
if self._files is not None:
self._mediator.notify(self, self._files)
def custom_csv_extract_only(self, path: str):
self._files.append(path)
if path is not None:
self._mediator.notify(self, [path])
else:
self._mediator.notify(self, [''])
def start_raport(self) -> None:
self.stop()
self._files = ['']
self._mediator.notify(self, [''])
def start_seeking(self) -> None:
self._init_state()
self.start()

View File

@ -0,0 +1,126 @@
import pandas as pd
import traceback
import sys
from loguru import logger
from utils.base.base import BasePointPassportFormer, BaseIdealDataBuilder
class idealDataBuilder(BaseIdealDataBuilder):
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_oncomingDF(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']-0.0001)
X1, X2, V1, V2 = X1*1000, X2*1000, V1*1000, V2*1000
data.append({"time":0, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
data.append({"time":self.welding_time, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
return pd.DataFrame(data)
def get_ideal_timings(self) -> list[float]:
data = self.Ts
ideal_timings = [data['tclose'], data['tgrow'], self.welding_time, self.getMarkOpen(), data['tmovement']]
return ideal_timings
class PassportFormer(BasePointPassportFormer):
def form_passports(self, data: list[pd.DataFrame]) -> list[list[pd.DataFrame, dict, int]]:
try:
return_data = [self._build_passports_pocket(dataframe) for dataframe in data]
except:
tb = sys.exc_info()[2]
tbinfo = traceback.format_tb(tb)[0]
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
logger.error(pymsg)
return_data = []
finally:
self._mediator.notify(self, return_data)
def _build_passports_pocket(self, dataframe: pd.DataFrame) -> list[pd.DataFrame, dict, int]:
if dataframe is not None:
events, point_quantity = self._filter_events(dataframe["time"], dataframe)
if point_quantity == 0:
return []
idx_shift = True if events[self._stages[-1]][0][0] == 0 else False
else:
events = None
key = list(self._params[0].keys())[0]
point_quantity = len(self._params[0][key])
points_pocket = []
system_settings = {key: value[0] for key, value in self._params[1].items()}
tesla_time = sum(self._params[0].get("Tesla summary time", []))
useful_data = {
"tesla_time": tesla_time,
"range": system_settings["gun_range"],
"k_hardness": system_settings["k_hardness_1"]
}
for i in range(point_quantity):
operator_settings = {
key: (value[i] if i < len(value) else value[0])
for key, value in self._params[0].items()
}
next_i = i + 1
P1 = operator_settings["object_position"] + 0.5 * operator_settings["object_thickness"]
P2 = operator_settings["object_position"] - 0.5 * operator_settings["object_thickness"]
if next_i < point_quantity:
next_operator_settings = {
key: (value[next_i] if i < len(value) else value[0])
for key, value in self._params[0].items()
}
next_P1 = next_operator_settings["object_position"] + 0.5*next_operator_settings["object_thickness"]
next_P2 = next_operator_settings["object_position"] - 0.5*next_operator_settings["object_thickness"]
displacement_me = next_P1 - P1
displacement_fe = next_P2 - P2
operator_settings["distance_h_end1"] -= displacement_fe
operator_settings["distance_h_end2"] += displacement_me
LFEAbs = operator_settings["distance_l_1"]
LMEAbs = operator_settings["distance_l_2"]
operator_settings["distance_l_1"] = P2 - LFEAbs
operator_settings["distance_l_2"] = LMEAbs - P1
params_list = [operator_settings, system_settings]
cache_key = self._generate_cache_key(params_list)
if cache_key in self._ideal_data_cashe:
ideal_data = self._ideal_data_cashe[cache_key]
else:
ideal_data = self._build_ideal_data(idealDataBuilder=idealDataBuilder, params=params_list)
self._ideal_data_cashe[cache_key] = ideal_data
if events is not None:
idx = i+1 if idx_shift else i
point_timeframe = [events[self._stages[0]][0][i], events[self._stages[-1]][1][idx]]
point_events = {key: [value[0][i], value[1][i]] for key, value in events.items()}
else:
point_timeframe, point_events = None, None
useful_p_data = {"thickness": operator_settings["object_thickness"],
"position": operator_settings["object_position"]-0.5*operator_settings["object_thickness"],
"force": operator_settings["force_target"],
"P1": operator_settings["object_position"] + 0.5*operator_settings["object_thickness"],
"P2": operator_settings["object_position"] - 0.5*operator_settings["object_thickness"]}
points_pocket.append([point_timeframe, ideal_data, point_events, useful_p_data])
return dataframe, points_pocket, useful_data
def update_settings(self, params: list[dict, dict]):
self._params = params

View File

@ -1,544 +0,0 @@
from typing import Optional, Any, Tuple, List, Dict
import numpy as np
import pandas as pd
from loguru import logger
from base.base import (
BasePointPassportFormer,
BaseIdealDataBuilder,
PointPassport,
GraphicPassport,
Settings,
UsefulGraphData,
PerformanceData
)
class PassportFormer(BasePointPassportFormer):
"""
Класс для формирования паспортов (графических и точечных) по данным трассировки.
Основные возможности:
- Формирование паспортов для каждого DataFrame.
- Формирование паспорта заказчика на основе DataFrame и событий.
- Генерация событий по DataFrame.
- Построение идеальных данных для каждой точки.
- Формирование графического паспорта, включающего полезные данные и перечень паспортов точек.
Внимание: многие атрибуты (например, self._mediator, self._stages, self._clear_stage,
self._settings, self._ideal_data_cache, self._OptAlgorithm_operator_params,
self._OptAlgorithm_system_params) предполагается задавать извне (например, в базовом классе).
"""
def form_passports(self, data: List[pd.DataFrame]) -> None:
"""
Формирует паспорта для каждого DataFrame из списка.
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
:param data: Список DataFrame с данными трассировки.
"""
try:
passports = [self._build_from_df_only(df) for df in data]
except Exception as e:
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
passports = []
finally:
self._mediator.notify(self, passports)
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки формирования паспортов.
:param settings: Объект настроек.
"""
self._settings = settings
def form_customer_passport(self, data: Tuple[pd.DataFrame, Dict, Tuple]) -> None:
"""
Формирует паспорт заказчика на основе DataFrame и словаря событий.
Ожидается, что data содержит:
- DataFrame с данными,
- словарь событий,
- кортеж координат ME (например, (part_pos, open_pos)).
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
:param data: Кортеж из (DataFrame, события, ME_coords).
"""
try:
dataframe, events, ME_coords = data
point_quantity = len(events["Squeeze"][0])
self._modify_coord_settings(ME_coords)
customer_passport = self._form_graphic_passport(dataframe, events, point_quantity)
result = [customer_passport]
except Exception as e:
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
result = []
finally:
self._mediator.notify(self, result)
def _modify_coord_settings(self, ME_coords: Tuple[List[float], List[float]]) -> None:
"""
Модифицирует настройки координат на основе переданных данных ME.
:param ME_coords: Кортеж из двух списков: (part_pos, open_pos).
"""
part_pos, open_pos = ME_coords
self._settings.operator["distance_l_2"] = open_pos
l1 = self._settings.operator["distance_l_1"]
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
@staticmethod
def _find_indexes(signal: str, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
"""
Находит индексы начала и окончания этапа для указанного сигнала.
:param signal: Имя столбца-сигнала.
:param df: DataFrame с данными.
:return: Кортеж из массивов индексов начала и окончания этапа.
"""
stage_diff = np.diff(df[signal])
start_idx = np.where(stage_diff == 1)
finish_idx = np.where(stage_diff == -1)
return start_idx[0], finish_idx[0]
@staticmethod
def _find_events(signal: str, times: pd.Series, df: pd.DataFrame) -> Tuple[List[float], List[float]]:
"""
Формирует списки времён начала и окончания событий для указанного сигнала.
Если у первого события не определено время начала, оно принимается за 0.
Если число стартов события больше числа финишей, последним финишем считается конец времён.
:param signal: Имя столбца-сигнала.
:param times: Series с временными метками.
:param df: DataFrame с данными.
:return: Кортеж из двух списков: (список времён начала, список времён окончания).
"""
start_idx, finish_idx = PassportFormer._find_indexes(signal, df)
start_list, end_list = [], []
if len(start_idx) > 0 and len(finish_idx) > 0:
if start_idx[0] > finish_idx[0]:
logger.debug(f"_find_events - не найдено начало события {signal} с окончанием в {times.iloc[finish_idx[0]]} секунд, принято равным 0")
start_idx = np.insert(start_idx, 0, 0)
if start_idx[-1] > finish_idx[-1]:
logger.debug(f"_find_events - не найден конец события {signal} с началом в {times.iloc[start_idx[-1]]} секунд, принято равным {times.iloc[-1]} секунд")
finish_idx = np.append(finish_idx, -1)
start_list = times.iloc[start_idx].tolist()
end_list = times.iloc[finish_idx].tolist()
return (start_list, end_list)
def _generate_events(self, times: pd.Series, df: pd.DataFrame) -> Tuple[Dict[str, List[List[float]]], int]:
"""
Генерирует словарь событий для каждого этапа, используя временные метки и данные.
Также определяет общее количество точек (на основе количества событий основного этапа).
Если для основного этапа (self._clear_stage) не найдено ни одного события, возвращается пустой словарь и 0 точек.
:param times: Серия временных меток.
:param df: DataFrame с данными.
:return: Кортеж (словарь событий, количество точек).
"""
events = {}
point_quantity = 0
if self._clear_stage in self._stages:
start_list, _ = self._find_events(self._clear_stage, times, df)
point_quantity = len(start_list)
if point_quantity == 0:
logger.error(f"_generate_events - Не найдены события для этапа '{self._clear_stage}'.")
return {}, 0
for stage in self._stages:
s_list, e_list = self._find_events(stage, times, df)
temp = max(len(s_list), len(e_list))
if temp < point_quantity:
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
f"Ожидается {point_quantity}, получено {temp}.")
events[stage] = [s_list, e_list]
if self._settings.system["performance_mode"][0] == 'client':
client_rob = self._settings.operator["time_robot_movement"]
move_start = events["Oncomming"][0]
if len(move_start) > len(client_rob):
move_start = move_start[1:]
events["Oncomming"] = [move_start, [client_rob[i]+ move_start[i] for i in range(len(move_start))]]
return (events, point_quantity)
def _build_ideal_data(self, idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
point_settings: Optional[Settings] = None) -> Dict:
"""
Строит идеальные данные с использованием билдера.
:param idealDataBuilder: Класс-билдер для генерации идеальных данных.
:param point_settings: Настройки для точки.
:return: Словарь с идеальными данными для этапов.
"""
try:
self._opt_algorithm = idealDataBuilder(point_settings)
stage_ideals = {
"Closing": self.opt_algorithm.get_closingDF(),
"Squeeze": self.opt_algorithm.get_compressionDF(),
"Welding": self.opt_algorithm.get_weldingDF(),
"Relief": self.opt_algorithm.get_openingDF(),
"Oncomming": self.opt_algorithm.get_oncomingDF(),
"Ideal cycle": self.opt_algorithm.get_cycle_time(),
"Ideal timings": self.opt_algorithm.get_ideal_timings()
}
return stage_ideals
except Exception as e:
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
return {}
def _generate_cache_key(self, point_settings: Settings) -> Tuple[Tuple[Tuple[str, Any], ...], Tuple[Tuple[str, Any], ...]]:
"""
Преобразует настройки точки в хешируемый ключ для кэша.
Использует только те параметры оператора и системы, которые присутствуют в
соответствующих наборах (self._OptAlgorithm_operator_params и self._OptAlgorithm_system_params).
:param point_settings: Объект настроек для точки.
:return: Кортеж из двух frozenset с параметрами.
"""
try:
operator_tuple = frozenset(
(key, value)
for key, value in point_settings.operator.items()
if str(key) in self._OptAlgorithm_operator_params
)
system_tuple = frozenset(
(key, value)
for key, value in point_settings.system.items()
if str(key) in self._OptAlgorithm_system_params
)
return (operator_tuple, system_tuple)
except Exception as e:
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
return ((), ())
def _build_from_df_only(self, df: pd.DataFrame) -> Optional[GraphicPassport]:
"""
Строит GraphicPassport на основе одного DataFrame.
Если DataFrame содержит необходимые столбцы (определяемые в self._stages),
генерируются события; иначе используется альтернативное определение количества точек.
Если количество точек равно нулю, логируется ошибка и возвращается None.
:param df: DataFrame с данными.
:return: Объект GraphicPassport или None в случае ошибки.
"""
try:
if df is not None and set(self._stages).issubset(set(df.columns.tolist())):
events, point_quantity = self._generate_events(df["time"], df)
if point_quantity == 0:
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
return None
else:
events = None
key = list(self._settings.operator.keys())[0]
point_quantity = len(self._settings.operator[key])
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
passport = self._form_graphic_passport(df, events, point_quantity)
return passport
except Exception as e:
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
return None
def _form_graphic_passport(self, df: pd.DataFrame, events: Dict, point_quantity: int) -> Optional[GraphicPassport]:
"""
Формирует графический паспорт, включающий полезные данные и список PointPassport.
Для каждой из точек:
- Извлекаются настройки оператора для данной точки.
- Формируется временной интервал и события.
- Рассчитываются идеальные и полезные данные.
- Создаётся объект PointPassport, который добавляется в список.
:param df: DataFrame с данными.
:param events: Словарь событий, сгенерированных для этапов.
:param point_quantity: Количество точек.
:return: Объект GraphicPassport или None в случае ошибки.
"""
try:
system_settings = {key: value[0] for key, value in self._settings.system.items()}
graphic_passport = GraphicPassport()
graphic_passport.dataframe = df
for i in range(point_quantity):
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
timeframe, po_events = self._form_point_events(events, i)
if timeframe and po_events:
point_settings.operator["time_robot_movement"] = po_events["Oncomming"][1] - po_events["Oncomming"][0]
ideal_data = self._form_point_ideal_data(point_settings)
useful_data = self._form_point_useful_data(point_settings.operator)
point_passport = PointPassport(timeframe, po_events, ideal_data, useful_data)
graphic_passport.points_pocket.append(point_passport)
graphic_passport.useful_data = self._form_graphic_useful_data(system_settings, graphic_passport.points_pocket)
return graphic_passport
except Exception as e:
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
return None
def _form_graphic_useful_data(self, system_settings: Dict, points:List[PointPassport]) -> UsefulGraphData:
"""
Формирует словарь полезных данных для графического паспорта.
:param system_settings: Словарь системных настроек.
:return: Объект UsefulGraphData.
"""
try:
performance_data = PerformanceData()
if points[0].timeframe is not None and points[0].events is not None:
client_time = system_settings["client_time"]
ideal_time = sum([sum(point.ideal_data["Ideal timings"])for point in points])
TWC_time = (
system_settings["time_before_start"] +
points[0].timeframe[1] - points[0].events["Closing"][0] +
sum([sum([item[1]-item[0] for key, item in point.events.items()]) for point in points[1:-1]]) +
points[-1].events["Relief"][1] - points[-1].timeframe[0] +
system_settings["time_after_end"]
)
performance_data.client_to_TWC = round((1 - TWC_time / client_time) * 100, 2) if client_time else 0.0
performance_data.client_to_ideal = round((1 - ideal_time / client_time) * 100, 2) if client_time else 0.0
performance_data.TWC_to_ideal = round((ideal_time / TWC_time) * 100, 2) if TWC_time else 0.0
useful_data = UsefulGraphData(
performance_data,
system_settings["Range ME, mm"],
system_settings["k_hardness_1"]
)
return useful_data
except Exception as e:
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
return UsefulGraphData()
def _form_point_useful_data(self, operator_settings: Dict) -> Dict:
"""
Формирует полезные данные для отдельной точки.
:param operator_settings: Словарь настроек оператора для точки.
:return: Словарь с полезными данными (толщина, позиция детали, сила) или пустой словарь.
"""
try:
useful_data = {
"thickness": operator_settings["object_thickness"],
"part_pos": operator_settings["part_pos"],
"force": operator_settings["force_target"]
}
return useful_data
except Exception as e:
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
return {}
def _form_point_ideal_data(self, point_settings: Settings) -> Dict:
"""
Формирует идеальные данные для отдельной точки с использованием кэша.
Генерируется кэш-ключ из настроек точки, затем либо извлекаются ранее рассчитанные
данные, либо вычисляются новые с помощью билдера.
:param point_settings: Настройки точки.
:return: Словарь с идеальными данными или пустой словарь в случае ошибки.
"""
try:
cache_key = self._generate_cache_key(point_settings)
ideal_data = self._ideal_data_cache.get(
cache_key,
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
)
self._ideal_data_cache[cache_key] = ideal_data
return ideal_data
except Exception as e:
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
return {}
def _get_operator_settings_part(self, idx: int) -> Dict:
"""
Извлекает часть настроек оператора для конкретного индекса.
Если индекс выходит за пределы списка, используется первый элемент.
:param idx: Индекс точки.
:return: Словарь настроек оператора для данной точки.
"""
try:
operator_settings = {
key: (value[idx] if idx < len(value) else value[0])
for key, value in self._settings.operator.items()
}
return operator_settings
except Exception as e:
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
return {}
def _form_point_events(self, events: Dict, idx: int) -> Tuple[Optional[List[float]], Optional[Dict[str, List[float]]]]:
"""
Формирует временной интервал и события для отдельной точки.
Если событий нет, возвращает (None, None).
:param events: Словарь с событиями для всех этапов.
:param idx: Индекс точки.
:return: Кортеж (timeframe, point_events) или (None, None) в случае ошибки.
"""
try:
timeframe, point_events = None, None
start, end = 0, 0
if events is not None:
# Если первое событие основного этапа начинается с 0, сдвигаем индекс
start = events[self._stages[0]][0][idx]
end = 0
point_events = {}
for key, value in events.items():
if len(value[0]) > idx and value[0][idx] >= start:
point_events[key] = [value[0][idx], value[1][idx]]
if value[1][idx] > end: end = value[1][idx]
else:
logger.warning(f"_form_point_events - Обнаружен аномальный порядок событий для точки {idx}")
for i in range(len(value[0])):
if value[0][i] >= start:
logger.info(f"_form_point_events - Найдено событе (вхождение {i}), соответствующее временным рамкам точки")
point_events[key] = [value[0][i], value[1][i]]
if value[1][i] > end: end = value[1][i]
break
point_events[key] = [end, end+0.01]
end += 0.01
if end != 0: timeframe = [start, end]
return timeframe, point_events
except Exception as e:
logger.error(f"_form_point_events - Ошибка при формировании событий для точки {idx}: {e}")
return None, None
class IdealDataBuilder(BaseIdealDataBuilder):
"""
Класс для построения идеальных данных по этапам.
Реализует методы получения DataFrame для различных этапов:
закрытия, сжатия, открытия, движения, сварки,
а также метод получения идеальных временных интервалов.
"""
def get_closingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа закрытия.
"""
try:
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
except Exception as e:
logger.error(f"get_closingDF - Ошибка при получении данных для этапа закрытия: {e}")
return pd.DataFrame()
def get_compressionDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сжатия.
"""
try:
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
except Exception as e:
logger.error(f"get_compressionDF - Ошибка при получении данных для этапа сжатия: {e}")
return pd.DataFrame()
def get_openingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа открытия.
"""
try:
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
except Exception as e:
logger.error(f"get_openingDF - Ошибка при получении данных для этапа открытия: {e}")
return pd.DataFrame()
def get_oncomingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа движения.
"""
try:
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
except Exception as e:
logger.error(f"get_oncomingDF - Ошибка при получении данных для этапа движения: {e}")
return pd.DataFrame()
def get_weldingDF(self) -> pd.DataFrame:
"""
Получает DataFrame для этапа сварки.
Используется функция calcPhaseGrow с небольшим сдвигом времени.
Значения масштабируются (умножаются на 1000) для перевода в нужные единицы.
"""
try:
data = []
# Используем небольшое смещение для расчёта сварки
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
points_num = 5
for i in range(points_num + 1):
data.append({
"time": self.welding_time * i / points_num,
"Position FE": X1,
"Position ME": X2,
"Rotor Speed FE": V1,
"Rotor Speed ME": V2,
"Force": F
})
return pd.DataFrame(data)
except Exception as e:
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
return pd.DataFrame()
def get_ideal_timings(self) -> List[float]:
"""
Получает список идеальных временных интервалов для этапов.
Список включает: tclose, tgrow, welding_time, getMarkOpen(), tmovement.
"""
try:
data = self.Ts
ideal_timings = [
data['tclose'],
data['tgrow'],
self.welding_time,
self.getMarkOpen(),
data['tmovement']
]
return ideal_timings
except Exception as e:
logger.error(f"get_ideal_timings - Ошибка при получении идеальных временных интервалов: {e}")
return []
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
"""
Получает данные до указанного времени (end_timestamp) с шагом, определяемым параметром mul.
Для каждого шага рассчитываются значения с использованием переданной функции func.
В конце добавляется строка с точным значением end_timestamp.
:param end_timestamp: Время окончания получения данных.
:param func: Функция для расчёта значений в зависимости от времени.
:return: DataFrame с рассчитанными данными.
"""
try:
data = []
# Генерируем данные с шагом 1/mul
for i in range(0, int(end_timestamp * self.mul) + 1):
time_val = i / self.mul
X1, X2, V1, V2, F = func(time_val)
data.append({
"time": time_val,
"Position FE": X1 * 1000,
"Position ME": X2 * 1000,
"Rotor Speed FE": V1 * 1000,
"Rotor Speed ME": V2 * 1000,
"Force": F
})
# Добавляем финальную строку с end_timestamp
X1, X2, V1, V2, F = func(end_timestamp)
data.append({
"time": end_timestamp,
"Position FE": X1 * 1000,
"Position ME": X2 * 1000,
"Rotor Speed FE": V1 * 1000,
"Rotor Speed ME": V2 * 1000,
"Force": F
})
return pd.DataFrame(data)
except Exception as e:
logger.error(f"_get_data - Ошибка при получении данных с end_timestamp={end_timestamp}: {e}")
return pd.DataFrame()

View File

@ -1,2 +1,2 @@
from .plotter import PlotWidget
from .settings_window import SettingsWindow
from .settings_window import settingsWindow

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

335
src/gui/mainGui.py Normal file
View File

@ -0,0 +1,335 @@
from datetime import datetime as dt
from typing import Optional
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap, QIcon
from PyQt5.QtWidgets import QSizePolicy as QSP
from utils.base.base import BaseMainWindow, BaseController
from gui.settings_window import SystemSettings, OperatorSettings
from gui.reportGui import ReportSettings
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
class MainWindow(BaseMainWindow):
def __init__(self,
controller: Optional[BaseController] = None) -> None:
super().__init__()
self._controller = controller
self._init_startUI()
self._init_dock_widgets()
self._init_menu()
# self.setLocale()
def _init_TabWidget(self) -> None:
self.tabWidget = QtWidgets.QTabWidget()
self.tabWidget.setTabsClosable(True)
self.tabWidget.tabCloseRequested.connect(self._close_tab)
self.tabWidget.currentChanged.connect(self._on_tab_changed)
def _init_startUI(self) -> None:
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
self.repSettings = ReportSettings()
self._clear()
self.resize(800,800)
seeking_mode_btn = QtWidgets.QPushButton("Real time folder scanning")
seeking_mode_btn.setFixedWidth(300)
seeking_mode_btn.clicked.connect(self._init_seekingUI)
raport_mode_btn = QtWidgets.QPushButton("Raport editor")
raport_mode_btn.setFixedWidth(300)
raport_mode_btn.clicked.connect(self._init_raportUI)
button_layout = QtWidgets.QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(seeking_mode_btn)
button_layout.addWidget(raport_mode_btn)
button_widget = QtWidgets.QWidget()
button_widget.setLayout(button_layout)
mainLayout = self._central_layout
label = QtWidgets.QLabel("Select work mode")
label.setStyleSheet(
"""QLabel{
color: #ffffff;
font-size: 40px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}"""
)
mainLayout.addWidget(label, alignment=Qt.AlignCenter)
mainLayout.addWidget(button_widget)
self._init_statusBar()
def _init_dock_widgets(self) -> None:
"""
Инициализация док-виджетов для настроек.
"""
# Создаем док-виджет для OperatorSettings
self.operator_dock = QtWidgets.QDockWidget("Operator Settings", self)
self.operator_dock.setWidget(self.operSettings)
self.operator_dock.setObjectName("OperatorSettings")
self.addDockWidget(Qt.RightDockWidgetArea, self.operator_dock)
self.operator_dock.hide() # Скрываем по умолчанию
# Создаем док-виджет для SystemSettings
self.system_dock = QtWidgets.QDockWidget("System Settings", self)
self.system_dock.setWidget(self.sysSettings)
self.system_dock.setObjectName("SystemSettings")
self.addDockWidget(Qt.RightDockWidgetArea, self.system_dock)
self.system_dock.hide() # Скрываем по умолчанию
# Создаем док-виджет для ReportSettings
self.report_dock = QtWidgets.QDockWidget("View settings", self)
self.report_dock.setWidget(self.repSettings)
self.report_dock.setObjectName("ReportSettings")
self.addDockWidget(Qt.RightDockWidgetArea, self.report_dock)
self.report_dock.hide() # Скрываем по умолчанию
# Настройка док-виджетов
self._set_dock_features(self.operator_dock)
self._set_dock_features(self.system_dock)
self._set_dock_features(self.report_dock)
def _init_menu(self) -> None:
"""
Инициализация главного меню.
"""
# Создаем главное меню
menu_bar = self.menuBar()
# Создаем меню "Режимы"
modes_menu = menu_bar.addMenu("Mode")
settings_menu = menu_bar.addMenu("Settings")
# Создаем действия для меню
seeking_action = QtWidgets.QAction("Real time folder scanning", self)
seeking_action.triggered.connect(self._init_seekingUI)
raport_action = QtWidgets.QAction("Raport editor", self)
raport_action.triggered.connect(self._init_raportUI)
system_settings = QtWidgets.QAction("System settings", self)
system_settings.setIcon(QIcon('resources/system_ico.png'))
system_settings.triggered.connect(lambda: self._toggle_visibility(self.system_dock))
operator_settings = QtWidgets.QAction("Operator settings ", self)
operator_settings.setIcon(QIcon('resources/operator_ico.png'))
operator_settings.triggered.connect(lambda: self._toggle_visibility(self.operator_dock))
view_settings = QtWidgets.QAction("View settings", self)
view_settings.setIcon(QIcon('resources/view_ico.png'))
view_settings.triggered.connect(lambda: self._toggle_visibility(self.report_dock))
view_settings.triggered.connect(lambda: self._on_tab_changed(0))
# Добавляем действия в меню "Режимы"
modes_menu.addAction(seeking_action)
modes_menu.addAction(raport_action)
settings_menu.addAction(system_settings)
settings_menu.addAction(operator_settings)
settings_menu.addAction(view_settings)
def _init_statusBar(self) -> None:
# Создание пользовательского виджета для StatusBar
note_widget = QtWidgets.QWidget()
note_layout = QtWidgets.QHBoxLayout(note_widget)
note_layout.setContentsMargins(10, 1, 10, 1)
note_layout.setSpacing(15) # Устанавливаем расстояние между элементами
# Создание QLabel и QProgressBar
self.mode_label = QtWidgets.QLabel()
self.note_label = QtWidgets.QLabel()
self.progress_bar = QtWidgets.QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setMinimumWidth(250)
self.progress_bar.setMaximumHeight(10)
self.progress_bar.setTextVisible(False)
# Создание QSpacerItem
# Установка политики размеров
self.mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
self.note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
self.progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
# Добавление виджетов в макет
note_layout.addWidget(self.progress_bar)
note_layout.addWidget(self.note_label)
note_layout.addStretch(1)
note_layout.addWidget(self.mode_label)
# Установка политики размеров для note_widget
note_widget.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
# Добавление пользовательского виджета в StatusBar как Permanent Widget
self.statusBar().addPermanentWidget(note_widget, 1)
def _toggle_visibility(self, body:QtWidgets.QDockWidget = None) -> None:
"""
Переключение видимости док-виджета.
"""
is_visible = body.isVisible()
body.setVisible(not is_visible)
def _set_dock_features(self, body:QtWidgets.QDockWidget = None) -> None:
"""
Настройка флагов док-виджета.
"""
flag_move = QtWidgets.QDockWidget.DockWidgetMovable
flag_close = QtWidgets.QDockWidget.DockWidgetClosable
flag_floating = QtWidgets.QDockWidget.DockWidgetFloatable
body.setFeatures(flag_move | flag_close | flag_floating)
def _clear(self) -> None:
if self._central_layout is not None:
while self._central_layout.count():
child = self._central_layout.takeAt(0)
if child.widget() is not None:
child.widget().deleteLater()
def _init_seekingUI(self) -> None:
self._clear()
self._init_TabWidget()
self._transfer_settings()
button_layout = QtWidgets.QHBoxLayout()
button_layout.setSpacing(2)
button_widget = QtWidgets.QWidget()
button_widget.setLayout(button_layout)
self.mode_label.setText("online mode")
self._central_layout.addWidget(self.tabWidget)
self._central_layout.addWidget(button_widget)
self._controller.seeking_mode()
def _init_raportUI(self) -> None:
self._clear()
self._init_TabWidget()
self._transfer_settings()
save_screen_btn = QtWidgets.QPushButton("Save state")
save_screen_btn.setFixedWidth(185)
save_screen_btn.clicked.connect(self._save_plots)
open_file_btn = QtWidgets.QPushButton("Open file")
open_file_btn.setFixedWidth(185)
open_file_btn.clicked.connect(self._open_file)
button_layout = QtWidgets.QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(save_screen_btn)
button_layout.addWidget(open_file_btn)
button_widget = QtWidgets.QWidget()
button_widget.setLayout(button_layout)
self.mode_label.setText("raport mode")
self._central_layout.addWidget(self.tabWidget)
self._central_layout.addWidget(button_widget)
self._controller.raport_mode()
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
for plot_widget in plot_widgets:
widget, reg_items, curve_items, qt_items = plot_widget
tab = QtWidgets.QWidget()
tab.setProperty("reg_items", reg_items)
tab.setProperty("curve_items", curve_items)
tab.setProperty("qt_items", qt_items)
grid = QtWidgets.QGridLayout()
grid.addWidget(widget)
tab.setLayout(grid)
self.tabWidget.addTab(tab, "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S'))
self.tabWidget.setCurrentWidget(tab)
tab_count = self.tabWidget.count()
if tab_count > 10:
for i in range(0, tab_count-10):
self._close_tab(i)
self.update_stateLabel("Done!")
def keyPressEvent(self, a0) -> None:
if a0.key() == Qt.Key_F5:
tab_count = self.tabWidget.count()
for i in range(0, tab_count):
self._close_tab(i)
def closeEvent(self, a0):
self.operSettings.close()
self.sysSettings.close()
self.repSettings.close()
super().closeEvent(a0)
def update_progressBar(self, percent:int) -> None:
if percent > 100: percent = 100
self.progress_bar.setValue(percent)
def update_stateLabel(self, msg: str = None) -> None:
self.note_label.setText(msg)
self.note_label.adjustSize()
def _transfer_settings(self) -> None:
operator_params = self.operSettings.getParams()
system_params = self.sysSettings.getParams()
self._controller.update_settings([operator_params, system_params])
def _upd_settings(self) -> None:
self._transfer_settings()
if self.mode_label.text():
self.tabWidget.clear()
self._controller.update_plots()
def _close_tab(self, index:int) -> None:
self.tabWidget.removeTab(index)
def _open_file(self) -> None:
path = self._select_csv()
if path is not None:
self._controller.open_file(path)
def _select_csv(self) -> Optional[str]:
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
if CSV_path:
print(CSV_path)
return CSV_path
return None
def _on_tab_changed(self, index):
try:
tab = self.tabWidget.currentWidget()
except:
tab = None
if tab is not None and self.report_dock.isVisible():
reg_items = tab.property("reg_items")
curve_items = tab.property("curve_items")
qt_items = tab.property("qt_items")
self.repSettings.build(index, reg_items, curve_items, qt_items)
def _save_plots(self) -> None:
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
tab = self.tabWidget.currentWidget()
if tab is not None:
pixmap = QPixmap(tab.size())
tab.render(pixmap)
pixmap.save(filepath)

View File

@ -1,185 +0,0 @@
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, pyqtSignal
from base.base import BaseMainWindow, Settings
from gui.start_widget import (CustomMenuBar, CustomStatusBar,
StartWidget, CustomTabWidget,
RaportWidget, SeekingWidget,
ClientAnalyzerWidget)
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
from gui.report_gui import ReportSettings
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
class MainWindow(BaseMainWindow):
signal_mode = pyqtSignal(int)
signal_settings = pyqtSignal(Settings)
signal_replot_all = pyqtSignal()
signal_open_file = pyqtSignal(str)
signal_save_file = pyqtSignal(list)
signal_open_dir = pyqtSignal(str)
signal_TCW_for_client = pyqtSignal()
def __init__(self) -> None:
super().__init__()
self._init_startUI()
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
for plot_widget in plot_widgets:
self._tab_widget.create_tab(plot_widget)
tab_count = self._tab_widget.count()
if tab_count > 10:
for i in range(0, tab_count-10):
self._tab_widget.close_tab(i)
self.status_widget.set_note("Done!")
self.status_widget.set_progress(100)
def keyPressEvent(self, a0) -> None:
if a0.key() == Qt.Key_F5:
tab_count = self._tab_widget.count()
for i in range(0, tab_count):
self._tab_widget.close_tab(i)
def closeEvent(self, a0):
self.operSettings.close()
self.sysSettings.close()
self.repSettings.close()
super().closeEvent(a0)
def _init_startUI(self) -> None:
self._init_settings()
self._init_menu()
self._init_status_bar()
start_widget = StartWidget()
start_widget.btn_scanning.clicked.connect(self._init_seekingUI)
start_widget.btn_report.clicked.connect(self._init_raportUI)
start_widget.btn_client.clicked.connect(self._init_client_UI)
self.setCentralWidget(start_widget.get_widget())
def _init_settings(self) -> None:
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
self.repSettings = ReportSettings()
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
self.filterSettings = FilterSettings("params/filter_params.json", "Client filter", self._upd_settings)
def _init_menu(self) -> None:
self.menu = CustomMenuBar(self.sysSettings, self.repSettings, self.operSettings, self.filterSettings)
self.menu.action_scanning.triggered.connect(self._init_seekingUI)
self.menu.action_report.triggered.connect(self._init_raportUI)
self.menu.action_client.triggered.connect(self._init_client_UI)
self.menu.action_view_settings.triggered.connect(lambda: self._on_tab_changed)
self.menu.setup(self)
def _init_status_bar(self) -> None:
self.status_widget = CustomStatusBar()
self.statusBar().addPermanentWidget(self.status_widget.get_widget(), 1)
def _init_tab_widget(self) -> None:
self._tab_widget = CustomTabWidget()
self._tab_widget.currentChanged.connect(self._on_tab_changed)
def _on_tab_changed(self):
tab = self._tab_widget.currentWidget()
if tab:
reg_items = tab.property("reg_items")
curve_items = tab.property("curve_items")
qt_items = tab.property("qt_items")
self.repSettings.build(reg_items, curve_items, qt_items)
def _clear(self) -> None:
if self.layout() is not None:
while self.layout().count():
child = self.layout().takeAt(0)
if child.widget() is not None:
child.widget().deleteLater()
self._init_tab_widget()
self._transfer_settings()
def _init_raportUI(self) -> None:
self._clear()
self._set_mode(1)
raport_widget = RaportWidget(self._tab_widget)
raport_widget.btn_open_file.clicked.connect(self._open_file)
raport_widget.btn_save_state.clicked.connect(self._save_plots)
self.setCentralWidget(raport_widget.get_widget())
def _init_seekingUI(self) -> None:
self._clear()
self._set_mode(2)
seeking_widget = SeekingWidget(self._tab_widget)
self.setCentralWidget(seeking_widget.get_widget())
def _init_client_UI(self) -> None:
self._clear()
self._set_mode(3)
client_widget = ClientAnalyzerWidget(self._tab_widget)
#TODO: привязать действия к кнопкам
client_widget.btn_open_folder.clicked.connect(self._open_folder)
client_widget.btn_save_state.clicked.connect(self._save_plots)
client_widget.btn_calc_tcw.clicked.connect(self._build_TCW_for_client)
self.setCentralWidget(client_widget.get_widget())
def _build_TCW_for_client(self):
self.signal_TCW_for_client.emit()
def _set_mode(self, num:int) -> None:
match num:
case 1:
self.status_widget.set_mode("raport mode")
self.signal_mode.emit(1)
case 2:
self.status_widget.set_mode("online mode")
self.signal_mode.emit(2)
case 3:
self.status_widget.set_mode("client processor mode")
self.signal_mode.emit(3)
def _transfer_settings(self) -> None:
settings = Settings()
settings.system = self.sysSettings.get_params()
settings.operator = self.operSettings.get_params()
settings.filter = self.filterSettings.get_params()
self.signal_settings.emit(settings)
def _upd_settings(self) -> None:
self._transfer_settings()
if self.status_widget._mode_label.text():
self._tab_widget.clear()
self.signal_replot_all.emit()
def _open_file(self) -> None:
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
if CSV_path:
self.signal_open_file.emit(CSV_path)
def _open_folder(self) -> None:
dir_path = QtWidgets.QFileDialog.getExistingDirectory(self,"Select folder with traces","",
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks)
if dir_path:
self.signal_open_dir.emit(dir_path)
def _save_plots(self) -> None:
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
tab = self._tab_widget.currentWidget()
if tab: self.signal_save_file.emit([filepath, tab])
# TODO: эта реализация работает, но для пользователя будет больно каждый раз писать имена и расширения
# сохраняемых файлов, не кажется? Можно сделать, как показано ниже, тогда файлам будет назначаться имя
# по умолчанию и с расширением. ХЗ правда, как эта форма на винде будет выглядеть - надо проверить.
# ========================================================
# dialog = QFileDialog()
# dialog.setOptions(QFileDialog.DontUseNativeDialog)
# dialog.setFileMode(QFileDialog.AnyFile)
# dialog.setAcceptMode(QFileDialog.AcceptSave)
# dialog.setDirectory(os.getcwd())
# dialog.selectFile("untitled.txt")
# dialog.exec_()
# if dialog.accepted:
# # emit signal to save file
# ...
# ========================================================

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +1,42 @@
import traceback
import sys
from typing import Union
import pyqtgraph as pg
from pyqtgraph.parametertree import Parameter, ParameterTree
from typing import Union
from PyQt5 import QtWidgets
import traceback
import sys
from loguru import logger
class ReportSettings(QtWidgets.QWidget):
def __init__(self, parent = None):
super().__init__(parent)
#self._tab_cashe = LRUCache(maxsize=1000)
def build(self, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
def build(self, index, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
try:
self._clear()
param_tree = ParameterTree()
layout = self.layout()
layout.addWidget(param_tree)
"""if index in self._tab_cashe:
body = self._tab_cashe[index]
else:
body= [
self._generate_reg_params(reg_items),
self._generate_curve_params(curve_items)
]
self._tab_cashe[index] = body"""
body= [
ReportSettings._generate_reg_params(reg_items),
ReportSettings._generate_curve_params(curve_items),
ReportSettings._generate_qt_params(qt_items)
self._generate_reg_params(reg_items),
self._generate_curve_params(curve_items),
self._generate_qt_params(qt_items)
]
# Добавляем параметры в дерево
params = Parameter.create(name='params', type='group', children=body)
params.sigTreeStateChanged.connect(
lambda: ReportSettings._update_settings(reg_items, curve_items, qt_items, params)
lambda: self._update_settings(reg_items, curve_items, qt_items, params)
)
param_tree.setParameters(params, showTop=False)
except:
@ -50,52 +59,69 @@ class ReportSettings(QtWidgets.QWidget):
else:
self.setLayout(QtWidgets.QVBoxLayout())
@staticmethod
def _generate_qt_params(qt_items: dict) -> dict:
def _generate_qt_params(self, qt_items: dict) -> dict:
"""Создает qt элементы"""
res = {'name': 'Qt elements', 'type': 'group', 'children':[
{'name': key, 'type': 'group', 'children': ReportSettings._create_qt_samples(item)} for key, item in qt_items.items()
{'name': key, 'type': 'group', 'children': self._create_qt_samples(item)} for key, item in qt_items.items()
]}
return res
@staticmethod
def _create_qt_samples(item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
def _create_qt_samples(self, item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
visibility = item.isVisible()
return [
{'name': 'Visibility', 'type': 'bool', 'value': visibility}
]
@staticmethod
def _generate_reg_params(reg_items: dict) -> dict:
def _generate_reg_params(self,
reg_items: dict) -> dict:
"""Созадет реальные и идеальные секторы"""
res = {'name': 'Sectors', 'type': 'group', 'children': [
{'name': 'Real sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["real"])},
{'name': 'Ideal sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["ideal"])},
{'name': 'Real sectors', 'type': 'group', 'children': self._create_samples(reg_items["real"])},
{'name': 'Ideal sectors', 'type': 'group', 'children': self._create_samples(reg_items["ideal"])},
]}
return res
@staticmethod
def _generate_curve_params(curve_items: dict) -> dict:
def _generate_curve_params(self,
curve_items: dict) -> dict:
"""Создает реальные и идеальные линии графиков"""
res = {'name': 'Plots', 'type': 'group', 'children': [
{'name': 'Real plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["real"])},
{'name': 'Ideal plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["ideal"])},
{'name': 'Real plots', 'type': 'group', 'children': self._create_samples(curve_items["real"])},
{'name': 'Ideal plots', 'type': 'group', 'children': self._create_ideal_curves(curve_items["ideal"])},
]}
return res
@staticmethod
def _create_samples(sector: dict) -> list[dict]:
"""Создает список представленных элементов с их параметрами"""
def _create_ideal_curves(self,
curve: dict) -> list[dict]:
"""Создает секторы с этапами циклограммы"""
res = []
for key, item in sector.items():
sample = item[0] if type(item) == list else item
param = {'name': key, 'type': 'group', 'children': ReportSettings._create_settings(sample)}
for key, item in curve.items():
param = {'name': key, 'type': 'group', 'children': self._create_samples(item)}
res.append(param)
return res
@staticmethod
def _create_settings(item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
def _create_samples(self,
sector: dict) -> list[dict]:
"""Создает список представленных элементов с их параметрами"""
res = []
for key, item in sector.items():
sample = item[0] if type(item) == list else item
param = {'name': key, 'type': 'group', 'children': self._create_settings(sample)}
res.append(param)
return res
def _create_settings(self,
item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
"""Получает настройки для элемента"""
if type(item) == pg.LinearRegionItem:
pen = item.lines[0].pen
brush = item.brush
@ -114,12 +140,14 @@ class ReportSettings(QtWidgets.QWidget):
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
]
@staticmethod
def _update_settings(reg_items: dict,
def _update_settings(self,
reg_items: dict,
curve_items: dict,
qt_items: dict,
params: Parameter) -> None:
"""Задает параметры элементов в соответствии с paramTree"""
real_sectors = params.child("Sectors").child("Real sectors")
ideal_sectors = params.child("Sectors").child("Ideal sectors")
@ -128,17 +156,21 @@ class ReportSettings(QtWidgets.QWidget):
qt_settings = params.child("Qt elements")
ReportSettings._set_sector_settings(reg_items["real"], real_sectors)
ReportSettings._set_sector_settings(reg_items["ideal"], ideal_sectors)
self._set_sector_settings(reg_items["real"], real_sectors)
self._set_sector_settings(reg_items["ideal"], ideal_sectors)
ReportSettings._set_plot_settings(curve_items["real"], real_plots)
ReportSettings._set_plot_settings(curve_items["ideal"], ideal_plots)
ReportSettings._set_qt_settings(qt_items, qt_settings)
self._set_plot_settings(curve_items["real"], real_plots)
for key, item_dict in curve_items["ideal"].items():
self._set_plot_settings(item_dict, ideal_plots.child(key))
@staticmethod
def _set_sector_settings(sectors: dict,
self._set_qt_settings(qt_items, qt_settings)
def _set_sector_settings(self,
sectors: dict,
settings: Parameter) -> None:
"""Задает параметры секторов в соответствии с настройками"""
for key, item in sectors.items():
sample = settings.child(key)
line_color = sample.child("Line color").value()
@ -154,10 +186,12 @@ class ReportSettings(QtWidgets.QWidget):
reg.lines[1].setPen(pen)
reg.setBrush(brush)
@staticmethod
def _set_plot_settings(curves: dict,
def _set_plot_settings(self,
curves: dict,
settings: Parameter) -> None:
"""Задает параметры кривых в соответствии с настройками"""
for key, item in curves.items():
sample = settings.child(key)
line_color = sample.child("Line color").value()
@ -172,10 +206,12 @@ class ReportSettings(QtWidgets.QWidget):
item.setVisible(visibility)
item.setPen(pen)
@staticmethod
def _set_qt_settings(qt_items: dict,
def _set_qt_settings(self,
qt_items: dict,
settings: Parameter) -> None:
"""Задает параметры Qt элементов в соответствии с настройками"""
for key, item in qt_items.items():
sample = settings.child(key)
visibility = sample.child("Visibility").value()

View File

@ -1,45 +1,40 @@
import locale
from typing import Callable, Optional, Any
from PyQt5.QtWidgets import (
QWidget, QPushButton, QLineEdit, QHBoxLayout, QVBoxLayout, QLabel,
QTableWidget, QTableWidgetItem, QStyledItemDelegate, QRadioButton, QGroupBox, QDoubleSpinBox
)
from PyQt5.QtWidgets import (QWidget, QPushButton,
QLineEdit, QHBoxLayout,
QVBoxLayout, QLabel,
QTableWidget, QTableWidgetItem,
QStyledItemDelegate)
from PyQt5.QtGui import QIntValidator, QDoubleValidator
from PyQt5 import QtCore
from utils.json_tools import read_json, write_json
from utils import qt_settings as qts
class SettingsWindow(QWidget):
"""
Окно настроек для редактирования параметров.
Загружает и сохраняет параметры из JSON-файла, отображает их в таблице,
позволяет редактировать значения и расширять число параметров.
:param path: Путь к JSON-файлу с настройками.
:param name: Название набора настроек.
:param upd_func: Функция-коллбэк для обновления после сохранения.
:param associated_names: Словарь отображения ключей настроек в читаемые имена.
"""
def __init__(self, path: str, name: str, upd_func: Callable[[], None], associated_names: dict):
class settingsWindow(QWidget):
def __init__(self, path: str, name: str, upd_func: Callable[[], None], names: dict):
"""
Окно настроек для редактирования параметров.
:param path: Путь к файлу настроек (JSON).
:param name: Название набора настроек.
:param upd_func: Функция обновления (коллбэк).
"""
super().__init__()
self._settings_path = path
self._settingsPath = path
self._name = name
self._data: dict[str, list[Any]] = {}
self._upd_func = upd_func
self._associated_names = associated_names
self._num_points: Optional[QLineEdit] = None
self._param_table: Optional[QTableWidget] = None
self._assosiated_names = names
self._init_ui()
self.load_settings()
self._populate_table()
self._init_ui()
def load_settings(self) -> None:
"""Загружает настройки из JSON-файла."""
data = read_json(self._settings_path)
data = read_json(self._settingsPath)
if isinstance(data, dict):
self._data = data
else:
@ -47,19 +42,17 @@ class SettingsWindow(QWidget):
def write_settings(self) -> None:
"""Записывает текущие настройки в JSON-файл."""
write_json(self._settings_path, self._data)
write_json(self._settingsPath, self._data)
def get_params(self) -> dict:
def getParams(self) -> dict:
"""Возвращает текущий словарь параметров."""
return self._data
def _init_ui(self) -> None:
"""Инициализирует пользовательский интерфейс: кнопки, поля ввода, таблицу."""
# Кнопки управления
"""Инициализирует UI: кнопки, поля ввода, таблицу."""
save_button = QPushButton("Save")
restore_button = QPushButton("Restore")
# Поле для ввода количества точек сварки
self._num_points = QLineEdit()
self._num_points.setPlaceholderText("Enter the number of welding points")
self._num_points.setValidator(QIntValidator())
@ -69,16 +62,13 @@ class SettingsWindow(QWidget):
control_layout.addWidget(restore_button)
control_layout.addWidget(self._num_points)
# Подключение сигналов к слотам
save_button.pressed.connect(self._save)
restore_button.pressed.connect(self._restore)
self._num_points.editingFinished.connect(self._expand)
# Таблица для отображения параметров
self._param_table = QTableWidget()
self._populate_table()
# Основной вертикальный макет
layout = QVBoxLayout()
header = QLabel(self._name)
layout.addWidget(header)
@ -88,18 +78,14 @@ class SettingsWindow(QWidget):
self.setStyleSheet(qts.dark_style)
def _populate_table(self) -> None:
"""
Заполняет таблицу значениями из self._data.
Если данных нет, таблица очищается. Для каждого параметра устанавливается
делегат в зависимости от типа данных (int, float, str).
"""
"""Заполняет таблицу значениями из self._data."""
# Если нет данных для заполнения
if not self._data:
self._param_table.setRowCount(0)
self._param_table.setColumnCount(0)
return
# Определяем количество столбцов на основе длины списка первого ключа
# Предполагаем, что у всех ключей одинаковая длина списков параметров.
first_key = next(iter(self._data), None)
if first_key is None:
self._param_table.setRowCount(0)
@ -109,31 +95,27 @@ class SettingsWindow(QWidget):
column_count = len(self._data[first_key])
self._param_table.setRowCount(len(self._data))
self._param_table.setColumnCount(column_count)
headers = [self._associated_names.get(key, key) for key in self._data.keys()]
headers = [self._assosiated_names[key] for key in self._data.keys()]
self._param_table.setVerticalHeaderLabels(headers)
# Создаем делегаты для различных типов данных
int_delegate = ValidatorDelegate(data_type='int', parent=self._param_table)
float_delegate = ValidatorDelegate(data_type='float', parent=self._param_table)
str_delegate = ValidatorDelegate(data_type='str', parent=self._param_table)
# Заполняем таблицу и устанавливаем делегаты для каждой строки
for i, (_, items) in enumerate(self._data.items()):
for j, item in enumerate(items):
self._param_table.setItem(i, j, QTableWidgetItem(str(item)))
# Используем тип последнего элемента строки для выбора делегата
if isinstance(items[-1], int):
if type(item) == int:
self._param_table.setItemDelegateForRow(i, int_delegate)
elif isinstance(items[-1], float):
elif type(item) == float:
self._param_table.setItemDelegateForRow(i, float_delegate)
else:
self._param_table.setItemDelegateForRow(i, str_delegate)
def _save_data(self) -> None:
"""
Сохраняет текущие параметры из таблицы в self._data,
записывает их в JSON-файл и вызывает функцию обновления.
"""
def _save(self) -> None:
"""Сохраняет текущие параметры из таблицы в self._data и вызывает _upd_func()."""
new_data = {}
col_count = self._param_table.columnCount()
for i, key in enumerate(self._data.keys()):
@ -143,7 +125,7 @@ class SettingsWindow(QWidget):
if cell_item is None:
continue
param_str = cell_item.text()
# Для ключа 'trace_storage_path' оставляем строковое значение
# Если ключ не trace_storage_path, конвертируем в float
if key != "trace_storage_path":
try:
param = float(param_str)
@ -152,31 +134,20 @@ class SettingsWindow(QWidget):
else:
param = param_str
row_data.append(param)
new_data[key] = row_data
self._data = new_data
def _push_data (self) -> None:
self.write_settings()
self._upd_func()
def _save(self) -> None:
self._save_data()
self._push_data()
def _restore(self) -> None:
"""Перезагружает данные из файла и обновляет таблицу."""
self.load_settings()
self._populate_table()
def _expand(self) -> None:
"""
Расширяет количество столбцов таблицы согласно введённому значению.
Количество столбцов таблицы
устанавливается равным заданному значению, а новые ячейки заполняются последним
известным значением для соответствующего параметра.
"""
"""Расширяет количество столбцов таблицы в зависимости от введённого значения."""
if not self._num_points:
return
@ -196,41 +167,34 @@ class SettingsWindow(QWidget):
self._param_table.setColumnCount(desired_columns)
# Заполнение новых столбцов последним значением для каждого параметра
# Новые столбцы заполняем последним известным параметром для каждого ключа
for i, (key, items) in enumerate(self._data.items()):
# Если нет данных, пропускаем
if not items:
continue
last_value = str(items[-1])
for col in range(prev_columns, desired_columns):
self._param_table.setItem(i, col, QTableWidgetItem(last_value))
# Добавляем новый элемент также в self._data для консистентности
# После этого можно будет сохранить при нажатии Save
# Дополним также и в self._data
additional_count = desired_columns - prev_columns
# Приведение типа: для ключа 'trace_storage_path' оставляем строку, иначе преобразуем к float
additional_values = (
[float(last_value)] * additional_count
if key != "trace_storage_path"
else [last_value] * additional_count
)
self._data[key].extend(additional_values)
self._data[key].extend([float(last_value) if key != "trace_storage_path" else last_value] * additional_count)
class SystemSettings(SettingsWindow):
"""
Настройки системы.
Окно для редактирования параметров, связанных с работой системы.
"""
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
associated_names = {
class SystemSettings(settingsWindow):
def __init__(self, path, name, upd_func):
assosiated_names = {
"trace_storage_path": "Trace path",
"monitor_update_period": "Monitoring period",
"a_max_1": "Max lin accel FE, m/s^2",
"v_max_1": "Max lin speed FE, m/s",
"a_max_2": "Max lin accel ME, m/s^2",
"v_max_2": "Max lin speed FE, m/s",
"a_max_1": "Max linear acceleration FE, m/s^2",
"v_max_1": "Max linear speed FE, m/s",
"a_max_2":"Max linear acceleration ME, m/s^2",
"v_max_2": "Max linear speed ME, m/s",
"mass_1": "Mass FE, kg",
"mass_2": "Mass ME, kg",
"k_hardness_1": "Hardness coef FE, N/m",
"k_hardness_2": "Hardness coef ME, N/m",
"k_hardness_1": "Hardness coefficient FE, N/m",
"k_hardness_2": "Hardness coefficient ME, N/m",
"torque_max_1": "Max torque FE, N*m",
"torque_max_2": "Max torque ME, N*m",
"transmission_ratio_1": "Transmission ratio FE",
@ -240,115 +204,21 @@ class SystemSettings(SettingsWindow):
"k_prop": "Proportionality factor",
"time_capture": "Calculated points per sec",
"UML_time_scaler": "UML_time_scaler",
"Range ME, mm": "Range ME, mm"
}
super().__init__(path, name, upd_func, associated_names)
"gun_range": "Range ME-FE, mm"
}
super().__init__(path, name, upd_func, assosiated_names)
self._num_points.setVisible(False)
def _init_ui(self) -> None:
super()._init_ui()
performance_layout = QVBoxLayout()
self.btn_trace_movement = QRadioButton()
self.btn_trace_movement.setText("Use robot movement time from trace (auto set)")
self.btn_trace_movement.setChecked(True)
self.btn_client_movement = QRadioButton()
self.btn_client_movement.setText("Use robot movement time from client (manual set)")
label_time_before_start = QLabel("Additional time before first Squeeze, ms")
self.spin_time_before_start = QDoubleSpinBox()
self.spin_time_before_start.setRange(0, 100000.000)
self.spin_time_before_start.setValue(0)
label_time_after_end = QLabel("Additional time after last Relief, ms")
self.spin_time_after_end = QDoubleSpinBox()
self.spin_time_after_end.setRange(0, 100000.000)
self.spin_time_after_end.setValue(0)
label_client_time = QLabel("Client full time, ms")
self.spin_client_time = QDoubleSpinBox()
self.spin_client_time.setRange(0, 100000.000)
self.spin_client_time.setValue(0)
widget_time_before_start = QWidget()
layout_time_before_start = QHBoxLayout(widget_time_before_start)
layout_time_before_start.addWidget(label_time_before_start)
layout_time_before_start.addWidget(self.spin_time_before_start)
widget_time_after_end = QWidget()
layout_time_after_end = QHBoxLayout(widget_time_after_end)
layout_time_after_end.addWidget(label_time_after_end)
layout_time_after_end.addWidget(self.spin_time_after_end)
widget_client_time = QWidget()
layout_client_time = QHBoxLayout(widget_client_time)
layout_client_time.addWidget(label_client_time)
layout_client_time.addWidget(self.spin_client_time)
performance_layout.addWidget(self.btn_trace_movement)
performance_layout.addWidget(self.btn_client_movement)
performance_layout.addWidget(widget_time_before_start)
performance_layout.addWidget(widget_time_after_end)
performance_layout.addWidget(widget_client_time)
performance_box = QGroupBox()
performance_box.setTitle("Performance settings")
performance_box.setLayout(performance_layout)
self.layout().addWidget(performance_box)
def _add_performance_mode(self) -> None:
if self.btn_trace_movement.isChecked():
mode = 'trace'
elif self.btn_client_movement.isChecked():
mode = 'client'
else:
mode = None
self._data["performance_mode"] = [mode]
self._data["time_before_start"] = [self.spin_time_before_start.value()/1000]
self._data["time_after_end"] = [self.spin_time_after_end.value()/1000]
self._data["client_time"] = [self.spin_client_time.value()/1000]
def _save_data(self) -> None:
super()._save_data()
self._add_performance_mode()
def _load_performance_settings(self) -> None:
mode = self._data["performance_mode"][0]
if mode == 'trace':
self.btn_trace_movement.setChecked(True)
elif mode == 'client':
self.btn_client_movement.setChecked(True)
self.spin_time_before_start.setValue(self._data["time_before_start"][0]*1000)
self.spin_time_after_end.setValue(self._data["time_after_end"][0]*1000)
self.spin_client_time.setValue(self._data["client_time"][0]*1000)
def load_settings(self) -> None:
super().load_settings()
try:
self._load_performance_settings()
except KeyError:
self._add_performance_mode()
def _expand(self) -> None:
# Для системных настроек расширение столбцов не требуется.
def _expand(self):
pass
class OperatorSettings(SettingsWindow):
"""
Настройки оператора.
Окно для редактирования параметров, связанных с работой оператора.
"""
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
associated_names = {
"distance_h_start_1": "Closing start dist FE, m",
class OperatorSettings(settingsWindow):
def __init__(self, path, name, upd_func):
assosiated_names = {
"distance_h_start_1": "Closing start dist FE, m" ,
"distance_h_start_2": "Closing start dist ME, m",
"distance_s_1": "Rob movement start dist FE, m",
"distance_s_2": "Rob movement start dist ME, m",
"distance_s_1": "Robot movement start dist FE, m",
"distance_s_2": "Robot movement start dist ME, m",
"distance_l_1": "Max oncoming dist FE, m",
"distance_l_2": "Max oncoming dist ME, m",
"distance_h_end1": "Oncoming end dist FE, m",
@ -357,6 +227,7 @@ class OperatorSettings(SettingsWindow):
"time_command": "Communication time compensator, sec",
"time_robot_movement": "Rob movement time, sec",
"object_thickness": "Workpiece thickness, m",
"object_position": "Workpiece position, m",
"force_target": "Target force, N",
"force_capture": "Capture force, N",
"Tesla closing": "Client closing time, sec",
@ -364,47 +235,12 @@ class OperatorSettings(SettingsWindow):
"Tesla welding": "Client welding time, sec",
"Tesla oncomming_relief": "Client moving to next point time, sec",
"Tesla summary time": "Client summary time, sec"
}
super().__init__(path, name, upd_func, associated_names)
class FilterSettings(SettingsWindow):
"""
Настройки фильтра.
Окно для редактирования параметров фильтрации.
"""
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
associated_names = {
"act_pos_decrease": "ME max pos change in Relief, mm",
"act_vel_min": "Minimum for ME speed in Closing mm/sec",
"act_vel_close": "Maximum for ME speed in Squeeze mm/sec",
"act_vel_thresh": "ME zero speed threshold mm/sec",
"act_vel_negative": "ME Relief speed mm/sec",
"rob_vel_thresh": "Robot zero speed threshold mm/sec",
"act_force_close": "Maximum ME force in Closing, N",
"act_force_weld": "Minimum ME force in Welding, N",
"force_increase": "ME force rising speed in Squeeze",
"force_decrease": "ME force falling speed in Relief"
}
super().__init__(path, name, upd_func, associated_names)
self._num_points.setVisible(False)
def _expand(self):
# Для настроек фильтра расширение столбцов не требуется.
pass
}
super().__init__(path, name, upd_func, assosiated_names)
pass
class ValidatorDelegate(QStyledItemDelegate):
"""
Валидация для ввода в ячейках таблицы.
В зависимости от типа данных ('int', 'float' или 'str') устанавливает соответствующий валидатор.
"""
def __init__(self, data_type='str', parent=None):
super().__init__(parent)
self.data_type = data_type
@ -413,6 +249,8 @@ class ValidatorDelegate(QStyledItemDelegate):
elif self.data_type == 'float':
self.validator = QDoubleValidator()
self.validator.setNotation(QDoubleValidator.StandardNotation)
self.validator.setLocale(QtCore.QLocale("en_US"))
# locale
else:
self.validator = None
@ -420,13 +258,21 @@ class ValidatorDelegate(QStyledItemDelegate):
editor = QLineEdit(parent)
if self.validator:
editor.setValidator(self.validator)
# Добавляем обработчик для замены запятой на точку
editor.textChanged.connect(self.replace_comma_with_dot)
return editor
def replace_comma_with_dot(self, text):
# Заменяем запятую на точку
if ',' in text:
text = text.replace(',', '.')
# Устанавливаем измененный текст обратно в редактор
self.sender().setText(text)
if __name__ == '__main__':
import pyqtgraph as pg
# Для демонстрации создаем окно настроек с фиктивной функцией обновления и пустым словарем имен
app = pg.mkQApp("Parameter Tree Example")
window = SettingsWindow('params/operator_params.json', 'operator', lambda: None, {})
window.show()
window = settingsWindow('params\operator_params.json', 'operator')
app.exec()

View File

@ -1,459 +0,0 @@
from datetime import datetime as dt
from PyQt5.QtWidgets import (
QWidget, QPushButton, QMenuBar, QHBoxLayout, QVBoxLayout, QLabel,
QDockWidget, QTabWidget, QProgressBar, QAction, QMainWindow, QGridLayout
)
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QSizePolicy as QSP
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
from gui.report_gui import ReportSettings
from base.base import PlotItems
class StartWidget(QWidget):
"""
Виджет выбора режима работы приложения.
Содержит кнопки для перехода в различные режимы:
- Сканирование папки в реальном времени
- Редактор отчётов
- Обработка клиентских трейсов
"""
def __init__(self) -> None:
super().__init__()
self._build_main_layout()
def get_widget(self) -> QWidget:
"""
Возвращает текущий виджет.
"""
return self
def _build_main_layout(self) -> None:
"""
Строит макет виджета с заголовком и кнопками выбора режима.
"""
self.resize(800, 800)
# Создаем кнопки для различных режимов работы
self.btn_scanning = QPushButton("Real time folder scanning")
self.btn_scanning.setFixedWidth(350)
self.btn_report = QPushButton("Raport editor")
self.btn_report.setFixedWidth(350)
self.btn_client = QPushButton("Client trace processor")
self.btn_client.setFixedWidth(350)
# Размещение кнопок в горизонтальном макете
button_layout = QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(self.btn_scanning)
button_layout.addWidget(self.btn_report)
button_layout.addWidget(self.btn_client)
button_container = QWidget()
button_container.setLayout(button_layout)
# Создаем основной вертикальный макет с заголовком и кнопками
main_layout = QVBoxLayout()
title_label = QLabel("Select work mode")
title_label.setStyleSheet(
"""QLabel{
color: #ffffff;
font-size: 40px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}"""
)
main_layout.addWidget(title_label, alignment=Qt.AlignCenter)
main_layout.addWidget(button_container)
self.setLayout(main_layout)
class CustomStatusBar(QWidget):
"""
Кастомный виджет для статус-бара, отображающий режим, заметку и прогресс.
"""
def __init__(self) -> None:
super().__init__()
self._build_status_bar()
def get_widget(self) -> QWidget:
"""
Возвращает виджет статус-бара.
"""
return self
def set_mode(self, msg: str = '') -> None:
"""
Устанавливает текст режима в статус-баре.
:param msg: Сообщение для отображения.
"""
if not isinstance(msg, str):
msg = str(msg)
self._mode_label.setText(msg)
self._mode_label.adjustSize()
def set_note(self, msg: str = '') -> None:
"""
Устанавливает текст заметки в статус-баре.
:param msg: Сообщение для отображения.
"""
if not isinstance(msg, str):
msg = str(msg)
self._note_label.setText(msg)
self._note_label.adjustSize()
def set_progress(self, percent: int = 0) -> None:
"""
Устанавливает значение прогресс-бара.
:param percent: Значение прогресса (от 0 до 100).
"""
if not isinstance(percent, int):
percent = 0
elif percent < 0:
percent = 0
elif percent > 100:
percent = 100
self._progress_bar.setValue(percent)
def _build_status_bar(self) -> None:
"""
Создает и настраивает компоненты статус-бара.
"""
# Создаем горизонтальный макет с отступами и промежутками
layout = QHBoxLayout()
layout.setContentsMargins(10, 1, 10, 1)
layout.setSpacing(15)
# Инициализируем компоненты статус-бара
self._mode_label = QLabel()
self._note_label = QLabel()
self._progress_bar = QProgressBar()
self._progress_bar.setRange(0, 100)
self._progress_bar.setValue(0)
self._progress_bar.setMinimumWidth(250)
self._progress_bar.setMaximumHeight(10)
self._progress_bar.setTextVisible(False)
# Устанавливаем политику размеров для компонентов
self._mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
self._note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
self._progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
# Добавляем компоненты в макет
layout.addWidget(self._progress_bar)
layout.addWidget(self._note_label)
layout.addStretch(1)
layout.addWidget(self._mode_label)
self.setLayout(layout)
self.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
class CustomTabWidget(QTabWidget):
"""
Кастомный виджет-вкладка.
Позволяет создавать вкладки с виджетом графика и закрывать их.
"""
def __init__(self):
super().__init__()
self._build_tab_widget()
def get_widget(self) -> QWidget:
"""
Возвращает виджет.
"""
return self
def _build_tab_widget(self) -> None:
"""
Настраивает виджет вкладок: включает возможность закрытия вкладок.
"""
self.setTabsClosable(True)
self.tabCloseRequested.connect(self.close_tab)
def create_tab(self, plot_widget: QWidget) -> None:
"""
Создает новую вкладку с виджетом графика.
Извлекает из виджета необходимые данные, сохраняет их в свойствах вкладки.
:param plot_widget: Виджет, содержащий график.
"""
# Извлекаем объект PlotItems, сохранённый в свойстве виджета
plot_items: PlotItems = plot_widget.property("pyqt_container")
new_tab = QWidget()
new_tab.setProperty("reg_items", plot_items.regions)
new_tab.setProperty("curve_items", plot_items.curves)
new_tab.setProperty("qt_items", plot_items.qt_items)
grid_layout = QGridLayout()
grid_layout.addWidget(plot_widget)
new_tab.setLayout(grid_layout)
tab_label = "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S')
self.addTab(new_tab, tab_label)
self.setCurrentWidget(new_tab)
def close_tab(self, index: int) -> None:
"""
Закрывает вкладку по заданному индексу.
:param index: Индекс вкладки для закрытия.
"""
if self.count() > index and index >= 0:
self.removeTab(index)
class CustomMenuBar(QMenuBar):
"""
Кастомное главное меню с док-виджетами для различных настроек.
"""
def __init__(self,
operator_settings: OperatorSettings,
system_settings: SystemSettings,
report_settings: ReportSettings,
filter_settings: FilterSettings) -> None:
super().__init__()
self._operator_settings = operator_settings
self._system_settings = system_settings
self._report_settings = report_settings
self._filter_settings = filter_settings
self._build_dock_widgets()
self._build_menu()
def get_widget(self) -> QWidget:
"""
Возвращает виджет меню.
"""
return self
def setup(self, parent: QMainWindow) -> None:
"""
Добавляет док-виджеты и устанавливает главное меню для главного окна.
:param parent: Главное окно приложения.
"""
parent.addDockWidget(Qt.RightDockWidgetArea, self._operator_dock)
parent.addDockWidget(Qt.RightDockWidgetArea, self._system_dock)
parent.addDockWidget(Qt.RightDockWidgetArea, self._report_dock)
parent.addDockWidget(Qt.RightDockWidgetArea, self._filter_dock)
parent.setMenuBar(self)
def _build_dock_widgets(self) -> None:
"""
Инициализирует док-виджеты для настроек и скрывает их по умолчанию.
"""
# Док-виджет для настроек оператора
self._operator_dock = QDockWidget("Operator Settings", self)
self._operator_dock.setWidget(self._operator_settings)
self._operator_dock.setObjectName("OperatorSettings")
self._operator_dock.hide()
# Док-виджет для системных настроек
self._system_dock = QDockWidget("System Settings", self)
self._system_dock.setWidget(self._system_settings)
self._system_dock.setObjectName("SystemSettings")
self._system_dock.hide()
# Док-виджет для настроек представления отчёта
self._report_dock = QDockWidget("View settings", self)
self._report_dock.setWidget(self._report_settings)
self._report_dock.setObjectName("ReportSettings")
self._report_dock.hide()
# Док-виджет для фильтр-настроек
self._filter_dock = QDockWidget("Filter settings", self)
self._filter_dock.setWidget(self._filter_settings)
self._filter_dock.setObjectName("FilterSetting")
self._filter_dock.hide()
# Устанавливаем функциональные возможности для каждого док-виджета
self._set_dock_features(self._operator_dock)
self._set_dock_features(self._system_dock)
self._set_dock_features(self._report_dock)
self._set_dock_features(self._filter_dock)
def _build_menu(self) -> None:
"""
Инициализирует элементы главного меню.
"""
# Создаем меню для выбора режима и настроек
modes_menu = self.addMenu("Mode")
settings_menu = self.addMenu("Settings")
# Действия для переключения режимов
self.action_scanning = QAction("Real time folder scanning", self)
self.action_report = QAction("Raport editor", self)
self.action_client = QAction("Client trace processor", self)
# Действия для настроек
system_settings_action = QAction("System settings", self)
system_settings_action.setIcon(QIcon('resources/system_ico.png'))
system_settings_action.triggered.connect(lambda: self._toggle_visibility(self._system_dock))
operator_settings_action = QAction("Operator settings", self)
operator_settings_action.setIcon(QIcon('resources/operator_ico.png'))
operator_settings_action.triggered.connect(lambda: self._toggle_visibility(self._operator_dock))
self.action_view_settings = QAction("View settings", self)
self.action_view_settings.setIcon(QIcon('resources/view_ico.png'))
self.action_view_settings.triggered.connect(lambda: self._toggle_visibility(self._report_dock))
self.action_filter_settings = QAction("Filter settings", self)
self.action_filter_settings.triggered.connect(lambda: self._toggle_visibility(self._filter_dock))
# Добавляем действия в меню "Mode"
modes_menu.addAction(self.action_scanning)
modes_menu.addAction(self.action_report)
modes_menu.addAction(self.action_client)
# Добавляем действия в меню "Settings"
settings_menu.addAction(system_settings_action)
settings_menu.addAction(operator_settings_action)
settings_menu.addAction(self.action_view_settings)
settings_menu.addAction(self.action_filter_settings)
def _toggle_visibility(self, dock: QDockWidget = None) -> None:
"""
Переключает видимость указанного док-виджета.
:param dock: Док-виджет для переключения.
"""
if dock:
is_visible = dock.isVisible()
dock.setVisible(not is_visible)
@staticmethod
def _set_dock_features(dock: QDockWidget = None) -> None:
"""
Устанавливает возможность перемещения, закрытия и открепления для док-виджета.
:param dock: Док-виджет для настройки.
"""
if dock:
features = QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable
dock.setFeatures(features)
class RaportWidget(QWidget):
"""
Виджет для режима редактирования отчёта.
Содержит вкладки и кнопки для сохранения состояния и открытия файла.
"""
def __init__(self, tab_widget: CustomTabWidget):
super().__init__()
self._tab_widget = tab_widget
self._build_widget()
def get_widget(self) -> QWidget:
"""
Возвращает виджет.
"""
return self
def _build_widget(self) -> None:
"""
Строит макет виджета с вкладками и кнопками управления.
"""
main_layout = QVBoxLayout()
self.btn_save_state = QPushButton("Save state")
self.btn_save_state.setFixedWidth(185)
self.btn_open_file = QPushButton("Open file")
self.btn_open_file.setFixedWidth(185)
button_layout = QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(self.btn_save_state)
button_layout.addWidget(self.btn_open_file)
button_container = QWidget()
button_container.setLayout(button_layout)
main_layout.addWidget(self._tab_widget)
main_layout.addWidget(button_container)
self.setLayout(main_layout)
class SeekingWidget(QWidget):
"""
Виджет для режима поиска (сканирования папок).
"""
def __init__(self, tab_widget: CustomTabWidget):
super().__init__()
self._tab_widget = tab_widget
self._build_widget()
def get_widget(self) -> QWidget:
"""
Возвращает виджет.
"""
return self
def _build_widget(self) -> None:
"""
Создает макет виджета с вкладками.
"""
main_layout = QVBoxLayout()
button_layout = QHBoxLayout()
button_layout.setSpacing(2)
button_container = QWidget()
button_container.setLayout(button_layout)
main_layout.addWidget(self._tab_widget)
main_layout.addWidget(button_container)
self.setLayout(main_layout)
class ClientAnalyzerWidget(QWidget):
"""
Виджет для клиентского анализа трассировки.
Содержит вкладки и кнопки для сохранения состояния, открытия папки и расчёта TCW.
"""
def __init__(self, tab_widget: CustomTabWidget):
super().__init__()
self._tab_widget = tab_widget
self._build_widget()
def get_widget(self) -> QWidget:
"""
Возвращает виджет.
"""
return self
def _build_widget(self) -> None:
"""
Строит макет виджета с вкладками и управляющими кнопками.
"""
main_layout = QVBoxLayout()
self.btn_save_state = QPushButton("Save state")
self.btn_save_state.setFixedWidth(185)
self.btn_open_folder = QPushButton("Open folder")
self.btn_open_folder.setFixedWidth(185)
self.btn_calc_tcw = QPushButton("Calculate TCW")
self.btn_calc_tcw.setFixedWidth(185)
button_layout = QHBoxLayout()
button_layout.setSpacing(2)
button_layout.addWidget(self.btn_save_state)
button_layout.addWidget(self.btn_open_folder)
button_layout.addWidget(self.btn_calc_tcw)
button_container = QWidget()
button_container.setLayout(button_layout)
main_layout.addWidget(self._tab_widget)
main_layout.addWidget(button_container)
self.setLayout(main_layout)

View File

@ -1,44 +1,36 @@
import sys
import pyqtgraph as pg
from PyQt5 import QtWidgets
from gui.main_gui import MainWindow
from controller.file_manager import DirectoryMonitor, FileManager
from gui.mainGui import MainWindow
from controller.monitor import DirectoryMonitor
from controller.mediator import Mediator
from controller.converter import DataConverter
from gui.plotter import PlotWidget
from controller.controller import Controller
from controller.passport_former import PassportFormer
from performance.roboter import TraceProcessor
from controller.passportFormer import PassportFormer
def main():
pg.setConfigOptions(useOpenGL=False, antialias=False)
app = QtWidgets.QApplication(sys.argv)
monitor = DirectoryMonitor()
file_manager = FileManager(monitor=monitor)
monitor._file_manager = file_manager
data_converter = DataConverter()
controller = Controller(file_manager=file_manager)
plot_widget_builder = PlotWidget(controller=controller)
plot_widget_builder = PlotWidget()
controller = Controller()
passport_former = PassportFormer()
window = MainWindow()
trace_processor = TraceProcessor()
mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager, trace_processor)
window = MainWindow(controller)
mediator = Mediator(monitor, data_converter, passport_former, plot_widget_builder, controller)
window.show()
window.signal_mode.connect(controller.set_working_mode)
window.signal_settings.connect(controller.update_settings)
window.signal_replot_all.connect(controller.update_plots)
window.signal_open_file.connect(controller.open_file)
window.signal_open_dir.connect(controller.open_dir)
window.signal_save_file.connect(controller.save_file)
window.signal_TCW_for_client.connect(controller.build_TCW_for_client)
controller.signal_widgets.connect(window.show_plot_tabs)
controller.signal_progress_bar.connect(window.status_widget.set_progress)
controller.signal_status_text.connect(window.status_widget.set_note)
controller.signal_statusBar.connect(window.update_progressBar)
controller.signal_statusText.connect(window.update_stateLabel)
controller.signal_settings.connect(mediator.update_settings)
controller.signal_open_file.connect(monitor.custom_csv_extract_only)
controller.signal_raport_mode.connect(monitor.start_raport)
controller.signal_seeking_mode.connect(monitor.start_seeking)
controller.signal_update_plots.connect(monitor.update_plots)
sys.exit(app.exec_())

Binary file not shown.

View File

@ -1,118 +1,24 @@
from typing import Optional, Tuple
from typing import Optional
import os
import numpy as np
import pandas as pd
from roboter import Performance
#TODO: Реализовать поиск времени простоя
class DowntimeAnalyzer:
def __init__(self) -> None:
...
class PerformanceProcessor:
def generate_downtime_report(self) -> list:
...
def _separate_conditions(self, TWC_raw:pd.DataFrame, robot_raw:pd.DataFrame) -> Tuple[dict, dict]:
robot_splitter = RobotConditionSplitter()
TWC_splitter = TWC_ConditionSplitter()
#comm_splitter = CommConditionSplitter()
rob_conditions = robot_splitter.split(robot_raw)
TWC_conditions = TWC_splitter.split(TWC_raw)
#comm_df = comm_splitter.split(rob_conditions["communication"], TWC_conditions["communication"])
return (rob_conditions, TWC_conditions)
def calc_performance(self, path:str, TWC_raw:pd.DataFrame):
factory = Performance()
comm_df, rob_df, TWC_df = factory.job(path, TWC_raw)
class ConditionSplitter:
def __init__(self):
self._signals = []
def _find_indexes(self,
signal: str,
dataframe: pd.DataFrame) -> list[list[float], list[float]]:
stage_diff = np.diff(dataframe[signal])
start_idx = np.where(stage_diff == 1)
finish_idx = np.where(stage_diff == -1)
return start_idx[0], finish_idx[0]
def _find_events(self,
dataframe: pd.DataFrame,
signals: list[str]) -> Optional[dict[dict[pd.Series]]]:
intervals = {}
end_time = 0
for signal in signals:
start_idx, finish_idx = np.array(self._find_indexes(signal, dataframe))
start_series = dataframe.loc[start_idx, "time"].reset_index(drop=True)
end_series = dataframe.loc[finish_idx, "time"].reset_index(drop=True)
end_series.fillna(end_time)
intervals[signal] = {"rise": start_series,
"fall": end_series}
return intervals
def _form_intervals(self,
start: pd.Series,
end: pd.Series) -> dict:
if len(start) != len(end):
for i in range(1, len(end)):
if end[i-1] > start[i]:
start = start.drop(i).reset_index(drop=True)
intervals = {'start':start.tolist, 'end':end.tolist}
return intervals
class RobotConditionSplitter(ConditionSplitter):
def __init__(self):
self._signals = [
"$OUT3012",
"$IN3003",
"$OUT3003",
"$OUT3244"
]
def split(self, dataframe: pd.DataFrame) -> dict:
events = self._find_events(dataframe, self._signals)
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
conditions = {"communication":communication_sig, "waiting": point_interval, "moving": movement_interval}
return conditions
class TWC_ConditionSplitter(ConditionSplitter):
def __init__(self):
self._signals = [
"Closing",
"Squeeze",
"Welding",
"Relief",
"Oncoming"
]
def split(self, dataframe: pd.DataFrame) -> dict:
events = self._find_events(dataframe, self._signals)
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
conditions = {
"communication":communication_sig,
'closing':closing_interval,
'squeeze':squeeze_interval,
'relief':relief_interval,
'oncoming':oncoming_interval
}
return conditions
class CommConditionSplitter(ConditionSplitter):
"""
Определяет промежуток, в который происходит взаимодействие между нодами.
"""
def split(self, node1: dict, node2: dict) -> pd.DataFrame:
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
if __name__ == "__main__":

View File

@ -1,627 +1,203 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional
import os
from typing import Tuple, Union, Optional, List
from dataclasses import dataclass, field
import numpy as np
import pandas as pd
from loguru import logger
from base.base import (
BaseKukaDataParser, BaseKukaTextParser,
BaseTraceStageDetector, BaseTextStageDetector,
BaseRawTraceProcessor, KukaDataHead,
KukaTXT, Settings
)
class KukaDataParser(BaseKukaDataParser):
"""
Класс для парсинга данных Кука.
Читает заголовочный файл (.dat) и соответствующий файл данных (.r64),
объединяет их в pandas.DataFrame.
"""
class BasePerformanceFactory(ABC):
def parse(self, head_filepath: str) -> pd.DataFrame:
"""
Основной метод парсинга. Читает заголовочный файл и файл данных.
@abstractmethod
def factory_method(self):
...
:param head_filepath: Путь к заголовочному файлу.
:return: DataFrame с объединёнными данными.
"""
header = self._parse_header_file(head_filepath)
body_filepath = os.path.join(os.path.dirname(head_filepath), header.filename)
dataframe = self._parse_body_file(body_filepath, header)
def job(self):
...
def _get_file_data(self, path) -> pd.DataFrame:
head, file = self._dat_parser(path)
self.dat_name = file[:-4]
path_r64 = os.path.dirname(path) + '\\' + file
time_axis, dataframe = self._r64_parser(path_r64, head)
dataframe = pd.concat([dataframe, time_axis], axis=1)
return dataframe
def _parse_header_file(self, filepath: str) -> KukaDataHead:
"""
Парсит заголовочный файл (.dat) и извлекает информацию о каналах.
:param filepath: Путь к заголовочному файлу.
:return: Объект KukaDataHead с информацией о данных.
"""
with open(filepath, 'r', encoding='cp1252') as file:
data_head = KukaDataHead(0, "", {})
is_inside_channel = False
self._current_channel = None # Текущее название канала
def _dat_parser(self, path: str) -> list[dict, str]:
with open(path, 'r') as file:
head = {'channels': 0}
inside_channel = False
channels = 0
for line in file:
line = line.strip()
if line in ('#BEGINCHANNELHEADER', "#BEGINGLOBALHEADER"):
is_inside_channel = True
elif line in ('#ENDCHANNELHEADER', "#ENDGLOBALHEADER"):
is_inside_channel = False
else:
if is_inside_channel:
self._parse_header_line(line, data_head)
return data_head
if line == '#BEGINCHANNELHEADER' or line == "#BEGINGLOBALHEADER":
inside_channel = True
def _parse_header_line(self, line: str, data_head: KukaDataHead) -> None:
"""
Обрабатывает отдельную строку заголовочного файла и обновляет объект data_head.
elif line == '#ENDCHANNELHEADER' or line == "#ENDGLOBALHEADER":
inside_channel = False
pass
# Формирование словаря
elif inside_channel:
_, data = line.split(',')
match _:
case '102':
head['rob_id'] = data
case '200':
ch_name = data
if ch_name != 'Zeit': channels +=1
head[ch_name] = {}
case '202':
head[ch_name]['unit'] = data
case '211':
file = data
case '220':
head[ch_name]['len'] = int(data)
case '221':
head[ch_name]['num'] = int(data)
case '241':
head[ch_name]['multiplyer'] = float(data)
head['channels'] = int(channels)
return head, file
:param line: Строка из файла.
:param data_head: Объект KukaDataHead для обновления.
"""
tag, value = line.split(',')
match tag:
case '102':
data_head.rob_ID = value
case '200':
self._current_channel = str(value)
data_head.channels[self._current_channel] = {}
case '202':
data_head.channels[self._current_channel]['unit'] = str(value)
case '211':
data_head.filename = str(value)
case '220':
data_head.channels[self._current_channel]['len'] = int(value)
case '221':
data_head.channels[self._current_channel]['num'] = int(value)
case '241':
# Переименовано в "multiplier" для повышения читаемости
data_head.channels[self._current_channel]['multiplier'] = float(value)
def _parse_body_file(self, filepath: str, data_head: KukaDataHead) -> pd.DataFrame:
"""
Парсит файл данных (.r64) и объединяет его с временной осью.
:param filepath: Путь к файлу данных.
:param data_head: Заголовочная информация.
:return: DataFrame с данными.
"""
time_axis = self._build_time_axis(data_head)
multipliers, channel_names = self._extract_channels_data(data_head)
raw_data = self._read_r64_file(filepath)
if raw_data.size % len(channel_names) != 0:
raise ValueError(f"Количество записей в {filepath} не кратно количеству найденных каналов ({len(channel_names)})")
try:
data_reshaped = raw_data.reshape(-1, len(channel_names))
except ValueError as e:
raise ValueError(f"Ошибка при изменении формы raw_data: {e}")
# Применяем множители к данным каналов
df_data = pd.DataFrame(data_reshaped * multipliers, columns=channel_names)
dataframe = pd.concat([time_axis, df_data], axis=1)
return dataframe
@staticmethod
def _extract_channels_data(data_head: KukaDataHead) -> Tuple[np.ndarray, List[str]]:
"""
Извлекает данные по каналам, сортируя их по номеру.
:param data_head: Заголовочная информация.
:return: Кортеж из массива множителей и списка имён каналов.
"""
sorted_channels = sorted(
(item for item in data_head.channels.items() if item[0] != "Zeit"),
key=lambda item: item[1]['num']
)
multipliers = np.array([info['multiplier'] for _, info in sorted_channels])
channel_names = [key for key, _ in sorted_channels]
return multipliers, channel_names
@staticmethod
def _build_time_axis(data_head: KukaDataHead) -> pd.Series:
"""
Строит временную ось на основе информации из канала 'Zeit'.
:param data_head: Заголовочная информация.
:return: Серия pandas с временными метками.
"""
num_timestamps = data_head.channels['Zeit']['len'] - 1
time_step = data_head.channels['Zeit']['multiplier']
time_axis = pd.Series(np.arange(0, num_timestamps * time_step, time_step))
def _r64_parser(self, path: str, head: dict) -> Optional[list[pd.Series, pd.DataFrame]]:
ch = head['channels']
keys = list(head.keys())[-ch:]
len_timestamps = head['Zeit']['len']
t_step = head['Zeit']['multiplyer']
time_axis = pd.Series(np.arange(0, len_timestamps*t_step, t_step))
time_axis.name = 'time'
return time_axis
@staticmethod
def _read_r64_file(filepath: str) -> np.ndarray:
"""
Считывает бинарный файл (.r64) и возвращает массив чисел.
:param filepath: Путь к файлу.
:return: Массив numpy с типом float.
"""
with open(filepath, 'rb') as file:
dataframe = pd.DataFrame({})
with open(path, 'rb') as file:
data = file.read()
numbers = np.frombuffer(data, dtype='<d')
return numbers
floats = np.frombuffer(data, dtype='<d') # Little-endian double
for key in keys:
step = head[key]['num']-1
result = pd.Series(np.array(floats[step::ch])* head[key]['multiplyer'])
result.name = key
dataframe = pd.concat([dataframe, result], axis=1)
return time_axis, dataframe
class KukaTextParser(BaseKukaTextParser):
"""
Класс для парсинга текстовых данных Кука.
Извлекает сообщения из файла.
"""
class BaseProduct(ABC):
def __init__(self):
super().__init__()
self._in_message = False
self._data_packs: List[KukaTXT] = []
self._signals = []
def parse(self, filepath: str) -> List[KukaTXT]:
"""
Парсит текстовый файл и возвращает список объектов KukaTXT.
def _find_indexes(self,
signal: str,
dataframe: pd.DataFrame) -> list[list[float], list[float]]:
stage_diff = np.diff(dataframe[signal])
start_idx = np.where(stage_diff == 1)
finish_idx = np.where(stage_diff == -1)
return start_idx[0], finish_idx[0]
:param filepath: Путь к текстовому файлу.
:return: Список объектов KukaTXT.
"""
self._data_packs = []
with open(filepath, 'r') as file:
current_pack = KukaTXT()
for line in file:
line = line.strip()
current_pack = self._process_message_flow(line, current_pack)
return self._data_packs
def _find_events(self,
dataframe: pd.DataFrame,
signals: list[str]) -> Optional[dict[dict[pd.Series]]]:
intervals = {}
end_time = 0
for signal in signals:
start_idx, finish_idx = np.array(self._find_indexes(signal, dataframe))
start_series = dataframe.loc[start_idx, "time"].reset_index(drop=True)
end_series = dataframe.loc[finish_idx, "time"].reset_index(drop=True)
end_series.fillna(end_time)
intervals[signal] = {"rise": start_series,
"fall": end_series}
return intervals
def _process_message_flow(self, line: str, current_pack: KukaTXT) -> KukaTXT:
"""
Обрабатывает поток сообщений, определяя начало и конец блока.
def _form_intervals(self,
start: pd.Series,
end: pd.Series) -> dict:
if len(start) != len(end):
for i in range(1, len(end)):
if end[i-1] > start[i]:
start = start.drop(i).reset_index(drop=True)
intervals = {'start':start.tolist, 'end':end.tolist}
return intervals
:param line: Строка из файла.
:param current_pack: Текущий объект KukaTXT.
:return: Обновлённый объект KukaTXT.
"""
if line == "#BEGINMOTIONINFO":
self._in_message = True
current_pack = KukaTXT()
elif line == "#ENDMOTIONINFO":
self._in_message = False
self._data_packs.append(current_pack)
current_pack = KukaTXT()
elif self._in_message:
current_pack = self._process_line(line, current_pack)
return current_pack
def _process_line(self, line: str, current_pack: KukaTXT) -> KukaTXT:
"""
Обрабатывает отдельную строку внутри блока сообщения.
:param line: Строка с данными.
:param current_pack: Текущий объект KukaTXT.
:return: Обновлённый объект KukaTXT.
"""
tag, value = line.split(":")
match tag:
case "TIME":
current_pack.time = float(value)
case "ENDTIME":
current_pack.endtime = float(value)
case "MODULE":
pass
case "FUNCTION/PROCEDURE":
current_pack.func = value
case "TYPE":
current_pack.type_ = value
case "SIGNAL":
current_pack.signal = value
case "LINE":
pass
case "POINT NAME":
pass
case "POINT COORDINATES":
pass
case "BLENDING":
pass
case "BLENDING PARAMETER":
pass
case "VELOCITIES":
pass
case "ACCELERATIONS":
pass
case "BASE":
pass
case "TOOL":
pass
case "IPO MODE":
pass
case "MOTION MODE":
pass
case "LOAD":
pass
case "LOAD A3":
pass
return current_pack
@abstractmethod
def operation(self):
...
class TraceStageDetector(BaseTraceStageDetector):
"""
Класс для детекции этапов (стадий) на основе данных трассировки.
class Performance(BasePerformanceFactory):
Определяет переходы между различными стадиями процесса по пороговым значениям.
"""
def __init__(self, parent: TraceProcessor = None):
super().__init__(parent)
def robot_method(self) -> BaseProduct:
return RobotData()
@staticmethod
def is_closing(robot_velocity: float, actual_velocity: float, actual_force: float, thresholds: dict) -> bool:
"""
Определяет, находится ли робот в стадии закрытия.
def TWC_method(self) -> BaseProduct:
return TWC_Data()
:param robot_velocity: Скорость робота.
:param actual_velocity: Фактическая скорость.
:param actual_force: Фактическое значение силы.
:param thresholds: Словарь пороговых значений.
:return: True, если условие стадии закрытия выполнено.
"""
return actual_velocity > thresholds['act_vel_min'] and abs(robot_velocity) < thresholds['rob_vel_thresh'] and actual_force < thresholds['act_force_close']
def comm_method(self) -> BaseProduct:
return CommData()
@staticmethod
def is_squeeze(robot_velocity: float, actual_velocity: float, actual_force: float, force_rate: float, thresholds: dict) -> bool:
"""
Определяет, находится ли робот в стадии сжатия.
:param robot_velocity: Скорость робота.
:param actual_velocity: Фактическая скорость.
:param actual_force: Фактическое значение силы.
:param force_rate: Темп изменения силы.
:param thresholds: Словарь пороговых значений.
:return: True, если условие стадии сжатия выполнено.
"""
return abs(robot_velocity) < thresholds['rob_vel_thresh'] and actual_velocity < thresholds['act_vel_close'] and force_rate > thresholds['force_increase']
@staticmethod
def is_welding(robot_velocity: float, actual_velocity: float, actual_force: float, thresholds: dict) -> bool:
"""
Определяет, находится ли робот в стадии сварки.
:param robot_velocity: Скорость робота.
:param actual_velocity: Фактическая скорость.
:param actual_force: Фактическое значение силы.
:param thresholds: Словарь пороговых значений.
:return: True, если условие стадии сварки выполнено.
"""
return abs(robot_velocity) < thresholds['rob_vel_thresh'] and abs(actual_velocity) < thresholds['act_vel_thresh'] and actual_force > thresholds['act_force_weld']
@staticmethod
def is_relief(actual_velocity: float, actual_position_diff: float, force_rate: float, thresholds: dict) -> bool:
"""
Определяет, находится ли робот в стадии снятия усилия (relief).
:param actual_velocity: Фактическая скорость.
:param actual_position_diff: Разница в позициях.
:param force_rate: Темп изменения силы.
:param thresholds: Словарь пороговых значений.
:return: True, если условие стадии снятия усилия выполнено.
"""
return force_rate < -thresholds['force_decrease'] and abs(actual_position_diff) < thresholds['act_pos_decrease'] and actual_velocity < -thresholds['act_vel_negative']
def detect_stages(self, df: pd.DataFrame) -> List[Tuple[str, float, float]]:
"""
Детектирует стадии процесса по данным трассировки.
:param df: DataFrame с данными трассировки.
:return: Список кортежей (название стадии, время начала, время окончания).
"""
timestamps = df['time'].to_list()
n = len(df)
# Извлекаем пороговые значения из настроек
thresholds = {key: item[0] for key, item in self._parent._settings.filter.items()}
# Вычисляем производную силы и разницу позиций
actual_force = df["DriveMotorTorq_Act7"].values
force_diff = np.diff(actual_force, prepend=actual_force[0])
actual_position = df["DriveMotorPos_Act7"].values
position_diff = np.diff(actual_position, prepend=actual_position[0])
stages = []
current_state = "Oncomming"
state_start = timestamps[0]
# Проходим по всем записям DataFrame
for i in range(n):
robot_velocity = df.loc[i, "CartVel_Act"]
actual_velocity = df.loc[i, "DriveMotorVel_Act7"]
force_value = df.loc[i, "DriveMotorTorq_Act7"]
current_force_rate = force_diff[i]
current_position_diff = position_diff[i]
if current_state == "Oncomming":
if self.is_closing(robot_velocity, actual_velocity, force_value, thresholds):
state_end = timestamps[i]
stages.append(("Oncomming", state_start, state_end))
current_state = "Closing"
state_start = timestamps[i]
elif current_state == "Closing":
if self.is_squeeze(robot_velocity, actual_velocity, force_value, current_force_rate, thresholds):
state_end = timestamps[i]
stages.append(("Closing", state_start, state_end))
current_state = "Squeeze"
state_start = timestamps[i]
elif current_state == "Squeeze":
if self.is_welding(robot_velocity, actual_velocity, force_value, thresholds):
state_end = timestamps[i]
stages.append(("Squeeze", state_start, state_end))
current_state = "Welding"
state_start = timestamps[i]
elif current_state == "Welding":
if self.is_relief(actual_velocity, current_position_diff, current_force_rate, thresholds):
state_end = timestamps[i]
stages.append(("Welding", state_start, state_end))
current_state = "Relief"
state_start = timestamps[i]
elif current_state == "Relief":
# Если признаки снятия усилия не соблюдаются, завершаем цикл и переходим в Oncomming
if not self.is_relief(actual_velocity, current_position_diff, current_force_rate, thresholds):
state_end = timestamps[i]
stages.append(("Relief", state_start, state_end))
current_state = "Oncomming"
state_start = timestamps[i]
# Фиксируем последний сегмент
stages.append((current_state, state_start, timestamps[-1]))
return stages
def job(self, path:str, TWC_raw:pd.DataFrame) -> list[pd.DataFrame, list[pd.DataFrame], list[pd.DataFrame]]:
robot = self.robot_method()
TWC = self.TWC_method()
comm=self.comm_method()
dataframe = self._get_file_data(path)
rob_comm, rob_df = robot.operation(dataframe)
TWC_comm, TWC_df = TWC.operation(TWC_raw)
comm_df = comm.operation(rob_comm, TWC_comm)
return comm_df, rob_df, TWC_df
class TextStageDetector(BaseTextStageDetector):
"""
Класс для детекции сварочных стадий на основе текстовых данных.
"""
def __init__(self, parent: TraceProcessor = None):
super().__init__(parent)
@staticmethod
def detect_welding(data: List[KukaTXT]) -> List[dict]:
"""
Детектирует этап сварки по текстовым сообщениям.
:param data: Список объектов KukaTXT.
:return: Список словарей с информацией о сварке (стадия, время начала и окончания).
"""
stages = []
for i in range(len(data) - 1): # Предотвращаем выход за пределы списка
if data[i].func == " SPOT" and data[i].signal == " END":
stages.append({
"stage": "welding",
"start_time": data[i].time,
"end_time": data[i + 1].time
})
return stages
class TraceProcessor(BaseRawTraceProcessor):
"""
Основной класс для обработки трассировок.
Объединяет данные, детектирует стадии и инициирует рендеринг через медиатор.
"""
class RobotData(BaseProduct):
def __init__(self):
self._settings = Settings()
data_parser = KukaDataParser()
text_parser = KukaTextParser()
trace_detector = TraceStageDetector(self)
text_detector = TextStageDetector(self)
self._signals = [
"$OUT3012",
"$IN3003",
"$OUT3003",
"$OUT3244"
]
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
events = self._find_events(dataframe, self._signals)
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
return communication_sig, pd.DataFrame({'in_point':point_interval,
'in_move':movement_interval})
class TWC_Data(BaseProduct):
def __init__(self):
self._signals = [
"Closing",
"Squeeze",
"Welding",
"Relief",
"Oncoming"
]
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
events = self._find_events(dataframe, self._signals)
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
return communication_sig, pd.DataFrame({'in_closing':closing_interval,
'in_squeeze':squeeze_interval,
'in_relief':relief_interval,
'in_oncoming':oncoming_interval})
class CommData(BaseProduct):
"""
Определяет промежуток, в который происходит взаимодействие между нодами.
Подразумевается следующая структура: node: tuple(list, list)
node[0] - время отправки пакетов. node[1] - время приема пакетов.
"""
def operation(self, node1: dict, node2: dict) -> pd.DataFrame:
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
super().__init__(data_parser, text_parser, trace_detector, text_detector)
def prerender(self, data: List[str]) -> None:
"""
Препроцессинг данных для отрисовки трейсов и обнаруженных событий.
:param data: Список путей к файлам данных.
"""
rendered_data = self._render_data(data)
self._mediator.prerender_TCW(self, rendered_data)
def final_render(self) -> None:
"""
Финальный рендеринг данных для отрисовки
трейсов клиента и расчета трейсов ТСК.
"""
events = self._detect_stages(self._trace_df, self._text_data)
renamed_df = self._rename_df_columns(self._trace_df)
part_position, max_open = self._detect_coords(renamed_df, events)
self._mediator.render_TCW(self, [renamed_df, events, [part_position, max_open]])
def _detect_coords(self, dataframe: pd.DataFrame, events: dict) -> Tuple[List[float], List[float]]:
"""
Детектирует координаты на основе событий и данных.
:param dataframe: DataFrame с данными.
:param events: Словарь с событиями.
:return: Кортеж из списков: координаты детали и максимальное открытие электрода.
"""
weld_timings = events["Welding"]
oncommings = events["Oncomming"]
open_positions = []
part_positions = []
for i in range(len(weld_timings[0])):
weld_start = weld_timings[0][i]
weld_end = weld_timings[1][i]
onc_start = oncommings[0][i]
onc_end = oncommings[1][i]
pos_part = dataframe[(dataframe["time"] > weld_start) & (dataframe["time"] < weld_end)]["Electrode Position, mm ME"].mean()
part_positions.append(float(pos_part) / 1000)
pos_open = dataframe[(dataframe["time"] > onc_start) & (dataframe["time"] < onc_end)]["Electrode Position, mm ME"].abs().max()
open_positions.append(float(pos_open) / 1000)
return (part_positions, open_positions)
def _render_data(self, data: List[str]) -> List:
"""
Обрабатывает входные файлы данных и возвращает предварительно обработанные данные.
:param data: Список путей к файлам (первый для трассировки, второй для текстовых данных).
:return: Список, содержащий DataFrame и события.
"""
if data and len(data) == 2:
trace_filepath = data[0]
text_filepath = data[1]
elif data:
trace_filepath = data[0]
text_filepath = None
else:
trace_filepath = None
text_filepath = None
self._trace_df = self._unpack_trace(trace_filepath)
self._text_data = self._unpack_text(text_filepath)
events = self._detect_stages(self._trace_df, self._text_data)
renamed_df = self._rename_df_columns(self._trace_df)
return [renamed_df, events]
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки обработки.
:param settings: Объект настроек.
"""
self._settings = settings
def _unpack_trace(self, trace_filepath: str = None) -> Optional[pd.DataFrame]:
"""
Распаковывает трассировочные данные из файла.
:param trace_filepath: Путь к файлу трассировки.
:return: DataFrame с данными или None.
"""
if trace_filepath:
return self._dataparser.parse(trace_filepath)
return None
def _unpack_text(self, text_filepath: str = None) -> Optional[List[KukaTXT]]:
"""
Распаковывает текстовые данные из файла.
:param text_filepath: Путь к текстовому файлу.
:return: Список объектов KukaTXT или None.
"""
if text_filepath:
return self._textparser.parse(text_filepath)
return None
def _detect_stages(self, trace_df: pd.DataFrame, text_data: List) -> Optional[dict]:
"""
Детектирует события на основе трассировочных и текстовых данных.
:param trace_df: DataFrame с трассировочными данными.
:param text_data: Список текстовых данных.
:return: Словарь с событиями или None.
"""
if trace_df is not None and text_data is not None:
trace_stages = self._data_detector.detect_stages(trace_df)
events = self._form_events(trace_stages)
return events
return None
@staticmethod
def _form_events(trace_stages: list) -> dict:
"""
Формирует словарь событий на основе списка этапов трассировки.
:param trace_stages: Список этапов (кортежи: название, время начала, время окончания).
:return: Словарь с событиями.
"""
events = {
"Closing": [[], []],
"Squeeze": [[], []],
"Welding": [[], []],
"Relief": [[], []],
"Oncomming": [[], []]
}
for stage in trace_stages:
name, start_time, end_time = stage
events[name][0].append(start_time)
events[name][1].append(end_time)
return events
@staticmethod
def _rename_df_columns(dataframe: pd.DataFrame) -> Optional[pd.DataFrame]:
"""
Переименовывает столбцы DataFrame на основе корректной карты соответствия.
:param dataframe: Исходный DataFrame.
:return: DataFrame с переименованными столбцами или None в случае ошибки.
"""
correct_mapping = {
"time": ["Time", "Timestamp"],
"Tool Coordinate, mm X": ["X_Act"],
"Tool Coordinate, mm Y": ["Y_Act"],
"Tool Coordinate, mm Z": ["Z_Act"],
"Electrode Force, N ME": ["DriveMotorTorq_Act7"],
"Electrode Position, mm ME": ["AxisPos_Act7"],
"Electrode Speed, mm ME": ["AxisVel_Act7"],
"Rotor Position, deg FE": ["DriveMotorPos_Act7"],
"Rotor Speed, deg/s ME": ["DriveMotorVel_Act7"],
"Rotor Current, A ME": ["DriveMotorCurr_Act7"],
"Cartesian Tool Speed, mm/s ME": ["CartVel_Act"],
}
try:
df_copy = dataframe.copy(deep=True)
working_mapping = {key: [item.lower() for item in items] for key, items in correct_mapping.items()}
new_columns = {}
for col in df_copy.columns:
col_lower = col.lower()
for key, values in working_mapping.items():
if col_lower in values:
new_columns[col] = key
break
else:
new_columns[col] = col
df_copy.rename(columns=new_columns, inplace=True)
df_copy = df_copy.loc[:, ~df_copy.columns.duplicated()]
return df_copy
except AttributeError as e:
logger.error(f"_rename_df_columns - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
return None
except Exception as e:
logger.error(f"_rename_df_columns - Непредвиденная ошибка: {e}")
return None
"""
Примеры комментариев для этапов процесса:
Перемещение:
# FUNCTION/PROCEDURE: SW_RSP030TL01_SN - какое-то перемещение
#
# ИЛИ
#
# FUNCTION/PROCEDURE: SGL_MoveToPos - перемещение между точками
# SIGNAL: BLENDING
Смыкание и набор усилия:
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - перемещение электрода с движением робота
# ...
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - перемещение 0.5 мм роботом с движением электрода
# ...
# FUNCTION/PROCEDURE: SGM_MOVE_TO_FORCE - остановка в позиции (может быть набор усилия?)
Сварка:
# FUNCTION/PROCEDURE: SPOT - Начало сварочного процесса
# SIGNAL: START
#
# FUNCTION/PROCEDURE: SPOT - Конец сварочного процесса
# SIGNAL: END
Снятие усилия и разъезд:
# FUNCTION/PROCEDURE: SGL_MoveToPos - Выход из контакта с точкой движением робота и электрода
# SIGNAL: START
"""

View File

@ -1,45 +1,22 @@
# TODO: Тесты должны лежать в директории tests.
# TODO: Правило хорошего тона при оформлении импортов: сначала встроенные библиотеки,
# через одну пустую строку - дополнительно установленные, и еще через одну пустую строку - собственные импорты.
from src.OptAlgorithm.OptAlgorithm import OptAlgorithm
from src.utils import read_json
from matplotlib import pyplot as plt, use
from numpy import cos, sin, sqrt, cbrt, arcsin, linspace, array
# TODO: Отступ перед блоком if __name__ == "__main__" - 2 пустых строки.
if __name__ == "__main__":
# TODO: Что означают переменные ниже? Имена переменных должны быть осмысленными, чтобы их смысл был понятен не
# не только автору кода.
tq = 1
ts = linspace(0, tq, 200000)
operator_params = read_json("../params/operator_params.json")
system_params = read_json("../params/system_params.json")
# TODO: 2 блока кода ниже (с циклами) нарушают принцип DRY (Don't Repeat Yourself):
# повторяющийся блок кода следует вынести в отдельную функцию.
# Например, в такую:
# def foo(params_dict: dict) -> dict:
# result_dict = {}
# i = 1
# for key, value in params_dict.items():
# if isinstance(value, list):
# result_dict[key] = value[i] if len(value) > i else value[0]
# else:
# result_dict[key] = value
# return result_dict
# И потом вызвать ее как-то так:
# non_array_operator_params, non_array_system_params = foo(operator_params), foo(system_params)
operator_params = read_json("params/operator_params.json")
system_params = read_json("params/system_params.json")
non_array_operator_params = {}
i = 1
for key, value in operator_params.items():
# TODO: Не совсем понятен смысл такой проверки. Если хотим проверить принадлежность value к list,
# то лучше использовать для этого isinstance.
if hasattr(value, "__len__"):
# TODO: Можно заменить однострочным выражением: result_dict[key] = value[i] if len(value) > i else value[0]
if len(value) > i:
non_array_operator_params[key] = value[i]
else:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,10 +1,7 @@
from uml.request_generator import Request
from src.uml.request_generator import Request
class UMLCreator:
# TODO: в методах, обремененных некой логикой, постоянно встречается проверка режима if not self.theor_mode.
# Есть предложение реализовать паттерн СТРАТЕГИЯ, который в зависимости от режима делал бы что-то
# разными способами. Это бы помогло избавиться от спагетти и обособить логику работы в зависимости от ситуации.
def __init__(self, request_generator: Request, path_to_save: str):
self._request_generator = request_generator
self.path_to_save = path_to_save
@ -14,7 +11,7 @@ class UMLCreator:
real_data = []
ideal_data = []
if not self.theor_mode:
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
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]
@ -26,15 +23,6 @@ class UMLCreator:
real_data.append([item[1]*self.scaler, '{-}'])
if key == 'compression':
# TODO: Дублирование проверки значения ключа. Можно объединить проверки вверху и тут
# в одном match - case выражении:
# match key:
# case "compression":
# ...
# case "opening":
# ...
# case _:
# ...
ideal_data.append([(item[1]-ideal_time)*self.scaler, str(key) + '#yellow'])
ideal_data.append([(item[1]-0.0001)*self.scaler, '{-}'])
else:
@ -42,16 +30,10 @@ class UMLCreator:
ideal_data.append([(item[0]+ideal_time)*self.scaler, '{-}'])
if key == 'opening':
ideal_data.append([item[1]*self.scaler+0.0001, 'coming #yellow'])
# TODO: elif key == 'opening': ideal_time = self._ideal_time[2]. Почему здесь при условии
# значения ключа opening уже self._ideal_time[3]?
ideal_data.append([(item[1]+self._ideal_time[3])*self.scaler, '{-}'])
else:
# TODO: real_data уже определена как [] в самом начале. Если не попадаем в верхнее условие,
# то она таковой и останется. Дублирование.
real_data = []
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот
# набор данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
ideal_data = [
[0.0, 'closure #yellow'],
[self._ideal_time[0] * self.scaler, '{-}'],
@ -63,12 +45,7 @@ class UMLCreator:
[(sum(self._ideal_time[:3]) + self.WeldTime) * self.scaler, 'coming #yellow'],
[(sum(self._ideal_time[:4]) + self.WeldTime) * self.scaler, '{-}'],
]
# TODO: Очень неудачный способ хранения данных разного типа: сложен в восприятии,
# легко ошибиться или запутаться.
# Используй NamedTuple или dataclass: будут определены поля, их типы.
# Взаимодействие также будет гораздо проще (по имени поля, а не индексу).
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот набор
# данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
client_data = [
[0.0 * self.scaler, 'closure'],
[0.165 * self.scaler, '{-}'],
@ -80,7 +57,7 @@ class UMLCreator:
[0.300 * self.scaler, '{-}'],
]
# TODO: Зачем метод возвращает self.bool_dict, если никаких манипуляций с ним не проводит?
return real_data, client_data, ideal_data, self.bool_dict
def _generate_svg(self, real_data, client_data, ideal_data, bool_data) -> None:
@ -93,8 +70,6 @@ class UMLCreator:
for i, [signal, changes] in enumerate(bool_data.items()):
name = 'bool_' + str(i)
# TODO: 3 строки ниже замени на list comprehension
# times = [[str(float(f[0])*scaler), f[1]] for f in changes]
times = []
for f in changes:
times.append([str(float(f[0])*self.scaler), f[1]])
@ -123,9 +98,7 @@ class UMLCreator:
timings_dict: dict,
mode: bool,
name:str):
# TODO: Аргумент mode вводит в заблуждение: ожидаешь увидеть какой-то режим, на деле же это флаг
# теоретического режима. Поправь название аргумента.
# TODO: Инициализацию атрибутов класса в конструкторе надо делать!
self._ideal_time = ideal_time
self.bool_dict = bool_dict
self.float_dict = float_dict

View File

@ -1,10 +1,8 @@
from os.path import abspath
from plantuml import PlantUML
from os.path import abspath
class Request:
# TODO: Имя класса не соответствует тому, что он делает.
def __init__(self, server_url: str):
self._server_url = server_url
@ -15,15 +13,12 @@ class Request:
self.clear()
def _startUML(self):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.reqArr.append('@startuml')
def _endUML(self):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.reqArr.append('@enduml')
def clear(self):
# TODO: Атрибуты класса инициализируются в конструкторе класса.
self.timestamps = {}
self.reqArr = []
self.variables = []
@ -32,36 +27,28 @@ class Request:
self._startUML()
def addAnalog(self, name, string):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.variables.append(f'analog "{string}" as {name}')
def addBinary(self, name, string, style = ''):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
if style: name = name + '<<' + style + '>>'
self.variables.append(f'binary "{string}" as {name}')
def addClock(self, name, string):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.variables.append(f'clock "{string}" as {name}')
def addConcise(self, name, string):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.variables.append(f'concise "{string}" as {name}')
def addRobust(self, name, string):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.variables.append(f'robust "{string}" as {name}')
def appendStr(self, string = ''):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.variables.append(string)
def addLineStyle(self, name, color = 'green', thicknes = 1):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.lineStyles[name] = [f'LineColor {color}', f'LineThickness {thicknes}']
def generateSVG(self, name):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self._compileUML(name)
filename = abspath(f'{name}.txt')
self.server.processes_file(filename, outfile=f'{name}.svg')
@ -69,7 +56,6 @@ class Request:
#return result
def setTimestamps(self, name, input):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
for time, state in input:
try:
self.timestamps[f'@{time}'].append(f'{name} is {state}')
@ -77,13 +63,11 @@ class Request:
self.timestamps[f'@{time}'] = [f'{name} is {state}']
def _addTimestamp(self, timecode, vars):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.reqArr.append(timecode)
for var in vars:
self.reqArr.append(var)
def _addHeader(self):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
self.reqArr.append('<style>')
if self.lineStyles:
self.reqArr.append('timingDiagram {')
@ -96,13 +80,9 @@ class Request:
self.reqArr.append('</style>')
def _addVariables(self):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
for var in self.variables: self.reqArr.append(str(var))
def _compileUML(self, name):
# TODO: Единый стиль именования методов! (snake_case / lowercase).
# TODO: Метод и строку собирает, и в файл пишет. Разделить логику формирования строки и записи в файл
# по разным методам.
self._addHeader()
self._addVariables()
@ -111,8 +91,6 @@ class Request:
self._addTimestamp(key, item)
self._endUML()
# TODO: Зачем делать из self.stringUML атрибут класса?
# Используется только тут - достаточно локальной переменной.
self.stringUML = [line + '\n' for line in self.reqArr]
with open(f'{name}.txt', 'w', encoding='utf-8') as file:
file.writelines(self.stringUML)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

613
src/utils/base/base.py Normal file
View File

@ -0,0 +1,613 @@
from __future__ import annotations
import os
from typing import Optional, Union, Any
from cachetools import LRUCache
import numpy as np
import pyqtgraph as pg
import pandas as pd
from PyQt5.QtCore import QObject, QTimer
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
from OptAlgorithm import OptAlgorithm
from utils.qt_settings import dark_style
class BaseMediator:
def __init__(self,
monitor: BaseDirectoryMonitor,
converter: BaseDataConverter,
passportFormer: BasePointPassportFormer,
plot: BasePlotWidget,
controller: BaseController):
self._monitor = monitor
self._monitor.mediator = self
self._converter = converter
self._converter.mediator = self
self._passportFormer = passportFormer
self._passportFormer.mediator = self
self._plot = plot
self._plot.mediator = self
self._controller = controller
self._controller.mediator = self
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
...
def update_settings (self, data: list[dict]):
...
def update_status(self, msg: Union[str, float]) -> None:
...
class BaseDirectoryMonitor:
update_timer = QTimer()
def __init__(self,
mediator: Optional[BaseMediator] = None):
super().__init__()
self._directory_path = None
self._update_time = None
self.isActive = False
self._files = []
self._mediator = mediator
@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.isActive = True
self.update_timer.start(int(self._update_time))
def stop(self):
self.isActive = False
self.update_timer.stop()
def update_settings(self, data: list[dict]) -> None:
...
def update_plots(self) -> None:
...
def force_all_dir(self):
...
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._stage_colors = {
"Closing": [220, 20, 60, 100], # Crimson
"Squeeze": [30, 144, 255, 100], # Dodger Blue
"Welding": [128, 128, 128, 100], # Gray
"Relief": [34, 139, 34, 100], # Forest Green
"Oncomming": [255, 165, 0, 100] # Orange
}
self._plt_channels = {
"Electrode Force, N & Welding Current, kA": {
"Settings": {
"zoom": False,
"stages": True,
"performance": True,
"ideals": True,
"mirror ME": False,
"workpiece": False,
"force compensation FE": False,
"force accuracy":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 Position, mm": {
"Settings": {
"zoom": False,
"stages": True,
"performance": False,
"ideals": True,
"mirror ME": True,
"workpiece": True,
"force compensation FE": True,
"force accuracy":False
},
"Real_signals": [
{
"name": "Rotor Position, mm ME",
"pen": {'color': 'r', 'width':2},
},
{
"name": "Rotor Position, mm FE",
"pen": {'color': 'w', 'width':2},
}
],
"Ideal_signals": [
{
"name": "Position ME",
"pen": {'color': 'g', 'width':4},
},
{
"name": "Position FE",
"pen": {'color': 'b', 'width':4},
}
]
},
"Electrode Speed, mm/s": {
"Settings": {
"zoom": False,
"stages": True,
"performance": False,
"ideals": True,
"mirror ME": False,
"workpiece": False,
"force compensation FE": False,
"force accuracy":False
},
"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': 'g', 'width':3},
"zoom": False
},
{
"name": "Rotor Speed FE",
"pen": {'color': 'b', 'width':3},
"zoom": False
}
]
},
}
def set_style(self, object: Union[QTabWidget, QWidget]) -> None:
object.setStyleSheet(
"""QLabel {
color: #ffffff;
font-size: 26px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}""")
def _downsample_data(self, x, y, max_points=5000):
"""
Понижает разрешение данных до заданного количества точек для улучшения производительности навигатора.
"""
if len(x) > max_points:
factor = len(x) // max_points
x_downsampled = x[::factor]
y_downsampled = y[::factor]
return x_downsampled, y_downsampled
return x, y
def _create_navigator(self,
time_region:tuple[float, float],
main_plot: pg.PlotItem) -> list[pg.PlotWidget, pg.LinearRegionItem]:
"""
Создаёт график-навигатор, отображающий все данные в уменьшенном масштабе.
"""
navigator = pg.PlotItem(title="Navigator")
navigator.setFixedHeight(100)
# Получение кривых из main_plot
for curve in main_plot.listDataItems():
# Извлекаем данные из кривой
x, y = curve.getData()
curve_name = curve.opts.get("name", None)
signal_pen = curve.opts.get("pen", None)
x_downsampled, y_downsampled = self._downsample_data(x, y, max_points=1000)
navigator.plot(x_downsampled, y_downsampled, pen=signal_pen, name=curve_name)
ROI_region = pg.LinearRegionItem(values=time_region, movable=True, brush=pg.mkBrush(0, 0, 255, 100), pen=pg.mkPen(width=4))
ROI_region.setBounds([0, x[-1]])
navigator.addItem(ROI_region)
navigator.getViewBox().setLimits(xMin=0, xMax=x[-1])
# Связываем изменение региона навигатора с обновлением области просмотра основного графика
ROI_region.sigRegionChanged.connect(lambda: self._sync_main_plot_with_navigator(main_plot, ROI_region))
return navigator, ROI_region
def _sync_main_plot_with_navigator(self,
main_plot: pg.PlotItem,
region: pg.LinearRegionItem):
"""
Синхронизирует область просмотра основного графика с регионом навигатора.
"""
x_min, x_max = region.getRegion()
if main_plot:
main_plot.blockSignals(True)
main_plot.setXRange(x_min, x_max, padding=0)
main_plot.blockSignals(False)
def _mirror_shift_data(self,
valid_str: str,
signals: list[dict],
dataframe: pd.DataFrame,
shift: float) -> pd.DataFrame:
keys = dataframe.keys()
for signal in signals:
if valid_str in signal["name"] and signal["name"] in keys:
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: shift-x)
return dataframe
def _mirror_shift_ideal(self,
name_ME: str, name_FE: str,
signals: list[dict],
dataframe: pd.DataFrame,
shift: float,
P1: float, P2: float) -> pd.DataFrame:
keys = dataframe.keys()
for signal in signals:
if name_ME in signal["name"] and signal["name"] in keys:
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: -x+P1)
if name_FE in signal["name"] and signal["name"] in keys:
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x+P2)
return dataframe
def _shift_data(self,
valid_str: str,
signals: list[dict],
dataframe: pd.DataFrame,
shift: float) -> pd.DataFrame:
keys = dataframe.keys()
for signal in signals:
if valid_str in signal["name"] and signal["name"] in keys:
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x + shift)
return dataframe
def _sync_navigator_with_main(self, main_plot: pg.PlotItem, region:pg.LinearRegionItem):
"""
Синхронизирует регион навигатора с областью просмотра основного графика.
"""
if region:
x_min, x_max = main_plot
region.blockSignals(True) # Предотвращаем рекурсию
region.setRegion([x_min, x_max])
region.blockSignals(False)
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator
@property
def opt(self) -> BaseIdealDataBuilder:
return self._opt
@opt.setter
def opt(self, opt: BaseIdealDataBuilder):
self._opt = opt
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
...
class BaseController(QObject):
def send_widgets(self, widgets: list[QWidget]) -> None:
...
def update_settings(self, settings: list[dict]) -> None:
...
def raport_mode (self) -> None:
...
def seeking_mode(self) -> None:
...
def open_file(self, filepath: str) -> None:
...
def update_plots(self) -> None:
...
def update_status(self, msg: Union[str, float]) -> None:
...
class BaseIdealDataBuilder(OptAlgorithm):
def __init__(self, params: list[dict]):
operator_params, system_params = params
self.mul = system_params['time_capture']
self.welding_time = operator_params['time_wielding']
super().__init__(operator_params, system_params)
def _get_data(self, end_timestamp:float, func:function) -> pd.DataFrame:
data = []
for i in range (0, int(end_timestamp*self.mul)+1):
time = i/self.mul
X1, X2, V1, V2, F = func(time)
data.append({"time":time, "Position FE":X1*1000,"Position ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
X1, X2, V1, V2, F = func(end_timestamp)
data.append({"time":end_timestamp, "Position FE":X1*1000,"Position 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:
...
def get_compressionDF(self) -> pd.DataFrame:
...
def get_openingDF(self) -> pd.DataFrame:
...
def get_tmovementDF(self) -> pd.DataFrame:
...
def get_weldingDF(self) -> pd.DataFrame:
...
def get_oncomingDF(self) -> pd.DataFrame:
...
def get_ideal_timings(self) -> list[float, float, float, float]:
...
def get_cycle_time(self) -> float:
result = sum(self.get_ideal_timings())
return result
class BaseMainWindow(QMainWindow):
def __init__(self,
controller: Optional[BaseController] = None):
super().__init__()
self.resize(200,200)
self._controller = controller
# Создаем центральный виджет и устанавливаем его
self._central_widget = QWidget()
self.setCentralWidget(self._central_widget)
# Устанавливаем основной вертикальный макет для центрального виджета
self._central_layout = QVBoxLayout()
self._central_widget.setLayout(self._central_layout)
self.set_style(self)
...
@property
def controller(self) -> BaseController:
return self._controller
@controller.setter
def controller(self, controller: BaseController) -> None:
self._controller = controller
def set_style(self, object: Union[QTabWidget, QWidget, QMainWindow]) -> None:
object.setStyleSheet(dark_style)
class BasePointPassportFormer:
def __init__(self,
mediator: Optional[BaseMediator] = None):
self._mediator = mediator
self._clear_stage = "Welding"
self._stages = [
"Closing",
"Squeeze",
"Welding",
"Relief",
"Oncomming"
]
self._tesla_stages = [
"Tesla squeeze",
"Tesla closing",
"Tesla welding",
"Tesla oncomming_relief"
]
self._params = []
self._ideal_data_cashe = LRUCache(maxsize=1000)
self._OptAlgorithm_operator_params = [
"dist_open_start_1",
"dist_open_start_2",
"dist_open_after_1",
"dist_open_after_2",
"dist_open_end_1",
"dist_open_end_2",
"dist_close_end_1",
"dist_close_end_2",
"time_command",
"time_robot_movement",
"object_thickness",
"object_position",
"distance_h_end1",
"distance_h_end2",
"distance_l_1",
"distance_l_2",
"force_target",
"force_capture",
"time_wielding"]
self._OptAlgorithm_system_params = [
"a_max_1",
"v_max_1",
"a_max_2",
"v_max_2",
"mass_1",
"mass_2",
"k_hardness_1",
"k_hardness_2",
"torque_max_1",
"torque_max_2",
"transmission_ratio_1",
"transmission_ratio_2",
"position_start_1",
"position_start_2",
"k_prop",
"time_capture"]
def _find_indexes(self,
signal: str,
dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
stage_diff = np.diff(dataframe[signal])
start_idx = np.where(stage_diff == 1)
finish_idx = np.where(stage_diff == -1)
return start_idx[0], finish_idx[0]
def _find_events(self,
signal: str,
times:pd.Series,
dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
start_idx, finish_idx = self._find_indexes(signal, dataframe)
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
start_idx = np.insert(start_idx, 0, 0)
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
if len(start_list) - len(end_list) == 1:
end_list.append(float(times.iloc[-1]))
return start_list, end_list
def _filter_events(self,
times: pd.Series,
dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
events = {}
point_quantity = 0
if self._clear_stage in self._stages:
start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
point_quantity = len(start_list)
if point_quantity == 0:
#TODO: добавить обработку исключения
return []
for stage in self._stages:
start_list, end_list = self._find_events(stage, times, dataframe)
temp = min([len(start_list), len(end_list)])
if temp < point_quantity:
print ("cant find enough", stage)
start_list += [0]*(point_quantity - temp)
end_list += [1]*(point_quantity - temp)
events[stage] = [start_list, end_list]
return events, point_quantity
def _build_ideal_data(self,
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
params: list[dict] = None) -> dict:
self.opt = idealDataBuilder(params)
stage_ideals = {
"Closing": self._opt.get_closingDF(),
"Squeeze": self._opt.get_compressionDF(),
"Welding": self._opt.get_weldingDF(),
"Relief": self._opt.get_openingDF(),
"Oncomming": self._opt.get_oncomingDF(),
"Ideal cycle": self._opt.get_cycle_time(),
"Ideal timings": self._opt.get_ideal_timings()
}
return stage_ideals
def form_passports(self) -> list[list[pd.DataFrame, dict, list]]:
...
def update_settings(self, params: list) -> None:
...
def _generate_cache_key(self,
params_list: list[dict[str, Any]]) -> tuple:
"""
Преобразует params_list в хешируемый ключ для кэша.
"""
operator_settings, system_settings = params_list
# Преобразуем словари в отсортированные кортежи пар ключ-значение
operator_tuple = frozenset((key, value)
for key, value in operator_settings.items()
if str(key) in self._OptAlgorithm_operator_params)
system_tuple = frozenset((key, value)
for key, value in system_settings.items()
if str(key) in self._OptAlgorithm_system_params)
return (operator_tuple, system_tuple)
@property
def opt(self) -> BaseIdealDataBuilder:
return self._opt
@opt.setter
def opt(self, opt: BaseIdealDataBuilder):
self._opt = opt
@property
def mediator(self) -> BaseMediator:
return self._mediator
@mediator.setter
def mediator(self, mediator: BaseMediator) -> None:
self._mediator = mediator

View File

@ -9,34 +9,27 @@ class DiagramParser:
system_config["Welding_signal"],
system_config["Release_signal"],
system_config["Oncomming_signal"]]
# TODO: Переменные в Python именуются в lowercase либо snake_case.
self.boolDict = {}
self.floatDict = {}
self.timingsDict = {}
self.theor_mode = False
def setData(self, path):
# TODO: Методы в Python именуются в lowercase либо snake_case.
# TODO: Данный метод устанавливает какой-то режим, читает CSV, наполняет словари некими данными.
# Декомпозировать на разные методы, каждый из которых будет решать какую-то свою задачу.
if not path:
self.theor_mode = True
else:
self.data = pd.read_csv(path)
# TODO: 2 цикла ниже: дублирование кода. Устранить.
for signalName in self.data.columns:
# TODO: if isinstance...
if type (self.data[signalName].iloc[0]) == np.bool:
self.boolDict[signalName] = self._getBoolChanges(signalName)
for signalName in self.data.columns:
# TODO: if isinstance...
if type (self.data[signalName].iloc[0]) == np.float64:
self.floatDict[signalName] = self._getFloatChanges(signalName)
for key, items in self.boolDict.items():
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
if key == self.signals[0]: name = "closure"
elif key == self.signals[1]: name = "compression"
elif key == self.signals[2]: name = "welding"
@ -53,34 +46,22 @@ class DiagramParser:
self.timingsDict[name].append([items[i][0], items[i][0]+0.01])
def getBoolDict (self) -> dict:
# TODO: сделать через декоратор @property:
# @property
# def bool_dict(self) -> dict:
# return self._bool_dict
# TODO: Методы в Python именуются в lowercase либо snake_case.
return self.boolDict
def getFloatDict (self) -> dict:
# TODO: сделать через декоратор @property:
# TODO: Методы в Python именуются в lowercase либо snake_case.
return self.floatDict
def getRealTimings(self) -> dict:
# TODO: сделать через декоратор @property:
# TODO: Методы в Python именуются в lowercase либо snake_case.
return self.timingsDict
def getMode(self) -> bool:
# TODO: сделать через декоратор @property:
# TODO: Методы в Python именуются в lowercase либо snake_case.
return self.theor_mode
def _getBoolChanges(self, signalName) -> list:
# TODO: Методы в Python именуются в lowercase либо snake_case.
timeCode = self.data['time']
signal_values = self.data[signalName]
changes = []
# TODO: if len(signal_values)...
if len(signal_values) > 0:
changes.append([float(timeCode.iloc[0]), 'high' if bool(signal_values.iloc[0]) else 'low'])
@ -92,7 +73,6 @@ class DiagramParser:
def _getFloatChanges(self, signalName) -> list:
# TODO: Методы в Python именуются в lowercase либо snake_case.
timeCode = self.data['time']
signal_values = self.data[signalName]
changes = []

View File

@ -1,5 +1,3 @@
# TODO: Было бы логично разместить данный модуль в каталоге gui.
dark_style = """
/*
------------------------------------------------------
@ -285,10 +283,6 @@ QTableView {
border: 1px solid #424242;
selection-background-color: #FFCC00; /* Жёлтый при выделении */
selection-color: #121212; /* Тёмный цвет текста при выделении */
font-size: 14px;
}
QTableView QLineEdit {
font-size: 14px;
}
/* Горизонтальный заголовок в таблицах */
@ -455,16 +449,6 @@ QLabel {
}"""
colors = [
'#FF6F61', # яркий коралловый
'#6B5B95', # приглушенный фиолетовый
'#88B04B', # яркий зеленый
'#F7CAC9', # светлый розовый
'#92A8D1', # светло-голубой
'#955251', # теплый терракотовый
'#B565A7', # лавандовый
'#009B77', # глубокий бирюзовый
'#DD4124', # ярко-красный
'#45B8AC', # мягкий мятный
'#FF6F61', # яркий коралловый
'#6B5B95', # приглушенный фиолетовый
'#88B04B', # яркий зеленый

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More