Merge branch 'refs/heads/feature-client-trace-modifier'

# Conflicts:
#	src/OptAlgorithm/ConstantCalculator.py
#	src/OptAlgorithm/OptAlgorithm.py
#	src/OptAlgorithm/OptTimeCalculator.py
#	src/gui/qt_settings.py
#	src/gui/settings_window.py
This commit is contained in:
Леонид Титов 2025-02-19 13:38:12 +03:00
commit f98e79fbf5
85 changed files with 44392 additions and 29646 deletions

8
.gitignore vendored
View File

@ -1,7 +1,11 @@
/__pycache__
**/__pycache__/
*.pyc
/tck_venv
/.venv
/.vscode
/.idea
/venv
*.txt
*.svg
*.svg
/test.py
/output

32
params/filter_params.json Normal file
View File

@ -0,0 +1,32 @@
{
"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

@ -1,16 +1,226 @@
{
"dist_open_start_1": 0.005,
"dist_open_start_2": 0.005,
"dist_open_after_1": 0.006,
"dist_open_after_2": 0.006,
"dist_open_end_1": 0.01,
"dist_open_end_2": 0.05,
"dist_close_end_1": 0.005,
"dist_close_end_2": 0.005,
"time_wielding": 1,
"time_command": 0.0,
"time_robot_movement": 0.2,
"object_thickness": 0.0045,
"force_target": 5000,
"force_capture": 500
"distance_h_start_1": [
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003
],
"distance_h_start_2": [
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_s_1": [
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001
],
"distance_s_2": [
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
0.001,
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
],
"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
],
"distance_h_end1": [
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003,
0.003
],
"distance_h_end2": [
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
],
"time_wielding": [
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
],
"time_command": [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
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
],
"object_thickness": [
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045,
0.0045
],
"force_target": [
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0,
5000.0
],
"force_capture": [
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0,
500.0
]
}

View File

@ -0,0 +1,325 @@
{
"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,22 +1,74 @@
{
"trace_storage_path": "D:/downloads/a22",
"monitor_update_period": 100,
"a_max_1": 5.41,
"v_max_1": 0.278,
"a_max_2": 35.81,
"v_max_2": 0.7,
"mass_1": 270,
"mass_2": 1,
"k_hardness_1": 2148570,
"k_hardness_2": 0,
"torque_max_1": 20,
"torque_max_2": 0,
"transmission_ratio_1": 0.00125,
"transmission_ratio_2": 1,
"position_start_1": 0.08,
"position_start_2": 0.08,
"k_prop": 0.05,
"time_capture": 100000,
"UML_time_scaler": 1000
"trace_storage_path": [
"/home/andrew/weldingspotperformance/trace_samples"
],
"monitor_update_period": [
1000.0
],
"a_max_1": [
7.96
],
"v_max_1": [
0.499
],
"a_max_2": [
35.81
],
"v_max_2": [
0.75
],
"mass_1": [
270.0
],
"mass_2": [
1.0
],
"k_hardness_1": [
2148570.0
],
"k_hardness_2": [
0.0
],
"torque_max_1": [
13.8
],
"torque_max_2": [
0.0
],
"transmission_ratio_1": [
0.00125
],
"transmission_ratio_2": [
1.0
],
"contact_distance_1": [
0.02
],
"contact_distance_2": [
0.081
],
"k_prop": [
0.075
],
"time_capture": [
1000.0
],
"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
]
}

View File

@ -1,8 +1,10 @@
cachetools==5.5.0
contourpy==1.3.0
cycler==0.12.1
fonttools==4.54.1
httplib2==0.22.0
kiwisolver==1.4.7
loguru==0.7.3
matplotlib==3.9.2
numpy==2.1.2
packaging==24.1

BIN
resources/operator_ico.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
resources/system_ico.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
resources/view_ico.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,4 +1,4 @@
from .AutoConfigClass import AutoConfigClass
from OptAlgorithm.AutoConfigClass import AutoConfigClass
from numpy import sqrt
class ConstantCalculator(AutoConfigClass):
@ -14,8 +14,6 @@ class ConstantCalculator(AutoConfigClass):
def calc(self):
constants = {}
#self.smin1t = self.smin1 - self.dblock / 2
#self.smin2t = self.smin2 - self.dblock / 2
constants["Fprop"] = self.k_prop * self.force_target
constants["freq"] = sqrt(self.k_hardness_1 / self.mass_1)
constants["eff_control"] = self.torque_max_1 / self.transmission_ratio_1

View File

@ -1,7 +1,7 @@
from .PhaseCalc import PhaseCalc
from .OptTimeCalculator import OptTimeCalculator
from .AutoConfigClass import AutoConfigClass
from .ConstantCalculator import ConstantCalculator
from OptAlgorithm.PhaseCalc import PhaseCalc
from OptAlgorithm.OptTimeCalculator import OptTimeCalculator
from OptAlgorithm.AutoConfigClass import AutoConfigClass
from OptAlgorithm.ConstantCalculator import ConstantCalculator
from numpy import cos, sin, sqrt, cbrt, arcsin
@ -16,16 +16,24 @@ class OptAlgorithm(AutoConfigClass):
calc = OptTimeCalculator(operator_config, system_config)
self.Ts = calc.T(self.dist_open_start_1,
self.dist_open_start_2,
self.dist_open_after_1,
self.dist_open_after_2,
self.dist_open_end_1,
self.dist_open_end_2)
if self.distance_l_1 < 0:
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода FE")
self.x1Contact = self.dist_open_start_1 + self.position_start_1
self.x2Contact = self.dist_open_start_2 + self.position_start_2
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,
self.distance_s_2,
self.distance_l_1,
self.distance_l_2)
self.x1_start = self.contact_distance_1 - self.distance_h_start_1
self.x2_start = self.contact_distance_2 - self.distance_h_start_2
self.x1_contact = self.contact_distance_1
self.x2_contact = self.contact_distance_2
self.pos0s, self.movementV0s = calc.Tmovement(self.getSpecific, self.getMarkOpen())
@ -35,19 +43,40 @@ class OptAlgorithm(AutoConfigClass):
return max(self.Ts["topen_2_mark"], self.Ts["topen_1_mark"]) - self.time_command
def V1Close(self, t: float):
if t < self.Ts["tclose_1_acc"]:
return self.a_max_1 * t
else:
return self.a_max_1 * self.Ts["tclose_1_acc"]
def X1Close(self, t: float):
t1 = min(t, self.Ts["tclose_1_acc"])
x0 = self.a_max_1 * t1 * t1 / 2
t2 = max(t - self.Ts["tclose_1_acc"], 0)
x1 = self.a_max_1 * self.Ts["tclose_1_acc"] * t2
return x0 + x1 + self.position_start_1
if "v1close" in self.__dict__.keys():
return self.v1close(t)
self.v1close = PhaseCalc(cummulative=False)
xwait = lambda t: 0
xacc = lambda t: self.a_max_1 * t
v1 = (self.Ts["tclose_1_acc"]) * self.a_max_1
xspeed = lambda t: v1
xdec = lambda t: v1 - self.a_max_1 * t
xend = lambda t: 0
self.v1close.add_phase(self.Ts["tclose_1_wait"], xwait)
self.v1close.add_phase(self.Ts["tclose_1_acc"], xacc)
self.v1close.add_phase(self.Ts["tclose_1_speed"], xspeed)
self.v1close.add_phase(self.Ts["tclose_1_dec"], xdec)
self.v1close.add_phase(self.INF, xend)
return self.v1close(t)
def X1Close(self, T: float):
if "x1close" in self.__dict__.keys():
return self.x1close(T)
self.x1close = PhaseCalc(cummulative=True)
xwait = lambda t: 0
xacc = lambda t: self.a_max_1 * t * t /2
v1 = (self.Ts["tclose_1_acc"]) * self.a_max_1
xspeed = lambda t: v1 * t
xdec = lambda t: v1 * t - self.a_max_1 * t * t /2
xstart = lambda t: self.x1_start
xend = lambda t: 0
self.x1close.add_phase(0, xstart)
self.x1close.add_phase(self.Ts["tclose_1_wait"], xwait)
self.x1close.add_phase(self.Ts["tclose_1_acc"], xacc)
self.x1close.add_phase(self.Ts["tclose_1_speed"], xspeed)
self.x1close.add_phase(self.Ts["tclose_1_dec"], xdec)
self.x1close.add_phase(self.INF, xend)
return self.x1close(T)
def V2Close(self, t: float):
if t < self.Ts["tclose_2_acc"]:
@ -68,7 +97,7 @@ class OptAlgorithm(AutoConfigClass):
t3 = max(min(t - self.Ts["tclose_2_speed"]- self.Ts["tclose_2_acc"], self.Ts["tclose_2_acc"]), 0)
x2 = self.a_max_2 * self.Ts["tclose_2_acc"] * t3 - self.a_max_2 * t3 * t3 / 2
return x0 + x1 + x2 + self.position_start_2
return x0 + x1 + x2 + self.x2_start
def FClose(self, t: float):
return 0
@ -95,7 +124,7 @@ class OptAlgorithm(AutoConfigClass):
def X1Grow(self, t: float):
F = self.FGrow(t)
x = F / self.k_hardness_1
return x + self.x1Contact
return x + self.x1_contact
def V2Grow(self, t: float):
"""
@ -109,7 +138,7 @@ class OptAlgorithm(AutoConfigClass):
Считается, что верхний электрод не влияет на набор усилия,
функция не реализована!, возвращает 0. Устанавливайте kturn = 0
"""
return self.x2Contact
return self.x2_contact
def FGrow(self, t: float):
v0 = self.a_max_1 * self.Ts["tclose_1_acc"]
@ -161,7 +190,7 @@ class OptAlgorithm(AutoConfigClass):
t3 = max(min(t - self.Ts["topen_1_speed"]- self.Ts["topen_1_acc"], self.Ts["topen_1_acc"]), 0)
x2 = -self.a_max_1 * self.Ts["topen_1_acc"] * t3 + self.a_max_1 * t3 * t3 / 2
return xm + x0 + x1 + x2 + self.x1Contact
return xm + x0 + x1 + x2 + self.x1_contact
def V2Open(self, t: float):
@ -187,18 +216,19 @@ class OptAlgorithm(AutoConfigClass):
t3 = max(min(t - self.Ts["topen_2_speed"]- self.Ts["topen_2_acc"], self.Ts["topen_2_acc"]), 0)
x2 = -self.a_max_2 * self.Ts["topen_2_acc"] * t3 + self.a_max_2 * t3 * t3 / 2
return x0 + x1 + x2 + self.x2Contact
return x0 + x1 + x2 + self.x2_contact
def FOpen(self, t: float):
x1 = self.X1Open(t)
x2 = self.X2Open(t)
F = self.k_hardness_1 * max(0, (x1 - self.x1Contact))
F = self.k_hardness_1 * max(0, (x1 - self.x1_contact))
return F
def FMovement(self, t: float):
x1 = self.X1Movement(t)
x2 = self.X2Movement(t)
F = self.k_hardness_1 * max(0, (x1 - self.x1Contact))
#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))
return F
def X1Movement(self, t: float):

View File

@ -1,77 +1,85 @@
from numpy import sqrt, arcsin, arccos, cos, sin
import numpy as np
from numpy import sqrt, arcsin, arccos, cos, sin, array
from OptAlgorithm.AutoConfigClass import AutoConfigClass
from OptAlgorithm.ConstantCalculator import ConstantCalculator
from .AutoConfigClass import AutoConfigClass
from .ConstantCalculator import ConstantCalculator
class OptTimeCalculator(AutoConfigClass):
params_list = []
def __init__(self, operator_config : dict, system_config : dict):
def __init__(self, operator_config: dict, system_config: dict):
cCalculator = ConstantCalculator(operator_config, system_config)
super().__init__(OptTimeCalculator.params_list, operator_config, system_config, cCalculator.calc())
self.allTimes = {}
def tGrowNominal(self, F : float) -> float:
return arcsin(F/(self.Ftogrow)) * sqrt(self.mass_1/self.k_hardness_1)
self.check_eps = 1e-7
def tGrowNominal(self, F: float) -> float:
return arcsin(F / (self.Ftogrow)) * sqrt(self.mass_1 / self.k_hardness_1)
def Tclose(self, h1: float, h2: float) -> None:
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)
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
t1 = v0 / self.a_max_1
t2t = max(0, (h1 - (self.a_max_1 * t1 * t1 /2)) / v0q)
T1 = t1 + t2t
t21 = sqrt(h2/self.a_max_2)
t21 = min(self.v_max_2/self.a_max_2, t21)
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))
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)
T1 = t1 + t2t + t3
t21 = sqrt(h2 / (self.a_max_2))
t21 = min(self.v_max_2 / self.a_max_2, t21)
t22 = max(0, (h2 - (self.a_max_2 * t21 * t21)) / self.v_max_2)
T2 = t22 + 2 * t21
Tclose = max(T1, T2)
tclose_1_acc, tclose_1_speed = self.calcFirstClose(Tclose, h1)
tclose_1_wait, tclose_1_acc, tclose_1_speed, tclose_1_dec = self.calcFirstClose(Tclose, h1)
tclose_2_acc, tclose_2_speed = self.calcSecondClose(Tclose, h2)
self.allTimes["tclose_1_acc"] = tclose_1_acc
self.allTimes["tclose_1_speed"] = tclose_1_speed
self.allTimes["tclose_1_wait"] = tclose_1_wait
self.allTimes["tclose_1_dec"] = tclose_1_dec
self.allTimes["tclose_2_acc"] = tclose_2_acc
self.allTimes["tclose_2_speed"] = tclose_2_speed
self.allTimes["tclose"] = Tclose
def Topen(self, s1 : float, s2 : float, l1 : float, l2 : float, Fs1 : float, Fs2 : float = 0) -> None:
t11 = sqrt((l1 + Fs1)/self.a_max_1)
t11 = min(self.v_max_1/self.a_max_1, t11)
t12 = max(0, ((l1+Fs1) - (self.a_max_1 * t11 * t11)) / self.v_max_1)
def Topen(self, s1: float, s2: float, l1: float, l2: float, Fs1: float, Fs2: float = 0) -> None:
t11 = sqrt((l1 + Fs1) / self.a_max_1)
t11 = min(self.v_max_1 / self.a_max_1, t11)
t12 = max(0, ((l1 + Fs1) - (self.a_max_1 * t11 * t11)) / self.v_max_1)
T1 = t12 + 2 * t11
offset = self.calcSecondOpenOffset(t11, t12, Fs1)
t21 = sqrt(l2/self.a_max_2)
t21 = min(self.v_max_2/self.a_max_2, t21)
t21 = sqrt(l2 / self.a_max_2)
t21 = min(self.v_max_2 / self.a_max_2, t21)
t22 = max(0, (l2 - (self.a_max_2 * t21 * t21)) / self.v_max_2)
T2 = t22 + 2 * t21 + offset
topen_1_acc, topen_1_speed = self.calcFirstOpen(T1, l1+Fs1)
topen_1_acc, topen_1_speed = self.calcFirstOpen(T1, l1 + Fs1)
offset = self.calcSecondOpenOffset(topen_1_acc, topen_1_speed, Fs1)
topen_2_acc, topen_2_speed = self.calcSecondOpen(T2 - offset, l2)
self.allTimes["topen_1_acc"] = topen_1_acc
self.allTimes["topen_2_offset"] = offset
self.allTimes["topen_1_acc"] = topen_1_acc
self.allTimes["topen_2_offset"] = offset
self.allTimes["topen_1_acc"] = topen_1_acc
self.allTimes["topen_1_speed"] = topen_1_speed
self.allTimes["topen_2_acc"] = topen_2_acc
self.allTimes["topen_2_speed"] = topen_2_speed
if s1 >= l1:
raise Exception("""S1 >= L1 - недопустимый сценарий,
проверьте dist_open_after_1, dist_close_end_1""")
проверьте distance_s_1, distance_h_end1""")
if s2 >= l2:
raise Exception("""S2 >= L2 - недопустимый сценарий,
проверьте dist_open_after_2, dist_close_end_2""")
проверьте distance_s_2, distance_h_end2""")
s1 += Fs1
topen_1_mark = sqrt(2 * s1 / self.a_max_1)
if topen_1_mark > topen_1_acc:
@ -79,165 +87,213 @@ 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(topen_1_acc**2 - 2*s1 / self.a_max_1)
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(max(0,topen_1_acc ** 2 - 2 * s1 / self.a_max_1))
else:
topen_1_mark = topen_1_acc + s1 / v1
topen_2_mark = sqrt(2 * s2 / self.a_max_2)
if topen_2_mark > topen_2_acc:
s2 -= topen_2_acc ** 2 * self.a_max_2 / 2
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(topen_2_acc**2 - 2*s2 / self.a_max_2)
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(max(0,topen_2_acc ** 2 - 2 * s2 / self.a_max_2))
else:
topen_2_mark = topen_2_acc + s2 / v2
self.allTimes["topen_1_mark"] = topen_1_mark
self.allTimes["topen_2_mark"] = topen_2_mark
def Tgrow(self) -> None:
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)
L = sqrt(self.k_hardness_1 / self.mass_1 * self.eff_control ** 2 + vF0*vF0)
tspeed = sqrt(self.mass_1/self.k_hardness_1) * (arcsin(vFmax / L) - arccos(sqrt(self.k_hardness_1 / self.mass_1) * self.eff_control / L))
Fspeed = - self.eff_control * cos(self.freq * tspeed) + self.eff_control + 1/self.freq * vF0 * sin(self.freq * tspeed)
def Tgrow(self) -> None:
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)
L = sqrt(self.k_hardness_1 / self.mass_1 * self.eff_control ** 2 + vF0 * vF0)
tspeed = sqrt(self.mass_1 / self.k_hardness_1) * (
arcsin(vFmax / L) - arccos(sqrt(self.k_hardness_1 / self.mass_1) * self.eff_control / L))
Fspeed = - self.eff_control * cos(self.freq * tspeed) + self.eff_control + 1 / self.freq * vF0 * sin(
self.freq * tspeed)
eps = 1e1
if self.freq**2 * self.Ftogrow**2 - vFmax**2 < -eps:
if self.freq ** 2 * self.Ftogrow ** 2 - vFmax ** 2 < -eps:
raise Exception("""Номинальная траектория набора усилия не может быть достигнута, максимальная скорость превысила скорость траектории
, проверьте параметры k_hardness_1, mass_1, k_prop""")
Fmeet = 1/self.freq * sqrt(self.freq**2 * self.Ftogrow**2 - vFmax**2 + eps)
Fmeet = 1 / self.freq * sqrt(self.freq ** 2 * self.Ftogrow ** 2 - vFmax ** 2 + eps)
Fstart_prop = self.Fstart_prop
if Fmeet > Fstart_prop:
raise Exception("""Номинальная траектория набора усилия была достигнута на фазе подпора
, проверьте параметры v_max_1, k_prop""")
tmeet = (Fmeet - Fspeed)/vFmax
tmeet = (Fmeet - Fspeed) / vFmax
tend = self.tGrowNominal(Fstart_prop) - self.tGrowNominal(Fmeet)
vp = 1/sqrt(self.k_hardness_1 * self.mass_1) * sqrt(self.Ftogrow**2 - self.Fstart_prop**2)
vp = 1 / sqrt(self.k_hardness_1 * self.mass_1) * sqrt(self.Ftogrow ** 2 - self.Fstart_prop ** 2)
ap = Fstart_prop / self.mass_1
tprop = 2*vp / ap
tprop = 2 * vp / ap
self.allTimes["tspeed"] = tspeed
self.allTimes["tmeet"] = tmeet
self.allTimes["tend"] = tend
self.allTimes["tprop"] = tprop
self.allTimes["tgrow"] = tspeed + tmeet + tend + tprop
def T(self, h1 : float, h2 : float, s1 : float, s2 : float, l1 : float, l2 : float) -> dict:
def T(self, h1: float, h2: float, s1: float, s2: float, l1: float, l2: float) -> dict:
self.Tclose(h1, h2)
self.Tgrow()
self.Topen(s1, s2, l1, l2, self.force_target / self.k_hardness_1, 0)
return self.allTimes
def Tmovement(self, closeAlgo, tmark) -> None:
contact = [self.dist_open_start_1 + self.position_start_1, self.dist_open_start_2 + self.position_start_2]
def Tmovement(self, closeAlgo, tmark):
contact = [self.contact_distance_1, self.contact_distance_2]
v0s = []
pos0s = []
for i in range(1,3):
for i in range(1, 3):
if tmark < 0:
raise Exception("""Отрицательное время этапа раскрытия,
проверьте dist_open_after_{1,2}, time_command""")
v0 = closeAlgo("V"+str(i), "Open", tmark)
проверьте distance_s_{1,2}, time_command""")
v0 = closeAlgo("V" + str(i), "Open", tmark)
v0s.append(v0)
x0 = closeAlgo("X"+str(i), "Open", tmark)
x1 = contact[i-1] - self.__dict__["dist_close_end_"+str(i)]
x0 = closeAlgo("X" + str(i), "Open", tmark)
x1 = contact[i - 1] - self.__dict__["distance_h_end" + str(i)]
x = x1 - x0
pos0s.append(closeAlgo("X"+str(i), "Open", tmark))
pos0s.append(closeAlgo("X" + str(i), "Open", tmark))
Tfull = self.time_robot_movement
L = self.__dict__["dist_open_end_"+str(i)]
maxL = contact[i-1] - L - x0
L = self.__dict__["distance_l_" + str(i)]
maxL = 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)]
a = self.__dict__["a_max_"+str(i)]
t3 = (Tfull + v0 / a) / 2
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} за указанное время,
проверьте dist_open_after_{i}, dist_close_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)
t1 = max(0, min(t1, -v0/a + sqrt(v0**2 / (a**2) + (abs(maxL)-v0*v0/a)/a)))
t31 = v0/a + t1
t5max = (Tfull - v0/a)/2 - t1
v1 = v0 + a * t1
S1 = v0*t1 + a*t1*t1/2 + v1*t31 - a*t31*t31/2
S2max = Sfull + S1
t5 = min(t5max, (vmax)/a, sqrt(S2max / a))
t3 = abs(v0)/a + t1 + t5
t32 = t5
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
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)
self.allTimes["tmovement_"+str(i)+"_acc"] = t1
self.allTimes["tmovement_"+str(i)+"_speed"] = t2
self.allTimes["tmovement_"+str(i)+"_slow"] = t31
self.allTimes["tmovement_"+str(i)+"_stay"] = tstay
self.allTimes["tmovement_"+str(i)] = t1 + t2 + t31 + tstay
self.allTimes["tpreclose_"+str(i)+"_slow"] = t32
self.allTimes["tpreclose_"+str(i)+"_speed"] = t4
self.allTimes["tpreclose_"+str(i)+"_acc"] = t5
self.allTimes["tpreclose_"+str(i)] = t32 + t4 + t5
vmax = self.__dict__["v_max_" + str(i)]
if vmax < v0 - self.check_eps:
raise Exception("Ошибка - скорость в начале перемещения больше макимальной - проверьте скорость")
a = self.__dict__["a_max_" + str(i)]
Sfull = -Sfull
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("Ошибка - прохождение второй части перемещения - отрицательно, неизвестное поведение")
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
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)
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
self.allTimes["tmovement_" + str(i) + "_stay"] = tstay
self.allTimes["tmovement_" + str(i)] = t1 + t2 + t31 + tstay
self.allTimes["tpreclose_" + str(i) + "_slow"] = t32
self.allTimes["tpreclose_" + str(i) + "_speed"] = t4
self.allTimes["tpreclose_" + str(i) + "_acc"] = t5
self.allTimes["tpreclose_" + str(i)] = t32 + t4 + t5
T = Tfull
self.allTimes["tmovement"] = T
def calcFirstClose(self, T : float, s : float) -> tuple[float, float]:
t1 = T - sqrt(max(0, T**2 - 2 * s / self.a_max_1))
t1 = min(t1, self.v_max_1 / self.a_max_1)
t2 = sqrt(max(0, T**2 - 2 * s / self.a_max_1))
def calcFirstClose(self, T: float, s: float):
v0q = min(sqrt(2 * self.a_max_1 * s), self.v_max_1)
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
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
t4 = 0
else:
t1 = 0
t2 = min(v0q / self.a_max_1, (T + v0/self.a_max_1)/2 - sqrt(max(0, (T + v0/self.a_max_1)**2 - 4 * (v0**2 / (2 * self.a_max_1**2) + s / self.a_max_1))) / 2)
v1 = t2 * self.a_max_1
t4 = max(0, v1-v0) / self.a_max_1
t3 = max(0, T - t2 - t4)
return t1, t2, t3, t4
def calcFirstOpen(self, T: float, s: float):
t1 = T / 2 - sqrt(max(0, T ** 2 - 4 * s / self.a_max_1)) / 2
if t1 > self.v_max_1 / self.a_max_1 + self.check_eps:
raise Exception("""Мы вышли за границы разгона - раскрытие FE, вообще не знаю как так получилось""")
t2 = max(0, (s - self.a_max_1 * t1 ** 2 / 2) / (self.a_max_1 * t1))
return t1, t2
def calcFirstOpen(self, T : float, s : float) -> tuple[float, float]:
t1 = T / 2 - sqrt(max(0, T**2 - 4 * s / self.a_max_1)) / 2
t1 = min(t1, self.v_max_1 / self.a_max_1)
t2 = sqrt(max(0, T * T - 4 * s / self.a_max_1))
def calcSecondOpen(self, T: float, s: float):
t1 = T / 2 - sqrt(max(0, T ** 2 - 4 * s / self.a_max_2)) / 2
if t1 > self.v_max_2 / self.a_max_2 + self.check_eps:
raise Exception("""Мы вышли за границы разгона - раскрытие ME, вообще не знаю как так получилось""")
t2 = max(0, (s - self.a_max_2 * t1 ** 2) / (self.a_max_2 * t1))
return t1, t2
def calcSecondOpen(self, T : float, s : float) -> tuple[float, float]:
t1 = T / 2 - sqrt(max(0, T**2 - 4 * s / self.a_max_2)) / 2
t1 = min(t1, self.v_max_2 / self.a_max_2)
t2 = sqrt(max(0, T * T - 4 * s / self.a_max_2))
def calcSecondClose(self, T: float, s: float):
t1 = T / 2 - sqrt(max(0, T ** 2 - 4 * s / self.a_max_2)) / 2
if t1 > self.v_max_2 / self.a_max_2 + self.check_eps:
raise Exception("""Мы вышли за границы разгона - смыкание ME, вообще не знаю как так получилось""")
t2 = max(0, (s - self.a_max_2 * t1 ** 2) / (self.a_max_2 * t1))
return t1, t2
def calcSecondClose(self, T : float, s : float) -> tuple[float, float]:
t1 = T / 2 - sqrt(max(0, T**2 - 4 * s / self.a_max_2)) / 2
t1 = min(t1, self.v_max_2 / self.a_max_2)
t2 = sqrt(max(0, T * T - 4 * s / self.a_max_2))
return t1, t2
def calcSecondOpenOffset(self, t1 : float, t2 : float, sq : float) -> float:
def calcSecondOpenOffset(self, t1: float, t2: float, sq: float) -> float:
s = sq * 1
offset = sqrt(2 * s / self.a_max_1)
if offset > t1:
s -= t1 ** 2 * self.a_max_1 / 2
v1 = t1 * self.a_max_1
if s > t2 * v1:
s -= t2 * v1
offset = 2*t1 + t2 - sqrt(t1**2 - 2*s / self.a_max_1)
offset = 2 * t1 + t2 - sqrt(t1 ** 2 - 2 * s / self.a_max_1)
else:
offset = t1 + s / v1
return offset

Binary file not shown.

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

@ -0,0 +1,751 @@
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 = PerformanceData()
range_ME: float = 0
k_hardness: float = 0
@dataclass
class GraphicPassport:
"""
Графический паспорт, объединяющий DataFrame с данными,
список паспортов точек и полезные данные для построения графика.
"""
dataframe: pd.DataFrame = pd.DataFrame({})
points_pocket: List[PointPassport] = field(default_factory=list)
useful_data: UsefulGraphData = 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:
"""
Детектирует этап сварки по текстовым данным.
"""
...

View File

@ -1,16 +1,102 @@
from PyQt5.QtWidgets import QWidget
from typing import Tuple
from PyQt5.QtWidgets import QWidget, QTabWidget
from PyQt5.QtGui import QPixmap
from PyQt5.QtCore import pyqtSignal
from src.utils.base.base import BaseController
from base.base import BaseController, Settings
class Controller(BaseController):
"""
Контроллер приложения.
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
медиатор и т.д.
"""
signal_widgets = pyqtSignal(list)
signal_settings = 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()
def send_widgets(self, widgets: list[QWidget]) -> None:
"""
Отправляет список виджетов для дальнейшего использования.
:param widgets: Список виджетов.
"""
self.signal_widgets.emit(widgets)
def push_settings(self, settings: list[dict]) -> None:
self.signal_settings.emit(settings)
def update_settings(self, settings: Settings) -> None:
"""
Обновляет настройки приложения, передавая их через медиатор.
:param settings: Объект настроек.
"""
self._mediator.notify(self, settings)
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 open_file(self, filepath: str) -> None:
"""
Открывает файл по указанному пути через файловый менеджер.
:param filepath: Путь к файлу.
"""
self._file_manager.open_custom_file(filepath)
def open_dir(self, dirpath: str) -> None:
"""
Инициирует предварительный рендеринг данных из директории.
: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,20 +1,104 @@
import pandas as pd
from loguru import logger
#FIXME: костыль для выключения предупреждения "replace deprecated". Потом надо поправить.
from base.base import BaseDataConverter
# FIXME: костыль для выключения предупреждения "replace deprecated"
pd.set_option('future.no_silent_downcasting', True)
from src.utils.base.base import BaseDataConverter
class DataConverter(BaseDataConverter):
"""
Класс для преобразования данных из CSV-файлов.
Основные задачи:
- Исправление заголовков столбцов по заданному шаблону.
- Преобразование булевых значений в целочисленные.
- Обработка ошибок при конвертации и уведомление через медиатора.
"""
@staticmethod
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
bool_columns = dataframe.columns[dataframe.isin([True, False]).any()]
dataframe[bool_columns] = dataframe[bool_columns].replace({True: 1, False: 0})
return dataframe
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
"""
Преобразует столбцы, содержащие булевы значения, в целочисленные.
:param df: DataFrame с исходными данными.
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
"""
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}")
return None
def convert_data(self, files: list[str]) -> None:
dataframes = [pd.read_csv(file) for file in files]
converted_dataframes = list(map(self._replace_bool, dataframes))
self._mediator.notify(self, converted_dataframes)
"""
Считывает данные из списка 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 = [None]
finally:
# Передаем результат медиатору
self._mediator.notify(self, converted_dataframes)

View File

@ -0,0 +1,189 @@
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, 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(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,28 +1,127 @@
import pandas as pd
from typing import Union
import pandas as pd
from PyQt5.QtWidgets import QWidget
from src.utils.base.base import BaseMediator, BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget
from base.base import (
BaseMediator,
BaseFileManager,
BaseDataConverter,
BasePlotWidget,
BasePointPassportFormer,
BaseController,
GraphicPassport,
Settings,
BaseRawTraceProcessor
)
class Mediator(BaseMediator):
"""
Медиатор для организации взаимодействия между различными компонентами приложения.
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
"""
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget],
data: Union[list[str], list[pd.DataFrame], list[QWidget]]):
if issubclass(source.__class__, BaseDirectoryMonitor):
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...")
self._converter.convert_data(data)
if issubclass(source.__class__, BaseDataConverter):
# Если источник – конвертер данных: обновляем прогресс и формируем паспорта
if isinstance(source, BaseDataConverter):
self._controller.update_progress(1)
self._passport_former.form_passports(data)
# Если источник – формирователь паспортов точек: обновляем прогресс и строим графический паспорт
if isinstance(source, BasePointPassportFormer):
self._controller.update_progress(10)
self._plot.build(data)
if issubclass(source.__class__, BasePlotWidget):
# Если источник – виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
if isinstance(source, BasePlotWidget):
self._controller.update_progress(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 prerender_TCW(
self,
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
data: Union[str, list[str], list[pd.DataFrame], dict]
) -> None:
"""
Выполняет предварительный рендеринг TCW (trace control widget) в зависимости от источника уведомления.
:param source: Компонент, инициирующий предварительный рендеринг.
:param data: Передаваемые данные, могут быть строкой или списком путей/DataFrame и словарём.
"""
if isinstance(source, BaseController):
self._controller.update_progress(5)
self._file_manager.open_raw_traces_dir(data)
def push_settings(self, settings: list[dict]):
self._monitor.update_settings(settings)
self._plot.update_settings(settings)
self._monitor.force_all_dir()
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)

View File

@ -1,51 +0,0 @@
from time import sleep
import os
from loguru import logger
from src.utils.base.base import BaseDirectoryMonitor
class DirectoryMonitor(BaseDirectoryMonitor):
def _init_state(self):
files = os.listdir(self._directory_path)
self._files = files
self.update_timer.timeout.connect(self._monitor)
logger.info("Monitor initiated!")
def _monitor(self):
files = os.listdir(self._directory_path)
new_files = sorted(list(map(lambda x: os.path.join(self._directory_path, x),
filter(lambda x: x not in self._files, files))))
if new_files:
logger.info(f"New files detected: {new_files}")
self._mediator.notify(self, new_files)
self._files = files
if not files:
self._files = []
def update_settings(self, data: list[dict]) -> None:
self.stop()
operator_params, system_params = data
self._directory_path = system_params['trace_storage_path']
self._update_time = system_params['monitor_update_period']
self._init_state()
self.start()
def force_all_dir(self):
files = os.listdir(self._directory_path)
self._files = files
all_files = []
for x in self._files:
path = os.path.join(self._directory_path, x)
all_files.append(path)
if all_files:
logger.info(f"Plotting all files: {all_files}")
self._mediator.notify(self, all_files)
else:
logger.info(f"Failed pushing {str(all_files)}")

View File

@ -0,0 +1,544 @@
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

View File

@ -1,68 +0,0 @@
from datetime import datetime as dt
from typing import Optional
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt
from src.utils.base.base import BaseMainWindow, BaseController
from src.gui.settings_window import settingsWindow
class MainWindow(BaseMainWindow):
def __init__(self,
controller: Optional[BaseController] = None):
super().__init__()
self._controller = controller
self.initUI()
self.set_style(self)
self.settings_button.clicked.connect(self._show_settings)
self.operSettings = settingsWindow("params\operator_params.json", 'Operator', self._updater_trigger)
self.sysSettings = settingsWindow("params\system_params.json", 'System', self._updater_trigger)
def initUI(self) -> None:
self.tabWidget = QtWidgets.QTabWidget()
self.tabWidget.setTabsClosable(True)
self.tabWidget.tabCloseRequested.connect(self._close_tab)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.tabWidget)
self.settings_button = QtWidgets.QPushButton("Show settings")
self.settings_button.setFixedWidth(160)
layout.addWidget(self.settings_button)
self.setLayout(layout)
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
for plot_widget in plot_widgets:
tab = QtWidgets.QWidget()
grid = QtWidgets.QGridLayout()
grid.addWidget(plot_widget)
tab.setLayout(grid)
self.tabWidget.addTab(tab, "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S'))
self.tabWidget.setCurrentWidget(tab)
tab_count = self.tabWidget.count()
if tab_count > 10:
for i in range(0, tab_count-2):
self._close_tab(i)
def keyPressEvent(self, a0):
if a0.key() == Qt.Key_F5:
self.clear()
def _show_settings(self):
self.operSettings.show()
self.sysSettings.show()
def push_settings(self) -> None:
self._updater_trigger()
def _updater_trigger(self) -> None:
self.tabWidget.clear()
operator_params = self.operSettings.getParams()
system_params = self.sysSettings.getParams()
self._controller.push_settings([operator_params, system_params])
def _close_tab(self, index:int) -> None:
self.tabWidget.removeTab(index)

185
src/gui/main_gui.py Normal file
View File

@ -0,0 +1,185 @@
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
# ...
# ========================================================

View File

@ -1,210 +1,687 @@
import pandas as pd
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
import copy
import traceback
import sys
from typing import Optional, Tuple, Callable, List, Any, Dict
from dataclasses import dataclass, field
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QGraphicsRectItem, QSpacerItem, QSizePolicy
)
from PyQt5.QtCore import Qt
from loguru import logger
import pyqtgraph as pg
import pandas as pd
import numpy as np
from numpy import floating
from typing import Optional, Any, NamedTuple
from src.utils.base.base import BasePlotWidget
from src.utils.base.base import 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'])
data.append({"time":0, "Posicion FE":X1,"Posicion ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
data.append({"time":self.welding_time, "Posicion FE":X1,"Posicion ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
return pd.DataFrame(data)
def get_ideal_timings(self) -> list[float, float, float, float]:
data = self.Ts
ideal_timings = [data['tclose'], data['tgrow'], self.welding_time, self.getMarkOpen()] # TODO: add data['tmovement'], Oncoming не учитывается в производительности
return ideal_timings
class ProcessStage(NamedTuple):
mean_value: floating[Any]
start_index: int
finish_index: int
from base.base import (
BasePlotWidget, GraphicPassport, PlotItems, PointPassport,
UsefulGraphData, BaseController, PerformanceData
)
from utils.json_tools import read_json
from utils import qt_settings as qts
# =============================================================================
# Класс PlotWidget – построение графических виджетов на основе графических паспортов
# =============================================================================
class PlotWidget(BasePlotWidget):
def _create_curve_ideal(self,
stage: str,
signal: str,
start_timestamp: float,
finish_timestamp: float) -> Optional[pg.PlotDataItem]:
data = self._stage_ideals[stage]
"""
Виджет построения графиков. На основе полученных графических паспортов
создаёт набор виджетов (для отображения нескольких наборов данных),
"""
if start_timestamp and finish_timestamp:
plot = pg.PlotDataItem(x=start_timestamp+data["time"], y=data[signal["name"]], pen=signal["pen"])
return plot
return None
def __init__(self, controller: BaseController):
"""
Инициализирует PlotWidget, читая параметры структуры графиков из JSON.
"""
super().__init__(controller=controller)
self._plt_structures = read_json("params/plot_structure_params.json")
self._plt_channels = None # Будет установлен в зависимости от режима
def build(self, data: List[GraphicPassport]) -> None:
"""
Создает набор виджетов на основе списка графических паспортов.
При ошибке выводится информационная метка об ошибке.
def _create_stage_region(self,
stage: str,
start_timestamp: float,
finish_timestamp: float) -> Optional[pg.LinearRegionItem]:
:param data: Список объектов GraphicPassport с данными для построения графиков.
"""
try:
self._datalen = len(data)
widgets_datapack = []
# Проходим по каждому графическому паспорту и обновляем текущий индекс
for idx, passport in enumerate(data):
self._datastep = idx # Обновляем текущий шаг для обновления статуса
widget = self._build_widget(passport)
widgets_datapack.append(widget)
except (KeyError, ValueError, TypeError) as e:
logger.error(f"Ошибка при сборке данных графика: {e}")
logger.debug(traceback.format_exc())
error_label = QLabel("Произошла ошибка при формировании графиков. Проверьте корректность данных.")
widgets_datapack = [error_label]
except Exception as e:
logger.error(f"Непредвиденная ошибка при формировании графиков: {e}")
logger.debug(traceback.format_exc())
error_label = QLabel("Непредвиденная ошибка при формировании графиков.")
widgets_datapack = [error_label]
finally:
self._mediator.notify(self, widgets_datapack)
if start_timestamp and finish_timestamp:
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
region.setBrush(pg.mkBrush(self._stage_colors[stage]))
return region
return None
def build_raw_trace(self, data: Tuple[pd.DataFrame, dict]) -> None:
"""
Создаёт виджет с одним графиком, отображающим все данные сырой трассировки.
:param data: Кортеж из DataFrame с трассировочными данными и словаря событий.
"""
container_widget, container_layout, pyqt_container = self._generate_widget_container()
plot = self._build_raw_plotitem(data, pyqt_container)
container_layout.addWidget(plot)
container_widget.setProperty("pyqt_container", pyqt_container)
self._mediator.notify(self, [container_widget])
def set_mode(self, mode: int) -> None:
"""
Устанавливает режим построения графиков, выбирая соответствующий набор каналов.
:param mode: Режим работы (1 – Online Path Scanner, 2 – Report Editor, 3 – Client Trace Watcher).
"""
match mode:
case 1:
self._plt_channels = self._plt_structures["Online Path Scanner"]
case 2:
self._plt_channels = self._plt_structures["Report Editor"]
case 3:
self._plt_channels = self._plt_structures["Client Trace Watcher"]
def _build_raw_plotitem(self, data: Tuple[pd.DataFrame, dict],
pyqt_container: PlotItems) -> pg.GraphicsLayoutWidget:
"""
Строит график необработанных трейсов клиента. Добавляет линии для каждого канала и области для событий.
:param data: Кортеж (dataframe, events)
:param pyqt_container: Контейнер для хранения ссылок на QT-объекты графиков.
:return: Виджет GraphicsLayoutWidget с построенным графиком.
"""
plot_item, legend = PlotItemGenerator._init_plot_item("Customer data")
dataframe, events = data
channels = dataframe.columns.tolist()
# Добавляем линии для каждого канала
for i, channel in enumerate(channels):
plot = plot_item.plot(dataframe["time"], dataframe[channel], pen=qts.colors[i], fast=True)
legend.addItem(plot, channel)
pyqt_container.curves.setdefault("real", {})[channel] = plot
# Для каждого события этапа Squeeze добавляем регионы
for i in range(len(events.get("Squeeze", [[], []])[0])):
point_events = {}
for key, item in events.items():
point_events[key] = [item[0][i], item[1][i]]
PlotItemGenerator._add_stage_regions(self._stage_colors, plot_item, point_events, pyqt_container.regions, 20)
plot_layout = pg.GraphicsLayoutWidget()
plot_layout.addItem(plot_item)
return plot_layout
def _build_performance_label(self, performance:PerformanceData, qt_items: Dict) -> QWidget:
"""
Создает QLabel с информацией о производительности (сокращение длительности, идеальное значение, КДИП).
:param qt_items: Словарь для сохранения ссылок на созданные виджеты.
:return: Виджет с меткой производительности.
"""
label_widget = QWidget()
label_layout = QHBoxLayout(label_widget)
start_label = QLabel("Сокращение длительности: ")
real_label = QLabel(f"фактическое = {performance.client_to_TWC} % ")
if performance.client_to_TWC == 0:
real_label.setVisible(False)
ideal_label = QLabel(f"идеальное = {performance.client_to_ideal} % ")
if performance.client_to_ideal == 0:
ideal_label.setVisible(False)
kdip_label = QLabel(f"КДИП = {performance.TWC_to_ideal}% ")
if performance.TWC_to_ideal == 0:
kdip_label.setVisible(False)
label_layout.addWidget(start_label, alignment=Qt.AlignLeft)
label_layout.addWidget(real_label, alignment=Qt.AlignLeft)
label_layout.addWidget(ideal_label, alignment=Qt.AlignLeft)
label_layout.addWidget(kdip_label, alignment=Qt.AlignLeft)
spacer = QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Minimum)
label_layout.addSpacerItem(spacer)
self.set_style(label_widget)
qt_items["performance label"] = label_widget
qt_items["real performance"] = real_label
qt_items["ideal performance"] = ideal_label
qt_items["real to ideal performance"] = kdip_label
return label_widget
def _build_widget(self, graphic_passport: GraphicPassport) -> QWidget:
"""
Собирает графический виджет для одного набора данных.
Генерирует контейнер, создаёт CustomPlotLayout для построения графика,
добавляет, при необходимости, метку производительности.
:param graphic_passport: Объект GraphicPassport с данными для построения графика.
:return: Готовый QWidget для отображения.
"""
container_widget, container_layout, pyqt_container = self._generate_widget_container()
plot_layout = CustomPlotLayout(graphic_passport, len(self._plt_channels), self._stage_colors, self)
plot_layout.build(pyqt_container, self._plt_channels)
perf_widget = self._build_performance_label(graphic_passport.useful_data.performance, pyqt_container.qt_items)
container_layout.addWidget(perf_widget)
container_layout.addWidget(plot_layout)
container_widget.setProperty("pyqt_container", pyqt_container)
return container_widget
@staticmethod
def _generate_widget_container() -> Tuple[QWidget, QVBoxLayout, PlotItems]:
"""
Создает контейнер для виджета графика, его макет и объект PlotItems для хранения ссылок.
:return: Кортеж (контейнер, вертикальный макет, PlotItems).
"""
container_widget = QWidget()
container_layout = QVBoxLayout(container_widget)
pyqt_container = PlotItems({"real": {}, "ideal": {}}, {"real": {}, "ideal": {}}, {})
return container_widget, container_layout, pyqt_container
def _update_status(self, widget_steps: int, point_steps: int, cur_widget: int, cur_point: int) -> None:
"""
Вычисляет текущее значение прогресса и обновляет его через контроллер.
:param widget_steps: Общее число шагов по виджетам.
:param point_steps: Общее число точек в виджете.
:param cur_widget: Текущий номер виджета.
:param cur_point: Текущий номер точки внутри виджета.
"""
if self._datalen:
cycle_progress = self._datastep / self._datalen * 100 + 1
period1 = 99 / self._datalen
else:
cycle_progress = 1
period1 = 100
period2 = period1 / widget_steps if widget_steps != 0 else period1
period3 = period2 / point_steps if point_steps != 0 else period2
progress = int(cycle_progress + period2 * cur_widget + period3 * cur_point)
self._controller.update_progress(progress)
# =============================================================================
# Класс CustomPlotLayout – кастомное размещение графических элементов
# =============================================================================
class CustomPlotLayout(pg.GraphicsLayoutWidget):
"""
Кастомный виджет для размещения графиков, построенных с помощью PlotItemGenerator.
def _get_timestamp(self,
stage: str,
times: pd.Series,
dataframe: pd.DataFrame) -> Optional[list[float]]:
stage_diff = np.diff(dataframe[stage])
start_index = np.where(stage_diff == 1)[0]
finish_index = np.where(stage_diff == -1)[0]
if start_index.size:
start_timestamp = times[start_index[0]]
finish_timestamp = times[finish_index[0]] if finish_index.size else times[len(times) - 1]
return start_timestamp, finish_timestamp
Он формирует графики по каналам, связывает их между собой (XLink)
и добавляет навигатор для общего обзора.
"""
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
super().__init__()
self._plotter = PlotItemGenerator(graphic_passport, widget_steps, colors, parent)
self.setProperty("performance", None)
def build(self, pyqt_container: PlotItems, plt_channels: Dict) -> None:
"""
Строит графики для каждого канала на основе параметров из plt_channels.
Затем добавляет навигатор (NavigatorPlot) для синхронизации обзора.
:param pyqt_container: Контейнер для хранения ссылок на графические объекты.
:param plt_channels: Словарь с описаниями каналов и их настройками.
"""
main_plot = None
for widget_num, (channel, description) in enumerate(plt_channels.items()):
plot_item = self._plotter.generate_plot_item(widget_num, channel, description, pyqt_container)
if widget_num == 0:
main_plot = plot_item
else:
plot_item.setXLink(main_plot)
self.addItem(plot_item, widget_num, 0)
navigator = NavigatorPlot([0, 10], main_plot)
if navigator is not None:
self.addItem(navigator, widget_num + 1, 0)
# =============================================================================
# Класс PlotItemGenerator – генерация элементов графика и добавление регионов
# =============================================================================
class PlotItemGenerator:
"""
Генератор графических элементов (PlotItem) для построения графиков.
На основе графического паспорта создаются линии, регионы для этапов, добавляются
идеальные сигналы и рассчитываются показатели производительности.
"""
def __init__(self, graphic_passport: GraphicPassport, widget_steps: int, colors: Dict, parent: PlotWidget = None) -> None:
self._stage_colors = colors
self._ideal_mode = graphic_passport.dataframe is None
self._parent = parent
if not self._ideal_mode:
dataframe_headers = graphic_passport.dataframe.columns.tolist()
point_steps = len(graphic_passport.points_pocket)
else:
dataframe_headers = []
point_steps = 1
# Используем словарь для более понятной организации данных
self._datapack: Dict[str, Any] = {
"dataframe": graphic_passport.dataframe,
"headers": dataframe_headers,
"useful_data": graphic_passport.useful_data,
"points_pocket": graphic_passport.points_pocket,
"widget_steps": widget_steps,
"point_steps": point_steps
}
def generate_plot_item(self, widget_num: int, channel: str, description: Dict[str, Any],
pyqt_container: PlotItems) -> pg.PlotItem:
"""
Генерирует PlotItem для заданного канала с добавлением регионов, компенсаций и идеальных данных.
:param widget_num: Номер текущего виджета.
:param channel: Имя канала.
:param description: Словарь настроек для данного канала.
:param pyqt_container: Контейнер для хранения ссылок на объекты графиков.
:return: PlotItem
"""
dp = self._datapack
dataframe = dp["dataframe"]
useful_data:UsefulGraphData = dp["useful_data"]
points_pocket = dp["points_pocket"]
widget_steps = dp["widget_steps"]
point_steps = dp["point_steps"]
# Инициализируем PlotItem и легенду
plot_item, legend = self._init_plot_item(title=channel)
settings: Dict = description["Settings"]
global_shift = 0
ideal_df = pd.DataFrame({})
# При необходимости – зеркальное отражение данных для ME
if settings.get("mirror ME", False) and not self._ideal_mode:
dataframe = self._shift_data("ME", description["Real_signals"], dataframe,
lambda x: useful_data.range_ME - x)
# Итерация по точкам паспорта
for cur_point, point_data in enumerate(points_pocket):
ideal_data = copy.deepcopy(point_data.ideal_data)
if self._ideal_mode:
global_shift, point_data.events, point_data.timeframe = self._generate_synthetic_events(global_shift, ideal_data)
else:
if settings.get("force compensation FE", False):
force = point_data.useful_data["force"]
k_hardness = useful_data.k_hardness
signals = description["Real_signals"]
dataframe = self._apply_force_compensation(force, k_hardness, dataframe, point_data.timeframe, signals)
if settings.get("stages", False):
self._add_stage_regions(self._stage_colors, plot_item, point_data.events, pyqt_container.regions, 75)
if settings.get("force accuracy", False):
force = point_data.useful_data["force"]
self._add_force_accuracy_region(point_data.events["Welding"], force, plot_item)
if settings.get("ideals", False) and settings.get("mirror ME", False):
for stage in point_data.events.keys():
ideal_data[stage] = self._shift_data("ME", description["Ideal_signals"], ideal_data[stage],
lambda x: useful_data.range_ME - x)
if settings.get("workpiece", False):
self._add_workpiece(point_data, plot_item)
if settings.get("ideals", False):
self._add_ideal_stage_regions(self._stage_colors, plot_item, ideal_data, point_data.events,
pyqt_container.regions, 100)
ideal_df = self._modify_ideal_df(ideal_df, ideal_data, point_data.events)
# Обновляем статус через родительский PlotWidget
self._parent._update_status(widget_steps, point_steps, widget_num, cur_point)
# Добавляем идеальные сигналы (если указано в настройках)
if settings.get("ideals", False):
self._add_signals(plot_item, ideal_df, description["Ideal_signals"], legend, pyqt_container.curves["ideal"])
# Добавляем реальные сигналы, если не включен режим идеала
if not self._ideal_mode:
self._add_signals(plot_item, dataframe, description["Real_signals"], legend, pyqt_container.curves["real"])
return plot_item
@staticmethod
def _shift_data(valid_str: str, signals: List[Dict], dataframe: pd.DataFrame, func: Callable) -> pd.DataFrame:
"""
Применяет заданную функцию (например, смещение) к столбцам, удовлетворяющим условию.
:param valid_str: Подстрока, по которой определяется, к каким сигналам применять функцию.
:param signals: Список описаний сигналов.
:param dataframe: DataFrame с данными.
:param func: Функция, применяемая к значениям.
:return: Измененный 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(func)
return dataframe
@staticmethod
def _init_plot_item(title: str) -> Tuple[pg.PlotItem, pg.LegendItem]:
"""
Инициализирует PlotItem с заданным заголовком, включает сетку и легенду.
:param title: Заголовок графика.
:return: Кортеж (PlotItem, LegendItem).
"""
plot_item = pg.PlotItem(title=title)
# Подписываемся на изменение диапазона оси X для автоматической даунсэмплинга
plot_item.sigXRangeChanged.connect(lambda: PlotItemGenerator._update_plots_downsample(plot_item))
plot_item.showGrid(x=True, y=True)
plot_item.setClipToView(True)
legend = plot_item.addLegend(offset=(70, 20))
return plot_item, legend
@staticmethod
def _create_stage_region(colors: Dict, stage: str, start_timestamp: float, finish_timestamp: float,
transparency: int) -> Optional[pg.LinearRegionItem]:
"""
Создает регион (LinearRegionItem) для заданного этапа, если заданы границы.
:param colors: Словарь с цветами для этапов.
:param stage: Имя этапа.
:param start_timestamp: Время начала этапа.
:param finish_timestamp: Время окончания этапа.
:param transparency: Значение прозрачности (alpha).
:return: Объект LinearRegionItem или None.
"""
if start_timestamp is not None and finish_timestamp is not None:
region = pg.LinearRegionItem([start_timestamp, finish_timestamp], movable=False)
color = colors.get(stage, [100, 100, 100, 100])
# Устанавливаем кисть с заданной прозрачностью
region.setBrush(pg.mkBrush(color[:3] + [transparency]))
return region
return None
@staticmethod
def _init_plot_widget(title: str) -> tuple[pg.PlotWidget, pg.LegendItem]:
plot_widget = pg.PlotWidget(title=title)
plot_widget.showGrid(x=True, y=True)
legend = pg.LegendItem((80, 60), offset=(70, 20))
legend.setParentItem(plot_widget.graphicsItem())
return plot_widget, legend
def get_stage_info(self,
stage: str,
dataframe: pd.DataFrame,
signal_name: str) -> Optional[ProcessStage]:
if stage in self._stages:
stage_diff = np.diff(dataframe[stage])
start_index = np.where(stage_diff == 1)[0]
finish_index = np.where(stage_diff == -1)[0]
data = dataframe[signal_name] if signal_name in dataframe.columns.tolist() else []
if data.size and start_index.size:
start = start_index[0]
finish = finish_index[0] if finish_index.size else (len(data) - 1)
data_slice = data[start:finish]
mean = np.mean(data_slice)
return ProcessStage(mean_value=mean, start_index=int(start), finish_index=int(finish))
return None
def _add_stage_regions(colors: Dict, plot_item: pg.PlotItem, point_events: Dict, reg_items: Dict, transparency: int = 75) -> None:
"""
Добавляет регионы для реальных этапов на PlotItem.
def _build_widget(self, dataframe: pd.DataFrame) -> QWidget:
widget = QWidget()
layout = QVBoxLayout()
:param colors: Словарь с цветами для этапов.
:param plot_item: PlotItem, на который добавляются регионы.
:param point_events: Словарь событий с временными рамками.
:param reg_items: Словарь для хранения созданных регионов.
:param transparency: Прозрачность регионов.
"""
for stage, times in point_events.items():
start_t, end_t = times
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
if region is not None:
region.setZValue(-20)
plot_item.addItem(region)
reg_items.setdefault("real", {}).setdefault(stage, []).append(region)
time_axis = dataframe["time"]
@staticmethod
def _add_ideal_stage_regions(colors: Dict, plot_item: pg.PlotItem, ideal_data: Dict[str, Any],
point_events: Dict[str, List[float]], reg_items: Dict, transparency: int = 125) -> None:
"""
Добавляет регионы для идеальных этапов.
:param colors: Словарь с цветами.
:param plot_item: PlotItem для добавления регионов.
:param ideal_data: Словарь с идеальными данными, включая "Ideal timings".
:param point_events: События с временными рамками.
:param reg_items: Словарь для хранения созданных регионов.
:param transparency: Прозрачность регионов.
"""
ideal_timings = ideal_data["Ideal timings"]
stages = list(point_events.keys())
for i, stage in enumerate(stages):
start_t = point_events[stage][0]
end_t = start_t + ideal_timings[i]
region = PlotItemGenerator._create_stage_region(colors, stage, start_t, end_t, transparency)
if region:
region.setZValue(-10)
plot_item.addItem(region)
reg_items.setdefault("ideal", {}).setdefault(stage, []).append(region)
@staticmethod
def _modify_ideal_df(ideal_df: pd.DataFrame, ideal_data: Dict[str, Any], point_events: Dict[str, List[float]]) -> pd.DataFrame:
"""
Добавляет идеальные сигналы для каждого этапа к DataFrame.
Если ideal_df не пуст, добавляет разделитель между сигналами.
:param ideal_df: DataFrame с идеальными данными.
:param ideal_data: Словарь с идеальными данными для этапов.
:param point_events: События с временными рамками.
:return: Обновленный DataFrame с идеальными сигналами.
"""
for stage in point_events.keys():
if not ideal_df.empty:
last_time = ideal_df['time'].iloc[-1]
separator_row = {col: np.nan if col != "time" else last_time + 0.01 for col in ideal_df.columns}
separator_df = pd.DataFrame([separator_row])
worker_df = ideal_data[stage].copy(deep=True)
worker_df["time"] = worker_df["time"] + point_events[stage][0]
ideal_df = pd.concat([ideal_df, separator_df, worker_df], ignore_index=True)
else:
ideal_df = ideal_data[stage].copy()
return ideal_df
@staticmethod
def _add_signals(plot_item: pg.PlotItem, dataframe: pd.DataFrame, real_signals: List[Dict[str, Any]],
legend: pg.LegendItem, curve_items: Dict) -> None:
"""
Добавляет сигналы из DataFrame на PlotItem.
:param plot_item: PlotItem для добавления сигналов.
:param dataframe: DataFrame с данными.
:param real_signals: Список описаний сигналов, содержащих имя, ручку (pen) и т.д.
:param legend: Легенда для PlotItem.
:param curve_items: Словарь для хранения созданных объектов графиков.
"""
dataframe_headers = dataframe.columns.tolist()
for signal in real_signals:
if signal["name"] in dataframe_headers:
plot = plot_item.plot(dataframe["time"], dataframe[signal["name"]], pen=signal["pen"], fast=True)
plot.setZValue(0)
legend.addItem(plot, signal["name"])
curve_items.setdefault(signal["name"], {}) # гарантируем, что ключ существует
curve_items[signal["name"]] = plot
for channel, description in self._plt_channels.items():
plot_widget, legend = self._init_plot_widget(title=channel)
@staticmethod
def _update_plots_downsample(plot_item: pg.PlotItem):
"""
Настраивает даунсэмплинг данных на PlotItem в зависимости от видимого диапазона по оси X.
:param plot_item: PlotItem, для которого устанавливается даунсэмплинг.
"""
visible_range = plot_item.getViewBox().viewRange()[0]
diapason = visible_range[1] - visible_range[0]
if diapason >= 30:
plot_item.setDownsampling(ds=50, auto=True, mode='peak')
elif diapason >= 10:
plot_item.setDownsampling(ds=20, auto=True, mode='peak')
elif diapason >= 4:
plot_item.setDownsampling(ds=10, auto=True, mode='peak')
else:
plot_item.setDownsampling(ds=1, auto=True, mode='peak')
settings = description["Settings"]
if settings["stages"] and all([stage in dataframe_headers for stage in self._stages]):
for stage in self._stages:
start_timestamp, finish_timestamp = self._get_timestamp(stage, time_axis, dataframe)
region = self._create_stage_region(stage, start_timestamp, finish_timestamp)
if region:
plot_widget.addItem(region)
for signal in description["Ideal_signals"]:
ideal_plot = self._create_curve_ideal(stage, signal, start_timestamp, finish_timestamp)
if ideal_plot:
plot_widget.addItem(ideal_plot)
@staticmethod
def _add_force_accuracy_region(event: List[float], force: float, plot_item: pg.PlotItem) -> None:
"""
Добавляет прямоугольную область для отображения точности силы на PlotItem.
:param event: Список [начало, конец] события.
:param force: Значение силы.
:param plot_item: PlotItem для добавления прямоугольника.
"""
modifier = 0.05
x1 = event[0]
dx = event[1] - x1
y1 = force * (1 - modifier)
dy = force * (2 * modifier)
end_timestamp = time_axis[len(time_axis) - 1]
region = self._create_stage_region("Oncoming", finish_timestamp, end_timestamp)
if region:
plot_widget.addItem(region)
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
rect_item.setZValue(-5)
rect_item.setBrush(pg.mkBrush((0, 255, 0, 50)))
rect_item.setPen(pg.mkPen('black', width=0))
plot_item.addItem(rect_item)
for signal in description["Ideal_signals"]:
ideal_plot = self._create_curve_ideal("Oncoming", signal, finish_timestamp, end_timestamp)
if ideal_plot:
plot_widget.addItem(ideal_plot)
@staticmethod
def _add_workpiece(point_data: PointPassport, plot_item: pg.PlotItem) -> None:
"""
Добавляет область, обозначающую заготовку, на PlotItem.
:param point_data: Объект PointPassport с данными точки.
:param plot_item: PlotItem для добавления области.
"""
x1 = point_data.events["Closing"][0]
dx = point_data.events["Relief"][1] - x1
y1 = point_data.useful_data["part_pos"] * 1000
dy = point_data.useful_data["thickness"] * 1000
if settings["performance"] and all([stage in dataframe_headers for stage in self._stages]):
delta_timestamp = 0
for stage in self._stages:
start_timestamp, finish_timestamp = self._get_timestamp(stage, time_axis, dataframe)
delta_timestamp += finish_timestamp - start_timestamp
ideal_delta = self._opt.get_cycle_time()
performance = round(ideal_delta/delta_timestamp*100, 2)
performance_label = QLabel(f"Performance = {performance} %")
layout.addWidget(performance_label)
if settings["zoom"]:
if max(time_axis) < 5.0:
stages = [self.get_stage_info("Welding",
dataframe,
signal["name"]) for signal in description["Real_signals"]]
if stages:
means_raw = [stage.mean_value for stage in stages]
mean = max(means_raw)
start = time_axis[stages[0].start_index]
finish = time_axis[stages[0].finish_index]
overshoot = pg.BarGraphItem(x0=0,
y0=mean - mean * 0.05,
height=mean * 0.05 * 2,
width=start,
brush=pg.mkBrush([0, 250, 0, 100]))
plot_widget.addItem(overshoot)
stable = pg.BarGraphItem(x0=start,
y0=mean - mean * 0.015,
height=mean * 0.015 * 2,
width=finish - start,
brush=pg.mkBrush([0, 250, 0, 100]))
plot_widget.addItem(stable)
plot_widget.setYRange(mean - 260, mean + 260)
plot_widget.setInteractive(False)
else:
max_value = min([max(dataframe[signal["name"]]) for signal in description["Real_signals"]])
region = pg.LinearRegionItem([max_value - max_value * 0.015,
max_value + max_value * 0.015],
movable=False,
orientation="horizontal")
region.setBrush(pg.mkBrush([0, 250, 0, 100]))
plot_widget.setYRange(max_value - 200, max_value + 200)
plot_widget.setXRange(3.5, 4.5)
plot_widget.addItem(region)
plot_widget.setInteractive(False)
for signal in description["Real_signals"]:
if signal["name"] in dataframe_headers:
plot = plot_widget.plot(time_axis, dataframe[signal["name"]], pen=signal["pen"])
legend.addItem(plot, signal["name"])
layout.addWidget(plot_widget)
widget.setLayout(layout)
return widget
def build(self, data: list[pd.DataFrame]) -> None:
widgets = [self._build_widget(data_sample) for data_sample in data]
self._mediator.notify(self, widgets)
def update_settings(self, data: list[dict]):
self._initIdealBuilder(idealDataBuilder=idealDataBuilder, data=data)
rect_item = QGraphicsRectItem(x1, y1, dx, dy)
rect_item.setZValue(-5)
rect_item.setBrush(pg.mkBrush('grey'))
rect_item.setPen(pg.mkPen('black', width=3))
plot_item.addItem(rect_item)
@staticmethod
def _generate_synthetic_events(global_shift:float, ideal_data: Dict) -> Tuple[float, Dict, List[float]]:
"""
Генерирует синтетические события для случая, когда данные отсутствуют.
:param global_shift: Смещение точки по линии времени относительно нуля.
:param ideal_data: Словарь с идеальными данными, содержащий "Ideal cycle" и "Ideal timings".
:return: Кортеж (обновленные global_shift, сгенерированные события, временной интервал точки).
"""
point_timeframe = [global_shift, global_shift + ideal_data["Ideal cycle"]]
point_events = {}
keys = list(ideal_data.keys())
shift = 0
for i, time in enumerate(ideal_data["Ideal timings"]):
point_events[keys[i]] = [global_shift + shift, global_shift + time + shift]
shift += time
global_shift += ideal_data["Ideal cycle"]
return global_shift, point_events, point_timeframe
@staticmethod
def _apply_force_compensation(force: float, k_hardness: float, dataframe: pd.DataFrame,
point_timeframe: List[float], real_signals: List[Dict]) -> pd.DataFrame:
"""
Применяет компенсацию силы к данным в заданном интервале времени.
:param force: Значение силы.
:param k_hardness: Коэффициент твердости.
:param dataframe: DataFrame с данными.
:param point_timeframe: Временной интервал, для которого производится компенсация.
:param real_signals: Список описаний реальных сигналов.
:return: Измененный DataFrame с примененной компенсацией.
"""
F_comp = -force / k_hardness
point_idxs = dataframe[(dataframe["time"] >= point_timeframe[0]) & (dataframe["time"] <= point_timeframe[1])].index
dataframe.loc[point_idxs] = PlotItemGenerator._shift_data("FE", real_signals, dataframe.loc[point_idxs],
lambda x: x + F_comp)
return dataframe
@staticmethod
def _parent_ideal_mode() -> bool:
"""
Вспомогательный метод-заглушка для определения режима идеальных данных.
(При необходимости можно реализовать проверку из родительского класса)
"""
# Здесь можно вернуть значение из родительского контекста, если требуется
return False
# =============================================================================
# Класс NavigatorPlot – навигатор для графика, позволяющий управлять областью просмотра
# =============================================================================
class NavigatorPlot(pg.PlotItem):
"""
График-навигатор, отображающий уменьшенную копию данных основного графика,
позволяющий синхронизировать область просмотра основного графика с регионом навигатора.
"""
def __init__(self, time_region: Tuple[float, float], main_plot: pg.PlotItem):
"""
Инициализирует NavigatorPlot, синхронизируя его с основным графиком.
:param time_region: Временной интервал, который должен быть выделен.
:param main_plot: Основной PlotItem, с которым будет синхронизация.
"""
super().__init__()
self._init_navigator(time_region, main_plot)
self._init_syncranisation(main_plot)
@staticmethod
def _sync_main_plot_with_navigator(main_plot: pg.PlotItem, region: pg.LinearRegionItem) -> None:
"""
Синхронизирует область просмотра основного графика с регионом навигатора.
:param main_plot: Основной график.
:param region: Регион навигатора.
"""
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)
@staticmethod
def _sync_navigator_with_main(main_plot: pg.PlotItem, region: pg.LinearRegionItem):
"""
Синхронизирует регион навигатора с текущей областью просмотра основного графика.
:param main_plot: Основной график.
:param region: Регион навигатора.
"""
if region:
x_min, x_max = main_plot.getViewBox().viewRange()[0]
region.blockSignals(True)
region.setRegion([x_min, x_max])
region.blockSignals(False)
def _init_syncranisation(self, main_plot: pg.PlotItem) -> None:
"""
Настраивает синхронизацию между навигатором и основным графиком.
"""
self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
self.ROI_region.sigRegionChanged.connect(
lambda: self._sync_main_plot_with_navigator(main_plot, self.ROI_region)
)
main_plot.sigXRangeChanged.connect(
lambda: self._sync_navigator_with_main(main_plot, self.ROI_region)
)
def _init_navigator(self, time_region: Tuple[float, float], main_plot: pg.PlotItem) -> None:
"""
Создает график-навигатор, отображающий уменьшенную копию данных.
:param time_region: Временной интервал, который необходимо выделить.
:param main_plot: Основной график, из которого будут извлекаться данные.
"""
self.setTitle("Navigator")
self.setFixedHeight(100)
# Извлекаем данные из каждого элемента основного графика и даунсэмплируем их
for curve in main_plot.listDataItems():
x, y = curve.getData()
curve_name = curve.opts.get("name", None)
signal_pen = curve.opts.get("pen", None)
plot = self.plot(x, y, pen=signal_pen, name=curve_name)
#plot.setDownsampling(ds = 10, auto = True, method = 'peak')
# Создаем регион для выделения области
self.ROI_region = pg.LinearRegionItem(
movable=True,
brush=pg.mkBrush(0, 0, 255, 100),
pen=pg.mkPen(width=4)
)
max_bound = x[-1] if x.any() else 50
self.ROI_region.setBounds([0, max_bound])
self.ROI_region.setRegion(time_region)
self.addItem(self.ROI_region)
self.getViewBox().setLimits(xMin=0, xMax=max_bound)

View File

@ -1,270 +0,0 @@
white_style = """
QMainWindow {
background-color: #ffffff;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QMessageBox {
background-color: #ffffff;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QPushButton {
background-color: #d3d3d3;
color: #000000;
padding: 10px 20px;
border: none;
border-radius: 4px;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QPushButton:hover:!disabled {
background-color: #b0b0b0;
}
QPushButton:disabled {
background-color: #a9a9a9;
color: #7f7f7f;
}
QCheckBox {
color: #000000;
font-size: 14px;
font-family: "Segoe UI", sans-serif;
}
QLineEdit {
background-color: #f0f0f0;
color: #000000;
padding: 5px;
border: 1px solid #a9a9a9;
border-radius: 4px;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QLabel {
color: #000000;
font-size: 16px;
font-family: "Segoe UI", sans-serif;
}
QGroupBox {
color: #000000;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QRadioButton {
color: #000000;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
"""
dark_style = """
QWidget {
background-color: #0D1117; /* Тёмный, современный цвет для фона */
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QMessageBox {
background-color: #161B22;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QPushButton {
background-color: #FFCC00; /* Яркий жёлтый цвет для акцента */
color: #0D1117; /* Темный цвет текста для контраста с желтым */
padding: 12px 25px;
border: 2px solid #E6B800;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover:!disabled {
background-color: #FFD700; /* Светлый желтый цвет для эффекта наведения */
}
QPushButton:disabled {
background-color: #555555;
color: #cccccc;
border: none;
}
QCheckBox {
color: #ffffff;
font-size: 14px;
font-family: "Segoe UI", sans-serif;
font-weight: bold;
}
QLineEdit {
background-color: #21262D;
color: #ffffff;
padding: 8px;
border: 2px solid #30363D;
border-radius: 6px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QLabel {
color: #ffffff;
font-size: 16px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}
QGroupBox {
color: #ffffff;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
border: 1px solid #30363D;
border-radius: 6px;
margin-top: 10px;
}
QRadioButton {
color: #ffffff;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QSpinBox {
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QDoubleSpinBox {
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
"""
dis_robots = """
QPushButton {
background-color: #555555;
color: #cccccc;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
}
QWidget#robot {
border: none;
padding: 5px;
background-color: rgba(33, 33, 33, 100);
}
"""
selected_robot = """
QWidget#robot {
border: 2px solid #E6B800;
border-radius: 10px;
padding: 5px;
}
"""
en_button = """
QPushButton {
background-color: #FFCC00;
color: #0D1117;
padding: 12px 25px;
border: 2px solid #E6B800;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover:!disabled {
background-color: #FFD700;
}
"""
dis_button = """
QPushButton {
background-color: #555555;
color: #cccccc;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
}
"""
saved_button = """
QPushButton {
background-color: #28a745;
color: #ffffff;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
"""
start_button = """
QPushButton {
background-color: #FFCC00;
color: #0D1117;
padding: 20px 40px;
border: 2px solid #E6B800;
border-radius: 10px;
font-family: "Segoe UI", sans-serif;
font-size: 24px;
font-weight: bold;
background-image: linear-gradient(to bottom, #FFD700, #FFCC00);
}
QPushButton:hover:!disabled {
background-color: #FFD700;
}
QPushButton:pressed {
background-color: #E6B800;
padding-top: 22px;
padding-bottom: 18px;
}
QPushButton:disabled {
background-color: #555555;
color: #cccccc;
border: none;
}
"""
BigSuccessLabel = """
QLabel {
color: #ffffff;
font-size: 26px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}"""
colors = [
'#FF6F61', # яркий коралловый
'#6B5B95', # приглушенный фиолетовый
'#88B04B', # яркий зеленый
'#F7CAC9', # светлый розовый
'#92A8D1', # светло-голубой
'#955251', # теплый терракотовый
'#B565A7', # лавандовый
'#009B77', # глубокий бирюзовый
'#DD4124', # ярко-красный
'#45B8AC', # мягкий мятный
"#00FF00",
"#00FF00",
"#00FF00",
"#00FF00",
"#00FF00",
"#00FF00",
"#00FF00",
"#00FF00",
]
RGBA = [(255, 255, 0),
(32, 178, 70),
(255, 69, 0),
(123, 104, 238),
(99, 42, 15)
]

182
src/gui/report_gui.py Normal file
View File

@ -0,0 +1,182 @@
import traceback
import sys
from typing import Union
import pyqtgraph as pg
from pyqtgraph.parametertree import Parameter, ParameterTree
from PyQt5 import QtWidgets
from loguru import logger
class ReportSettings(QtWidgets.QWidget):
def __init__(self, parent = None):
super().__init__(parent)
def build(self, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
try:
self._clear()
param_tree = ParameterTree()
layout = self.layout()
layout.addWidget(param_tree)
body= [
ReportSettings._generate_reg_params(reg_items),
ReportSettings._generate_curve_params(curve_items),
ReportSettings._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)
)
param_tree.setParameters(params, showTop=False)
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)
def _clear(self) -> None:
"""
Приводит виджет в готовое к работе состояние.
Удаляет все содержимое, если имеется
"""
main = self.layout()
if self.layout() is not None:
while main.count():
child = main.takeAt(0)
if child.widget() is not None:
child.widget().deleteLater()
else:
self.setLayout(QtWidgets.QVBoxLayout())
@staticmethod
def _generate_qt_params(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()
]}
return res
@staticmethod
def _create_qt_samples(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:
"""Созадет реальные и идеальные секторы"""
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"])},
]}
return res
@staticmethod
def _generate_curve_params(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"])},
]}
return res
@staticmethod
def _create_samples(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': ReportSettings._create_settings(sample)}
res.append(param)
return res
@staticmethod
def _create_settings(item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
"""Получает настройки для элемента"""
if type(item) == pg.LinearRegionItem:
pen = item.lines[0].pen
brush = item.brush
fill_color = brush.color().getRgb()
else:
pen = pg.mkPen(item.opts.get("pen"))
fill_color = None
line_color = pen.color().getRgb()
line_thickness = pen.width()
visibility = item.isVisible()
return [
{'name': 'Line color', 'type': 'color', 'value': line_color},
{'name': 'Line thickness', 'type': 'int', 'value': line_thickness, 'limits': (1, 10)},
{'name': 'Visibility', 'type': 'bool', 'value': visibility},
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
]
@staticmethod
def _update_settings(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")
real_plots = params.child("Plots").child("Real plots")
ideal_plots = params.child("Plots").child("Ideal plots")
qt_settings = params.child("Qt elements")
ReportSettings._set_sector_settings(reg_items["real"], real_sectors)
ReportSettings._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)
@staticmethod
def _set_sector_settings(sectors: dict,
settings: Parameter) -> None:
"""Задает параметры секторов в соответствии с настройками"""
for key, item in sectors.items():
sample = settings.child(key)
line_color = sample.child("Line color").value()
line_width = sample.child("Line thickness").value()
visibility = sample.child("Visibility").value()
fill_color = sample.child("Fill color").value()
pen = pg.mkPen(color=line_color, width=line_width)
brush=pg.mkBrush(fill_color)
for reg in item:
reg.setVisible(visibility)
reg.lines[0].setPen(pen)
reg.lines[1].setPen(pen)
reg.setBrush(brush)
@staticmethod
def _set_plot_settings(curves: dict,
settings: Parameter) -> None:
"""Задает параметры кривых в соответствии с настройками"""
for key, item in curves.items():
sample = settings.child(key)
line_color = sample.child("Line color").value()
line_width = sample.child("Line thickness").value()
visibility = sample.child("Visibility").value()
pen = pg.mkPen(color=line_color, width=line_width)
if type(item) == list:
for curve in item:
curve.setVisible(visibility)
curve.setPen(pen)
else:
item.setVisible(visibility)
item.setPen(pen)
@staticmethod
def _set_qt_settings(qt_items: dict,
settings: Parameter) -> None:
"""Задает параметры Qt элементов в соответствии с настройками"""
for key, item in qt_items.items():
sample = settings.child(key)
visibility = sample.child("Visibility").value()
item.setVisible(visibility)

View File

@ -1,59 +1,432 @@
import pyqtgraph as pg
from pyqtgraph.Qt import QtWidgets
from pyqtgraph.parametertree import Parameter, ParameterTree
from typing import Callable, Optional, Any
from PyQt5.QtWidgets import (
QWidget, QPushButton, QLineEdit, QHBoxLayout, QVBoxLayout, QLabel,
QTableWidget, QTableWidgetItem, QStyledItemDelegate, QRadioButton, QGroupBox, QDoubleSpinBox
)
from PyQt5.QtGui import QIntValidator, QDoubleValidator
from utils.json_tools import read_json, write_json
from gui import qt_settings as qts
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):
super().__init__()
self._settings_path = 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
class settingsWindow(QtWidgets.QWidget):
def __init__(self, path: str, name: str, upd_func):
super(settingsWindow, self).__init__()
self.settingsPath = path
self.name = name
self.data = {}
self.params = None
self.load_settings()
self._init_ui()
self.params.sigTreeStateChanged.connect(upd_func)
self.load_settings()
self._populate_table()
def load_settings(self) -> None:
self.data = read_json(self.settingsPath)
"""Загружает настройки из JSON-файла."""
data = read_json(self._settings_path)
if isinstance(data, dict):
self._data = data
else:
self._data = {}
def write_settings(self) -> None:
self.getParams()
write_json(self.settingsPath, self.data)
"""Записывает текущие настройки в JSON-файл."""
write_json(self._settings_path, self._data)
def _getTreeStructure(self) -> list:
params = []
for key, value in self.data.items():
params.append({'name': str(key), 'type': type(value).__name__, 'value': value})
params.append({'name': 'Save', 'type': 'action'})
return params
def get_params(self) -> dict:
"""Возвращает текущий словарь параметров."""
return self._data
def _init_ui(self) -> None:
"""Инициализирует пользовательский интерфейс: кнопки, поля ввода, таблицу."""
# Кнопки управления
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())
control_layout = QHBoxLayout()
control_layout.addWidget(save_button)
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()
# Основной вертикальный макет
layout = QVBoxLayout()
header = QLabel(self._name)
layout.addWidget(header)
layout.addLayout(control_layout)
layout.addWidget(self._param_table)
self.setLayout(layout)
self.setStyleSheet(qts.dark_style)
def _populate_table(self) -> None:
"""
Заполняет таблицу значениями из self._data.
Если данных нет, таблица очищается. Для каждого параметра устанавливается
делегат в зависимости от типа данных (int, float, str).
"""
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)
self._param_table.setColumnCount(0)
return
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()]
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):
self._param_table.setItemDelegateForRow(i, int_delegate)
elif isinstance(items[-1], float):
self._param_table.setItemDelegateForRow(i, float_delegate)
else:
self._param_table.setItemDelegateForRow(i, str_delegate)
def _save_data(self) -> None:
"""
Сохраняет текущие параметры из таблицы в self._data,
записывает их в JSON-файл и вызывает функцию обновления.
"""
new_data = {}
col_count = self._param_table.columnCount()
for i, key in enumerate(self._data.keys()):
row_data = []
for j in range(col_count):
cell_item = self._param_table.item(i, j)
if cell_item is None:
continue
param_str = cell_item.text()
# Для ключа 'trace_storage_path' оставляем строковое значение
if key != "trace_storage_path":
try:
param = float(param_str)
except ValueError:
param = 0.0
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
num_points_text = self._num_points.text()
if not num_points_text.isdigit():
return
num_points = int(num_points_text)
if num_points < 0:
return
prev_columns = self._param_table.columnCount()
desired_columns = num_points
if desired_columns <= 0:
return
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))
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)
class SystemSettings(SettingsWindow):
"""
Настройки системы.
Окно для редактирования параметров, связанных с работой системы.
"""
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
associated_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",
"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",
"torque_max_1": "Max torque FE, N*m",
"torque_max_2": "Max torque ME, N*m",
"transmission_ratio_1": "Transmission ratio FE",
"transmission_ratio_2": "Transmission ratio ME",
"contact_distance_1": "Contact distance FE, m",
"contact_distance_2": "Contact distance ME, m",
"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)
self._num_points.setVisible(False)
def _init_ui(self) -> None:
temp = self._getTreeStructure()
self.params = Parameter.create(name=self.name, type='group', children=temp)
self.params.param('Save').sigActivated.connect(self.write_settings)
ParamsTree = ParameterTree()
ParamsTree.setParameters(self.params, showTop=True)
layout = QtWidgets.QGridLayout()
layout.addWidget(ParamsTree, 0,0)
self.setLayout(layout)
self.setStyleSheet(qts.white_style)
# self.show()
super()._init_ui()
def getParams(self) -> dict:
self.data = {}
for p in self.params:
if p.name() != 'Save':
self.data[p.name()] = p.value()
return self.data
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:
# Для системных настроек расширение столбцов не требуется.
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",
"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_l_1": "Max oncoming dist FE, m",
"distance_l_2": "Max oncoming dist ME, m",
"distance_h_end1": "Oncoming end dist FE, m",
"distance_h_end2": "Oncoming end dist FE, m",
"time_wielding": "Time of welding, sec",
"time_command": "Communication time compensator, sec",
"time_robot_movement": "Rob movement time, sec",
"object_thickness": "Workpiece thickness, m",
"force_target": "Target force, N",
"force_capture": "Capture force, N",
"Tesla closing": "Client closing time, sec",
"Tesla squeeze": "Client squeeze time, sec",
"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)
if __name__ == '__main__':
app = pg.mkQApp("Parameter Tree Example")
window = settingsWindow('params\operator_params.json', 'operator')
app.exec()
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
class ValidatorDelegate(QStyledItemDelegate):
"""
Валидация для ввода в ячейках таблицы.
В зависимости от типа данных ('int', 'float' или 'str') устанавливает соответствующий валидатор.
"""
def __init__(self, data_type='str', parent=None):
super().__init__(parent)
self.data_type = data_type
if self.data_type == 'int':
self.validator = QIntValidator()
elif self.data_type == 'float':
self.validator = QDoubleValidator()
self.validator.setNotation(QDoubleValidator.StandardNotation)
else:
self.validator = None
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
if self.validator:
editor.setValidator(self.validator)
return editor
if __name__ == '__main__':
import pyqtgraph as pg
# Для демонстрации создаем окно настроек с фиктивной функцией обновления и пустым словарем имен
app = pg.mkQApp("Parameter Tree Example")
window = SettingsWindow('params/operator_params.json', 'operator', lambda: None, {})
window.show()
app.exec()

459
src/gui/start_widget.py Normal file
View File

@ -0,0 +1,459 @@
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,28 +1,43 @@
import sys
import pyqtgraph as pg
from PyQt5 import QtWidgets
from src.gui.mainGui import MainWindow
from src.controller.monitor import DirectoryMonitor
from src.controller.mediator import Mediator
from src.controller.converter import DataConverter
from src.gui.plotter import PlotWidget
from src.controller.controller import Controller
from gui.main_gui import MainWindow
from controller.file_manager import DirectoryMonitor, FileManager
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
def main():
pg.setConfigOptions(useOpenGL=False, antialias=False)
app = QtWidgets.QApplication(sys.argv)
monitor = DirectoryMonitor()
file_manager = FileManager(monitor=monitor)
data_converter = DataConverter()
plot_widget_builder = PlotWidget()
controller = Controller()
window = MainWindow(controller)
mediator = Mediator(monitor, data_converter, plot_widget_builder, controller)
controller = Controller(file_manager=file_manager)
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.show()
controller.signal_widgets.connect(window.show_plot_tabs)
controller.signal_settings.connect(mediator.push_settings)
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)
window.push_settings()
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)
sys.exit(app.exec_())

View File

@ -0,0 +1,119 @@
from typing import Optional, Tuple
import numpy as np
import pandas as pd
#TODO: Реализовать поиск времени простоя
class DowntimeAnalyzer:
def __init__(self) -> None:
...
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)
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__":
path = "D:\downloads\Test2\TeslaTIME29_71_KRCIO.dat"

627
src/performance/roboter.py Normal file
View File

@ -0,0 +1,627 @@
from __future__ import annotations
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.
"""
def parse(self, head_filepath: str) -> pd.DataFrame:
"""
Основной метод парсинга. Читает заголовочный файл и файл данных.
: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)
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 # Текущее название канала
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
def _parse_header_line(self, line: str, data_head: KukaDataHead) -> None:
"""
Обрабатывает отдельную строку заголовочного файла и обновляет объект data_head.
: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))
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:
data = file.read()
numbers = np.frombuffer(data, dtype='<d')
return numbers
class KukaTextParser(BaseKukaTextParser):
"""
Класс для парсинга текстовых данных Кука.
Извлекает сообщения из файла.
"""
def __init__(self):
super().__init__()
self._in_message = False
self._data_packs: List[KukaTXT] = []
def parse(self, filepath: str) -> List[KukaTXT]:
"""
Парсит текстовый файл и возвращает список объектов KukaTXT.
: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 _process_message_flow(self, line: str, current_pack: KukaTXT) -> KukaTXT:
"""
Обрабатывает поток сообщений, определяя начало и конец блока.
: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
class TraceStageDetector(BaseTraceStageDetector):
"""
Класс для детекции этапов (стадий) на основе данных трассировки.
Определяет переходы между различными стадиями процесса по пороговым значениям.
"""
def __init__(self, parent: TraceProcessor = None):
super().__init__(parent)
@staticmethod
def is_closing(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 actual_velocity > thresholds['act_vel_min'] and abs(robot_velocity) < thresholds['rob_vel_thresh'] and actual_force < thresholds['act_force_close']
@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
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):
"""
Основной класс для обработки трассировок.
Объединяет данные, детектирует стадии и инициирует рендеринг через медиатор.
"""
def __init__(self):
self._settings = Settings()
data_parser = KukaDataParser()
text_parser = KukaTextParser()
trace_detector = TraceStageDetector(self)
text_detector = TextStageDetector(self)
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
"""

66
src/testAlgo.py Normal file
View File

@ -0,0 +1,66 @@
# 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)
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:
non_array_operator_params[key] = value[0]
else:
non_array_operator_params[key] = value
non_array_system_params = {}
for key, value in system_params.items():
if hasattr(value, "__len__"):
if len(value) > i:
non_array_system_params[key] = value[i]
else:
non_array_system_params[key] = value[0]
else:
non_array_system_params[key] = value
opt = OptAlgorithm(non_array_operator_params, non_array_system_params)
Xs = array([opt.getVar("X1", t) for t in ts])
plt.plot(ts, Xs)
plt.show()

View File

@ -2,6 +2,9 @@ from 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
@ -11,7 +14,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]
@ -21,8 +24,17 @@ class UMLCreator:
for item in items:
real_data.append([item[0]*self.scaler+0.0001, str(key) + '#green'])
real_data.append([item[1]*self.scaler, '{-}'])
if key == 'compression':
# 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:
@ -30,10 +42,16 @@ 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:
else:
# TODO: real_data уже определена как [] в самом начале. Если не попадаем в верхнее условие,
# то она таковой и останется. Дублирование.
real_data = []
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот
# набор данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
ideal_data = [
[0.0, 'closure #yellow'],
[self._ideal_time[0] * self.scaler, '{-}'],
@ -45,7 +63,12 @@ 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, '{-}'],
@ -57,7 +80,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:
@ -70,6 +93,8 @@ 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]])
@ -98,7 +123,9 @@ 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,8 +1,10 @@
from plantuml import PlantUML
from os.path import abspath
from plantuml import PlantUML
class Request:
# TODO: Имя класса не соответствует тому, что он делает.
def __init__(self, server_url: str):
self._server_url = server_url
@ -13,12 +15,15 @@ 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 = []
@ -27,28 +32,36 @@ 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')
@ -56,6 +69,7 @@ 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}')
@ -63,11 +77,13 @@ 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 {')
@ -80,9 +96,13 @@ 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()
@ -91,6 +111,8 @@ 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)

View File

@ -1,338 +0,0 @@
from __future__ import annotations
import os
from typing import Optional, Union
import pandas as pd
from PyQt5.QtCore import QThread, QObject, QTimer
from PyQt5.QtWidgets import QWidget, QTabWidget
from src.OptAlgorithm import OptAlgorithm
import pandas as pd
class BaseMediator:
def __init__(self,
monitor: BaseDirectoryMonitor,
converter: BaseDataConverter,
plot: BasePlotWidget,
controller: BaseController):
self._monitor = monitor
self._monitor.mediator = self
self._converter = converter
self._converter.mediator = self
self._plot = plot
self._plot.mediator = self
self._controller = controller
self._controller.mediator = self
def notify(self,
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePlotWidget, BaseMainWindow],
data: Union[list[str], list[pd.DataFrame], list[QWidget], list[dict]]):
...
def push_settings (self, data: list[dict]):
...
class BaseDirectoryMonitor:
update_timer = QTimer()
def __init__(self,
mediator: Optional[BaseMediator] = None):
super().__init__()
self._directory_path = None
self._update_time = None
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.update_timer.start(self._update_time)
def stop(self):
self.update_timer.stop()
def update_settings(self, data: list[dict]) -> 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._stages = [
"Closing",
"Squeeze",
"Welding",
"Relief"
]
self._stage_colors = {
"Closing": [208, 28, 31, 100],
"Squeeze": [45, 51, 89, 150],
"Welding": [247, 183, 24, 100],
"Relief": [0, 134, 88, 100],
"Oncoming": [222, 184, 135, 100]
}
self._plt_channels = {
"Electrode Force, N & Welding Current, kA": {
"Settings": {
"zoom": False,
"stages": True,
"performance": True
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": 'r',
},
{
"name": "Electrode Force, N FE",
"pen": 'w',
},
{
"name": "Welding Current ME",
"pen": "y",
}
],
"Ideal_signals": [
{
"name": "Force",
"pen": {'color': 'g', 'width':3},
}
]
},
"Electrode Force, N": {
"Settings": {
"zoom": True,
"stages": False,
"performance": False
},
"Real_signals": [
{
"name": "Electrode Force, N ME",
"pen": 'r',
},
{
"name": "Electrode Force, N FE",
"pen": 'w',
}
],
"Ideal_signals": [
{
"name": "Force",
"pen": {'color': 'r', 'width':3},
}
]
},
"Electrode Speed, mm/s": {
"Settings": {
"zoom": False,
"stages": True,
"performance": 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': 'y', 'width':3},
"zoom": False
},
{
"name": "Rotor Speed FE",
"pen": {'color': 'g', 'width':3},
"zoom": False
}
]
},
}
def _initIdealBuilder(self,
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
data: list[dict] = None):
self.opt = idealDataBuilder(data)
self._stage_ideals = {
"Closing": self._opt.get_closingDF(),
"Squeeze": self._opt.get_compressionDF(),
"Welding": self._opt.get_weldingDF(),
"Relief": self._opt.get_openingDF(),
"Oncoming": self._opt.get_oncomingDF()
}
@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]:
...
def update_settings(self, data: list[dict]) -> None:
...
class BaseController(QObject):
def send_widgets(self, widgets: list[QWidget]) -> None:
...
def push_settings(self, settings: list[dict]) -> None:
...
# FIXME: WeldingDF показывает только 1 секунду
class BaseIdealDataBuilder(OptAlgorithm):
def __init__(self, data: list[dict]):
operator_params, system_params = data
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)):
time = i/self.mul
X1, X2, V1, V2, F = func(time)
data.append({"time":time, "Posicion FE":X1*1000,"Posicion ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
return pd.DataFrame(data)
def get_closingDF(self) -> pd.DataFrame:
...
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(QWidget):
def __init__(self,
controller: Optional[BaseController] = None):
super().__init__()
self._controller = controller
...
@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]) -> None:
object.setStyleSheet("""
QWidget {
background-color: #0D1117;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QMessageBox {
background-color: #161B22;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
}
QPushButton {
background-color: #FFCC00;
color: #0D1117;
padding: 12px 25px;
border: 2px solid #E6B800;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover:!disabled {
background-color: #FFD700;
}
QPushButton:disabled {
background-color: #555555;
color: #cccccc;
border: none;
}
QLabel {
color: #ffffff;
font-size: 16px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}
""")

View File

@ -9,27 +9,34 @@ 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"
@ -46,22 +53,34 @@ 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'])
@ -73,6 +92,7 @@ class DiagramParser:
def _getFloatChanges(self, signalName) -> list:
# TODO: Методы в Python именуются в lowercase либо snake_case.
timeCode = self.data['time']
signal_values = self.data[signalName]
changes = []

485
src/utils/qt_settings.py Normal file
View File

@ -0,0 +1,485 @@
# TODO: Было бы логично разместить данный модуль в каталоге gui.
dark_style = """
/*
------------------------------------------------------
Общие стили и базовые настройки
------------------------------------------------------
*/
QWidget {
background-color: #0D1117; /* Основной фон приложения */
font-family: "Segoe UI", sans-serif;
font-size: 14px;
color: #E0E0E0; /* Светло-серый текст для контраста на тёмном фоне */
}
QMainWindow {
background-color: #0D1117;
/* Дополнительное визуальное разделение центральной части */
border: 1px solid #424242;
}
/*
------------------------------------------------------
Для небольшого визуального разделения между зонами
можно настроить QFrame, если используете фреймы:
QFrame {
border: 1px solid #424242;
border-radius: 4px;
}
------------------------------------------------------
*/
/*
------------------------------------------------------
Стиль для окон сообщений
------------------------------------------------------
*/
QMessageBox {
background-color: #0D1117;
font-family: "Segoe UI", sans-serif;
font-size: 14px;
color: #E0E0E0;
}
/*
------------------------------------------------------
Кнопки (QPushButton)
------------------------------------------------------
*/
QPushButton {
background-color: #FFCC00; /* Жёлтый цвет – акцент */
color: #121212;
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
}
/* Эффект при наведении */
QPushButton:hover:!disabled {
background-color: #FFD700; /* Более светлый жёлтый */
}
/* Эффект при нажатии */
QPushButton:pressed {
background-color: #E6B800; /* Более тёмный жёлтый */
}
/* Стиль для отключённых кнопок */
QPushButton:disabled {
background-color: #424242;
color: #BDBDBD;
}
/*
------------------------------------------------------
Чекбоксы и радиокнопки
------------------------------------------------------
*/
QCheckBox, QRadioButton {
color: #E0E0E0;
font-size: 14px;
font-weight: 500;
}
/*
------------------------------------------------------
Поля ввода (QLineEdit), спинбоксы, комбобоксы и т.п.
------------------------------------------------------
*/
QLineEdit, QComboBox {
background-color: #1E1E1E;
color: #E0E0E0;
padding: 8px;
border: 1px solid #424242;
border-radius: 4px;
font-size: 16px;
}
QSpinBox, QDoubleSpinBox {
background-color: #1E1E1E;
color: #E0E0E0;
padding: 5px;
border: 1px solid #424242;
border-radius: 4px;
font-size: 16px;
}
/*
------------------------------------------------------
Метки и группбоксы
------------------------------------------------------
*/
QLabel {
color: #E0E0E0;
font-size: 16px;
font-weight: 500;
}
QGroupBox {
color: #E0E0E0;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 600;
border: 1px solid #424242;
border-radius: 6px;
margin-top: 10px;
padding: 10px;
}
/*
------------------------------------------------------
Меню и панель меню
------------------------------------------------------
*/
QMenuBar {
background-color: #1E1E1E;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 600;
}
QMenuBar::item {
background: transparent;
padding: 18px 26px;
color: #E0E0E0;
}
QMenuBar::item:selected {
background-color: #333333;
color: #FFD700; /* Жёлтый при наведении */
}
QMenuBar::item:pressed {
background-color: #555555;
color: #FFD700; /* Жёлтый при нажатии */
}
QMenu {
background-color: #1E1E1E;
color: #E0E0E0;
border: 3px solid #424242;
}
QMenu::item {
padding: 10px 24px;
color: #E0E0E0;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: 600;
}
QMenu::item:selected {
background-color: #333333;
color: #FFD700;
}
QMenu::item:pressed {
background-color: #555555;
color: #FFD700;
}
/*
------------------------------------------------------
Полосы прокрутки (QScrollBar)
------------------------------------------------------
*/
QScrollBar:vertical {
background: #2C2C2C;
width: 12px;
margin: 0;
}
QScrollBar::handle:vertical {
background: #555555;
min-height: 20px;
border-radius: 6px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
background: #2C2C2C;
height: 0px;
subcontrol-origin: margin;
}
QScrollBar::up-arrow:vertical, QScrollBar::down-arrow:vertical {
background: none;
}
QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical {
background: none;
}
/*
------------------------------------------------------
Табы (QTabWidget, QTabBar)
------------------------------------------------------
*/
QTabWidget::pane {
border: 1px solid #424242;
background-color: #0D1117;
}
QTabBar::tab {
background: #1E1E1E;
color: #E0E0E0;
padding: 8px 16px;
margin: 2px;
border-radius: 4px;
}
QTabBar::tab:selected {
background: #333333;
font-weight: 600;
color: #FFD700; /* Выделим текст жёлтым при активной вкладке */
}
/*
------------------------------------------------------
Dock Widgets (QDockWidget)
------------------------------------------------------
*/
QDockWidget {
background: #1E1E1E;
border: 1px solid #424242;
}
/* Заголовок дока */
QDockWidget::title {
background-color: #2C2C2C;
text-align: left;
font-size: 14px;
font-weight: 600;
padding: 6px 10px;
border-bottom: 1px solid #424242;
color: #E0E0E0;
}
/* Кнопки закрытия и открепления (float) */
QDockWidget::close-button, QDockWidget::float-button, QDockWidget::close-button:disabled {
border: none;
background-color: #FFCC00;
icon-size: 18px;
}
QDockWidget::close-button:hover, QDockWidget::float-button:hover {
border: none;
background-color: #FFD700 ;
icon-size: 18px;
}
/*
------------------------------------------------------
Таблицы (QTableWidget / QTableView)
------------------------------------------------------
*/
QTableCornerButton::section {
background-color: #1E1E1E;
}
QTableView {
background-color: #1E1E1E;
gridline-color: #424242; /* Линии сетки */
color: #E0E0E0; /* Текст ячеек */
border: 1px solid #424242;
selection-background-color: #FFCC00; /* Жёлтый при выделении */
selection-color: #121212; /* Тёмный цвет текста при выделении */
font-size: 14px;
}
QTableView QLineEdit {
font-size: 14px;
}
/* Горизонтальный заголовок в таблицах */
QHeaderView::section {
background-color: #2C2C2C;
color: #E0E0E0;
padding: 6px;
border: 1px solid #424242;
font-weight: 600;
font-size: 14px;
}
/* Вертикальный заголовок в таблицах */
QHeaderView::section:vertical {
background-color: #2C2C2C;
color: #E0E0E0;
padding: 4px;
border: 1px solid #424242;
font-weight: 600;
font-size: 14px;
}
/* Полоса прокрутки в QTableView (если своя нужна) */
QTableView QScrollBar:vertical {
background: #2C2C2C;
width: 12px;
margin: 0;
}
QTableView QScrollBar::handle:vertical {
background: #555555;
min-height: 20px;
border-radius: 6px;
}
/*
При желании можно добавить эффекты при наведении/клике на строки QTableView:
QTableView::item:hover {
background-color: #333333;
color: #FFD700;
}
*/
QProgressBar {
border: 2px solid grey;
border-radius: 5px;
text-align: center;
}
/*
------------------------------------------------------
Завершение стиля
------------------------------------------------------
*/
"""
dis_robots = """
QPushButton {
background-color: #555555;
color: #cccccc;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
}
QWidget#robot {
border: none;
padding: 5px;
background-color: rgba(33, 33, 33, 100);
}
"""
selected_robot = """
QWidget#robot {
border: 2px solid #E6B800;
border-radius: 10px;
padding: 5px;
}
"""
en_button = """
QPushButton {
background-color: #FFCC00;
color: #0D1117;
padding: 12px 25px;
border: 2px solid #E6B800;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
QPushButton:hover:!disabled {
background-color: #FFD700;
}
"""
dis_button = """
QPushButton {
background-color: #555555;
color: #cccccc;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
}
"""
saved_button = """
QPushButton {
background-color: #28a745;
color: #ffffff;
padding: 12px 25px;
border: none;
border-radius: 8px;
font-family: "Segoe UI", sans-serif;
font-size: 16px;
font-weight: bold;
}
"""
start_button = """
QPushButton {
background-color: #FFCC00;
color: #0D1117;
padding: 20px 40px;
border: 2px solid #E6B800;
border-radius: 10px;
font-family: "Segoe UI", sans-serif;
font-size: 24px;
font-weight: bold;
background-image: linear-gradient(to bottom, #FFD700, #FFCC00);
}
QPushButton:hover:!disabled {
background-color: #FFD700;
}
QPushButton:pressed {
background-color: #E6B800;
padding-top: 22px;
padding-bottom: 18px;
}
QPushButton:disabled {
background-color: #555555;
color: #cccccc;
border: none;
}
"""
BigSuccessLabel = """
QLabel {
color: #ffffff;
font-size: 26px;
font-weight: bold;
font-family: "Segoe UI", sans-serif;
}"""
colors = [
'#FF6F61', # яркий коралловый
'#6B5B95', # приглушенный фиолетовый
'#88B04B', # яркий зеленый
'#F7CAC9', # светлый розовый
'#92A8D1', # светло-голубой
'#955251', # теплый терракотовый
'#B565A7', # лавандовый
'#009B77', # глубокий бирюзовый
'#DD4124', # ярко-красный
'#45B8AC', # мягкий мятный
'#FF6F61', # яркий коралловый
'#6B5B95', # приглушенный фиолетовый
'#88B04B', # яркий зеленый
'#F7CAC9', # светлый розовый
'#92A8D1', # светло-голубой
'#955251', # теплый терракотовый
'#B565A7', # лавандовый
'#009B77', # глубокий бирюзовый
'#DD4124', # ярко-красный
'#45B8AC' # мягкий мятный
]
RGBA = [(255, 255, 0),
(32, 178, 70),
(255, 69, 0),
(123, 104, 238),
(99, 42, 15)
]

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

3807
trace_samples/sample.csv Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,175 @@
DIAEXTENDED
#BEGINGLOBALHEADER
1,DOS
101,---
102,rob "#KR210R2700_2 C4 FLR"
103,---
104,27.10.2022
105,11:48:43
106,668500000
110,#dd.mm.yyyy HH:MM:SS ns
111,9.9000000000E+34
112,High -> Low
#ENDGLOBALHEADER
#BEGINCHANNELHEADER
200,Zeit
201,Zeitkanal
202,sec
210,IMPLIZIT
220,3806
240,0.00000
241,1.20000000e-02
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,X_Act
201,IpoActual
202,mm
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,1
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,Y_Act
201,IpoActual
202,mm
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,2
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,Z_Act
201,IpoActual
202,mm
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,3
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,AxisPos_Act7
201,IpoActual
202,mm
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,4
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,DriveMotorPos_Act7
201,AxisValAtMMInterface
202,<2C>
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,5
240,0
241,5.729578e+01
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,DriveMotorVel_Act7
201,AxisValAtMMInterface
202,<2C>/s
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,6
240,0
241,5.729578e+01
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,DriveMotorTorq_Act7
201,AxisValAtMMInterface
202,Nm
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,7
240,0
241,1.000000e+03
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,DriveMotorCurr_Act7
201,AxisValAtMMInterface
202,A
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,8
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,AxisVel_Act7
201,IpoVelocities
202,mm/s
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,9
240,0
241,1.000000e+00
#ENDCHANNELHEADER
#BEGINCHANNELHEADER
200,CartVel_Act
201,IpoVelocities
202,m/s
210,EXPLIZIT
211,teslaSP_VelTCP_KRCIpo.r64
213,BLOCK
214,REAL64
220,3806
221,10
240,0
241,1.000000e+00
#ENDCHANNELHEADER

Binary file not shown.

File diff suppressed because it is too large Load Diff