Compare commits
No commits in common. "master" and "feature-positioning" have entirely different histories.
master
...
feature-po
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,5 +1,4 @@
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
/__pycache__
|
||||
/tck_venv
|
||||
/.venv
|
||||
/.vscode
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
{
|
||||
"act_pos_decrease": [
|
||||
5.0
|
||||
],
|
||||
"act_vel_min": [
|
||||
1900.0
|
||||
],
|
||||
"act_vel_close": [
|
||||
1700.0
|
||||
],
|
||||
"act_vel_thresh": [
|
||||
10.0
|
||||
],
|
||||
"act_vel_negative": [
|
||||
0.0
|
||||
],
|
||||
"rob_vel_thresh": [
|
||||
4.0
|
||||
],
|
||||
"act_force_close": [
|
||||
700.0
|
||||
],
|
||||
"act_force_weld": [
|
||||
1300.0
|
||||
],
|
||||
"force_increase": [
|
||||
200.0
|
||||
],
|
||||
"force_decrease": [
|
||||
80.0
|
||||
]
|
||||
}
|
||||
@ -64,36 +64,36 @@
|
||||
0.001
|
||||
],
|
||||
"distance_l_1": [
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02,
|
||||
0.02
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005,
|
||||
0.005
|
||||
],
|
||||
"distance_l_2": [
|
||||
0.0275,
|
||||
0.0255,
|
||||
0.0242,
|
||||
0.0245,
|
||||
0.0228,
|
||||
0.0236,
|
||||
0.0229,
|
||||
0.0248,
|
||||
0.024,
|
||||
0.0235,
|
||||
0.025,
|
||||
0.0276,
|
||||
0.0234,
|
||||
0.0215
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625,
|
||||
0.0625
|
||||
],
|
||||
"distance_h_end1": [
|
||||
0.003,
|
||||
@ -127,6 +127,22 @@
|
||||
0.005,
|
||||
0.005
|
||||
],
|
||||
"object_position": [
|
||||
0.02743,
|
||||
0.0249,
|
||||
0.024,
|
||||
0.02425,
|
||||
0.0225,
|
||||
0.0233,
|
||||
0.0226,
|
||||
0.02442,
|
||||
0.02357,
|
||||
0.02321,
|
||||
0.02464,
|
||||
0.02745,
|
||||
0.02321,
|
||||
0.021348
|
||||
],
|
||||
"time_wielding": [
|
||||
1.332,
|
||||
1.644,
|
||||
@ -160,20 +176,20 @@
|
||||
0.0
|
||||
],
|
||||
"time_robot_movement": [
|
||||
0.516,
|
||||
0.492,
|
||||
0.636,
|
||||
0.492,
|
||||
0.42,
|
||||
0.54,
|
||||
0.444,
|
||||
0.66,
|
||||
0.521,
|
||||
0.557,
|
||||
0.51,
|
||||
0.51,
|
||||
0.534,
|
||||
0.0
|
||||
0.314,
|
||||
0.331,
|
||||
0.356,
|
||||
0.428,
|
||||
0.418,
|
||||
0.454,
|
||||
0.458,
|
||||
0.44,
|
||||
0.49,
|
||||
0.47,
|
||||
0.44,
|
||||
0.425,
|
||||
0.464,
|
||||
0.5
|
||||
],
|
||||
"object_thickness": [
|
||||
0.0045,
|
||||
@ -222,5 +238,85 @@
|
||||
500.0,
|
||||
500.0,
|
||||
500.0
|
||||
],
|
||||
"Tesla closing": [
|
||||
0.216,
|
||||
0.228,
|
||||
0.252,
|
||||
0.216,
|
||||
0.228,
|
||||
0.216,
|
||||
0.228,
|
||||
0.228,
|
||||
0.228,
|
||||
0.216,
|
||||
0.228,
|
||||
0.216,
|
||||
0.216,
|
||||
0.216
|
||||
],
|
||||
"Tesla squeeze": [
|
||||
0.276,
|
||||
0.288,
|
||||
0.264,
|
||||
0.264,
|
||||
0.276,
|
||||
0.276,
|
||||
0.312,
|
||||
0.276,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24,
|
||||
0.24
|
||||
],
|
||||
"Tesla welding": [
|
||||
1.332,
|
||||
1.644,
|
||||
1.644,
|
||||
1.428,
|
||||
1.284,
|
||||
1.308,
|
||||
1.272,
|
||||
1.38,
|
||||
1.416,
|
||||
1.392,
|
||||
1.38,
|
||||
1.404,
|
||||
1.452,
|
||||
1.452
|
||||
],
|
||||
"Tesla oncomming_relief": [
|
||||
0.528,
|
||||
0.528,
|
||||
0.54,
|
||||
0.636,
|
||||
0.504,
|
||||
0.468,
|
||||
0.492,
|
||||
0.54,
|
||||
0.563,
|
||||
0.588,
|
||||
0.541,
|
||||
0.564,
|
||||
0.576,
|
||||
0.507
|
||||
],
|
||||
"Tesla summary time": [
|
||||
2.34,
|
||||
2.652,
|
||||
2.796,
|
||||
2.4,
|
||||
2.208,
|
||||
2.34,
|
||||
2.256,
|
||||
2.544,
|
||||
2.405,
|
||||
2.405,
|
||||
2.358,
|
||||
2.37,
|
||||
2.442,
|
||||
1.908
|
||||
]
|
||||
}
|
||||
@ -1,325 +0,0 @@
|
||||
{
|
||||
"Online Path Scanner": {
|
||||
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": true,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "g", "width":3}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {"color": "r", "width":2}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {"color": "w", "width":2}
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": "r",
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": "w",
|
||||
"zoom": false
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "g", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "b", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Report Editor": {
|
||||
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": true,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "g", "width":3}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {"color": "r", "width":2}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {"color": "w", "width":2}
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"mirror ME": false,
|
||||
"workpiece": false,
|
||||
"force compensation FE": false,
|
||||
"force accuracy":false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": "r",
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": "w",
|
||||
"zoom": false
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "g", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "b", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Client Trace Watcher":{
|
||||
|
||||
"Client Trace": {
|
||||
"Settings": {
|
||||
"zoom": false,
|
||||
"stages": true
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Position, mm ME",
|
||||
"pen": "g"
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, deg/s ME",
|
||||
"pen": "b"
|
||||
},
|
||||
{
|
||||
"name": "Cartesian Tool Speed, mm/s ME",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": []
|
||||
},
|
||||
|
||||
"TCW Trace": {
|
||||
"Settings": {
|
||||
"stages": true,
|
||||
"ideals": true,
|
||||
"mirror ME": true,
|
||||
"workpiece": true,
|
||||
"force compensation FE": true
|
||||
},
|
||||
"Real_signals": [],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "r", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
|
||||
"Combined Trace": {
|
||||
"Settings": {
|
||||
"stages": true,
|
||||
"performance": false,
|
||||
"ideals": true,
|
||||
"workpiece": false
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": "r"
|
||||
},
|
||||
{
|
||||
"name": "Electrode Position, mm ME",
|
||||
"pen": "g"
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, deg/s ME",
|
||||
"pen": "b"
|
||||
},
|
||||
{
|
||||
"name": "Cartesian Tool Speed, mm/s ME",
|
||||
"pen": "w"
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {"color": "r", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {"color": "g", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {"color": "b", "width":4}
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {"color": "w", "width":3},
|
||||
"zoom": false
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"trace_storage_path": [
|
||||
"/home/leonid/PycharmProjects/WeldingSpotPerformance/trace_samples"
|
||||
"D:/downloads/a22"
|
||||
],
|
||||
"monitor_update_period": [
|
||||
1000.0
|
||||
@ -21,7 +21,7 @@
|
||||
270.0
|
||||
],
|
||||
"mass_2": [
|
||||
1.0
|
||||
270.0
|
||||
],
|
||||
"k_hardness_1": [
|
||||
2148570.0
|
||||
@ -33,22 +33,22 @@
|
||||
13.8
|
||||
],
|
||||
"torque_max_2": [
|
||||
0.0
|
||||
13.8
|
||||
],
|
||||
"transmission_ratio_1": [
|
||||
0.00125
|
||||
],
|
||||
"transmission_ratio_2": [
|
||||
1.0
|
||||
0.00125
|
||||
],
|
||||
"contact_distance_1": [
|
||||
0.02
|
||||
0.0
|
||||
],
|
||||
"contact_distance_2": [
|
||||
0.081
|
||||
0.0
|
||||
],
|
||||
"k_prop": [
|
||||
0.075
|
||||
0.005
|
||||
],
|
||||
"time_capture": [
|
||||
1000.0
|
||||
@ -56,19 +56,7 @@
|
||||
"UML_time_scaler": [
|
||||
1000.0
|
||||
],
|
||||
"Range ME, mm": [
|
||||
115.0
|
||||
],
|
||||
"client_time": [
|
||||
35.488
|
||||
],
|
||||
"performance_mode": [
|
||||
"client"
|
||||
],
|
||||
"time_before_start": [
|
||||
0.924
|
||||
],
|
||||
"time_after_end": [
|
||||
1.14
|
||||
"gun_range": [
|
||||
112.5
|
||||
]
|
||||
}
|
||||
@ -4,7 +4,7 @@ cycler==0.12.1
|
||||
fonttools==4.54.1
|
||||
httplib2==0.22.0
|
||||
kiwisolver==1.4.7
|
||||
loguru==0.7.3
|
||||
loguru==0.7.2
|
||||
matplotlib==3.9.2
|
||||
numpy==2.1.2
|
||||
packaging==24.1
|
||||
@ -21,3 +21,4 @@ pytz==2024.2
|
||||
scipy==1.14.1
|
||||
six==1.16.0
|
||||
tzdata==2024.2
|
||||
watchdog==6.0.0
|
||||
|
||||
@ -16,12 +16,6 @@ class OptAlgorithm(AutoConfigClass):
|
||||
|
||||
calc = OptTimeCalculator(operator_config, system_config)
|
||||
|
||||
if self.distance_l_1 < 0:
|
||||
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода FE")
|
||||
|
||||
if self.distance_l_2 < 0:
|
||||
raise Exception("Ошибка - заготовка вышла за пределы маскимального раскрытия электрода ME")
|
||||
|
||||
self.Ts = calc.T(self.distance_h_start_1,
|
||||
self.distance_h_start_2,
|
||||
self.distance_s_1,
|
||||
@ -227,8 +221,7 @@ class OptAlgorithm(AutoConfigClass):
|
||||
def FMovement(self, t: float):
|
||||
x1 = self.X1Movement(t)
|
||||
x2 = self.X2Movement(t)
|
||||
#its not true, but theres no need to track collisions at this point
|
||||
F = 0#self.k_hardness_1 * max(0, (x1 - self.x1_contact))
|
||||
F = self.k_hardness_1 * max(0, (x1 - self.x1_contact))
|
||||
return F
|
||||
|
||||
def X1Movement(self, t: float):
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import numpy as np
|
||||
from numpy import sqrt, arcsin, arccos, cos, sin, array
|
||||
from numpy import sqrt, arcsin, arccos, cos, sin
|
||||
|
||||
from OptAlgorithm.AutoConfigClass import AutoConfigClass
|
||||
from OptAlgorithm.ConstantCalculator import ConstantCalculator
|
||||
@ -22,8 +21,8 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0q = min(sqrt(2 * self.a_max_1 * h1), self.v_max_1)
|
||||
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
|
||||
t1 = v0 / self.a_max_1
|
||||
hleft = max(0, h1 - (self.a_max_1 * t1 * t1 / 2))
|
||||
t3 = min(max(0, v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
|
||||
hleft = (h1 - (self.a_max_1 * t1 * t1 / 2))
|
||||
t3 = min((v0q - v0)/ self.a_max_1, sqrt(self.a_max_1 * hleft))
|
||||
v1 = (t1 + t3) * self.a_max_1
|
||||
hleft2 = h1 - (self.a_max_1 * (t1+t3) * (t1+t3) / 2) - v1 * t3 + t3**2 * self.a_max_1 / 2
|
||||
t2t = max(0, hleft2 / v1)
|
||||
@ -87,7 +86,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v1 = topen_1_acc * self.a_max_1
|
||||
if s1 > topen_1_speed * v1:
|
||||
s1 -= topen_1_speed * v1
|
||||
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(max(0,topen_1_acc ** 2 - 2 * s1 / self.a_max_1))
|
||||
topen_1_mark = 2 * topen_1_acc + topen_1_speed - sqrt(topen_1_acc ** 2 - 2 * s1 / self.a_max_1)
|
||||
else:
|
||||
topen_1_mark = topen_1_acc + s1 / v1
|
||||
|
||||
@ -97,7 +96,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v2 = topen_2_acc * self.a_max_2
|
||||
if s2 > topen_2_speed * v2:
|
||||
s2 -= topen_2_speed * v2
|
||||
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(max(0,topen_2_acc ** 2 - 2 * s2 / self.a_max_2))
|
||||
topen_2_mark = 2 * topen_2_acc + topen_2_speed - sqrt(topen_2_acc ** 2 - 2 * s2 / self.a_max_2)
|
||||
else:
|
||||
topen_2_mark = topen_2_acc + s2 / v2
|
||||
|
||||
@ -109,7 +108,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0 = self.allTimes["tclose_1_acc"] * self.a_max_1
|
||||
vF0 = v0 * self.k_hardness_1
|
||||
|
||||
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / self.mass_1) * self.Ftogrow)
|
||||
vFmax = min(self.v_max_1 * self.k_hardness_1, sqrt(self.k_hardness_1 / (self.mass_1)) * self.Ftogrow)
|
||||
|
||||
L = sqrt(self.k_hardness_1 / self.mass_1 * self.eff_control ** 2 + vF0 * vF0)
|
||||
tspeed = sqrt(self.mass_1 / self.k_hardness_1) * (
|
||||
@ -160,78 +159,56 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
Tfull = self.time_robot_movement
|
||||
|
||||
L = self.__dict__["distance_l_" + str(i)]
|
||||
maxL = L + x0
|
||||
maxL = contact[i - 1] - L - x0
|
||||
self.Tmovementi(i, x, Tfull, v0, maxL)
|
||||
return pos0s, v0s
|
||||
|
||||
def Tmovementi(self, i, Sfull, Tfull, v0, maxL) -> None:
|
||||
v0 = abs(v0)
|
||||
vmax = self.__dict__["v_max_" + str(i)]
|
||||
|
||||
if vmax < v0 - self.check_eps:
|
||||
raise Exception("Ошибка - скорость в начале перемещения больше макимальной - проверьте скорость")
|
||||
a = self.__dict__["a_max_" + str(i)]
|
||||
Sfull = -Sfull
|
||||
t3 = (Tfull + v0 / a) / 2
|
||||
|
||||
time_eps = 1e-8
|
||||
T = 0
|
||||
if Sfull > 0:
|
||||
S1_min = max(Sfull,v0*v0 / (2*a)) + 1e-7
|
||||
else:
|
||||
S1_min = v0*v0 / (2*a) + 1e-7
|
||||
S1_max = maxL
|
||||
if S1_min > S1_max:
|
||||
raise Exception("Ошибка - невозможно перемещение робота из-за слишком большого оффсета следующей заготовки - не хватает максимального раскрытия")
|
||||
# calc for S1
|
||||
def calc_Tmovement_for_S1(S1):
|
||||
S2 = -Sfull + S1
|
||||
if S2 < 0:
|
||||
raise Exception("Ошибка - прохождение второй части перемещения - отрицательно, неизвестное поведение")
|
||||
sqrtval = a ** 2 * (
|
||||
a ** 2 * (Tfull + 2 * t3) ** 2 - 8 * a * Sfull + 2 * a * v0 * (Tfull + 2 * t3) - 3 * v0 ** 2)
|
||||
if sqrtval < 0:
|
||||
raise Exception("""Невозможно с S_{i} добраться но H*_{i} за указанное время,
|
||||
проверьте distance_s_{i}, distance_h_end{i}, time_command, time_robot_movement""")
|
||||
t1max = ((Tfull + 2 * t3) + v0 / a) / (2) - sqrt(sqrtval) * sqrt(2) / (4 * a ** 2)
|
||||
t1 = min(t1max, (vmax - abs(v0)) / a)
|
||||
t1Llimit = -v0 / a + sqrt(v0 ** 2 / (a ** 2) + (abs(maxL) - v0 * v0 / a) / a)
|
||||
t1 = max(0, min(t1, t1Llimit))
|
||||
t31 = v0 / a + t1
|
||||
v1 = v0 + a * t1
|
||||
|
||||
#try to push t2 to limit S3_2-5
|
||||
maxiter = 10
|
||||
t2 = 0
|
||||
for j in range(maxiter):
|
||||
S1 = v0 * t1 + a * t1 * t1 / 2 + v1 * t31 - a * t31 * t31 / 2 + t2 * v1
|
||||
S2max = Sfull + S1
|
||||
t5max = sqrt(S2max / a)
|
||||
t5 = min(t5max, (vmax) / a)
|
||||
t32 = t5
|
||||
t3 = t31 + t32
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
t2max = (timeleft - Sleft / v3) / (1 + v1 / v3)
|
||||
Smovement = -v0 * t1 - a / 2 * t1 ** 2 - v1 * t31 + a / 2 * t31 ** 2
|
||||
if v1 == 0:
|
||||
t2 = 0
|
||||
else:
|
||||
t2 = max(0, min(t2max, (abs(maxL) - abs(Smovement)) / v1))
|
||||
t4 = max(0, Sleft / v3 + v1 / v3 * t2)
|
||||
|
||||
tstay = max(0, Tfull - t1 - t2 - t3 - t4 - t5)
|
||||
|
||||
t1, t2, t31, t32, t4, t5 = tarray
|
||||
for j, t in enumerate(tarray):
|
||||
if t < 0:
|
||||
raise Exception(f"""Ошибка - время перехода во время фазы {j + 1} {i}-го электрода отрицательно - переход невозможен с такими параметрами,
|
||||
проверьте скорость, ускорение, смещение """)
|
||||
self.allTimes["tmovement_" + str(i) + "_acc"] = t1
|
||||
self.allTimes["tmovement_" + str(i) + "_speed"] = t2
|
||||
self.allTimes["tmovement_" + str(i) + "_slow"] = t31
|
||||
@ -249,7 +226,7 @@ class OptTimeCalculator(AutoConfigClass):
|
||||
v0 = min(v0q, sqrt(1 / (self.k_hardness_1 * self.mass_1)) * self.Ftogrow)
|
||||
t2 = T - sqrt(max(0, T ** 2 - 2 * s / self.a_max_1))
|
||||
if t2 * self.a_max_1 < v0:
|
||||
#we should wait to end with max speed but dont do it
|
||||
#we should wait to end with max speed
|
||||
t2 = v0 / self.a_max_1
|
||||
t3 = max(0, (s - self.a_max_1 * t2 ** 2 / 2) / (self.a_max_1 * t2))
|
||||
t1 = T - t2 - t3
|
||||
|
||||
BIN
src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/AutoConfigClass.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/ConstantCalculator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/OptAlgorithm.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/OptTimeCalculator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/PhaseCalc.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/OptAlgorithm/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/main.cpython-310.pyc
Normal file
BIN
src/__pycache__/main.cpython-310.pyc
Normal file
Binary file not shown.
751
src/base/base.py
751
src/base/base.py
@ -1,751 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
from typing import Optional, Union, List, Tuple, Dict, Any
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from cachetools import LRUCache
|
||||
import pandas as pd
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
|
||||
|
||||
from OptAlgorithm import OptAlgorithm
|
||||
from utils.qt_settings import dark_style
|
||||
|
||||
|
||||
# ========= Дата-классы для хранения данных =========
|
||||
|
||||
@dataclass
|
||||
class KukaTXT:
|
||||
"""
|
||||
Класс для представления текстовых данных Кука.
|
||||
"""
|
||||
time: float = 0
|
||||
endtime: float = 0
|
||||
func: str = ""
|
||||
type_: str = ""
|
||||
signal: str = ""
|
||||
# Дополнительные поля (закомментированы) можно добавить при необходимости
|
||||
# module: str
|
||||
# line: int = 0
|
||||
# point_name: str = ""
|
||||
# ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class KukaDataHead:
|
||||
"""
|
||||
Класс, описывающий заголовок данных Кука.
|
||||
"""
|
||||
rob_ID: int = 0
|
||||
filename: str = ""
|
||||
channels: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlotItems:
|
||||
"""
|
||||
Класс для хранения элементов графика: регионы, кривые и QT-виджеты.
|
||||
"""
|
||||
regions: dict = field(default_factory=dict)
|
||||
curves: dict = field(default_factory=dict)
|
||||
qt_items: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PointPassport:
|
||||
"""
|
||||
Паспорт точки, содержащий временной интервал, события,
|
||||
идеальные данные и полезную информацию для этой точки.
|
||||
"""
|
||||
timeframe: List[Any] = field(default_factory=list)
|
||||
events: Dict = field(default_factory=dict)
|
||||
ideal_data: Dict = field(default_factory=dict)
|
||||
useful_data: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PerformanceData:
|
||||
client_to_TWC: float = 0
|
||||
client_to_ideal: float = 0
|
||||
TWC_to_ideal: float = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class UsefulGraphData:
|
||||
"""
|
||||
Полезные данные для графика.
|
||||
|
||||
:param client_time: Время, рассчитанное для клиента.
|
||||
:param range_ME: Диапазон для ME.
|
||||
:param k_hardness: Коэффициент твердости.
|
||||
"""
|
||||
performance: PerformanceData = field(default_factory=PerformanceData)
|
||||
range_ME: float = 0
|
||||
k_hardness: float = 0
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class GraphicPassport:
|
||||
"""
|
||||
Графический паспорт, объединяющий DataFrame с данными,
|
||||
список паспортов точек и полезные данные для построения графика.
|
||||
"""
|
||||
dataframe: pd.DataFrame = field(default_factory=pd.DataFrame)
|
||||
points_pocket: List[PointPassport] = field(default_factory=list)
|
||||
useful_data: UsefulGraphData = field(default_factory=UsefulGraphData)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Settings:
|
||||
"""
|
||||
Настройки приложения, разделённые на категории:
|
||||
- operator: настройки, связанные с оператором,
|
||||
- system: системные настройки,
|
||||
- filter: настройки фильтрации трейса клиента для определения событий.
|
||||
"""
|
||||
operator: Dict = field(default_factory=dict)
|
||||
system: Dict = field(default_factory=dict)
|
||||
filter: Dict = field(default_factory=dict)
|
||||
|
||||
|
||||
# ========= Базовые классы для взаимодействия компонентов =========
|
||||
|
||||
class BaseMediator:
|
||||
"""
|
||||
Базовый класс медиатора для организации взаимодействия между компонентами приложения.
|
||||
|
||||
Медиатор связывает такие модули, как конвертер данных, формирователь паспортов,
|
||||
виджет построения графиков, контроллер, файловый менеджер и процессор трассировки.
|
||||
"""
|
||||
def __init__(self,
|
||||
converter: BaseDataConverter,
|
||||
passport_former: BasePointPassportFormer,
|
||||
plot: BasePlotWidget,
|
||||
controller: BaseController,
|
||||
file_manager: BaseFileManager,
|
||||
trace_processor: BaseRawTraceProcessor):
|
||||
self._converter = converter
|
||||
self._converter.mediator = self
|
||||
self._passport_former = passport_former
|
||||
self._passport_former.mediator = self
|
||||
self._plot = plot
|
||||
self._plot.mediator = self
|
||||
self._controller = controller
|
||||
self._controller.mediator = self
|
||||
self._file_manager = file_manager
|
||||
self._file_manager.mediator = self
|
||||
self._trace_processor = trace_processor
|
||||
self._trace_processor.mediator = self
|
||||
|
||||
def notify(self,
|
||||
source: Union[
|
||||
BaseDirectoryMonitor,
|
||||
BaseDataConverter,
|
||||
BasePointPassportFormer,
|
||||
BasePlotWidget,
|
||||
BaseRawTraceProcessor
|
||||
],
|
||||
data: Union[
|
||||
List[str],
|
||||
List[pd.DataFrame],
|
||||
List[list],
|
||||
List[QWidget],
|
||||
pd.DataFrame
|
||||
]) -> None:
|
||||
"""
|
||||
Получает уведомление от компонента-источника и распределяет данные по соответствующим модулям.
|
||||
"""
|
||||
...
|
||||
|
||||
def prerender_TCW(self,
|
||||
source: Union[BaseController, BaseFileManager, BaseRawTraceProcessor],
|
||||
data: Union[str, List[str], List[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Выполняет операции для отображения предварительных данных клиента.
|
||||
"""
|
||||
...
|
||||
|
||||
def render_TCW(self,
|
||||
source: Union[BaseController, BaseRawTraceProcessor],
|
||||
data: Union[str, List[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Выполняет операции для отображения обработанных данных клиента и ТСК.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseDirectoryMonitor:
|
||||
"""
|
||||
Базовый класс для мониторинга директории.
|
||||
|
||||
Использует QTimer для периодической проверки содержимого директории.
|
||||
"""
|
||||
update_timer = QTimer()
|
||||
|
||||
def __init__(self, file_manager: Optional[BaseFileManager] = None):
|
||||
self._directory_path: Optional[str] = None
|
||||
self._update_time: Optional[float] = None
|
||||
self.isActive: bool = False
|
||||
self._files: List[str] = []
|
||||
self._file_manager = file_manager
|
||||
|
||||
@property
|
||||
def directory_path(self) -> str:
|
||||
return self._directory_path
|
||||
|
||||
@property
|
||||
def update_time(self) -> int:
|
||||
return self._update_time
|
||||
|
||||
@property
|
||||
def files(self) -> List[str]:
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def file_manager(self) -> BaseFileManager:
|
||||
return self._file_manager
|
||||
|
||||
@file_manager.setter
|
||||
def file_manager(self, file_manager: BaseFileManager) -> None:
|
||||
self._file_manager = file_manager
|
||||
|
||||
def init_state(self) -> None:
|
||||
"""
|
||||
Инициализирует состояние мониторинга, считывая список файлов из директории.
|
||||
"""
|
||||
files = os.listdir(self._directory_path)
|
||||
self._files = files
|
||||
|
||||
def start(self) -> None:
|
||||
"""
|
||||
Запускает мониторинг директории, устанавливая флаг активности и запуская таймер.
|
||||
"""
|
||||
self.isActive = True
|
||||
self.update_timer.start(int(self._update_time))
|
||||
|
||||
def stop(self) -> None:
|
||||
"""
|
||||
Останавливает мониторинг директории.
|
||||
"""
|
||||
self.isActive = False
|
||||
self.update_timer.stop()
|
||||
|
||||
def pause(self) -> None:
|
||||
"""
|
||||
Приостанавливает мониторинг директории (останавливает таймер).
|
||||
"""
|
||||
self.update_timer.stop()
|
||||
|
||||
|
||||
class BaseDataConverter:
|
||||
"""
|
||||
Базовый класс для конвертации данных.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def convert_data(self, files: List[str]) -> None:
|
||||
"""
|
||||
Конвертирует данные из списка файлов.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BasePlotWidget:
|
||||
"""
|
||||
Базовый класс для виджета построения графиков.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, controller: BaseController = None):
|
||||
self._mediator: BaseMediator = mediator
|
||||
self._controller: BaseController = controller
|
||||
self._datalen: int = 0
|
||||
self._datastep: int = 0
|
||||
self._stage_colors = {
|
||||
"Closing": [220, 20, 60, 100], # Crimson
|
||||
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
||||
"Welding": [128, 128, 128, 100], # Gray
|
||||
"Relief": [34, 139, 34, 100], # Forest Green
|
||||
"Oncomming": [255, 165, 0, 100] # Orange
|
||||
}
|
||||
self._plt_channels = None
|
||||
|
||||
@staticmethod
|
||||
def set_style(obj: Union[QTabWidget, QWidget]) -> None:
|
||||
"""
|
||||
Устанавливает стиль для переданного объекта.
|
||||
"""
|
||||
obj.setStyleSheet(
|
||||
"""QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
|
||||
@property
|
||||
def controller(self) -> BaseController:
|
||||
return self._controller
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def build(self, data: List[pd.DataFrame]) -> List[QWidget]:
|
||||
"""
|
||||
Строит графики на основе списка DataFrame.
|
||||
"""
|
||||
...
|
||||
|
||||
def build_raw_trace(self, data: pd.DataFrame) -> None:
|
||||
"""
|
||||
Строит график трейсов клиента с предварительным разбиением на этапы.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим отображения графиков.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseController(QObject):
|
||||
"""
|
||||
Базовый класс контроллера, отвечающий за обновление интерфейса и взаимодействие с медиатором.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, file_manager: Optional[BaseFileManager] = None):
|
||||
super().__init__()
|
||||
self._mediator = mediator
|
||||
self._file_manager = file_manager
|
||||
|
||||
def send_widgets(self, widgets: List[QWidget]) -> None:
|
||||
"""
|
||||
Отправляет список виджетов для дальнейшей обработки.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, settings: List[Dict]) -> None:
|
||||
"""
|
||||
Обновляет настройки приложения.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_working_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает рабочий режим приложения.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
"""
|
||||
Открывает файл по заданному пути.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_dir(self, dirpath: str) -> None:
|
||||
"""
|
||||
Открывает директорию.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
"""
|
||||
Обновляет графики.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_status(self, msg: str) -> None:
|
||||
"""
|
||||
Обновляет статусное сообщение в интерфейсе.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_progress(self, progress: int) -> None:
|
||||
"""
|
||||
Обновляет значение прогресс-бара.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def file_manager(self) -> BaseFileManager:
|
||||
return self._file_manager
|
||||
|
||||
@file_manager.setter
|
||||
def file_manager(self, file_manager: BaseFileManager) -> None:
|
||||
self._file_manager = file_manager
|
||||
|
||||
|
||||
class BaseFileManager:
|
||||
"""
|
||||
Базовый класс файлового менеджера.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None, monitor: Optional[BaseDirectoryMonitor] = None):
|
||||
self._mediator = mediator
|
||||
self._monitor = monitor
|
||||
self._paths_library: set = set()
|
||||
self._mode: int = 0
|
||||
|
||||
@property
|
||||
def paths_library(self) -> set:
|
||||
return self._paths_library
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def monitor(self) -> BaseDirectoryMonitor:
|
||||
return self._monitor
|
||||
|
||||
@monitor.setter
|
||||
def monitor(self, monitor: BaseDirectoryMonitor) -> None:
|
||||
self._monitor = monitor
|
||||
|
||||
def replot_all(self) -> None:
|
||||
"""
|
||||
Переотрисовывает все данные, используя текущую библиотеку путей.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_custom_file(self, path: str) -> None:
|
||||
"""
|
||||
Открывает пользовательский файл по заданному пути.
|
||||
"""
|
||||
...
|
||||
|
||||
def open_raw_traces_dir(self, path: str) -> None:
|
||||
"""
|
||||
Открывает директорию с сырыми трассировочными данными.
|
||||
"""
|
||||
...
|
||||
|
||||
def set_mode(self, num: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы файлового менеджера.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_monitor_settings(self, settings: List[Dict]) -> None:
|
||||
"""
|
||||
Обновляет настройки мониторинга директории.
|
||||
"""
|
||||
...
|
||||
|
||||
def add_new_paths(self, paths: List[str]) -> None:
|
||||
"""
|
||||
Добавляет новые пути в библиотеку, если их ранее не было.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseIdealDataBuilder(OptAlgorithm):
|
||||
"""
|
||||
Базовый класс для построения идеальных данных, расширяющий функциональность OptAlgorithm.
|
||||
|
||||
При инициализации на основе настроек устанавливаются множитель времени и время сварки.
|
||||
"""
|
||||
def __init__(self, settings: Settings):
|
||||
self.mul = settings.system['time_capture']
|
||||
self.welding_time = settings.operator['time_wielding']
|
||||
self._settings = settings
|
||||
super().__init__(settings.system, settings.operator)
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа закрытия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сжатия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа открытия.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_tmovementDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сварки.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения (oncomming).
|
||||
"""
|
||||
...
|
||||
|
||||
def get_ideal_timings(self) -> Tuple[float, float, float, float]:
|
||||
"""
|
||||
Получает идеальные временные интервалы для этапов.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_cycle_time(self) -> float:
|
||||
"""
|
||||
Вычисляет общее время цикла как сумму идеальных временных интервалов.
|
||||
"""
|
||||
result = sum(self.get_ideal_timings())
|
||||
return result
|
||||
|
||||
|
||||
class BaseMainWindow(QMainWindow):
|
||||
"""
|
||||
Базовое главное окно приложения.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.resize(200, 200)
|
||||
# Создаем центральный виджет и устанавливаем его
|
||||
self._central_widget = QWidget()
|
||||
self.setCentralWidget(self._central_widget)
|
||||
# Устанавливаем основной вертикальный макет для центрального виджета
|
||||
self._central_layout = QVBoxLayout()
|
||||
self._central_widget.setLayout(self._central_layout)
|
||||
self.set_style(self)
|
||||
...
|
||||
|
||||
def set_style(self, obj: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
||||
"""
|
||||
Устанавливает стиль для переданного объекта.
|
||||
"""
|
||||
obj.setStyleSheet(dark_style)
|
||||
|
||||
|
||||
class BasePointPassportFormer:
|
||||
"""
|
||||
Базовый класс для формирования паспортов точек.
|
||||
|
||||
Содержит базовые настройки, список этапов, кэш для идеальных данных и набор параметров для алгоритма.
|
||||
"""
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._clear_stage: str = "Welding"
|
||||
self._settings = Settings()
|
||||
self._stages: List[str] = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncomming"
|
||||
]
|
||||
self._tesla_stages: List[str] = [
|
||||
"Tesla squeeze",
|
||||
"Tesla closing",
|
||||
"Tesla welding",
|
||||
"Tesla oncomming_relief"
|
||||
]
|
||||
self._ideal_data_cache = LRUCache(maxsize=1000)
|
||||
self._OptAlgorithm_operator_params = [
|
||||
"dist_open_start_1",
|
||||
"dist_open_start_2",
|
||||
"dist_open_after_1",
|
||||
"dist_open_after_2",
|
||||
"dist_open_end_1",
|
||||
"dist_open_end_2",
|
||||
"dist_close_end_1",
|
||||
"dist_close_end_2",
|
||||
"time_command",
|
||||
"time_robot_movement",
|
||||
"object_thickness",
|
||||
"force_target",
|
||||
"force_capture",
|
||||
"time_wielding"
|
||||
]
|
||||
self._OptAlgorithm_system_params = [
|
||||
"a_max_1",
|
||||
"v_max_1",
|
||||
"a_max_2",
|
||||
"v_max_2",
|
||||
"mass_1",
|
||||
"mass_2",
|
||||
"k_hardness_1",
|
||||
"k_hardness_2",
|
||||
"torque_max_1",
|
||||
"torque_max_2",
|
||||
"transmission_ratio_1",
|
||||
"transmission_ratio_2",
|
||||
"position_start_1",
|
||||
"position_start_2",
|
||||
"k_prop",
|
||||
"time_capture"
|
||||
]
|
||||
|
||||
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||
"""
|
||||
Формирует паспорта для набора данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def form_customer_passport(self, data: List[Union[pd.DataFrame, dict]]) -> None:
|
||||
"""
|
||||
Формирует паспорт заказчика на основе DataFrame и событий.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, params: List) -> None:
|
||||
"""
|
||||
Обновляет настройки для формирования паспортов.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def opt_algorithm(self) -> BaseIdealDataBuilder:
|
||||
return self._opt_algorithm
|
||||
|
||||
@opt_algorithm.setter
|
||||
def opt_algorithm(self, opt_algorithm: BaseIdealDataBuilder) -> None:
|
||||
self._opt_algorithm = opt_algorithm
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
class BaseRawTraceProcessor:
|
||||
"""
|
||||
Базовый класс для обработки сырых трассировочных данных.
|
||||
"""
|
||||
def __init__(self,
|
||||
dataparser: BaseKukaDataParser,
|
||||
textparser: BaseKukaTextParser,
|
||||
data_detector: BaseTraceStageDetector,
|
||||
text_detector: BaseTextStageDetector,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._dataparser = dataparser
|
||||
self._textparser = textparser
|
||||
self._data_detector = data_detector
|
||||
self._text_detector = text_detector
|
||||
self._trace_df: Optional[pd.DataFrame] = None
|
||||
self._text_data: Optional[Any] = None
|
||||
|
||||
def prerender(self, data: List[str]) -> None:
|
||||
"""
|
||||
Выполняет предварительный рендеринг данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def final_render(self) -> None:
|
||||
"""
|
||||
Выполняет финальный рендеринг данных.
|
||||
"""
|
||||
...
|
||||
|
||||
def update_settings(self, data: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки обработки трассировочных данных.
|
||||
"""
|
||||
...
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
class BaseKukaDataParser:
|
||||
"""
|
||||
Базовый класс для парсинга данных Кука.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._ch_name: Optional[str] = None
|
||||
|
||||
def parse(self, head_path: str) -> pd.DataFrame:
|
||||
"""
|
||||
Парсит файл заголовка и возвращает DataFrame с данными.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseKukaTextParser:
|
||||
"""
|
||||
Базовый класс для парсинга текстовых данных Кука.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._in_msg = None
|
||||
self._datapacks = None
|
||||
|
||||
def parse(self, path: str) -> List[KukaTXT]:
|
||||
"""
|
||||
Парсит текстовый файл и возвращает список объектов KukaTXT.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseTraceStageDetector:
|
||||
"""
|
||||
Базовый класс для детекции стадий на основе данных трассировки.
|
||||
"""
|
||||
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||
self._parent = parent
|
||||
|
||||
def detect_stages(self, df: pd.DataFrame) -> List:
|
||||
"""
|
||||
Детектирует стадии процесса по данным DataFrame.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class BaseTextStageDetector:
|
||||
"""
|
||||
Базовый класс для детекции сварочных стадий на основе текстовых данных.
|
||||
"""
|
||||
def __init__(self, parent: Optional[BaseRawTraceProcessor] = None):
|
||||
self._parent = parent
|
||||
|
||||
def detect_welding(self, data: List[KukaTXT]) -> List:
|
||||
"""
|
||||
Детектирует этап сварки по текстовым данным.
|
||||
"""
|
||||
...
|
||||
BIN
src/controller/__pycache__/controller.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/controller.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/converter.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/converter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/ideal_data_builder.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/ideal_data_builder.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/mediator.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/mediator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/monitor.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/monitor.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/monitor.cpython-311.pyc
Normal file
BIN
src/controller/__pycache__/monitor.cpython-311.pyc
Normal file
Binary file not shown.
BIN
src/controller/__pycache__/passportFormer.cpython-310.pyc
Normal file
BIN
src/controller/__pycache__/passportFormer.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,102 +1,43 @@
|
||||
from typing import Tuple
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from typing import Union
|
||||
|
||||
from base.base import BaseController, Settings
|
||||
from utils.base.base import BaseController
|
||||
|
||||
|
||||
class Controller(BaseController):
|
||||
"""
|
||||
Контроллер приложения.
|
||||
|
||||
Отвечает за управление сигналами для обновления интерфейса (виджеты, статус, прогресс),
|
||||
установку рабочего режима, а также делегирует действия таким модулям, как файловый менеджер,
|
||||
медиатор и т.д.
|
||||
"""
|
||||
|
||||
signal_widgets = pyqtSignal(list)
|
||||
signal_progress_bar = pyqtSignal(int)
|
||||
signal_status_text = pyqtSignal(str)
|
||||
|
||||
def set_working_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает рабочий режим приложения через медиатор.
|
||||
|
||||
:param mode: Режим работы (целое число).
|
||||
"""
|
||||
self._mediator.set_mode(mode)
|
||||
|
||||
def update_plots(self) -> None:
|
||||
"""
|
||||
Инициирует переотрисовку графиков через файловый менеджер.
|
||||
"""
|
||||
self._file_manager.replot_all()
|
||||
signal_settings = pyqtSignal(list)
|
||||
signal_open_file = pyqtSignal(str)
|
||||
signal_statusBar = pyqtSignal(int)
|
||||
signal_statusText = pyqtSignal(str)
|
||||
signal_raport_mode = pyqtSignal()
|
||||
signal_seeking_mode = pyqtSignal()
|
||||
signal_update_plots = pyqtSignal()
|
||||
|
||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
||||
"""
|
||||
Отправляет список виджетов для дальнейшего использования.
|
||||
|
||||
:param widgets: Список виджетов.
|
||||
"""
|
||||
self.signal_widgets.emit(widgets)
|
||||
|
||||
def update_settings(self, settings: 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 raport_mode(self) -> None:
|
||||
self.signal_raport_mode.emit()
|
||||
|
||||
def seeking_mode(self) -> None:
|
||||
self.signal_seeking_mode.emit()
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
"""
|
||||
Открывает файл по указанному пути через файловый менеджер.
|
||||
self.signal_open_file.emit(filepath)
|
||||
|
||||
:param filepath: Путь к файлу.
|
||||
"""
|
||||
self._file_manager.open_custom_file(filepath)
|
||||
def update_plots(self) -> None:
|
||||
self.signal_update_plots.emit()
|
||||
|
||||
def open_dir(self, dirpath: str) -> None:
|
||||
"""
|
||||
Инициирует предварительный рендеринг данных из директории.
|
||||
|
||||
: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)
|
||||
def update_status(self, msg: Union[str, float, int]) -> None:
|
||||
if type(msg) == float or type(msg) == int:
|
||||
self.signal_statusBar.emit(int(msg))
|
||||
else:
|
||||
self.signal_statusText.emit(msg)
|
||||
|
||||
|
||||
@ -1,104 +1,35 @@
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from base.base import BaseDataConverter
|
||||
|
||||
# FIXME: костыль для выключения предупреждения "replace deprecated"
|
||||
pd.set_option('future.no_silent_downcasting', True)
|
||||
|
||||
from utils.base.base import BaseDataConverter
|
||||
|
||||
|
||||
class DataConverter(BaseDataConverter):
|
||||
"""
|
||||
Класс для преобразования данных из CSV-файлов.
|
||||
|
||||
Основные задачи:
|
||||
- Исправление заголовков столбцов по заданному шаблону.
|
||||
- Преобразование булевых значений в целочисленные.
|
||||
- Обработка ошибок при конвертации и уведомление через медиатора.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _replace_bool(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Преобразует столбцы, содержащие булевы значения, в целочисленные.
|
||||
|
||||
:param df: DataFrame с исходными данными.
|
||||
:return: DataFrame с преобразованными булевыми столбцами или None, если произошла ошибка.
|
||||
"""
|
||||
def _replace_bool(dataframe: pd.DataFrame) -> pd.DataFrame:
|
||||
try:
|
||||
# Находим столбцы, в которых все значения являются True или False
|
||||
bool_columns = df.columns[df.isin([True, False]).all()]
|
||||
# Приводим найденные столбцы к типу int
|
||||
df = df.astype({col: int for col in bool_columns})
|
||||
return df
|
||||
except AttributeError as e:
|
||||
logger.warning(f"_replace_bool - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||
return None
|
||||
except (TypeError, ValueError) as e:
|
||||
logger.error(f"_replace_bool - Ошибка типа или значения: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"_replace_bool - Непредвиденная ошибка: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _fix_headers(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
Приводит заголовки столбцов DataFrame к корректным именам согласно заранее заданному списку.
|
||||
|
||||
:param df: DataFrame с исходными заголовками.
|
||||
:return: DataFrame с исправленными заголовками или None, если произошла ошибка.
|
||||
"""
|
||||
correct_columns = [
|
||||
"time", "Closing", "Electrode Force, N FE", "Electrode Force, N ME",
|
||||
"Force Control FE", "Force Control ME", "Hold Position ME", "Oncomming",
|
||||
"Position Control FE", "Position Control ME", "Relief",
|
||||
"Rotor Position, mm FE", "Rotor Position, mm ME",
|
||||
"Rotor Speed, mm/s FE", "Rotor Speed, mm/s ME",
|
||||
"Squeeze", "Welding", "Welding Current ME", "Welding Voltage ME"
|
||||
]
|
||||
try:
|
||||
# Создаем словарь соответствий: ключ - имя в нижнем регистре, значение - корректное имя
|
||||
correct_mapping = {name.lower(): name for name in correct_columns}
|
||||
new_columns = []
|
||||
# Для каждого столбца заменяем имя, если оно присутствует в корректном отображении
|
||||
for col in df.columns:
|
||||
fixed_col = correct_mapping.get(col.lower(), col)
|
||||
new_columns.append(fixed_col)
|
||||
df.columns = new_columns
|
||||
# Удаляем повторяющиеся столбцы, оставляя первое вхождение
|
||||
df = df.loc[:, ~df.columns.duplicated()]
|
||||
return df
|
||||
except AttributeError as e:
|
||||
logger.warning(f"_fix_headers - AttributeError: Проверьте, что переданный объект является DataFrame. {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"_fix_headers - Непредвиденная ошибка: {e}")
|
||||
bool_columns = dataframe.columns[dataframe.isin([True, False]).any()]
|
||||
dataframe[bool_columns] = dataframe[bool_columns].replace({True: 1, False: 0})
|
||||
return dataframe
|
||||
except:
|
||||
return None
|
||||
|
||||
def convert_data(self, files: list[str]) -> None:
|
||||
"""
|
||||
Считывает данные из списка CSV-файлов, исправляет заголовки и преобразует булевые столбцы,
|
||||
после чего уведомляет медиатора о завершении преобразования.
|
||||
|
||||
:param files: Список путей к CSV-файлам.
|
||||
"""
|
||||
try:
|
||||
# Считываем данные из файлов (если путь не пустой, иначе None)
|
||||
dataframes = [pd.read_csv(file) if file != '' else None for file in files]
|
||||
# Применяем исправление заголовков к каждому DataFrame
|
||||
renamed_dataframes = list(map(self._fix_headers, dataframes))
|
||||
# Преобразуем булевые значения для каждого DataFrame
|
||||
converted_dataframes = list(map(self._replace_bool, renamed_dataframes))
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"convert_data - FileNotFoundError: Файл не найден. {e}")
|
||||
converted_dataframes = [None]
|
||||
except pd.errors.EmptyDataError as e:
|
||||
logger.error(f"convert_data - EmptyDataError: Файл пустой. {e}")
|
||||
converted_dataframes = [None]
|
||||
except Exception as e:
|
||||
logger.error(f"convert_data - Непредвиденная ошибка: {e}")
|
||||
converted_dataframes = list(map(self._replace_bool, dataframes))
|
||||
except:
|
||||
# Get the traceback object
|
||||
tb = sys.exc_info()[2]
|
||||
tbinfo = traceback.format_tb(tb)[0]
|
||||
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
|
||||
logger.error(pymsg)
|
||||
converted_dataframes = [None]
|
||||
finally:
|
||||
# Передаем результат медиатору
|
||||
self._mediator.notify(self, converted_dataframes)
|
||||
|
||||
@ -1,189 +0,0 @@
|
||||
import os
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from base.base import BaseDirectoryMonitor, BaseFileManager, Settings
|
||||
|
||||
|
||||
class FileManager(BaseFileManager):
|
||||
"""
|
||||
Менеджер файлов для работы с библиотекой путей.
|
||||
|
||||
Основные функции:
|
||||
- Переотправка (replot) списка найденных файлов.
|
||||
- Добавление нового пути (например, выбранного пользователем).
|
||||
- Переключение режимов работы (создание отчёта, онлайн-мониторинг, работа с трейсами клиента).
|
||||
- Обновление настроек мониторинга.
|
||||
- Поиск файлов в директории с трейсами: поиск файлов .dat и .txt.
|
||||
"""
|
||||
|
||||
def replot_all(self) -> None:
|
||||
"""
|
||||
Переотправляет все пути из библиотеки.
|
||||
|
||||
Если текущий режим не равен 3, уведомляет медиатора со списком путей.
|
||||
Если режим равен 3, открывает директорию с трейсами.
|
||||
"""
|
||||
if self._paths_library is not None:
|
||||
if self._mode != 3:
|
||||
self._mediator.notify(self, list(self._paths_library))
|
||||
else:
|
||||
self.open_raw_traces_dir(list(self._paths_library)[0])
|
||||
|
||||
def open_custom_file(self, path: str) -> None:
|
||||
"""
|
||||
Добавляет указанный путь в библиотеку и уведомляет медиатора.
|
||||
|
||||
:param path: Путь к файлу, выбранному пользователем.
|
||||
"""
|
||||
self._paths_library.add(path)
|
||||
# Передаем уведомление с указанным путем (если путь пустой, передаем пустую строку)
|
||||
self._mediator.notify(self, [path if path else ''])
|
||||
|
||||
def set_mode(self, num: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы менеджера файлов.
|
||||
|
||||
Режимы:
|
||||
1 - Режим создания отчёта.
|
||||
2 - Режим онлайн-мониторинга папки.
|
||||
3 - Режим работы с трейсами клиента.
|
||||
|
||||
:param num: Целое число, определяющее режим работы.
|
||||
"""
|
||||
match num:
|
||||
case 1:
|
||||
# Режим создания отчёта
|
||||
self._monitor.stop()
|
||||
self._paths_library.clear()
|
||||
self._paths_library.add('')
|
||||
self._mediator.notify(self.monitor, list(self._paths_library))
|
||||
self._mode = 1
|
||||
case 2:
|
||||
# Режим онлайн-мониторинга папки
|
||||
self._monitor.init_state()
|
||||
self._monitor.start()
|
||||
self._mode = 2
|
||||
case 3:
|
||||
# Режим работы с трейсами клиента
|
||||
self._monitor.stop()
|
||||
self._paths_library.clear()
|
||||
self._mode = 3
|
||||
|
||||
def update_monitor_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки мониторинга директории.
|
||||
|
||||
Из объекта настроек извлекается путь к директории и интервал обновления.
|
||||
Выполняется проверка существования директории и корректности интервала обновления.
|
||||
При необходимости приостанавливается или возобновляется монитор.
|
||||
|
||||
:param settings: Объект Settings с системными настройками.
|
||||
"""
|
||||
directory_path = settings.system['trace_storage_path'][0]
|
||||
update_time = settings.system['monitor_update_period'][0]
|
||||
|
||||
if not os.path.exists(directory_path):
|
||||
logger.warning(f"Путь {directory_path} не существует.")
|
||||
# Можно раскомментировать raise, если требуется прерывание работы
|
||||
# raise FileNotFoundError(f"Путь {directory_path} не существует.")
|
||||
|
||||
if update_time <= 0.01:
|
||||
logger.warning(f"Интервал между проверками папки слишком мал: {update_time}")
|
||||
|
||||
if self._monitor.isActive:
|
||||
self._monitor.pause()
|
||||
self._monitor._directory_path = directory_path
|
||||
self._monitor._update_time = update_time
|
||||
if self._monitor.isActive:
|
||||
self._monitor.start()
|
||||
|
||||
def add_new_paths(self, paths: list) -> None:
|
||||
"""
|
||||
Добавляет новые пути файлов в библиотеку, если они ранее не присутствовали,
|
||||
и уведомляет медиатора о новых путях.
|
||||
|
||||
:param paths: Список новых путей.
|
||||
"""
|
||||
paths_set = set(paths)
|
||||
new_paths = self._paths_library.difference(paths_set)
|
||||
self._paths_library.update(new_paths)
|
||||
self._mediator.notify(self, list(new_paths))
|
||||
|
||||
def open_raw_traces_dir(self, path: str) -> None:
|
||||
"""
|
||||
Открывает указанную директорию, ищет в ней файлы с расширениями .dat и .txt,
|
||||
и при их наличии инициирует предварительный рендеринг через медиатора.
|
||||
|
||||
:param path: Путь к директории с трейсами.
|
||||
"""
|
||||
self._paths_library.clear()
|
||||
self._paths_library.add(path)
|
||||
dat_file, txt_file = None, None
|
||||
|
||||
# Перебираем содержимое директории
|
||||
for entry in os.listdir(path):
|
||||
full_path = os.path.join(path, entry)
|
||||
if os.path.isfile(full_path):
|
||||
_, ext = os.path.splitext(entry)
|
||||
ext = ext.lower()
|
||||
if ext == '.dat' and dat_file is None:
|
||||
dat_file = full_path
|
||||
elif ext == '.txt' and txt_file is None:
|
||||
txt_file = full_path
|
||||
if dat_file and txt_file:
|
||||
break
|
||||
|
||||
if dat_file and txt_file:
|
||||
# Уведомляем медиатора для запуска предварительного рендеринга трейс-контента
|
||||
self._mediator.prerender_TCW(self, [dat_file, txt_file])
|
||||
|
||||
|
||||
class DirectoryMonitor(BaseDirectoryMonitor):
|
||||
"""
|
||||
Мониторинг директории для поиска CSV-файлов.
|
||||
|
||||
Инициализирует состояние, подключает таймер обновления и отслеживает появление новых файлов.
|
||||
При обнаружении новых файлов уведомляет менеджер файлов.
|
||||
"""
|
||||
|
||||
def init_state(self):
|
||||
"""
|
||||
Инициализирует состояние мониторинга директории.
|
||||
|
||||
Проверяет, существует ли директория. Если директория не существует, генерирует ошибку.
|
||||
Собирает список файлов с расширением .csv и подключает метод мониторинга к таймеру.
|
||||
"""
|
||||
if not os.path.exists(self._directory_path):
|
||||
logger.error(f"Путь {self._directory_path} не существует.")
|
||||
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
||||
|
||||
self._files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
|
||||
self.update_timer.timeout.connect(self._monitor)
|
||||
logger.info("Monitor initiated!")
|
||||
|
||||
def _monitor(self):
|
||||
"""
|
||||
Метод, вызываемый по таймеру, для проверки появления новых CSV-файлов.
|
||||
|
||||
Если обнаружены новые файлы, уведомляет менеджер файлов и обновляет список файлов.
|
||||
Если в директории отсутствуют файлы, сбрасывает список.
|
||||
"""
|
||||
current_files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
# Определяем файлы, которых еще не было в предыдущем списке
|
||||
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
||||
if new_files:
|
||||
logger.info(f"New files detected: {new_files}")
|
||||
self._file_manager.add_new_paths(new_files)
|
||||
self._files = current_files
|
||||
if not current_files:
|
||||
self._files = []
|
||||
@ -1,127 +1,41 @@
|
||||
from typing import Union
|
||||
|
||||
import pandas as pd
|
||||
import time
|
||||
|
||||
from typing import Union
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
|
||||
from base.base import (
|
||||
BaseMediator,
|
||||
BaseFileManager,
|
||||
BaseDataConverter,
|
||||
BasePlotWidget,
|
||||
BasePointPassportFormer,
|
||||
BaseController,
|
||||
GraphicPassport,
|
||||
Settings,
|
||||
BaseRawTraceProcessor
|
||||
)
|
||||
from utils.base.base import (BaseMediator, BaseDirectoryMonitor,
|
||||
BaseDataConverter, BasePlotWidget,
|
||||
BasePointPassportFormer)
|
||||
|
||||
|
||||
class Mediator(BaseMediator):
|
||||
"""
|
||||
Медиатор для организации взаимодействия между различными компонентами приложения.
|
||||
|
||||
Компоненты (менеджеры, конвертеры, контроллер, виджеты, процессоры) регистрируются
|
||||
и вызываются через медиатора, который распределяет уведомления и передаваемые данные.
|
||||
"""
|
||||
|
||||
def notify(
|
||||
self,
|
||||
source: Union[
|
||||
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...")
|
||||
def notify(self,
|
||||
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
|
||||
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
|
||||
|
||||
if issubclass(source.__class__, BaseDirectoryMonitor):
|
||||
self.update_status("CSV found! Calculating...")
|
||||
self._converter.convert_data(data)
|
||||
|
||||
# Если источник – конвертер данных: обновляем прогресс и формируем паспорта
|
||||
if isinstance(source, BaseDataConverter):
|
||||
self._controller.update_progress(1)
|
||||
self._passport_former.form_passports(data)
|
||||
if issubclass(source.__class__, BaseDataConverter):
|
||||
self.update_status(0.5)
|
||||
self._passportFormer.form_passports(data)
|
||||
|
||||
# Если источник – формирователь паспортов точек: обновляем прогресс и строим графический паспорт
|
||||
if isinstance(source, BasePointPassportFormer):
|
||||
self._controller.update_progress(10)
|
||||
if issubclass(source.__class__, BasePointPassportFormer):
|
||||
self.update_status(1)
|
||||
self._plot.build(data)
|
||||
|
||||
# Если источник – виджет для построения графиков: завершаем построение и передаём виджеты контроллеру
|
||||
if isinstance(source, BasePlotWidget):
|
||||
self._controller.update_progress(100)
|
||||
if issubclass(source.__class__, BasePlotWidget):
|
||||
self.update_status(100)
|
||||
self._controller.send_widgets(data)
|
||||
|
||||
# Если источник – контроллер: обновляем настройки у различных модулей
|
||||
if isinstance(source, BaseController):
|
||||
self._file_manager.update_monitor_settings(data)
|
||||
self._passport_former.update_settings(data)
|
||||
self._trace_processor.update_settings(data)
|
||||
|
||||
def 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)
|
||||
|
||||
if isinstance(source, BaseFileManager):
|
||||
self._controller.update_progress(15)
|
||||
self._trace_processor.prerender(data)
|
||||
def update_settings(self, settings: list[dict]):
|
||||
self._monitor.update_settings(settings)
|
||||
self._passportFormer.update_settings(settings)
|
||||
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
self._controller.update_status(msg)
|
||||
|
||||
if isinstance(source, BaseRawTraceProcessor):
|
||||
self._controller.update_progress(40)
|
||||
self._plot.build_raw_trace(data)
|
||||
|
||||
def render_TCW(
|
||||
self,
|
||||
source: Union[BaseController, BaseRawTraceProcessor],
|
||||
data: Union[str, list[pd.DataFrame], dict] = None
|
||||
) -> None:
|
||||
"""
|
||||
Выполняет финальный рендеринг TCW.
|
||||
|
||||
:param source: Компонент, инициирующий финальный рендеринг.
|
||||
:param data: Передаваемые данные (опционально).
|
||||
"""
|
||||
if isinstance(source, BaseController):
|
||||
self._controller.update_progress(5)
|
||||
self._trace_processor.final_render()
|
||||
|
||||
if isinstance(source, BaseRawTraceProcessor):
|
||||
self._controller.update_progress(5)
|
||||
self._passport_former.form_customer_passport(data)
|
||||
|
||||
def set_mode(self, mode: int) -> None:
|
||||
"""
|
||||
Устанавливает режим работы для графиков и файлового менеджера.
|
||||
|
||||
:param mode: Целое число, определяющее режим работы.
|
||||
"""
|
||||
self._plot.set_mode(mode)
|
||||
self._file_manager.set_mode(mode)
|
||||
|
||||
72
src/controller/monitor.py
Normal file
72
src/controller/monitor.py
Normal file
@ -0,0 +1,72 @@
|
||||
import os
|
||||
from loguru import logger
|
||||
|
||||
from utils.base.base import BaseDirectoryMonitor
|
||||
|
||||
|
||||
class DirectoryMonitor(BaseDirectoryMonitor):
|
||||
|
||||
def _init_state(self):
|
||||
|
||||
self._files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
|
||||
self.update_timer.timeout.connect(self._monitor)
|
||||
logger.info("Monitor initiated!")
|
||||
|
||||
def _monitor(self):
|
||||
current_files = [
|
||||
os.path.join(self._directory_path, file)
|
||||
for file in os.listdir(self._directory_path)
|
||||
if file.lower().endswith('.csv')
|
||||
]
|
||||
new_files = sorted(list(filter(lambda x: x not in self._files, current_files)))
|
||||
if new_files:
|
||||
logger.info(f"New files detected: {new_files}")
|
||||
self._mediator.notify(self, new_files)
|
||||
self._files = current_files
|
||||
if not current_files:
|
||||
self._files = []
|
||||
|
||||
def update_settings(self, data: list[dict]) -> None:
|
||||
if self.isActive:
|
||||
self.stop()
|
||||
_, system_params = data
|
||||
self._directory_path = system_params['trace_storage_path'][0]
|
||||
self._update_time = system_params['monitor_update_period'][0]
|
||||
|
||||
if not os.path.exists(self._directory_path):
|
||||
logger.error(f"Путь {self._directory_path} не существует.")
|
||||
raise FileNotFoundError(f"Путь {self._directory_path} не существует.")
|
||||
else:
|
||||
self._init_state()
|
||||
self.start()
|
||||
else:
|
||||
_, system_params = data
|
||||
self._directory_path = system_params['trace_storage_path'][0]
|
||||
self._update_time = system_params['monitor_update_period'][0]
|
||||
|
||||
def update_plots(self):
|
||||
if self._files is not None:
|
||||
self._mediator.notify(self, self._files)
|
||||
|
||||
def custom_csv_extract_only(self, path: str):
|
||||
self._files.append(path)
|
||||
if path is not None:
|
||||
self._mediator.notify(self, [path])
|
||||
else:
|
||||
self._mediator.notify(self, [''])
|
||||
|
||||
def start_raport(self) -> None:
|
||||
self.stop()
|
||||
self._files = ['']
|
||||
self._mediator.notify(self, [''])
|
||||
|
||||
|
||||
def start_seeking(self) -> None:
|
||||
self._init_state()
|
||||
self.start()
|
||||
|
||||
126
src/controller/passportFormer.py
Normal file
126
src/controller/passportFormer.py
Normal file
@ -0,0 +1,126 @@
|
||||
import pandas as pd
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
from utils.base.base import BasePointPassportFormer, BaseIdealDataBuilder
|
||||
|
||||
|
||||
class idealDataBuilder(BaseIdealDataBuilder):
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
data = []
|
||||
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow']-0.0001)
|
||||
X1, X2, V1, V2 = X1*1000, X2*1000, V1*1000, V2*1000
|
||||
data.append({"time":0, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
|
||||
data.append({"time":self.welding_time, "Position FE":X1,"Position ME":X2, "Rotor Speed FE":V1, "Rotor Speed ME":V2, "Force":F})
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def get_ideal_timings(self) -> list[float]:
|
||||
data = self.Ts
|
||||
ideal_timings = [data['tclose'], data['tgrow'], self.welding_time, self.getMarkOpen(), data['tmovement']]
|
||||
return ideal_timings
|
||||
|
||||
class PassportFormer(BasePointPassportFormer):
|
||||
|
||||
def form_passports(self, data: list[pd.DataFrame]) -> list[list[pd.DataFrame, dict, int]]:
|
||||
try:
|
||||
return_data = [self._build_passports_pocket(dataframe) for dataframe in data]
|
||||
except:
|
||||
tb = sys.exc_info()[2]
|
||||
tbinfo = traceback.format_tb(tb)[0]
|
||||
pymsg = "Traceback info:\n" + tbinfo + "\nError Info:\n" + str(sys.exc_info()[1])
|
||||
logger.error(pymsg)
|
||||
return_data = []
|
||||
finally:
|
||||
self._mediator.notify(self, return_data)
|
||||
|
||||
|
||||
def _build_passports_pocket(self, dataframe: pd.DataFrame) -> list[pd.DataFrame, dict, int]:
|
||||
|
||||
if dataframe is not None:
|
||||
events, point_quantity = self._filter_events(dataframe["time"], dataframe)
|
||||
if point_quantity == 0:
|
||||
return []
|
||||
idx_shift = True if events[self._stages[-1]][0][0] == 0 else False
|
||||
else:
|
||||
events = None
|
||||
key = list(self._params[0].keys())[0]
|
||||
point_quantity = len(self._params[0][key])
|
||||
|
||||
points_pocket = []
|
||||
system_settings = {key: value[0] for key, value in self._params[1].items()}
|
||||
tesla_time = sum(self._params[0].get("Tesla summary time", []))
|
||||
useful_data = {
|
||||
"tesla_time": tesla_time,
|
||||
"range": system_settings["gun_range"],
|
||||
"k_hardness": system_settings["k_hardness_1"]
|
||||
}
|
||||
|
||||
for i in range(point_quantity):
|
||||
operator_settings = {
|
||||
key: (value[i] if i < len(value) else value[0])
|
||||
for key, value in self._params[0].items()
|
||||
}
|
||||
|
||||
next_i = i + 1
|
||||
|
||||
P1 = operator_settings["object_position"] + 0.5 * operator_settings["object_thickness"]
|
||||
P2 = operator_settings["object_position"] - 0.5 * operator_settings["object_thickness"]
|
||||
|
||||
if next_i < point_quantity:
|
||||
next_operator_settings = {
|
||||
key: (value[next_i] if i < len(value) else value[0])
|
||||
for key, value in self._params[0].items()
|
||||
}
|
||||
|
||||
next_P1 = next_operator_settings["object_position"] + 0.5*next_operator_settings["object_thickness"]
|
||||
next_P2 = next_operator_settings["object_position"] - 0.5*next_operator_settings["object_thickness"]
|
||||
|
||||
displacement_me = next_P1 - P1
|
||||
displacement_fe = next_P2 - P2
|
||||
|
||||
operator_settings["distance_h_end1"] -= displacement_fe
|
||||
operator_settings["distance_h_end2"] += displacement_me
|
||||
|
||||
LFEAbs = operator_settings["distance_l_1"]
|
||||
LMEAbs = operator_settings["distance_l_2"]
|
||||
|
||||
operator_settings["distance_l_1"] = P2 - LFEAbs
|
||||
operator_settings["distance_l_2"] = LMEAbs - P1
|
||||
|
||||
params_list = [operator_settings, system_settings]
|
||||
cache_key = self._generate_cache_key(params_list)
|
||||
if cache_key in self._ideal_data_cashe:
|
||||
ideal_data = self._ideal_data_cashe[cache_key]
|
||||
else:
|
||||
ideal_data = self._build_ideal_data(idealDataBuilder=idealDataBuilder, params=params_list)
|
||||
self._ideal_data_cashe[cache_key] = ideal_data
|
||||
if events is not None:
|
||||
idx = i+1 if idx_shift else i
|
||||
point_timeframe = [events[self._stages[0]][0][i], events[self._stages[-1]][1][idx]]
|
||||
point_events = {key: [value[0][i], value[1][i]] for key, value in events.items()}
|
||||
else:
|
||||
point_timeframe, point_events = None, None
|
||||
useful_p_data = {"thickness": operator_settings["object_thickness"],
|
||||
"position": operator_settings["object_position"]-0.5*operator_settings["object_thickness"],
|
||||
"force": operator_settings["force_target"],
|
||||
"P1": operator_settings["object_position"] + 0.5*operator_settings["object_thickness"],
|
||||
"P2": operator_settings["object_position"] - 0.5*operator_settings["object_thickness"]}
|
||||
|
||||
points_pocket.append([point_timeframe, ideal_data, point_events, useful_p_data])
|
||||
return dataframe, points_pocket, useful_data
|
||||
|
||||
def update_settings(self, params: list[dict, dict]):
|
||||
self._params = params
|
||||
@ -1,544 +0,0 @@
|
||||
from typing import Optional, Any, Tuple, List, Dict
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from base.base import (
|
||||
BasePointPassportFormer,
|
||||
BaseIdealDataBuilder,
|
||||
PointPassport,
|
||||
GraphicPassport,
|
||||
Settings,
|
||||
UsefulGraphData,
|
||||
PerformanceData
|
||||
)
|
||||
|
||||
|
||||
class PassportFormer(BasePointPassportFormer):
|
||||
"""
|
||||
Класс для формирования паспортов (графических и точечных) по данным трассировки.
|
||||
|
||||
Основные возможности:
|
||||
- Формирование паспортов для каждого DataFrame.
|
||||
- Формирование паспорта заказчика на основе DataFrame и событий.
|
||||
- Генерация событий по DataFrame.
|
||||
- Построение идеальных данных для каждой точки.
|
||||
- Формирование графического паспорта, включающего полезные данные и перечень паспортов точек.
|
||||
|
||||
Внимание: многие атрибуты (например, self._mediator, self._stages, self._clear_stage,
|
||||
self._settings, self._ideal_data_cache, self._OptAlgorithm_operator_params,
|
||||
self._OptAlgorithm_system_params) предполагается задавать извне (например, в базовом классе).
|
||||
"""
|
||||
|
||||
def form_passports(self, data: List[pd.DataFrame]) -> None:
|
||||
"""
|
||||
Формирует паспорта для каждого DataFrame из списка.
|
||||
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||
|
||||
:param data: Список DataFrame с данными трассировки.
|
||||
"""
|
||||
try:
|
||||
passports = [self._build_from_df_only(df) for df in data]
|
||||
except Exception as e:
|
||||
logger.error(f"form_passports - Непредвиденная ошибка при формировании паспортов: {e}")
|
||||
passports = []
|
||||
finally:
|
||||
self._mediator.notify(self, passports)
|
||||
|
||||
def update_settings(self, settings: Settings) -> None:
|
||||
"""
|
||||
Обновляет настройки формирования паспортов.
|
||||
|
||||
:param settings: Объект настроек.
|
||||
"""
|
||||
self._settings = settings
|
||||
|
||||
def form_customer_passport(self, data: Tuple[pd.DataFrame, Dict, Tuple]) -> None:
|
||||
"""
|
||||
Формирует паспорт заказчика на основе DataFrame и словаря событий.
|
||||
Ожидается, что data содержит:
|
||||
- DataFrame с данными,
|
||||
- словарь событий,
|
||||
- кортеж координат ME (например, (part_pos, open_pos)).
|
||||
|
||||
В случае ошибки логируется сообщение, а медиатору отправляется пустой список.
|
||||
|
||||
:param data: Кортеж из (DataFrame, события, ME_coords).
|
||||
"""
|
||||
try:
|
||||
dataframe, events, ME_coords = data
|
||||
point_quantity = len(events["Squeeze"][0])
|
||||
self._modify_coord_settings(ME_coords)
|
||||
customer_passport = self._form_graphic_passport(dataframe, events, point_quantity)
|
||||
result = [customer_passport]
|
||||
except Exception as e:
|
||||
logger.error(f"form_customer_passport - Непредвиденная ошибка при формировании паспорта заказчика: {e}")
|
||||
result = []
|
||||
finally:
|
||||
self._mediator.notify(self, result)
|
||||
|
||||
def _modify_coord_settings(self, ME_coords: Tuple[List[float], List[float]]) -> None:
|
||||
"""
|
||||
Модифицирует настройки координат на основе переданных данных ME.
|
||||
|
||||
:param ME_coords: Кортеж из двух списков: (part_pos, open_pos).
|
||||
"""
|
||||
part_pos, open_pos = ME_coords
|
||||
self._settings.operator["distance_l_2"] = open_pos
|
||||
l1 = self._settings.operator["distance_l_1"]
|
||||
self._settings.operator["part_pos"] = [part_pos[i] + l1[i] for i in range(len(part_pos))]
|
||||
|
||||
@staticmethod
|
||||
def _find_indexes(signal: str, df: pd.DataFrame) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Находит индексы начала и окончания этапа для указанного сигнала.
|
||||
|
||||
:param signal: Имя столбца-сигнала.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж из массивов индексов начала и окончания этапа.
|
||||
"""
|
||||
stage_diff = np.diff(df[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
@staticmethod
|
||||
def _find_events(signal: str, times: pd.Series, df: pd.DataFrame) -> Tuple[List[float], List[float]]:
|
||||
"""
|
||||
Формирует списки времён начала и окончания событий для указанного сигнала.
|
||||
|
||||
Если у первого события не определено время начала, оно принимается за 0.
|
||||
Если число стартов события больше числа финишей, последним финишем считается конец времён.
|
||||
|
||||
:param signal: Имя столбца-сигнала.
|
||||
:param times: Series с временными метками.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж из двух списков: (список времён начала, список времён окончания).
|
||||
"""
|
||||
start_idx, finish_idx = PassportFormer._find_indexes(signal, df)
|
||||
start_list, end_list = [], []
|
||||
if len(start_idx) > 0 and len(finish_idx) > 0:
|
||||
if start_idx[0] > finish_idx[0]:
|
||||
logger.debug(f"_find_events - не найдено начало события {signal} с окончанием в {times.iloc[finish_idx[0]]} секунд, принято равным 0")
|
||||
start_idx = np.insert(start_idx, 0, 0)
|
||||
if start_idx[-1] > finish_idx[-1]:
|
||||
logger.debug(f"_find_events - не найден конец события {signal} с началом в {times.iloc[start_idx[-1]]} секунд, принято равным {times.iloc[-1]} секунд")
|
||||
finish_idx = np.append(finish_idx, -1)
|
||||
start_list = times.iloc[start_idx].tolist()
|
||||
end_list = times.iloc[finish_idx].tolist()
|
||||
return (start_list, end_list)
|
||||
|
||||
def _generate_events(self, times: pd.Series, df: pd.DataFrame) -> Tuple[Dict[str, List[List[float]]], int]:
|
||||
"""
|
||||
Генерирует словарь событий для каждого этапа, используя временные метки и данные.
|
||||
Также определяет общее количество точек (на основе количества событий основного этапа).
|
||||
|
||||
Если для основного этапа (self._clear_stage) не найдено ни одного события, возвращается пустой словарь и 0 точек.
|
||||
|
||||
:param times: Серия временных меток.
|
||||
:param df: DataFrame с данными.
|
||||
:return: Кортеж (словарь событий, количество точек).
|
||||
"""
|
||||
events = {}
|
||||
point_quantity = 0
|
||||
if self._clear_stage in self._stages:
|
||||
start_list, _ = self._find_events(self._clear_stage, times, df)
|
||||
point_quantity = len(start_list)
|
||||
if point_quantity == 0:
|
||||
logger.error(f"_generate_events - Не найдены события для этапа '{self._clear_stage}'.")
|
||||
return {}, 0
|
||||
for stage in self._stages:
|
||||
s_list, e_list = self._find_events(stage, times, df)
|
||||
temp = max(len(s_list), len(e_list))
|
||||
if temp < point_quantity:
|
||||
logger.warning(f"_generate_events - Недостаточное количество событий для этапа '{stage}'. "
|
||||
f"Ожидается {point_quantity}, получено {temp}.")
|
||||
events[stage] = [s_list, e_list]
|
||||
|
||||
if self._settings.system["performance_mode"][0] == 'client':
|
||||
client_rob = self._settings.operator["time_robot_movement"]
|
||||
move_start = events["Oncomming"][0]
|
||||
if len(move_start) > len(client_rob):
|
||||
move_start = move_start[1:]
|
||||
events["Oncomming"] = [move_start, [client_rob[i]+ move_start[i] for i in range(len(move_start))]]
|
||||
return (events, point_quantity)
|
||||
|
||||
def _build_ideal_data(self, idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
||||
point_settings: Optional[Settings] = None) -> Dict:
|
||||
"""
|
||||
Строит идеальные данные с использованием билдера.
|
||||
|
||||
:param idealDataBuilder: Класс-билдер для генерации идеальных данных.
|
||||
:param point_settings: Настройки для точки.
|
||||
:return: Словарь с идеальными данными для этапов.
|
||||
"""
|
||||
try:
|
||||
self._opt_algorithm = idealDataBuilder(point_settings)
|
||||
stage_ideals = {
|
||||
"Closing": self.opt_algorithm.get_closingDF(),
|
||||
"Squeeze": self.opt_algorithm.get_compressionDF(),
|
||||
"Welding": self.opt_algorithm.get_weldingDF(),
|
||||
"Relief": self.opt_algorithm.get_openingDF(),
|
||||
"Oncomming": self.opt_algorithm.get_oncomingDF(),
|
||||
"Ideal cycle": self.opt_algorithm.get_cycle_time(),
|
||||
"Ideal timings": self.opt_algorithm.get_ideal_timings()
|
||||
}
|
||||
return stage_ideals
|
||||
except Exception as e:
|
||||
logger.error(f"_build_ideal_data - Ошибка при построении идеальных данных: {e}")
|
||||
return {}
|
||||
|
||||
def _generate_cache_key(self, point_settings: Settings) -> Tuple[Tuple[Tuple[str, Any], ...], Tuple[Tuple[str, Any], ...]]:
|
||||
"""
|
||||
Преобразует настройки точки в хешируемый ключ для кэша.
|
||||
|
||||
Использует только те параметры оператора и системы, которые присутствуют в
|
||||
соответствующих наборах (self._OptAlgorithm_operator_params и self._OptAlgorithm_system_params).
|
||||
|
||||
:param point_settings: Объект настроек для точки.
|
||||
:return: Кортеж из двух frozenset с параметрами.
|
||||
"""
|
||||
try:
|
||||
operator_tuple = frozenset(
|
||||
(key, value)
|
||||
for key, value in point_settings.operator.items()
|
||||
if str(key) in self._OptAlgorithm_operator_params
|
||||
)
|
||||
system_tuple = frozenset(
|
||||
(key, value)
|
||||
for key, value in point_settings.system.items()
|
||||
if str(key) in self._OptAlgorithm_system_params
|
||||
)
|
||||
return (operator_tuple, system_tuple)
|
||||
except Exception as e:
|
||||
logger.error(f"_generate_cache_key - Ошибка при генерации ключа кэша: {e}")
|
||||
return ((), ())
|
||||
|
||||
def _build_from_df_only(self, df: pd.DataFrame) -> Optional[GraphicPassport]:
|
||||
"""
|
||||
Строит GraphicPassport на основе одного DataFrame.
|
||||
|
||||
Если DataFrame содержит необходимые столбцы (определяемые в self._stages),
|
||||
генерируются события; иначе используется альтернативное определение количества точек.
|
||||
Если количество точек равно нулю, логируется ошибка и возвращается None.
|
||||
|
||||
:param df: DataFrame с данными.
|
||||
:return: Объект GraphicPassport или None в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
if df is not None and set(self._stages).issubset(set(df.columns.tolist())):
|
||||
events, point_quantity = self._generate_events(df["time"], df)
|
||||
if point_quantity == 0:
|
||||
logger.error("_build_from_df_only - Не найдено ни одного события в DataFrame.")
|
||||
return None
|
||||
else:
|
||||
events = None
|
||||
key = list(self._settings.operator.keys())[0]
|
||||
point_quantity = len(self._settings.operator[key])
|
||||
self._settings.operator["part_pos"] = self._settings.operator["distance_l_2"]
|
||||
passport = self._form_graphic_passport(df, events, point_quantity)
|
||||
return passport
|
||||
except Exception as e:
|
||||
logger.error(f"_build_from_df_only - Непредвиденная ошибка: {e}")
|
||||
return None
|
||||
|
||||
def _form_graphic_passport(self, df: pd.DataFrame, events: Dict, point_quantity: int) -> Optional[GraphicPassport]:
|
||||
"""
|
||||
Формирует графический паспорт, включающий полезные данные и список PointPassport.
|
||||
|
||||
Для каждой из точек:
|
||||
- Извлекаются настройки оператора для данной точки.
|
||||
- Формируется временной интервал и события.
|
||||
- Рассчитываются идеальные и полезные данные.
|
||||
- Создаётся объект PointPassport, который добавляется в список.
|
||||
|
||||
:param df: DataFrame с данными.
|
||||
:param events: Словарь событий, сгенерированных для этапов.
|
||||
:param point_quantity: Количество точек.
|
||||
:return: Объект GraphicPassport или None в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
system_settings = {key: value[0] for key, value in self._settings.system.items()}
|
||||
graphic_passport = GraphicPassport()
|
||||
graphic_passport.dataframe = df
|
||||
|
||||
for i in range(point_quantity):
|
||||
point_settings = Settings(self._get_operator_settings_part(i), system_settings)
|
||||
timeframe, po_events = self._form_point_events(events, i)
|
||||
if timeframe and po_events:
|
||||
point_settings.operator["time_robot_movement"] = po_events["Oncomming"][1] - po_events["Oncomming"][0]
|
||||
ideal_data = self._form_point_ideal_data(point_settings)
|
||||
useful_data = self._form_point_useful_data(point_settings.operator)
|
||||
point_passport = PointPassport(timeframe, po_events, ideal_data, useful_data)
|
||||
graphic_passport.points_pocket.append(point_passport)
|
||||
graphic_passport.useful_data = self._form_graphic_useful_data(system_settings, graphic_passport.points_pocket)
|
||||
return graphic_passport
|
||||
except Exception as e:
|
||||
logger.error(f"_form_graphic_passport - Ошибка при формировании графического паспорта: {e}")
|
||||
return None
|
||||
|
||||
def _form_graphic_useful_data(self, system_settings: Dict, points:List[PointPassport]) -> UsefulGraphData:
|
||||
"""
|
||||
Формирует словарь полезных данных для графического паспорта.
|
||||
|
||||
:param system_settings: Словарь системных настроек.
|
||||
:return: Объект UsefulGraphData.
|
||||
"""
|
||||
try:
|
||||
performance_data = PerformanceData()
|
||||
if points[0].timeframe is not None and points[0].events is not None:
|
||||
client_time = system_settings["client_time"]
|
||||
ideal_time = sum([sum(point.ideal_data["Ideal timings"])for point in points])
|
||||
TWC_time = (
|
||||
system_settings["time_before_start"] +
|
||||
points[0].timeframe[1] - points[0].events["Closing"][0] +
|
||||
sum([sum([item[1]-item[0] for key, item in point.events.items()]) for point in points[1:-1]]) +
|
||||
points[-1].events["Relief"][1] - points[-1].timeframe[0] +
|
||||
system_settings["time_after_end"]
|
||||
)
|
||||
performance_data.client_to_TWC = round((1 - TWC_time / client_time) * 100, 2) if client_time else 0.0
|
||||
performance_data.client_to_ideal = round((1 - ideal_time / client_time) * 100, 2) if client_time else 0.0
|
||||
performance_data.TWC_to_ideal = round((ideal_time / TWC_time) * 100, 2) if TWC_time else 0.0
|
||||
|
||||
useful_data = UsefulGraphData(
|
||||
performance_data,
|
||||
system_settings["Range ME, mm"],
|
||||
system_settings["k_hardness_1"]
|
||||
)
|
||||
return useful_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_graphic_useful_data - Ошибка при формировании полезных данных: {e}")
|
||||
return UsefulGraphData()
|
||||
|
||||
def _form_point_useful_data(self, operator_settings: Dict) -> Dict:
|
||||
"""
|
||||
Формирует полезные данные для отдельной точки.
|
||||
|
||||
:param operator_settings: Словарь настроек оператора для точки.
|
||||
:return: Словарь с полезными данными (толщина, позиция детали, сила) или пустой словарь.
|
||||
"""
|
||||
try:
|
||||
useful_data = {
|
||||
"thickness": operator_settings["object_thickness"],
|
||||
"part_pos": operator_settings["part_pos"],
|
||||
"force": operator_settings["force_target"]
|
||||
}
|
||||
return useful_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_useful_data - Ошибка при формировании полезных данных точки: {e}")
|
||||
return {}
|
||||
|
||||
def _form_point_ideal_data(self, point_settings: Settings) -> Dict:
|
||||
"""
|
||||
Формирует идеальные данные для отдельной точки с использованием кэша.
|
||||
|
||||
Генерируется кэш-ключ из настроек точки, затем либо извлекаются ранее рассчитанные
|
||||
данные, либо вычисляются новые с помощью билдера.
|
||||
|
||||
:param point_settings: Настройки точки.
|
||||
:return: Словарь с идеальными данными или пустой словарь в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
cache_key = self._generate_cache_key(point_settings)
|
||||
ideal_data = self._ideal_data_cache.get(
|
||||
cache_key,
|
||||
self._build_ideal_data(idealDataBuilder=IdealDataBuilder, point_settings=point_settings)
|
||||
)
|
||||
self._ideal_data_cache[cache_key] = ideal_data
|
||||
return ideal_data
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_ideal_data - Ошибка при формировании идеальных данных точки: {e}")
|
||||
return {}
|
||||
|
||||
def _get_operator_settings_part(self, idx: int) -> Dict:
|
||||
"""
|
||||
Извлекает часть настроек оператора для конкретного индекса.
|
||||
|
||||
Если индекс выходит за пределы списка, используется первый элемент.
|
||||
|
||||
:param idx: Индекс точки.
|
||||
:return: Словарь настроек оператора для данной точки.
|
||||
"""
|
||||
try:
|
||||
operator_settings = {
|
||||
key: (value[idx] if idx < len(value) else value[0])
|
||||
for key, value in self._settings.operator.items()
|
||||
}
|
||||
return operator_settings
|
||||
except Exception as e:
|
||||
logger.error(f"_get_operator_settings_part - Ошибка при получении настроек оператора для индекса {idx}: {e}")
|
||||
return {}
|
||||
|
||||
def _form_point_events(self, events: Dict, idx: int) -> Tuple[Optional[List[float]], Optional[Dict[str, List[float]]]]:
|
||||
"""
|
||||
Формирует временной интервал и события для отдельной точки.
|
||||
|
||||
Если событий нет, возвращает (None, None).
|
||||
|
||||
:param events: Словарь с событиями для всех этапов.
|
||||
:param idx: Индекс точки.
|
||||
:return: Кортеж (timeframe, point_events) или (None, None) в случае ошибки.
|
||||
"""
|
||||
try:
|
||||
timeframe, point_events = None, None
|
||||
start, end = 0, 0
|
||||
if events is not None:
|
||||
# Если первое событие основного этапа начинается с 0, сдвигаем индекс
|
||||
start = events[self._stages[0]][0][idx]
|
||||
end = 0
|
||||
point_events = {}
|
||||
for key, value in events.items():
|
||||
if len(value[0]) > idx and value[0][idx] >= start:
|
||||
point_events[key] = [value[0][idx], value[1][idx]]
|
||||
if value[1][idx] > end: end = value[1][idx]
|
||||
else:
|
||||
logger.warning(f"_form_point_events - Обнаружен аномальный порядок событий для точки {idx}")
|
||||
for i in range(len(value[0])):
|
||||
if value[0][i] >= start:
|
||||
logger.info(f"_form_point_events - Найдено событе (вхождение {i}), соответствующее временным рамкам точки")
|
||||
point_events[key] = [value[0][i], value[1][i]]
|
||||
if value[1][i] > end: end = value[1][i]
|
||||
break
|
||||
point_events[key] = [end, end+0.01]
|
||||
end += 0.01
|
||||
if end != 0: timeframe = [start, end]
|
||||
return timeframe, point_events
|
||||
except Exception as e:
|
||||
logger.error(f"_form_point_events - Ошибка при формировании событий для точки {idx}: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
class IdealDataBuilder(BaseIdealDataBuilder):
|
||||
"""
|
||||
Класс для построения идеальных данных по этапам.
|
||||
|
||||
Реализует методы получения DataFrame для различных этапов:
|
||||
закрытия, сжатия, открытия, движения, сварки,
|
||||
а также метод получения идеальных временных интервалов.
|
||||
"""
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа закрытия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tclose'], self.calcPhaseClose)
|
||||
except Exception as e:
|
||||
logger.error(f"get_closingDF - Ошибка при получении данных для этапа закрытия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сжатия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tgrow'], self.calcPhaseGrow)
|
||||
except Exception as e:
|
||||
logger.error(f"get_compressionDF - Ошибка при получении данных для этапа сжатия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа открытия.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.getMarkOpen(), self.calcPhaseOpen)
|
||||
except Exception as e:
|
||||
logger.error(f"get_openingDF - Ошибка при получении данных для этапа открытия: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа движения.
|
||||
"""
|
||||
try:
|
||||
return self._get_data(self.Ts['tmovement'], self.calcPhaseMovement)
|
||||
except Exception as e:
|
||||
logger.error(f"get_oncomingDF - Ошибка при получении данных для этапа движения: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
"""
|
||||
Получает DataFrame для этапа сварки.
|
||||
|
||||
Используется функция calcPhaseGrow с небольшим сдвигом времени.
|
||||
Значения масштабируются (умножаются на 1000) для перевода в нужные единицы.
|
||||
"""
|
||||
try:
|
||||
data = []
|
||||
# Используем небольшое смещение для расчёта сварки
|
||||
X1, X2, V1, V2, F = self.calcPhaseGrow(self.Ts['tgrow'] - 0.0001)
|
||||
X1, X2, V1, V2 = X1 * 1000, X2 * 1000, V1 * 1000, V2 * 1000
|
||||
points_num = 5
|
||||
for i in range(points_num + 1):
|
||||
data.append({
|
||||
"time": self.welding_time * i / points_num,
|
||||
"Position FE": X1,
|
||||
"Position ME": X2,
|
||||
"Rotor Speed FE": V1,
|
||||
"Rotor Speed ME": V2,
|
||||
"Force": F
|
||||
})
|
||||
return pd.DataFrame(data)
|
||||
except Exception as e:
|
||||
logger.error(f"get_weldingDF - Ошибка при получении данных для этапа сварки: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_ideal_timings(self) -> List[float]:
|
||||
"""
|
||||
Получает список идеальных временных интервалов для этапов.
|
||||
Список включает: tclose, tgrow, welding_time, getMarkOpen(), tmovement.
|
||||
"""
|
||||
try:
|
||||
data = self.Ts
|
||||
ideal_timings = [
|
||||
data['tclose'],
|
||||
data['tgrow'],
|
||||
self.welding_time,
|
||||
self.getMarkOpen(),
|
||||
data['tmovement']
|
||||
]
|
||||
return ideal_timings
|
||||
except Exception as e:
|
||||
logger.error(f"get_ideal_timings - Ошибка при получении идеальных временных интервалов: {e}")
|
||||
return []
|
||||
|
||||
def _get_data(self, end_timestamp: float, func) -> pd.DataFrame:
|
||||
"""
|
||||
Получает данные до указанного времени (end_timestamp) с шагом, определяемым параметром mul.
|
||||
|
||||
Для каждого шага рассчитываются значения с использованием переданной функции func.
|
||||
В конце добавляется строка с точным значением end_timestamp.
|
||||
|
||||
:param end_timestamp: Время окончания получения данных.
|
||||
:param func: Функция для расчёта значений в зависимости от времени.
|
||||
:return: DataFrame с рассчитанными данными.
|
||||
"""
|
||||
try:
|
||||
data = []
|
||||
# Генерируем данные с шагом 1/mul
|
||||
for i in range(0, int(end_timestamp * self.mul) + 1):
|
||||
time_val = i / self.mul
|
||||
X1, X2, V1, V2, F = func(time_val)
|
||||
data.append({
|
||||
"time": time_val,
|
||||
"Position FE": X1 * 1000,
|
||||
"Position ME": X2 * 1000,
|
||||
"Rotor Speed FE": V1 * 1000,
|
||||
"Rotor Speed ME": V2 * 1000,
|
||||
"Force": F
|
||||
})
|
||||
# Добавляем финальную строку с end_timestamp
|
||||
X1, X2, V1, V2, F = func(end_timestamp)
|
||||
data.append({
|
||||
"time": end_timestamp,
|
||||
"Position FE": X1 * 1000,
|
||||
"Position ME": X2 * 1000,
|
||||
"Rotor Speed FE": V1 * 1000,
|
||||
"Rotor Speed ME": V2 * 1000,
|
||||
"Force": F
|
||||
})
|
||||
return pd.DataFrame(data)
|
||||
except Exception as e:
|
||||
logger.error(f"_get_data - Ошибка при получении данных с end_timestamp={end_timestamp}: {e}")
|
||||
return pd.DataFrame()
|
||||
@ -1,2 +1,2 @@
|
||||
from .plotter import PlotWidget
|
||||
from .settings_window import SettingsWindow
|
||||
from .settings_window import settingsWindow
|
||||
|
||||
BIN
src/gui/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/app.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/app.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/mainGui.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/mainGui.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/plot_window.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/plot_window.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/plotter.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/plotter.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/qt_settings.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/qt_settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/reportGui.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/reportGui.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/settings_window.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/settings_window.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/gui/__pycache__/settings_windows.cpython-310.pyc
Normal file
BIN
src/gui/__pycache__/settings_windows.cpython-310.pyc
Normal file
Binary file not shown.
335
src/gui/mainGui.py
Normal file
335
src/gui/mainGui.py
Normal file
@ -0,0 +1,335 @@
|
||||
from datetime import datetime as dt
|
||||
from typing import Optional
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap, QIcon
|
||||
from PyQt5.QtWidgets import QSizePolicy as QSP
|
||||
|
||||
from utils.base.base import BaseMainWindow, BaseController
|
||||
from gui.settings_window import SystemSettings, OperatorSettings
|
||||
from gui.reportGui import ReportSettings
|
||||
|
||||
|
||||
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
|
||||
class MainWindow(BaseMainWindow):
|
||||
def __init__(self,
|
||||
controller: Optional[BaseController] = None) -> None:
|
||||
super().__init__()
|
||||
self._controller = controller
|
||||
self._init_startUI()
|
||||
self._init_dock_widgets()
|
||||
self._init_menu()
|
||||
# self.setLocale()
|
||||
|
||||
|
||||
def _init_TabWidget(self) -> None:
|
||||
self.tabWidget = QtWidgets.QTabWidget()
|
||||
self.tabWidget.setTabsClosable(True)
|
||||
self.tabWidget.tabCloseRequested.connect(self._close_tab)
|
||||
self.tabWidget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
def _init_startUI(self) -> None:
|
||||
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
|
||||
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
|
||||
self.repSettings = ReportSettings()
|
||||
|
||||
|
||||
self._clear()
|
||||
self.resize(800,800)
|
||||
seeking_mode_btn = QtWidgets.QPushButton("Real time folder scanning")
|
||||
seeking_mode_btn.setFixedWidth(300)
|
||||
seeking_mode_btn.clicked.connect(self._init_seekingUI)
|
||||
raport_mode_btn = QtWidgets.QPushButton("Raport editor")
|
||||
raport_mode_btn.setFixedWidth(300)
|
||||
raport_mode_btn.clicked.connect(self._init_raportUI)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(seeking_mode_btn)
|
||||
button_layout.addWidget(raport_mode_btn)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
mainLayout = self._central_layout
|
||||
label = QtWidgets.QLabel("Select work mode")
|
||||
label.setStyleSheet(
|
||||
"""QLabel{
|
||||
color: #ffffff;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
mainLayout.addWidget(label, alignment=Qt.AlignCenter)
|
||||
mainLayout.addWidget(button_widget)
|
||||
|
||||
self._init_statusBar()
|
||||
|
||||
def _init_dock_widgets(self) -> None:
|
||||
"""
|
||||
Инициализация док-виджетов для настроек.
|
||||
"""
|
||||
|
||||
# Создаем док-виджет для OperatorSettings
|
||||
self.operator_dock = QtWidgets.QDockWidget("Operator Settings", self)
|
||||
self.operator_dock.setWidget(self.operSettings)
|
||||
self.operator_dock.setObjectName("OperatorSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.operator_dock)
|
||||
self.operator_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Создаем док-виджет для SystemSettings
|
||||
self.system_dock = QtWidgets.QDockWidget("System Settings", self)
|
||||
self.system_dock.setWidget(self.sysSettings)
|
||||
self.system_dock.setObjectName("SystemSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.system_dock)
|
||||
self.system_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Создаем док-виджет для ReportSettings
|
||||
self.report_dock = QtWidgets.QDockWidget("View settings", self)
|
||||
self.report_dock.setWidget(self.repSettings)
|
||||
self.report_dock.setObjectName("ReportSettings")
|
||||
self.addDockWidget(Qt.RightDockWidgetArea, self.report_dock)
|
||||
self.report_dock.hide() # Скрываем по умолчанию
|
||||
|
||||
# Настройка док-виджетов
|
||||
self._set_dock_features(self.operator_dock)
|
||||
self._set_dock_features(self.system_dock)
|
||||
self._set_dock_features(self.report_dock)
|
||||
|
||||
def _init_menu(self) -> None:
|
||||
"""
|
||||
Инициализация главного меню.
|
||||
"""
|
||||
# Создаем главное меню
|
||||
menu_bar = self.menuBar()
|
||||
|
||||
# Создаем меню "Режимы"
|
||||
modes_menu = menu_bar.addMenu("Mode")
|
||||
settings_menu = menu_bar.addMenu("Settings")
|
||||
|
||||
# Создаем действия для меню
|
||||
seeking_action = QtWidgets.QAction("Real time folder scanning", self)
|
||||
seeking_action.triggered.connect(self._init_seekingUI)
|
||||
|
||||
raport_action = QtWidgets.QAction("Raport editor", self)
|
||||
raport_action.triggered.connect(self._init_raportUI)
|
||||
|
||||
system_settings = QtWidgets.QAction("System settings", self)
|
||||
system_settings.setIcon(QIcon('resources/system_ico.png'))
|
||||
system_settings.triggered.connect(lambda: self._toggle_visibility(self.system_dock))
|
||||
|
||||
operator_settings = QtWidgets.QAction("Operator settings ", self)
|
||||
operator_settings.setIcon(QIcon('resources/operator_ico.png'))
|
||||
operator_settings.triggered.connect(lambda: self._toggle_visibility(self.operator_dock))
|
||||
|
||||
view_settings = QtWidgets.QAction("View settings", self)
|
||||
view_settings.setIcon(QIcon('resources/view_ico.png'))
|
||||
view_settings.triggered.connect(lambda: self._toggle_visibility(self.report_dock))
|
||||
view_settings.triggered.connect(lambda: self._on_tab_changed(0))
|
||||
|
||||
|
||||
# Добавляем действия в меню "Режимы"
|
||||
modes_menu.addAction(seeking_action)
|
||||
modes_menu.addAction(raport_action)
|
||||
|
||||
settings_menu.addAction(system_settings)
|
||||
settings_menu.addAction(operator_settings)
|
||||
settings_menu.addAction(view_settings)
|
||||
|
||||
def _init_statusBar(self) -> None:
|
||||
# Создание пользовательского виджета для StatusBar
|
||||
note_widget = QtWidgets.QWidget()
|
||||
note_layout = QtWidgets.QHBoxLayout(note_widget)
|
||||
note_layout.setContentsMargins(10, 1, 10, 1)
|
||||
note_layout.setSpacing(15) # Устанавливаем расстояние между элементами
|
||||
|
||||
# Создание QLabel и QProgressBar
|
||||
self.mode_label = QtWidgets.QLabel()
|
||||
self.note_label = QtWidgets.QLabel()
|
||||
self.progress_bar = QtWidgets.QProgressBar()
|
||||
self.progress_bar.setRange(0, 100)
|
||||
self.progress_bar.setValue(0)
|
||||
self.progress_bar.setMinimumWidth(250)
|
||||
self.progress_bar.setMaximumHeight(10)
|
||||
self.progress_bar.setTextVisible(False)
|
||||
|
||||
# Создание QSpacerItem
|
||||
|
||||
# Установка политики размеров
|
||||
self.mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
|
||||
self.note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
|
||||
self.progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
|
||||
|
||||
# Добавление виджетов в макет
|
||||
note_layout.addWidget(self.progress_bar)
|
||||
note_layout.addWidget(self.note_label)
|
||||
note_layout.addStretch(1)
|
||||
note_layout.addWidget(self.mode_label)
|
||||
|
||||
# Установка политики размеров для note_widget
|
||||
note_widget.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
|
||||
|
||||
# Добавление пользовательского виджета в StatusBar как Permanent Widget
|
||||
self.statusBar().addPermanentWidget(note_widget, 1)
|
||||
|
||||
def _toggle_visibility(self, body:QtWidgets.QDockWidget = None) -> None:
|
||||
"""
|
||||
Переключение видимости док-виджета.
|
||||
"""
|
||||
is_visible = body.isVisible()
|
||||
body.setVisible(not is_visible)
|
||||
|
||||
def _set_dock_features(self, body:QtWidgets.QDockWidget = None) -> None:
|
||||
"""
|
||||
Настройка флагов док-виджета.
|
||||
"""
|
||||
flag_move = QtWidgets.QDockWidget.DockWidgetMovable
|
||||
flag_close = QtWidgets.QDockWidget.DockWidgetClosable
|
||||
flag_floating = QtWidgets.QDockWidget.DockWidgetFloatable
|
||||
body.setFeatures(flag_move | flag_close | flag_floating)
|
||||
|
||||
def _clear(self) -> None:
|
||||
if self._central_layout is not None:
|
||||
while self._central_layout.count():
|
||||
child = self._central_layout.takeAt(0)
|
||||
if child.widget() is not None:
|
||||
child.widget().deleteLater()
|
||||
|
||||
def _init_seekingUI(self) -> None:
|
||||
self._clear()
|
||||
self._init_TabWidget()
|
||||
self._transfer_settings()
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
self.mode_label.setText("online mode")
|
||||
self._central_layout.addWidget(self.tabWidget)
|
||||
self._central_layout.addWidget(button_widget)
|
||||
self._controller.seeking_mode()
|
||||
|
||||
def _init_raportUI(self) -> None:
|
||||
self._clear()
|
||||
self._init_TabWidget()
|
||||
self._transfer_settings()
|
||||
|
||||
save_screen_btn = QtWidgets.QPushButton("Save state")
|
||||
save_screen_btn.setFixedWidth(185)
|
||||
save_screen_btn.clicked.connect(self._save_plots)
|
||||
open_file_btn = QtWidgets.QPushButton("Open file")
|
||||
open_file_btn.setFixedWidth(185)
|
||||
open_file_btn.clicked.connect(self._open_file)
|
||||
|
||||
button_layout = QtWidgets.QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
|
||||
button_layout.addWidget(save_screen_btn)
|
||||
button_layout.addWidget(open_file_btn)
|
||||
button_widget = QtWidgets.QWidget()
|
||||
button_widget.setLayout(button_layout)
|
||||
|
||||
self.mode_label.setText("raport mode")
|
||||
self._central_layout.addWidget(self.tabWidget)
|
||||
self._central_layout.addWidget(button_widget)
|
||||
|
||||
self._controller.raport_mode()
|
||||
|
||||
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
|
||||
for plot_widget in plot_widgets:
|
||||
widget, reg_items, curve_items, qt_items = plot_widget
|
||||
|
||||
tab = QtWidgets.QWidget()
|
||||
tab.setProperty("reg_items", reg_items)
|
||||
tab.setProperty("curve_items", curve_items)
|
||||
tab.setProperty("qt_items", qt_items)
|
||||
grid = QtWidgets.QGridLayout()
|
||||
grid.addWidget(widget)
|
||||
tab.setLayout(grid)
|
||||
self.tabWidget.addTab(tab, "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S'))
|
||||
self.tabWidget.setCurrentWidget(tab)
|
||||
|
||||
tab_count = self.tabWidget.count()
|
||||
if tab_count > 10:
|
||||
for i in range(0, tab_count-10):
|
||||
self._close_tab(i)
|
||||
|
||||
self.update_stateLabel("Done!")
|
||||
|
||||
def keyPressEvent(self, a0) -> None:
|
||||
if a0.key() == Qt.Key_F5:
|
||||
tab_count = self.tabWidget.count()
|
||||
for i in range(0, tab_count):
|
||||
self._close_tab(i)
|
||||
|
||||
def closeEvent(self, a0):
|
||||
self.operSettings.close()
|
||||
self.sysSettings.close()
|
||||
self.repSettings.close()
|
||||
super().closeEvent(a0)
|
||||
|
||||
def update_progressBar(self, percent:int) -> None:
|
||||
if percent > 100: percent = 100
|
||||
self.progress_bar.setValue(percent)
|
||||
|
||||
def update_stateLabel(self, msg: str = None) -> None:
|
||||
self.note_label.setText(msg)
|
||||
self.note_label.adjustSize()
|
||||
|
||||
def _transfer_settings(self) -> None:
|
||||
operator_params = self.operSettings.getParams()
|
||||
system_params = self.sysSettings.getParams()
|
||||
self._controller.update_settings([operator_params, system_params])
|
||||
|
||||
def _upd_settings(self) -> None:
|
||||
self._transfer_settings()
|
||||
if self.mode_label.text():
|
||||
self.tabWidget.clear()
|
||||
self._controller.update_plots()
|
||||
|
||||
|
||||
|
||||
def _close_tab(self, index:int) -> None:
|
||||
self.tabWidget.removeTab(index)
|
||||
|
||||
def _open_file(self) -> None:
|
||||
path = self._select_csv()
|
||||
if path is not None:
|
||||
self._controller.open_file(path)
|
||||
|
||||
def _select_csv(self) -> Optional[str]:
|
||||
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
|
||||
if CSV_path:
|
||||
print(CSV_path)
|
||||
return CSV_path
|
||||
return None
|
||||
|
||||
def _on_tab_changed(self, index):
|
||||
try:
|
||||
tab = self.tabWidget.currentWidget()
|
||||
except:
|
||||
tab = None
|
||||
if tab is not None and self.report_dock.isVisible():
|
||||
reg_items = tab.property("reg_items")
|
||||
curve_items = tab.property("curve_items")
|
||||
qt_items = tab.property("qt_items")
|
||||
self.repSettings.build(index, reg_items, curve_items, qt_items)
|
||||
|
||||
def _save_plots(self) -> None:
|
||||
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
|
||||
tab = self.tabWidget.currentWidget()
|
||||
if tab is not None:
|
||||
pixmap = QPixmap(tab.size())
|
||||
tab.render(pixmap)
|
||||
pixmap.save(filepath)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,185 +0,0 @@
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
|
||||
from base.base import BaseMainWindow, Settings
|
||||
from gui.start_widget import (CustomMenuBar, CustomStatusBar,
|
||||
StartWidget, CustomTabWidget,
|
||||
RaportWidget, SeekingWidget,
|
||||
ClientAnalyzerWidget)
|
||||
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
|
||||
from gui.report_gui import ReportSettings
|
||||
|
||||
|
||||
# FIXME: При отркытии RaportMode все прочитанные трейсы должны удаляться, но они открываются...
|
||||
class MainWindow(BaseMainWindow):
|
||||
|
||||
signal_mode = pyqtSignal(int)
|
||||
signal_settings = pyqtSignal(Settings)
|
||||
signal_replot_all = pyqtSignal()
|
||||
signal_open_file = pyqtSignal(str)
|
||||
signal_save_file = pyqtSignal(list)
|
||||
signal_open_dir = pyqtSignal(str)
|
||||
signal_TCW_for_client = pyqtSignal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._init_startUI()
|
||||
|
||||
def show_plot_tabs(self, plot_widgets: list[QtWidgets.QWidget]) -> None:
|
||||
for plot_widget in plot_widgets:
|
||||
self._tab_widget.create_tab(plot_widget)
|
||||
|
||||
tab_count = self._tab_widget.count()
|
||||
if tab_count > 10:
|
||||
for i in range(0, tab_count-10):
|
||||
self._tab_widget.close_tab(i)
|
||||
|
||||
self.status_widget.set_note("Done!")
|
||||
self.status_widget.set_progress(100)
|
||||
|
||||
def keyPressEvent(self, a0) -> None:
|
||||
if a0.key() == Qt.Key_F5:
|
||||
tab_count = self._tab_widget.count()
|
||||
for i in range(0, tab_count):
|
||||
self._tab_widget.close_tab(i)
|
||||
|
||||
def closeEvent(self, a0):
|
||||
self.operSettings.close()
|
||||
self.sysSettings.close()
|
||||
self.repSettings.close()
|
||||
super().closeEvent(a0)
|
||||
|
||||
def _init_startUI(self) -> None:
|
||||
self._init_settings()
|
||||
self._init_menu()
|
||||
self._init_status_bar()
|
||||
start_widget = StartWidget()
|
||||
start_widget.btn_scanning.clicked.connect(self._init_seekingUI)
|
||||
start_widget.btn_report.clicked.connect(self._init_raportUI)
|
||||
start_widget.btn_client.clicked.connect(self._init_client_UI)
|
||||
self.setCentralWidget(start_widget.get_widget())
|
||||
|
||||
def _init_settings(self) -> None:
|
||||
self.sysSettings = SystemSettings("params/system_params.json", 'System', self._upd_settings)
|
||||
self.repSettings = ReportSettings()
|
||||
self.operSettings = OperatorSettings("params/operator_params.json", 'Operator', self._upd_settings)
|
||||
self.filterSettings = FilterSettings("params/filter_params.json", "Client filter", self._upd_settings)
|
||||
|
||||
def _init_menu(self) -> None:
|
||||
self.menu = CustomMenuBar(self.sysSettings, self.repSettings, self.operSettings, self.filterSettings)
|
||||
self.menu.action_scanning.triggered.connect(self._init_seekingUI)
|
||||
self.menu.action_report.triggered.connect(self._init_raportUI)
|
||||
self.menu.action_client.triggered.connect(self._init_client_UI)
|
||||
self.menu.action_view_settings.triggered.connect(lambda: self._on_tab_changed)
|
||||
self.menu.setup(self)
|
||||
|
||||
def _init_status_bar(self) -> None:
|
||||
self.status_widget = CustomStatusBar()
|
||||
self.statusBar().addPermanentWidget(self.status_widget.get_widget(), 1)
|
||||
|
||||
def _init_tab_widget(self) -> None:
|
||||
self._tab_widget = CustomTabWidget()
|
||||
self._tab_widget.currentChanged.connect(self._on_tab_changed)
|
||||
|
||||
def _on_tab_changed(self):
|
||||
tab = self._tab_widget.currentWidget()
|
||||
if tab:
|
||||
reg_items = tab.property("reg_items")
|
||||
curve_items = tab.property("curve_items")
|
||||
qt_items = tab.property("qt_items")
|
||||
self.repSettings.build(reg_items, curve_items, qt_items)
|
||||
|
||||
def _clear(self) -> None:
|
||||
if self.layout() is not None:
|
||||
while self.layout().count():
|
||||
child = self.layout().takeAt(0)
|
||||
if child.widget() is not None:
|
||||
child.widget().deleteLater()
|
||||
self._init_tab_widget()
|
||||
self._transfer_settings()
|
||||
|
||||
def _init_raportUI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(1)
|
||||
raport_widget = RaportWidget(self._tab_widget)
|
||||
raport_widget.btn_open_file.clicked.connect(self._open_file)
|
||||
raport_widget.btn_save_state.clicked.connect(self._save_plots)
|
||||
self.setCentralWidget(raport_widget.get_widget())
|
||||
|
||||
def _init_seekingUI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(2)
|
||||
seeking_widget = SeekingWidget(self._tab_widget)
|
||||
self.setCentralWidget(seeking_widget.get_widget())
|
||||
|
||||
def _init_client_UI(self) -> None:
|
||||
self._clear()
|
||||
self._set_mode(3)
|
||||
client_widget = ClientAnalyzerWidget(self._tab_widget)
|
||||
#TODO: привязать действия к кнопкам
|
||||
client_widget.btn_open_folder.clicked.connect(self._open_folder)
|
||||
client_widget.btn_save_state.clicked.connect(self._save_plots)
|
||||
client_widget.btn_calc_tcw.clicked.connect(self._build_TCW_for_client)
|
||||
self.setCentralWidget(client_widget.get_widget())
|
||||
|
||||
def _build_TCW_for_client(self):
|
||||
self.signal_TCW_for_client.emit()
|
||||
|
||||
def _set_mode(self, num:int) -> None:
|
||||
match num:
|
||||
case 1:
|
||||
self.status_widget.set_mode("raport mode")
|
||||
self.signal_mode.emit(1)
|
||||
case 2:
|
||||
self.status_widget.set_mode("online mode")
|
||||
self.signal_mode.emit(2)
|
||||
case 3:
|
||||
self.status_widget.set_mode("client processor mode")
|
||||
self.signal_mode.emit(3)
|
||||
|
||||
def _transfer_settings(self) -> None:
|
||||
settings = Settings()
|
||||
settings.system = self.sysSettings.get_params()
|
||||
settings.operator = self.operSettings.get_params()
|
||||
settings.filter = self.filterSettings.get_params()
|
||||
self.signal_settings.emit(settings)
|
||||
|
||||
def _upd_settings(self) -> None:
|
||||
self._transfer_settings()
|
||||
if self.status_widget._mode_label.text():
|
||||
self._tab_widget.clear()
|
||||
self.signal_replot_all.emit()
|
||||
|
||||
def _open_file(self) -> None:
|
||||
CSV_path, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Select csv file", "", "CSV Files (*.csv)")
|
||||
if CSV_path:
|
||||
self.signal_open_file.emit(CSV_path)
|
||||
|
||||
def _open_folder(self) -> None:
|
||||
dir_path = QtWidgets.QFileDialog.getExistingDirectory(self,"Select folder with traces","",
|
||||
QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontResolveSymlinks)
|
||||
if dir_path:
|
||||
self.signal_open_dir.emit(dir_path)
|
||||
|
||||
def _save_plots(self) -> None:
|
||||
filepath, _ = QtWidgets.QFileDialog.getSaveFileName(self, "Save file", "", "Image Files (*.png *.jpeg)")
|
||||
tab = self._tab_widget.currentWidget()
|
||||
if tab: self.signal_save_file.emit([filepath, tab])
|
||||
|
||||
# TODO: эта реализация работает, но для пользователя будет больно каждый раз писать имена и расширения
|
||||
# сохраняемых файлов, не кажется? Можно сделать, как показано ниже, тогда файлам будет назначаться имя
|
||||
# по умолчанию и с расширением. ХЗ правда, как эта форма на винде будет выглядеть - надо проверить.
|
||||
# ========================================================
|
||||
# dialog = QFileDialog()
|
||||
# dialog.setOptions(QFileDialog.DontUseNativeDialog)
|
||||
# dialog.setFileMode(QFileDialog.AnyFile)
|
||||
# dialog.setAcceptMode(QFileDialog.AcceptSave)
|
||||
# dialog.setDirectory(os.getcwd())
|
||||
# dialog.selectFile("untitled.txt")
|
||||
# dialog.exec_()
|
||||
# if dialog.accepted:
|
||||
# # emit signal to save file
|
||||
# ...
|
||||
# ========================================================
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,33 +1,42 @@
|
||||
import traceback
|
||||
import sys
|
||||
from typing import Union
|
||||
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.parametertree import Parameter, ParameterTree
|
||||
from typing import Union
|
||||
from PyQt5 import QtWidgets
|
||||
import traceback
|
||||
import sys
|
||||
from loguru import logger
|
||||
|
||||
|
||||
|
||||
class ReportSettings(QtWidgets.QWidget):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
#self._tab_cashe = LRUCache(maxsize=1000)
|
||||
|
||||
def build(self, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
||||
def build(self, index, reg_items: dict, curve_items: dict, qt_items: dict) -> None:
|
||||
"""Создает ParameterTree для элементов всех графиков выбранной вкладки"""
|
||||
try:
|
||||
self._clear()
|
||||
param_tree = ParameterTree()
|
||||
layout = self.layout()
|
||||
layout.addWidget(param_tree)
|
||||
"""if index in self._tab_cashe:
|
||||
body = self._tab_cashe[index]
|
||||
else:
|
||||
body= [
|
||||
self._generate_reg_params(reg_items),
|
||||
self._generate_curve_params(curve_items)
|
||||
]
|
||||
self._tab_cashe[index] = body"""
|
||||
body= [
|
||||
ReportSettings._generate_reg_params(reg_items),
|
||||
ReportSettings._generate_curve_params(curve_items),
|
||||
ReportSettings._generate_qt_params(qt_items)
|
||||
self._generate_reg_params(reg_items),
|
||||
self._generate_curve_params(curve_items),
|
||||
self._generate_qt_params(qt_items)
|
||||
]
|
||||
# Добавляем параметры в дерево
|
||||
params = Parameter.create(name='params', type='group', children=body)
|
||||
params.sigTreeStateChanged.connect(
|
||||
lambda: ReportSettings._update_settings(reg_items, curve_items, qt_items, params)
|
||||
lambda: self._update_settings(reg_items, curve_items, qt_items, params)
|
||||
)
|
||||
param_tree.setParameters(params, showTop=False)
|
||||
except:
|
||||
@ -50,52 +59,69 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
else:
|
||||
self.setLayout(QtWidgets.QVBoxLayout())
|
||||
|
||||
@staticmethod
|
||||
def _generate_qt_params(qt_items: dict) -> dict:
|
||||
def _generate_qt_params(self, qt_items: dict) -> dict:
|
||||
"""Создает qt элементы"""
|
||||
res = {'name': 'Qt elements', 'type': 'group', 'children':[
|
||||
{'name': key, 'type': 'group', 'children': ReportSettings._create_qt_samples(item)} for key, item in qt_items.items()
|
||||
{'name': key, 'type': 'group', 'children': self._create_qt_samples(item)} for key, item in qt_items.items()
|
||||
]}
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_qt_samples(item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
||||
def _create_qt_samples(self, item: Union[QtWidgets.QWidget, QtWidgets.QLabel]) -> dict:
|
||||
visibility = item.isVisible()
|
||||
return [
|
||||
{'name': 'Visibility', 'type': 'bool', 'value': visibility}
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _generate_reg_params(reg_items: dict) -> dict:
|
||||
def _generate_reg_params(self,
|
||||
reg_items: dict) -> dict:
|
||||
|
||||
"""Созадет реальные и идеальные секторы"""
|
||||
|
||||
res = {'name': 'Sectors', 'type': 'group', 'children': [
|
||||
{'name': 'Real sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["real"])},
|
||||
{'name': 'Ideal sectors', 'type': 'group', 'children': ReportSettings._create_samples(reg_items["ideal"])},
|
||||
{'name': 'Real sectors', 'type': 'group', 'children': self._create_samples(reg_items["real"])},
|
||||
{'name': 'Ideal sectors', 'type': 'group', 'children': self._create_samples(reg_items["ideal"])},
|
||||
]}
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _generate_curve_params(curve_items: dict) -> dict:
|
||||
def _generate_curve_params(self,
|
||||
curve_items: dict) -> dict:
|
||||
|
||||
"""Создает реальные и идеальные линии графиков"""
|
||||
|
||||
res = {'name': 'Plots', 'type': 'group', 'children': [
|
||||
{'name': 'Real plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["real"])},
|
||||
{'name': 'Ideal plots', 'type': 'group', 'children': ReportSettings._create_samples(curve_items["ideal"])},
|
||||
{'name': 'Real plots', 'type': 'group', 'children': self._create_samples(curve_items["real"])},
|
||||
{'name': 'Ideal plots', 'type': 'group', 'children': self._create_ideal_curves(curve_items["ideal"])},
|
||||
]}
|
||||
return res
|
||||
|
||||
def _create_ideal_curves(self,
|
||||
curve: dict) -> list[dict]:
|
||||
|
||||
"""Создает секторы с этапами циклограммы"""
|
||||
|
||||
res = []
|
||||
for key, item in curve.items():
|
||||
param = {'name': key, 'type': 'group', 'children': self._create_samples(item)}
|
||||
res.append(param)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_samples(sector: dict) -> list[dict]:
|
||||
def _create_samples(self,
|
||||
sector: dict) -> list[dict]:
|
||||
|
||||
"""Создает список представленных элементов с их параметрами"""
|
||||
|
||||
res = []
|
||||
for key, item in sector.items():
|
||||
sample = item[0] if type(item) == list else item
|
||||
param = {'name': key, 'type': 'group', 'children': ReportSettings._create_settings(sample)}
|
||||
param = {'name': key, 'type': 'group', 'children': self._create_settings(sample)}
|
||||
res.append(param)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def _create_settings(item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
||||
def _create_settings(self,
|
||||
item: Union[pg.LinearRegionItem, pg.PlotDataItem]) -> list[dict]:
|
||||
|
||||
"""Получает настройки для элемента"""
|
||||
|
||||
if type(item) == pg.LinearRegionItem:
|
||||
pen = item.lines[0].pen
|
||||
brush = item.brush
|
||||
@ -114,12 +140,14 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
{'name': 'Fill color', 'type': 'color', 'value': fill_color},
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _update_settings(reg_items: dict,
|
||||
def _update_settings(self,
|
||||
reg_items: dict,
|
||||
curve_items: dict,
|
||||
qt_items: dict,
|
||||
params: Parameter) -> None:
|
||||
|
||||
"""Задает параметры элементов в соответствии с paramTree"""
|
||||
|
||||
real_sectors = params.child("Sectors").child("Real sectors")
|
||||
ideal_sectors = params.child("Sectors").child("Ideal sectors")
|
||||
|
||||
@ -128,17 +156,21 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
|
||||
qt_settings = params.child("Qt elements")
|
||||
|
||||
ReportSettings._set_sector_settings(reg_items["real"], real_sectors)
|
||||
ReportSettings._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
||||
self._set_sector_settings(reg_items["real"], real_sectors)
|
||||
self._set_sector_settings(reg_items["ideal"], ideal_sectors)
|
||||
|
||||
ReportSettings._set_plot_settings(curve_items["real"], real_plots)
|
||||
ReportSettings._set_plot_settings(curve_items["ideal"], ideal_plots)
|
||||
ReportSettings._set_qt_settings(qt_items, qt_settings)
|
||||
self._set_plot_settings(curve_items["real"], real_plots)
|
||||
for key, item_dict in curve_items["ideal"].items():
|
||||
self._set_plot_settings(item_dict, ideal_plots.child(key))
|
||||
|
||||
@staticmethod
|
||||
def _set_sector_settings(sectors: dict,
|
||||
self._set_qt_settings(qt_items, qt_settings)
|
||||
|
||||
def _set_sector_settings(self,
|
||||
sectors: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры секторов в соответствии с настройками"""
|
||||
|
||||
for key, item in sectors.items():
|
||||
sample = settings.child(key)
|
||||
line_color = sample.child("Line color").value()
|
||||
@ -154,10 +186,12 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
reg.lines[1].setPen(pen)
|
||||
reg.setBrush(brush)
|
||||
|
||||
@staticmethod
|
||||
def _set_plot_settings(curves: dict,
|
||||
def _set_plot_settings(self,
|
||||
curves: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры кривых в соответствии с настройками"""
|
||||
|
||||
for key, item in curves.items():
|
||||
sample = settings.child(key)
|
||||
line_color = sample.child("Line color").value()
|
||||
@ -172,10 +206,12 @@ class ReportSettings(QtWidgets.QWidget):
|
||||
item.setVisible(visibility)
|
||||
item.setPen(pen)
|
||||
|
||||
@staticmethod
|
||||
def _set_qt_settings(qt_items: dict,
|
||||
def _set_qt_settings(self,
|
||||
qt_items: dict,
|
||||
settings: Parameter) -> None:
|
||||
|
||||
"""Задает параметры Qt элементов в соответствии с настройками"""
|
||||
|
||||
for key, item in qt_items.items():
|
||||
sample = settings.child(key)
|
||||
visibility = sample.child("Visibility").value()
|
||||
@ -1,45 +1,40 @@
|
||||
import locale
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QPushButton, QLineEdit, QHBoxLayout, QVBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem, QStyledItemDelegate, QRadioButton, QGroupBox, QDoubleSpinBox
|
||||
)
|
||||
from PyQt5.QtWidgets import (QWidget, QPushButton,
|
||||
QLineEdit, QHBoxLayout,
|
||||
QVBoxLayout, QLabel,
|
||||
QTableWidget, QTableWidgetItem,
|
||||
QStyledItemDelegate)
|
||||
from PyQt5.QtGui import QIntValidator, QDoubleValidator
|
||||
from PyQt5 import QtCore
|
||||
|
||||
from utils.json_tools import read_json, write_json
|
||||
from utils import qt_settings as qts
|
||||
|
||||
|
||||
class SettingsWindow(QWidget):
|
||||
"""
|
||||
Окно настроек для редактирования параметров.
|
||||
|
||||
Загружает и сохраняет параметры из JSON-файла, отображает их в таблице,
|
||||
позволяет редактировать значения и расширять число параметров.
|
||||
|
||||
:param path: Путь к JSON-файлу с настройками.
|
||||
:param name: Название набора настроек.
|
||||
:param upd_func: Функция-коллбэк для обновления после сохранения.
|
||||
:param associated_names: Словарь отображения ключей настроек в читаемые имена.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None], associated_names: dict):
|
||||
class settingsWindow(QWidget):
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None], names: dict):
|
||||
"""
|
||||
Окно настроек для редактирования параметров.
|
||||
:param path: Путь к файлу настроек (JSON).
|
||||
:param name: Название набора настроек.
|
||||
:param upd_func: Функция обновления (коллбэк).
|
||||
"""
|
||||
super().__init__()
|
||||
self._settings_path = path
|
||||
self._settingsPath = path
|
||||
self._name = name
|
||||
self._data: dict[str, list[Any]] = {}
|
||||
self._upd_func = upd_func
|
||||
self._associated_names = associated_names
|
||||
|
||||
self._num_points: Optional[QLineEdit] = None
|
||||
self._param_table: Optional[QTableWidget] = None
|
||||
self._assosiated_names = names
|
||||
|
||||
self._init_ui()
|
||||
self.load_settings()
|
||||
self._populate_table()
|
||||
self._init_ui()
|
||||
|
||||
def load_settings(self) -> None:
|
||||
"""Загружает настройки из JSON-файла."""
|
||||
data = read_json(self._settings_path)
|
||||
data = read_json(self._settingsPath)
|
||||
if isinstance(data, dict):
|
||||
self._data = data
|
||||
else:
|
||||
@ -47,19 +42,17 @@ class SettingsWindow(QWidget):
|
||||
|
||||
def write_settings(self) -> None:
|
||||
"""Записывает текущие настройки в JSON-файл."""
|
||||
write_json(self._settings_path, self._data)
|
||||
write_json(self._settingsPath, self._data)
|
||||
|
||||
def get_params(self) -> dict:
|
||||
def getParams(self) -> dict:
|
||||
"""Возвращает текущий словарь параметров."""
|
||||
return self._data
|
||||
|
||||
def _init_ui(self) -> None:
|
||||
"""Инициализирует пользовательский интерфейс: кнопки, поля ввода, таблицу."""
|
||||
# Кнопки управления
|
||||
"""Инициализирует UI: кнопки, поля ввода, таблицу."""
|
||||
save_button = QPushButton("Save")
|
||||
restore_button = QPushButton("Restore")
|
||||
|
||||
# Поле для ввода количества точек сварки
|
||||
self._num_points = QLineEdit()
|
||||
self._num_points.setPlaceholderText("Enter the number of welding points")
|
||||
self._num_points.setValidator(QIntValidator())
|
||||
@ -69,16 +62,13 @@ class SettingsWindow(QWidget):
|
||||
control_layout.addWidget(restore_button)
|
||||
control_layout.addWidget(self._num_points)
|
||||
|
||||
# Подключение сигналов к слотам
|
||||
save_button.pressed.connect(self._save)
|
||||
restore_button.pressed.connect(self._restore)
|
||||
self._num_points.editingFinished.connect(self._expand)
|
||||
|
||||
# Таблица для отображения параметров
|
||||
self._param_table = QTableWidget()
|
||||
|
||||
self._populate_table()
|
||||
|
||||
# Основной вертикальный макет
|
||||
layout = QVBoxLayout()
|
||||
header = QLabel(self._name)
|
||||
layout.addWidget(header)
|
||||
@ -88,18 +78,14 @@ class SettingsWindow(QWidget):
|
||||
self.setStyleSheet(qts.dark_style)
|
||||
|
||||
def _populate_table(self) -> None:
|
||||
"""
|
||||
Заполняет таблицу значениями из self._data.
|
||||
|
||||
Если данных нет, таблица очищается. Для каждого параметра устанавливается
|
||||
делегат в зависимости от типа данных (int, float, str).
|
||||
"""
|
||||
"""Заполняет таблицу значениями из self._data."""
|
||||
# Если нет данных для заполнения
|
||||
if not self._data:
|
||||
self._param_table.setRowCount(0)
|
||||
self._param_table.setColumnCount(0)
|
||||
return
|
||||
|
||||
# Определяем количество столбцов на основе длины списка первого ключа
|
||||
# Предполагаем, что у всех ключей одинаковая длина списков параметров.
|
||||
first_key = next(iter(self._data), None)
|
||||
if first_key is None:
|
||||
self._param_table.setRowCount(0)
|
||||
@ -109,31 +95,27 @@ class SettingsWindow(QWidget):
|
||||
column_count = len(self._data[first_key])
|
||||
self._param_table.setRowCount(len(self._data))
|
||||
self._param_table.setColumnCount(column_count)
|
||||
headers = [self._associated_names.get(key, key) for key in self._data.keys()]
|
||||
headers = [self._assosiated_names[key] for key in self._data.keys()]
|
||||
self._param_table.setVerticalHeaderLabels(headers)
|
||||
|
||||
# Создаем делегаты для различных типов данных
|
||||
int_delegate = ValidatorDelegate(data_type='int', parent=self._param_table)
|
||||
float_delegate = ValidatorDelegate(data_type='float', parent=self._param_table)
|
||||
str_delegate = ValidatorDelegate(data_type='str', parent=self._param_table)
|
||||
|
||||
# Заполняем таблицу и устанавливаем делегаты для каждой строки
|
||||
for i, (_, items) in enumerate(self._data.items()):
|
||||
for j, item in enumerate(items):
|
||||
self._param_table.setItem(i, j, QTableWidgetItem(str(item)))
|
||||
# Используем тип последнего элемента строки для выбора делегата
|
||||
if isinstance(items[-1], int):
|
||||
|
||||
if type(item) == int:
|
||||
self._param_table.setItemDelegateForRow(i, int_delegate)
|
||||
elif isinstance(items[-1], float):
|
||||
elif type(item) == float:
|
||||
self._param_table.setItemDelegateForRow(i, float_delegate)
|
||||
else:
|
||||
self._param_table.setItemDelegateForRow(i, str_delegate)
|
||||
|
||||
def _save_data(self) -> None:
|
||||
"""
|
||||
Сохраняет текущие параметры из таблицы в self._data,
|
||||
записывает их в JSON-файл и вызывает функцию обновления.
|
||||
"""
|
||||
|
||||
|
||||
def _save(self) -> None:
|
||||
"""Сохраняет текущие параметры из таблицы в self._data и вызывает _upd_func()."""
|
||||
new_data = {}
|
||||
col_count = self._param_table.columnCount()
|
||||
for i, key in enumerate(self._data.keys()):
|
||||
@ -143,7 +125,7 @@ class SettingsWindow(QWidget):
|
||||
if cell_item is None:
|
||||
continue
|
||||
param_str = cell_item.text()
|
||||
# Для ключа 'trace_storage_path' оставляем строковое значение
|
||||
# Если ключ не trace_storage_path, конвертируем в float
|
||||
if key != "trace_storage_path":
|
||||
try:
|
||||
param = float(param_str)
|
||||
@ -152,17 +134,12 @@ class SettingsWindow(QWidget):
|
||||
else:
|
||||
param = param_str
|
||||
row_data.append(param)
|
||||
|
||||
new_data[key] = row_data
|
||||
|
||||
self._data = new_data
|
||||
|
||||
def _push_data (self) -> None:
|
||||
self.write_settings()
|
||||
self._upd_func()
|
||||
|
||||
def _save(self) -> None:
|
||||
self._save_data()
|
||||
self._push_data()
|
||||
|
||||
def _restore(self) -> None:
|
||||
"""Перезагружает данные из файла и обновляет таблицу."""
|
||||
@ -170,13 +147,7 @@ class SettingsWindow(QWidget):
|
||||
self._populate_table()
|
||||
|
||||
def _expand(self) -> None:
|
||||
"""
|
||||
Расширяет количество столбцов таблицы согласно введённому значению.
|
||||
|
||||
Количество столбцов таблицы
|
||||
устанавливается равным заданному значению, а новые ячейки заполняются последним
|
||||
известным значением для соответствующего параметра.
|
||||
"""
|
||||
"""Расширяет количество столбцов таблицы в зависимости от введённого значения."""
|
||||
if not self._num_points:
|
||||
return
|
||||
|
||||
@ -196,41 +167,34 @@ class SettingsWindow(QWidget):
|
||||
|
||||
self._param_table.setColumnCount(desired_columns)
|
||||
|
||||
# Заполнение новых столбцов последним значением для каждого параметра
|
||||
# Новые столбцы заполняем последним известным параметром для каждого ключа
|
||||
for i, (key, items) in enumerate(self._data.items()):
|
||||
# Если нет данных, пропускаем
|
||||
if not items:
|
||||
continue
|
||||
last_value = str(items[-1])
|
||||
for col in range(prev_columns, desired_columns):
|
||||
self._param_table.setItem(i, col, QTableWidgetItem(last_value))
|
||||
# Добавляем новый элемент также в self._data для консистентности
|
||||
# После этого можно будет сохранить при нажатии Save
|
||||
# Дополним также и в self._data
|
||||
additional_count = desired_columns - prev_columns
|
||||
# Приведение типа: для ключа 'trace_storage_path' оставляем строку, иначе преобразуем к float
|
||||
additional_values = (
|
||||
[float(last_value)] * additional_count
|
||||
if key != "trace_storage_path"
|
||||
else [last_value] * additional_count
|
||||
)
|
||||
self._data[key].extend(additional_values)
|
||||
self._data[key].extend([float(last_value) if key != "trace_storage_path" else last_value] * additional_count)
|
||||
|
||||
|
||||
class SystemSettings(SettingsWindow):
|
||||
"""
|
||||
Настройки системы.
|
||||
|
||||
Окно для редактирования параметров, связанных с работой системы.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||
associated_names = {
|
||||
class SystemSettings(settingsWindow):
|
||||
def __init__(self, path, name, upd_func):
|
||||
assosiated_names = {
|
||||
"trace_storage_path": "Trace path",
|
||||
"monitor_update_period": "Monitoring period",
|
||||
"a_max_1": "Max lin accel FE, m/s^2",
|
||||
"v_max_1": "Max lin speed FE, m/s",
|
||||
"a_max_2": "Max lin accel ME, m/s^2",
|
||||
"v_max_2": "Max lin speed FE, m/s",
|
||||
"a_max_1": "Max linear acceleration FE, m/s^2",
|
||||
"v_max_1": "Max linear speed FE, m/s",
|
||||
"a_max_2":"Max linear acceleration ME, m/s^2",
|
||||
"v_max_2": "Max linear speed ME, m/s",
|
||||
"mass_1": "Mass FE, kg",
|
||||
"mass_2": "Mass ME, kg",
|
||||
"k_hardness_1": "Hardness coef FE, N/m",
|
||||
"k_hardness_2": "Hardness coef ME, N/m",
|
||||
"k_hardness_1": "Hardness coefficient FE, N/m",
|
||||
"k_hardness_2": "Hardness coefficient ME, N/m",
|
||||
"torque_max_1": "Max torque FE, N*m",
|
||||
"torque_max_2": "Max torque ME, N*m",
|
||||
"transmission_ratio_1": "Transmission ratio FE",
|
||||
@ -240,115 +204,21 @@ class SystemSettings(SettingsWindow):
|
||||
"k_prop": "Proportionality factor",
|
||||
"time_capture": "Calculated points per sec",
|
||||
"UML_time_scaler": "UML_time_scaler",
|
||||
"Range ME, mm": "Range ME, mm"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
"gun_range": "Range ME-FE, mm"
|
||||
}
|
||||
super().__init__(path, name, upd_func, assosiated_names)
|
||||
self._num_points.setVisible(False)
|
||||
|
||||
def _init_ui(self) -> None:
|
||||
super()._init_ui()
|
||||
|
||||
performance_layout = QVBoxLayout()
|
||||
self.btn_trace_movement = QRadioButton()
|
||||
self.btn_trace_movement.setText("Use robot movement time from trace (auto set)")
|
||||
self.btn_trace_movement.setChecked(True)
|
||||
|
||||
self.btn_client_movement = QRadioButton()
|
||||
self.btn_client_movement.setText("Use robot movement time from client (manual set)")
|
||||
def _expand(self):
|
||||
pass
|
||||
|
||||
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",
|
||||
class OperatorSettings(settingsWindow):
|
||||
def __init__(self, path, name, upd_func):
|
||||
assosiated_names = {
|
||||
"distance_h_start_1": "Closing start dist FE, m" ,
|
||||
"distance_h_start_2": "Closing start dist ME, m",
|
||||
"distance_s_1": "Rob movement start dist FE, m",
|
||||
"distance_s_2": "Rob movement start dist ME, m",
|
||||
"distance_s_1": "Robot movement start dist FE, m",
|
||||
"distance_s_2": "Robot movement start dist ME, m",
|
||||
"distance_l_1": "Max oncoming dist FE, m",
|
||||
"distance_l_2": "Max oncoming dist ME, m",
|
||||
"distance_h_end1": "Oncoming end dist FE, m",
|
||||
@ -357,6 +227,7 @@ class OperatorSettings(SettingsWindow):
|
||||
"time_command": "Communication time compensator, sec",
|
||||
"time_robot_movement": "Rob movement time, sec",
|
||||
"object_thickness": "Workpiece thickness, m",
|
||||
"object_position": "Workpiece position, m",
|
||||
"force_target": "Target force, N",
|
||||
"force_capture": "Capture force, N",
|
||||
"Tesla closing": "Client closing time, sec",
|
||||
@ -364,47 +235,12 @@ class OperatorSettings(SettingsWindow):
|
||||
"Tesla welding": "Client welding time, sec",
|
||||
"Tesla oncomming_relief": "Client moving to next point time, sec",
|
||||
"Tesla summary time": "Client summary time, sec"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class FilterSettings(SettingsWindow):
|
||||
"""
|
||||
Настройки фильтра.
|
||||
|
||||
Окно для редактирования параметров фильтрации.
|
||||
"""
|
||||
def __init__(self, path: str, name: str, upd_func: Callable[[], None]):
|
||||
associated_names = {
|
||||
"act_pos_decrease": "ME max pos change in Relief, mm",
|
||||
"act_vel_min": "Minimum for ME speed in Closing mm/sec",
|
||||
"act_vel_close": "Maximum for ME speed in Squeeze mm/sec",
|
||||
"act_vel_thresh": "ME zero speed threshold mm/sec",
|
||||
"act_vel_negative": "ME Relief speed mm/sec",
|
||||
"rob_vel_thresh": "Robot zero speed threshold mm/sec",
|
||||
"act_force_close": "Maximum ME force in Closing, N",
|
||||
"act_force_weld": "Minimum ME force in Welding, N",
|
||||
"force_increase": "ME force rising speed in Squeeze",
|
||||
"force_decrease": "ME force falling speed in Relief"
|
||||
}
|
||||
super().__init__(path, name, upd_func, associated_names)
|
||||
self._num_points.setVisible(False)
|
||||
|
||||
def _expand(self):
|
||||
# Для настроек фильтра расширение столбцов не требуется.
|
||||
pass
|
||||
}
|
||||
super().__init__(path, name, upd_func, assosiated_names)
|
||||
pass
|
||||
|
||||
|
||||
class ValidatorDelegate(QStyledItemDelegate):
|
||||
"""
|
||||
Валидация для ввода в ячейках таблицы.
|
||||
|
||||
В зависимости от типа данных ('int', 'float' или 'str') устанавливает соответствующий валидатор.
|
||||
"""
|
||||
def __init__(self, data_type='str', parent=None):
|
||||
super().__init__(parent)
|
||||
self.data_type = data_type
|
||||
@ -413,6 +249,8 @@ class ValidatorDelegate(QStyledItemDelegate):
|
||||
elif self.data_type == 'float':
|
||||
self.validator = QDoubleValidator()
|
||||
self.validator.setNotation(QDoubleValidator.StandardNotation)
|
||||
self.validator.setLocale(QtCore.QLocale("en_US"))
|
||||
# locale
|
||||
else:
|
||||
self.validator = None
|
||||
|
||||
@ -420,13 +258,21 @@ class ValidatorDelegate(QStyledItemDelegate):
|
||||
editor = QLineEdit(parent)
|
||||
if self.validator:
|
||||
editor.setValidator(self.validator)
|
||||
# Добавляем обработчик для замены запятой на точку
|
||||
editor.textChanged.connect(self.replace_comma_with_dot)
|
||||
return editor
|
||||
|
||||
def replace_comma_with_dot(self, text):
|
||||
# Заменяем запятую на точку
|
||||
if ',' in text:
|
||||
text = text.replace(',', '.')
|
||||
# Устанавливаем измененный текст обратно в редактор
|
||||
self.sender().setText(text)
|
||||
|
||||
if __name__ == '__main__':
|
||||
import pyqtgraph as pg
|
||||
# Для демонстрации создаем окно настроек с фиктивной функцией обновления и пустым словарем имен
|
||||
app = pg.mkQApp("Parameter Tree Example")
|
||||
window = SettingsWindow('params/operator_params.json', 'operator', lambda: None, {})
|
||||
window.show()
|
||||
window = settingsWindow('params\operator_params.json', 'operator')
|
||||
app.exec()
|
||||
|
||||
|
||||
|
||||
@ -1,459 +0,0 @@
|
||||
from datetime import datetime as dt
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QPushButton, QMenuBar, QHBoxLayout, QVBoxLayout, QLabel,
|
||||
QDockWidget, QTabWidget, QProgressBar, QAction, QMainWindow, QGridLayout
|
||||
)
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QSizePolicy as QSP
|
||||
|
||||
from gui.settings_window import SystemSettings, OperatorSettings, FilterSettings
|
||||
from gui.report_gui import ReportSettings
|
||||
from base.base import PlotItems
|
||||
|
||||
|
||||
class StartWidget(QWidget):
|
||||
"""
|
||||
Виджет выбора режима работы приложения.
|
||||
|
||||
Содержит кнопки для перехода в различные режимы:
|
||||
- Сканирование папки в реальном времени
|
||||
- Редактор отчётов
|
||||
- Обработка клиентских трейсов
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._build_main_layout()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает текущий виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_main_layout(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с заголовком и кнопками выбора режима.
|
||||
"""
|
||||
self.resize(800, 800)
|
||||
|
||||
# Создаем кнопки для различных режимов работы
|
||||
self.btn_scanning = QPushButton("Real time folder scanning")
|
||||
self.btn_scanning.setFixedWidth(350)
|
||||
self.btn_report = QPushButton("Raport editor")
|
||||
self.btn_report.setFixedWidth(350)
|
||||
self.btn_client = QPushButton("Client trace processor")
|
||||
self.btn_client.setFixedWidth(350)
|
||||
|
||||
# Размещение кнопок в горизонтальном макете
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_scanning)
|
||||
button_layout.addWidget(self.btn_report)
|
||||
button_layout.addWidget(self.btn_client)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
# Создаем основной вертикальный макет с заголовком и кнопками
|
||||
main_layout = QVBoxLayout()
|
||||
title_label = QLabel("Select work mode")
|
||||
title_label.setStyleSheet(
|
||||
"""QLabel{
|
||||
color: #ffffff;
|
||||
font-size: 40px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}"""
|
||||
)
|
||||
main_layout.addWidget(title_label, alignment=Qt.AlignCenter)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class CustomStatusBar(QWidget):
|
||||
"""
|
||||
Кастомный виджет для статус-бара, отображающий режим, заметку и прогресс.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._build_status_bar()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет статус-бара.
|
||||
"""
|
||||
return self
|
||||
|
||||
def set_mode(self, msg: str = '') -> None:
|
||||
"""
|
||||
Устанавливает текст режима в статус-баре.
|
||||
|
||||
:param msg: Сообщение для отображения.
|
||||
"""
|
||||
if not isinstance(msg, str):
|
||||
msg = str(msg)
|
||||
self._mode_label.setText(msg)
|
||||
self._mode_label.adjustSize()
|
||||
|
||||
def set_note(self, msg: str = '') -> None:
|
||||
"""
|
||||
Устанавливает текст заметки в статус-баре.
|
||||
|
||||
:param msg: Сообщение для отображения.
|
||||
"""
|
||||
if not isinstance(msg, str):
|
||||
msg = str(msg)
|
||||
self._note_label.setText(msg)
|
||||
self._note_label.adjustSize()
|
||||
|
||||
def set_progress(self, percent: int = 0) -> None:
|
||||
"""
|
||||
Устанавливает значение прогресс-бара.
|
||||
|
||||
:param percent: Значение прогресса (от 0 до 100).
|
||||
"""
|
||||
if not isinstance(percent, int):
|
||||
percent = 0
|
||||
elif percent < 0:
|
||||
percent = 0
|
||||
elif percent > 100:
|
||||
percent = 100
|
||||
self._progress_bar.setValue(percent)
|
||||
|
||||
def _build_status_bar(self) -> None:
|
||||
"""
|
||||
Создает и настраивает компоненты статус-бара.
|
||||
"""
|
||||
# Создаем горизонтальный макет с отступами и промежутками
|
||||
layout = QHBoxLayout()
|
||||
layout.setContentsMargins(10, 1, 10, 1)
|
||||
layout.setSpacing(15)
|
||||
|
||||
# Инициализируем компоненты статус-бара
|
||||
self._mode_label = QLabel()
|
||||
self._note_label = QLabel()
|
||||
self._progress_bar = QProgressBar()
|
||||
self._progress_bar.setRange(0, 100)
|
||||
self._progress_bar.setValue(0)
|
||||
self._progress_bar.setMinimumWidth(250)
|
||||
self._progress_bar.setMaximumHeight(10)
|
||||
self._progress_bar.setTextVisible(False)
|
||||
|
||||
# Устанавливаем политику размеров для компонентов
|
||||
self._mode_label.setSizePolicy(QSP.Policy.Preferred, QSP.Policy.Preferred)
|
||||
self._note_label.setSizePolicy(QSP.Policy.MinimumExpanding, QSP.Policy.Preferred)
|
||||
self._progress_bar.setSizePolicy(QSP.Policy.Fixed, QSP.Policy.Preferred)
|
||||
|
||||
# Добавляем компоненты в макет
|
||||
layout.addWidget(self._progress_bar)
|
||||
layout.addWidget(self._note_label)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self._mode_label)
|
||||
|
||||
self.setLayout(layout)
|
||||
self.setSizePolicy(QSP.Policy.Expanding, QSP.Policy.Preferred)
|
||||
|
||||
|
||||
class CustomTabWidget(QTabWidget):
|
||||
"""
|
||||
Кастомный виджет-вкладка.
|
||||
|
||||
Позволяет создавать вкладки с виджетом графика и закрывать их.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._build_tab_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_tab_widget(self) -> None:
|
||||
"""
|
||||
Настраивает виджет вкладок: включает возможность закрытия вкладок.
|
||||
"""
|
||||
self.setTabsClosable(True)
|
||||
self.tabCloseRequested.connect(self.close_tab)
|
||||
|
||||
def create_tab(self, plot_widget: QWidget) -> None:
|
||||
"""
|
||||
Создает новую вкладку с виджетом графика.
|
||||
|
||||
Извлекает из виджета необходимые данные, сохраняет их в свойствах вкладки.
|
||||
|
||||
:param plot_widget: Виджет, содержащий график.
|
||||
"""
|
||||
# Извлекаем объект PlotItems, сохранённый в свойстве виджета
|
||||
plot_items: PlotItems = plot_widget.property("pyqt_container")
|
||||
new_tab = QWidget()
|
||||
new_tab.setProperty("reg_items", plot_items.regions)
|
||||
new_tab.setProperty("curve_items", plot_items.curves)
|
||||
new_tab.setProperty("qt_items", plot_items.qt_items)
|
||||
grid_layout = QGridLayout()
|
||||
grid_layout.addWidget(plot_widget)
|
||||
new_tab.setLayout(grid_layout)
|
||||
tab_label = "SF_trace_" + dt.now().strftime('%Y_%m_%d-%H_%M_%S')
|
||||
self.addTab(new_tab, tab_label)
|
||||
self.setCurrentWidget(new_tab)
|
||||
|
||||
def close_tab(self, index: int) -> None:
|
||||
"""
|
||||
Закрывает вкладку по заданному индексу.
|
||||
|
||||
:param index: Индекс вкладки для закрытия.
|
||||
"""
|
||||
if self.count() > index and index >= 0:
|
||||
self.removeTab(index)
|
||||
|
||||
|
||||
class CustomMenuBar(QMenuBar):
|
||||
"""
|
||||
Кастомное главное меню с док-виджетами для различных настроек.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
operator_settings: OperatorSettings,
|
||||
system_settings: SystemSettings,
|
||||
report_settings: ReportSettings,
|
||||
filter_settings: FilterSettings) -> None:
|
||||
super().__init__()
|
||||
self._operator_settings = operator_settings
|
||||
self._system_settings = system_settings
|
||||
self._report_settings = report_settings
|
||||
self._filter_settings = filter_settings
|
||||
self._build_dock_widgets()
|
||||
self._build_menu()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет меню.
|
||||
"""
|
||||
return self
|
||||
|
||||
def setup(self, parent: QMainWindow) -> None:
|
||||
"""
|
||||
Добавляет док-виджеты и устанавливает главное меню для главного окна.
|
||||
|
||||
:param parent: Главное окно приложения.
|
||||
"""
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._operator_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._system_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._report_dock)
|
||||
parent.addDockWidget(Qt.RightDockWidgetArea, self._filter_dock)
|
||||
parent.setMenuBar(self)
|
||||
|
||||
def _build_dock_widgets(self) -> None:
|
||||
"""
|
||||
Инициализирует док-виджеты для настроек и скрывает их по умолчанию.
|
||||
"""
|
||||
# Док-виджет для настроек оператора
|
||||
self._operator_dock = QDockWidget("Operator Settings", self)
|
||||
self._operator_dock.setWidget(self._operator_settings)
|
||||
self._operator_dock.setObjectName("OperatorSettings")
|
||||
self._operator_dock.hide()
|
||||
|
||||
# Док-виджет для системных настроек
|
||||
self._system_dock = QDockWidget("System Settings", self)
|
||||
self._system_dock.setWidget(self._system_settings)
|
||||
self._system_dock.setObjectName("SystemSettings")
|
||||
self._system_dock.hide()
|
||||
|
||||
# Док-виджет для настроек представления отчёта
|
||||
self._report_dock = QDockWidget("View settings", self)
|
||||
self._report_dock.setWidget(self._report_settings)
|
||||
self._report_dock.setObjectName("ReportSettings")
|
||||
self._report_dock.hide()
|
||||
|
||||
# Док-виджет для фильтр-настроек
|
||||
self._filter_dock = QDockWidget("Filter settings", self)
|
||||
self._filter_dock.setWidget(self._filter_settings)
|
||||
self._filter_dock.setObjectName("FilterSetting")
|
||||
self._filter_dock.hide()
|
||||
|
||||
# Устанавливаем функциональные возможности для каждого док-виджета
|
||||
self._set_dock_features(self._operator_dock)
|
||||
self._set_dock_features(self._system_dock)
|
||||
self._set_dock_features(self._report_dock)
|
||||
self._set_dock_features(self._filter_dock)
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
"""
|
||||
Инициализирует элементы главного меню.
|
||||
"""
|
||||
# Создаем меню для выбора режима и настроек
|
||||
modes_menu = self.addMenu("Mode")
|
||||
settings_menu = self.addMenu("Settings")
|
||||
|
||||
# Действия для переключения режимов
|
||||
self.action_scanning = QAction("Real time folder scanning", self)
|
||||
self.action_report = QAction("Raport editor", self)
|
||||
self.action_client = QAction("Client trace processor", self)
|
||||
|
||||
# Действия для настроек
|
||||
system_settings_action = QAction("System settings", self)
|
||||
system_settings_action.setIcon(QIcon('resources/system_ico.png'))
|
||||
system_settings_action.triggered.connect(lambda: self._toggle_visibility(self._system_dock))
|
||||
|
||||
operator_settings_action = QAction("Operator settings", self)
|
||||
operator_settings_action.setIcon(QIcon('resources/operator_ico.png'))
|
||||
operator_settings_action.triggered.connect(lambda: self._toggle_visibility(self._operator_dock))
|
||||
|
||||
self.action_view_settings = QAction("View settings", self)
|
||||
self.action_view_settings.setIcon(QIcon('resources/view_ico.png'))
|
||||
self.action_view_settings.triggered.connect(lambda: self._toggle_visibility(self._report_dock))
|
||||
|
||||
self.action_filter_settings = QAction("Filter settings", self)
|
||||
self.action_filter_settings.triggered.connect(lambda: self._toggle_visibility(self._filter_dock))
|
||||
|
||||
# Добавляем действия в меню "Mode"
|
||||
modes_menu.addAction(self.action_scanning)
|
||||
modes_menu.addAction(self.action_report)
|
||||
modes_menu.addAction(self.action_client)
|
||||
|
||||
# Добавляем действия в меню "Settings"
|
||||
settings_menu.addAction(system_settings_action)
|
||||
settings_menu.addAction(operator_settings_action)
|
||||
settings_menu.addAction(self.action_view_settings)
|
||||
settings_menu.addAction(self.action_filter_settings)
|
||||
|
||||
def _toggle_visibility(self, dock: QDockWidget = None) -> None:
|
||||
"""
|
||||
Переключает видимость указанного док-виджета.
|
||||
|
||||
:param dock: Док-виджет для переключения.
|
||||
"""
|
||||
if dock:
|
||||
is_visible = dock.isVisible()
|
||||
dock.setVisible(not is_visible)
|
||||
|
||||
@staticmethod
|
||||
def _set_dock_features(dock: QDockWidget = None) -> None:
|
||||
"""
|
||||
Устанавливает возможность перемещения, закрытия и открепления для док-виджета.
|
||||
|
||||
:param dock: Док-виджет для настройки.
|
||||
"""
|
||||
if dock:
|
||||
features = QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetClosable | QDockWidget.DockWidgetFloatable
|
||||
dock.setFeatures(features)
|
||||
|
||||
|
||||
class RaportWidget(QWidget):
|
||||
"""
|
||||
Виджет для режима редактирования отчёта.
|
||||
|
||||
Содержит вкладки и кнопки для сохранения состояния и открытия файла.
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с вкладками и кнопками управления.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
self.btn_save_state = QPushButton("Save state")
|
||||
self.btn_save_state.setFixedWidth(185)
|
||||
self.btn_open_file = QPushButton("Open file")
|
||||
self.btn_open_file.setFixedWidth(185)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_save_state)
|
||||
button_layout.addWidget(self.btn_open_file)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class SeekingWidget(QWidget):
|
||||
"""
|
||||
Виджет для режима поиска (сканирования папок).
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Создает макет виджета с вкладками.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
|
||||
|
||||
class ClientAnalyzerWidget(QWidget):
|
||||
"""
|
||||
Виджет для клиентского анализа трассировки.
|
||||
|
||||
Содержит вкладки и кнопки для сохранения состояния, открытия папки и расчёта TCW.
|
||||
"""
|
||||
|
||||
def __init__(self, tab_widget: CustomTabWidget):
|
||||
super().__init__()
|
||||
self._tab_widget = tab_widget
|
||||
self._build_widget()
|
||||
|
||||
def get_widget(self) -> QWidget:
|
||||
"""
|
||||
Возвращает виджет.
|
||||
"""
|
||||
return self
|
||||
|
||||
def _build_widget(self) -> None:
|
||||
"""
|
||||
Строит макет виджета с вкладками и управляющими кнопками.
|
||||
"""
|
||||
main_layout = QVBoxLayout()
|
||||
self.btn_save_state = QPushButton("Save state")
|
||||
self.btn_save_state.setFixedWidth(185)
|
||||
self.btn_open_folder = QPushButton("Open folder")
|
||||
self.btn_open_folder.setFixedWidth(185)
|
||||
self.btn_calc_tcw = QPushButton("Calculate TCW")
|
||||
self.btn_calc_tcw.setFixedWidth(185)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.setSpacing(2)
|
||||
button_layout.addWidget(self.btn_save_state)
|
||||
button_layout.addWidget(self.btn_open_folder)
|
||||
button_layout.addWidget(self.btn_calc_tcw)
|
||||
button_container = QWidget()
|
||||
button_container.setLayout(button_layout)
|
||||
|
||||
main_layout.addWidget(self._tab_widget)
|
||||
main_layout.addWidget(button_container)
|
||||
self.setLayout(main_layout)
|
||||
36
src/main.py
36
src/main.py
@ -1,44 +1,36 @@
|
||||
import sys
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtWidgets
|
||||
|
||||
from gui.main_gui import MainWindow
|
||||
from controller.file_manager import DirectoryMonitor, FileManager
|
||||
from gui.mainGui import MainWindow
|
||||
from controller.monitor import DirectoryMonitor
|
||||
from controller.mediator import Mediator
|
||||
from controller.converter import DataConverter
|
||||
from gui.plotter import PlotWidget
|
||||
from controller.controller import Controller
|
||||
from controller.passport_former import PassportFormer
|
||||
from performance.roboter import TraceProcessor
|
||||
from controller.passportFormer import PassportFormer
|
||||
|
||||
|
||||
def main():
|
||||
pg.setConfigOptions(useOpenGL=False, antialias=False)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
monitor = DirectoryMonitor()
|
||||
file_manager = FileManager(monitor=monitor)
|
||||
monitor._file_manager = file_manager
|
||||
data_converter = DataConverter()
|
||||
controller = Controller(file_manager=file_manager)
|
||||
plot_widget_builder = PlotWidget(controller=controller)
|
||||
plot_widget_builder = PlotWidget()
|
||||
controller = Controller()
|
||||
passport_former = PassportFormer()
|
||||
window = MainWindow()
|
||||
trace_processor = TraceProcessor()
|
||||
mediator = Mediator(data_converter, passport_former, plot_widget_builder, controller, file_manager, trace_processor)
|
||||
window = MainWindow(controller)
|
||||
mediator = Mediator(monitor, data_converter, passport_former, plot_widget_builder, controller)
|
||||
window.show()
|
||||
|
||||
window.signal_mode.connect(controller.set_working_mode)
|
||||
window.signal_settings.connect(controller.update_settings)
|
||||
window.signal_replot_all.connect(controller.update_plots)
|
||||
window.signal_open_file.connect(controller.open_file)
|
||||
window.signal_open_dir.connect(controller.open_dir)
|
||||
window.signal_save_file.connect(controller.save_file)
|
||||
window.signal_TCW_for_client.connect(controller.build_TCW_for_client)
|
||||
|
||||
controller.signal_widgets.connect(window.show_plot_tabs)
|
||||
controller.signal_progress_bar.connect(window.status_widget.set_progress)
|
||||
controller.signal_status_text.connect(window.status_widget.set_note)
|
||||
controller.signal_statusBar.connect(window.update_progressBar)
|
||||
controller.signal_statusText.connect(window.update_stateLabel)
|
||||
controller.signal_settings.connect(mediator.update_settings)
|
||||
controller.signal_open_file.connect(monitor.custom_csv_extract_only)
|
||||
controller.signal_raport_mode.connect(monitor.start_raport)
|
||||
controller.signal_seeking_mode.connect(monitor.start_seeking)
|
||||
controller.signal_update_plots.connect(monitor.update_plots)
|
||||
|
||||
sys.exit(app.exec_())
|
||||
|
||||
|
||||
BIN
src/performance/__pycache__/roboter.cpython-310.pyc
Normal file
BIN
src/performance/__pycache__/roboter.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,119 +1,25 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from typing import Optional
|
||||
import os
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
|
||||
#TODO: Реализовать поиск времени простоя
|
||||
class DowntimeAnalyzer:
|
||||
def __init__(self) -> None:
|
||||
...
|
||||
from roboter import Performance
|
||||
|
||||
def generate_downtime_report(self) -> list:
|
||||
...
|
||||
class PerformanceProcessor:
|
||||
|
||||
def _separate_conditions(self, TWC_raw:pd.DataFrame, robot_raw:pd.DataFrame) -> Tuple[dict, dict]:
|
||||
robot_splitter = RobotConditionSplitter()
|
||||
TWC_splitter = TWC_ConditionSplitter()
|
||||
#comm_splitter = CommConditionSplitter()
|
||||
rob_conditions = robot_splitter.split(robot_raw)
|
||||
TWC_conditions = TWC_splitter.split(TWC_raw)
|
||||
#comm_df = comm_splitter.split(rob_conditions["communication"], TWC_conditions["communication"])
|
||||
return (rob_conditions, TWC_conditions)
|
||||
def calc_performance(self, path:str, TWC_raw:pd.DataFrame):
|
||||
factory = Performance()
|
||||
comm_df, rob_df, TWC_df = factory.job(path, TWC_raw)
|
||||
|
||||
|
||||
|
||||
class ConditionSplitter:
|
||||
def __init__(self):
|
||||
self._signals = []
|
||||
|
||||
def _find_indexes(self,
|
||||
signal: str,
|
||||
dataframe: pd.DataFrame) -> list[list[float], list[float]]:
|
||||
stage_diff = np.diff(dataframe[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
def _find_events(self,
|
||||
dataframe: pd.DataFrame,
|
||||
signals: list[str]) -> Optional[dict[dict[pd.Series]]]:
|
||||
intervals = {}
|
||||
end_time = 0
|
||||
for signal in signals:
|
||||
start_idx, finish_idx = np.array(self._find_indexes(signal, dataframe))
|
||||
start_series = dataframe.loc[start_idx, "time"].reset_index(drop=True)
|
||||
end_series = dataframe.loc[finish_idx, "time"].reset_index(drop=True)
|
||||
end_series.fillna(end_time)
|
||||
intervals[signal] = {"rise": start_series,
|
||||
"fall": end_series}
|
||||
return intervals
|
||||
|
||||
def _form_intervals(self,
|
||||
start: pd.Series,
|
||||
end: pd.Series) -> dict:
|
||||
if len(start) != len(end):
|
||||
for i in range(1, len(end)):
|
||||
if end[i-1] > start[i]:
|
||||
start = start.drop(i).reset_index(drop=True)
|
||||
intervals = {'start':start.tolist, 'end':end.tolist}
|
||||
return intervals
|
||||
|
||||
|
||||
class RobotConditionSplitter(ConditionSplitter):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"$OUT3012",
|
||||
"$IN3003",
|
||||
"$OUT3003",
|
||||
"$OUT3244"
|
||||
]
|
||||
|
||||
def split(self, dataframe: pd.DataFrame) -> dict:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
|
||||
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
|
||||
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
|
||||
conditions = {"communication":communication_sig, "waiting": point_interval, "moving": movement_interval}
|
||||
return conditions
|
||||
|
||||
|
||||
class TWC_ConditionSplitter(ConditionSplitter):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncoming"
|
||||
]
|
||||
|
||||
def split(self, dataframe: pd.DataFrame) -> dict:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
|
||||
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
|
||||
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
|
||||
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
|
||||
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
|
||||
conditions = {
|
||||
"communication":communication_sig,
|
||||
'closing':closing_interval,
|
||||
'squeeze':squeeze_interval,
|
||||
'relief':relief_interval,
|
||||
'oncoming':oncoming_interval
|
||||
}
|
||||
return conditions
|
||||
|
||||
|
||||
class CommConditionSplitter(ConditionSplitter):
|
||||
"""
|
||||
Определяет промежуток, в который происходит взаимодействие между нодами.
|
||||
"""
|
||||
|
||||
def split(self, node1: dict, node2: dict) -> pd.DataFrame:
|
||||
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
|
||||
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
|
||||
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
path = "D:\downloads\Test2\TeslaTIME29_71_KRCIO.dat"
|
||||
|
||||
@ -1,627 +1,203 @@
|
||||
from __future__ import annotations
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
import os
|
||||
from typing import Tuple, Union, Optional, List
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from base.base import (
|
||||
BaseKukaDataParser, BaseKukaTextParser,
|
||||
BaseTraceStageDetector, BaseTextStageDetector,
|
||||
BaseRawTraceProcessor, KukaDataHead,
|
||||
KukaTXT, Settings
|
||||
)
|
||||
|
||||
|
||||
class KukaDataParser(BaseKukaDataParser):
|
||||
"""
|
||||
Класс для парсинга данных Кука.
|
||||
|
||||
class BasePerformanceFactory(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def factory_method(self):
|
||||
...
|
||||
|
||||
Читает заголовочный файл (.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)
|
||||
def job(self):
|
||||
...
|
||||
|
||||
def _get_file_data(self, path) -> pd.DataFrame:
|
||||
head, file = self._dat_parser(path)
|
||||
self.dat_name = file[:-4]
|
||||
path_r64 = os.path.dirname(path) + '\\' + file
|
||||
time_axis, dataframe = self._r64_parser(path_r64, head)
|
||||
dataframe = pd.concat([dataframe, time_axis], axis=1)
|
||||
return dataframe
|
||||
|
||||
def _parse_header_file(self, filepath: str) -> KukaDataHead:
|
||||
"""
|
||||
Парсит заголовочный файл (.dat) и извлекает информацию о каналах.
|
||||
|
||||
:param filepath: Путь к заголовочному файлу.
|
||||
:return: Объект KukaDataHead с информацией о данных.
|
||||
"""
|
||||
with open(filepath, 'r', encoding='cp1252') as file:
|
||||
data_head = KukaDataHead(0, "", {})
|
||||
is_inside_channel = False
|
||||
self._current_channel = None # Текущее название канала
|
||||
|
||||
def _dat_parser(self, path: str) -> list[dict, str]:
|
||||
with open(path, 'r') as file:
|
||||
head = {'channels': 0}
|
||||
inside_channel = False
|
||||
channels = 0
|
||||
for line in file:
|
||||
line = line.strip()
|
||||
if line in ('#BEGINCHANNELHEADER', "#BEGINGLOBALHEADER"):
|
||||
is_inside_channel = True
|
||||
elif line in ('#ENDCHANNELHEADER', "#ENDGLOBALHEADER"):
|
||||
is_inside_channel = False
|
||||
else:
|
||||
if is_inside_channel:
|
||||
self._parse_header_line(line, data_head)
|
||||
return data_head
|
||||
line = line.strip()
|
||||
if line == '#BEGINCHANNELHEADER' or line == "#BEGINGLOBALHEADER":
|
||||
inside_channel = True
|
||||
|
||||
elif line == '#ENDCHANNELHEADER' or line == "#ENDGLOBALHEADER":
|
||||
inside_channel = False
|
||||
pass
|
||||
# Формирование словаря
|
||||
elif inside_channel:
|
||||
_, data = line.split(',')
|
||||
match _:
|
||||
case '102':
|
||||
head['rob_id'] = data
|
||||
case '200':
|
||||
ch_name = data
|
||||
if ch_name != 'Zeit': channels +=1
|
||||
head[ch_name] = {}
|
||||
case '202':
|
||||
head[ch_name]['unit'] = data
|
||||
case '211':
|
||||
file = data
|
||||
case '220':
|
||||
head[ch_name]['len'] = int(data)
|
||||
case '221':
|
||||
head[ch_name]['num'] = int(data)
|
||||
case '241':
|
||||
head[ch_name]['multiplyer'] = float(data)
|
||||
head['channels'] = int(channels)
|
||||
return head, file
|
||||
|
||||
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))
|
||||
def _r64_parser(self, path: str, head: dict) -> Optional[list[pd.Series, pd.DataFrame]]:
|
||||
ch = head['channels']
|
||||
keys = list(head.keys())[-ch:]
|
||||
len_timestamps = head['Zeit']['len']
|
||||
t_step = head['Zeit']['multiplyer']
|
||||
time_axis = pd.Series(np.arange(0, len_timestamps*t_step, t_step))
|
||||
time_axis.name = 'time'
|
||||
return time_axis
|
||||
|
||||
@staticmethod
|
||||
def _read_r64_file(filepath: str) -> np.ndarray:
|
||||
"""
|
||||
Считывает бинарный файл (.r64) и возвращает массив чисел.
|
||||
|
||||
:param filepath: Путь к файлу.
|
||||
:return: Массив numpy с типом float.
|
||||
"""
|
||||
with open(filepath, 'rb') as file:
|
||||
dataframe = pd.DataFrame({})
|
||||
with open(path, 'rb') as file:
|
||||
data = file.read()
|
||||
numbers = np.frombuffer(data, dtype='<d')
|
||||
return numbers
|
||||
floats = np.frombuffer(data, dtype='<d') # Little-endian double
|
||||
for key in keys:
|
||||
step = head[key]['num']-1
|
||||
result = pd.Series(np.array(floats[step::ch])* head[key]['multiplyer'])
|
||||
result.name = key
|
||||
dataframe = pd.concat([dataframe, result], axis=1)
|
||||
return time_axis, dataframe
|
||||
|
||||
|
||||
class KukaTextParser(BaseKukaTextParser):
|
||||
"""
|
||||
Класс для парсинга текстовых данных Кука.
|
||||
|
||||
Извлекает сообщения из файла.
|
||||
"""
|
||||
class BaseProduct(ABC):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._in_message = False
|
||||
self._data_packs: List[KukaTXT] = []
|
||||
self._signals = []
|
||||
|
||||
def parse(self, filepath: str) -> List[KukaTXT]:
|
||||
"""
|
||||
Парсит текстовый файл и возвращает список объектов KukaTXT.
|
||||
|
||||
: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 _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 __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)
|
||||
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
|
||||
|
||||
@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 _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
|
||||
|
||||
Объединяет данные, детектирует стадии и инициирует рендеринг через медиатор.
|
||||
"""
|
||||
@abstractmethod
|
||||
def operation(self):
|
||||
...
|
||||
|
||||
|
||||
class Performance(BasePerformanceFactory):
|
||||
|
||||
def robot_method(self) -> BaseProduct:
|
||||
return RobotData()
|
||||
|
||||
def TWC_method(self) -> BaseProduct:
|
||||
return TWC_Data()
|
||||
|
||||
def comm_method(self) -> BaseProduct:
|
||||
return CommData()
|
||||
|
||||
def job(self, path:str, TWC_raw:pd.DataFrame) -> list[pd.DataFrame, list[pd.DataFrame], list[pd.DataFrame]]:
|
||||
robot = self.robot_method()
|
||||
TWC = self.TWC_method()
|
||||
comm=self.comm_method()
|
||||
dataframe = self._get_file_data(path)
|
||||
rob_comm, rob_df = robot.operation(dataframe)
|
||||
TWC_comm, TWC_df = TWC.operation(TWC_raw)
|
||||
comm_df = comm.operation(rob_comm, TWC_comm)
|
||||
return comm_df, rob_df, TWC_df
|
||||
|
||||
|
||||
class RobotData(BaseProduct):
|
||||
def __init__(self):
|
||||
self._settings = Settings()
|
||||
data_parser = KukaDataParser()
|
||||
text_parser = KukaTextParser()
|
||||
trace_detector = TraceStageDetector(self)
|
||||
text_detector = TextStageDetector(self)
|
||||
self._signals = [
|
||||
"$OUT3012",
|
||||
"$IN3003",
|
||||
"$OUT3003",
|
||||
"$OUT3244"
|
||||
]
|
||||
|
||||
super().__init__(data_parser, text_parser, trace_detector, text_detector)
|
||||
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events["$OUT3244"]["fall"], 'received':events[""][""]}
|
||||
point_interval = self._form_intervals(start=events["$OUT3012"]["rise"], end=events["$OUT3012"]["fall"])
|
||||
movement_interval = self._form_intervals(start=events["$OUT3244"]["rise"], end=events["$OUT3244"]["fall"])
|
||||
return communication_sig, pd.DataFrame({'in_point':point_interval,
|
||||
'in_move':movement_interval})
|
||||
|
||||
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
|
||||
class TWC_Data(BaseProduct):
|
||||
def __init__(self):
|
||||
self._signals = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncoming"
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _rename_df_columns(dataframe: pd.DataFrame) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Переименовывает столбцы DataFrame на основе корректной карты соответствия.
|
||||
def operation(self, dataframe: pd.DataFrame) -> list[dict, pd.DataFrame]:
|
||||
events = self._find_events(dataframe, self._signals)
|
||||
communication_sig = {'sent':events[""][""], 'received':events[""][""]} #ситара что-то делает -конец сигнала
|
||||
closing_interval = self._form_intervals(start=events["Closing"]["rise"], end=events["Closing"]["fall"])
|
||||
squeeze_interval = self._form_intervals(start=events["Squeeze"]["rise"], end=events["Squeeze"]["fall"])
|
||||
relief_interval = self._form_intervals(start=events["Relief"]["rise"], end=events["Relief"]["fall"])
|
||||
oncoming_interval = self._form_intervals(start=events["Oncoming"]["rise"], end=events["Oncoming"]["fall"])
|
||||
return communication_sig, pd.DataFrame({'in_closing':closing_interval,
|
||||
'in_squeeze':squeeze_interval,
|
||||
'in_relief':relief_interval,
|
||||
'in_oncoming':oncoming_interval})
|
||||
|
||||
class CommData(BaseProduct):
|
||||
"""
|
||||
Определяет промежуток, в который происходит взаимодействие между нодами.
|
||||
Подразумевается следующая структура: node: tuple(list, list)
|
||||
node[0] - время отправки пакетов. node[1] - время приема пакетов.
|
||||
"""
|
||||
|
||||
def operation(self, node1: dict, node2: dict) -> pd.DataFrame:
|
||||
n1_to_n2 = self._form_intervals(start=node1['sent'], end=node2['received'])
|
||||
n2_to_n1 = self._form_intervals(start=node2['sent'], end=node1['received'])
|
||||
return pd.concat([pd.DataFrame(n1_to_n2), pd.DataFrame(n2_to_n1)])
|
||||
|
||||
|
||||
: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
|
||||
"""
|
||||
|
||||
@ -1,52 +1,29 @@
|
||||
# TODO: Тесты должны лежать в директории tests.
|
||||
|
||||
# TODO: Правило хорошего тона при оформлении импортов: сначала встроенные библиотеки,
|
||||
# через одну пустую строку - дополнительно установленные, и еще через одну пустую строку - собственные импорты.
|
||||
from src.OptAlgorithm.OptAlgorithm import OptAlgorithm
|
||||
from src.utils import read_json
|
||||
|
||||
from matplotlib import pyplot as plt, use
|
||||
|
||||
from numpy import cos, sin, sqrt, cbrt, arcsin, linspace, array
|
||||
# TODO: Отступ перед блоком if __name__ == "__main__" - 2 пустых строки.
|
||||
|
||||
if __name__ == "__main__":
|
||||
# TODO: Что означают переменные ниже? Имена переменных должны быть осмысленными, чтобы их смысл был понятен не
|
||||
# не только автору кода.
|
||||
|
||||
tq = 1
|
||||
ts = linspace(0, tq, 200000)
|
||||
|
||||
operator_params = read_json("../params/operator_params.json")
|
||||
system_params = read_json("../params/system_params.json")
|
||||
|
||||
# TODO: 2 блока кода ниже (с циклами) нарушают принцип DRY (Don't Repeat Yourself):
|
||||
# повторяющийся блок кода следует вынести в отдельную функцию.
|
||||
# Например, в такую:
|
||||
# def foo(params_dict: dict) -> dict:
|
||||
# result_dict = {}
|
||||
# i = 1
|
||||
# for key, value in params_dict.items():
|
||||
# if isinstance(value, list):
|
||||
# result_dict[key] = value[i] if len(value) > i else value[0]
|
||||
# else:
|
||||
# result_dict[key] = value
|
||||
# return result_dict
|
||||
# И потом вызвать ее как-то так:
|
||||
# non_array_operator_params, non_array_system_params = foo(operator_params), foo(system_params)
|
||||
|
||||
operator_params = read_json("params/operator_params.json")
|
||||
system_params = read_json("params/system_params.json")
|
||||
|
||||
non_array_operator_params = {}
|
||||
i = 1
|
||||
for key, value in operator_params.items():
|
||||
# TODO: Не совсем понятен смысл такой проверки. Если хотим проверить принадлежность value к list,
|
||||
# то лучше использовать для этого isinstance.
|
||||
if hasattr(value, "__len__"):
|
||||
# TODO: Можно заменить однострочным выражением: result_dict[key] = value[i] if len(value) > i else value[0]
|
||||
if len(value) > i:
|
||||
non_array_operator_params[key] = value[i]
|
||||
else:
|
||||
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__"):
|
||||
@ -56,7 +33,7 @@ if __name__ == "__main__":
|
||||
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])
|
||||
|
||||
BIN
src/uml/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/uml/__pycache__/creator.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/creator.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/uml/__pycache__/request_generator.cpython-310.pyc
Normal file
BIN
src/uml/__pycache__/request_generator.cpython-310.pyc
Normal file
Binary file not shown.
@ -1,10 +1,7 @@
|
||||
from uml.request_generator import Request
|
||||
from src.uml.request_generator import Request
|
||||
|
||||
|
||||
class UMLCreator:
|
||||
# TODO: в методах, обремененных некой логикой, постоянно встречается проверка режима if not self.theor_mode.
|
||||
# Есть предложение реализовать паттерн СТРАТЕГИЯ, который в зависимости от режима делал бы что-то
|
||||
# разными способами. Это бы помогло избавиться от спагетти и обособить логику работы в зависимости от ситуации.
|
||||
def __init__(self, request_generator: Request, path_to_save: str):
|
||||
self._request_generator = request_generator
|
||||
self.path_to_save = path_to_save
|
||||
@ -14,7 +11,7 @@ class UMLCreator:
|
||||
real_data = []
|
||||
ideal_data = []
|
||||
if not self.theor_mode:
|
||||
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
|
||||
|
||||
for key, items in self.timings_dict.items():
|
||||
if key == 'closure': ideal_time = self._ideal_time[0]
|
||||
elif key == 'compression': ideal_time = self._ideal_time[1]
|
||||
@ -24,17 +21,8 @@ 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:
|
||||
@ -42,16 +30,10 @@ class UMLCreator:
|
||||
ideal_data.append([(item[0]+ideal_time)*self.scaler, '{-}'])
|
||||
if key == 'opening':
|
||||
ideal_data.append([item[1]*self.scaler+0.0001, 'coming #yellow'])
|
||||
# TODO: elif key == 'opening': ideal_time = self._ideal_time[2]. Почему здесь при условии
|
||||
# значения ключа opening уже self._ideal_time[3]?
|
||||
ideal_data.append([(item[1]+self._ideal_time[3])*self.scaler, '{-}'])
|
||||
|
||||
else:
|
||||
# TODO: real_data уже определена как [] в самом начале. Если не попадаем в верхнее условие,
|
||||
# то она таковой и останется. Дублирование.
|
||||
else:
|
||||
real_data = []
|
||||
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот
|
||||
# набор данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
|
||||
ideal_data = [
|
||||
[0.0, 'closure #yellow'],
|
||||
[self._ideal_time[0] * self.scaler, '{-}'],
|
||||
@ -63,12 +45,7 @@ class UMLCreator:
|
||||
[(sum(self._ideal_time[:3]) + self.WeldTime) * self.scaler, 'coming #yellow'],
|
||||
[(sum(self._ideal_time[:4]) + self.WeldTime) * self.scaler, '{-}'],
|
||||
]
|
||||
# TODO: Очень неудачный способ хранения данных разного типа: сложен в восприятии,
|
||||
# легко ошибиться или запутаться.
|
||||
# Используй NamedTuple или dataclass: будут определены поля, их типы.
|
||||
# Взаимодействие также будет гораздо проще (по имени поля, а не индексу).
|
||||
# TODO: Похоже, что все данные в этом списке - константы. Следовательно, можно проинициализировать этот набор
|
||||
# данных однократно при инициализации класса, а не делать это время всякий раз при вызове метода.
|
||||
|
||||
client_data = [
|
||||
[0.0 * self.scaler, 'closure'],
|
||||
[0.165 * self.scaler, '{-}'],
|
||||
@ -80,7 +57,7 @@ class UMLCreator:
|
||||
[0.300 * self.scaler, '{-}'],
|
||||
]
|
||||
|
||||
# TODO: Зачем метод возвращает self.bool_dict, если никаких манипуляций с ним не проводит?
|
||||
|
||||
return real_data, client_data, ideal_data, self.bool_dict
|
||||
|
||||
def _generate_svg(self, real_data, client_data, ideal_data, bool_data) -> None:
|
||||
@ -93,8 +70,6 @@ class UMLCreator:
|
||||
|
||||
for i, [signal, changes] in enumerate(bool_data.items()):
|
||||
name = 'bool_' + str(i)
|
||||
# TODO: 3 строки ниже замени на list comprehension
|
||||
# times = [[str(float(f[0])*scaler), f[1]] for f in changes]
|
||||
times = []
|
||||
for f in changes:
|
||||
times.append([str(float(f[0])*self.scaler), f[1]])
|
||||
@ -123,9 +98,7 @@ class UMLCreator:
|
||||
timings_dict: dict,
|
||||
mode: bool,
|
||||
name:str):
|
||||
# TODO: Аргумент mode вводит в заблуждение: ожидаешь увидеть какой-то режим, на деле же это флаг
|
||||
# теоретического режима. Поправь название аргумента.
|
||||
# TODO: Инициализацию атрибутов класса в конструкторе надо делать!
|
||||
|
||||
self._ideal_time = ideal_time
|
||||
self.bool_dict = bool_dict
|
||||
self.float_dict = float_dict
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
from os.path import abspath
|
||||
|
||||
from plantuml import PlantUML
|
||||
from os.path import abspath
|
||||
|
||||
|
||||
class Request:
|
||||
# TODO: Имя класса не соответствует тому, что он делает.
|
||||
def __init__(self, server_url: str):
|
||||
self._server_url = server_url
|
||||
|
||||
@ -15,15 +13,12 @@ class Request:
|
||||
self.clear()
|
||||
|
||||
def _startUML(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('@startuml')
|
||||
|
||||
def _endUML(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('@enduml')
|
||||
|
||||
def clear(self):
|
||||
# TODO: Атрибуты класса инициализируются в конструкторе класса.
|
||||
self.timestamps = {}
|
||||
self.reqArr = []
|
||||
self.variables = []
|
||||
@ -32,36 +27,28 @@ class Request:
|
||||
self._startUML()
|
||||
|
||||
def addAnalog(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'analog "{string}" as {name}')
|
||||
|
||||
def addBinary(self, name, string, style = ''):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
if style: name = name + '<<' + style + '>>'
|
||||
self.variables.append(f'binary "{string}" as {name}')
|
||||
|
||||
def addClock(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'clock "{string}" as {name}')
|
||||
|
||||
def addConcise(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'concise "{string}" as {name}')
|
||||
|
||||
def addRobust(self, name, string):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(f'robust "{string}" as {name}')
|
||||
|
||||
def appendStr(self, string = ''):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.variables.append(string)
|
||||
|
||||
def addLineStyle(self, name, color = 'green', thicknes = 1):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.lineStyles[name] = [f'LineColor {color}', f'LineThickness {thicknes}']
|
||||
|
||||
def generateSVG(self, name):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self._compileUML(name)
|
||||
filename = abspath(f'{name}.txt')
|
||||
self.server.processes_file(filename, outfile=f'{name}.svg')
|
||||
@ -69,7 +56,6 @@ class Request:
|
||||
#return result
|
||||
|
||||
def setTimestamps(self, name, input):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
for time, state in input:
|
||||
try:
|
||||
self.timestamps[f'@{time}'].append(f'{name} is {state}')
|
||||
@ -77,13 +63,11 @@ class Request:
|
||||
self.timestamps[f'@{time}'] = [f'{name} is {state}']
|
||||
|
||||
def _addTimestamp(self, timecode, vars):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append(timecode)
|
||||
for var in vars:
|
||||
self.reqArr.append(var)
|
||||
|
||||
def _addHeader(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
self.reqArr.append('<style>')
|
||||
if self.lineStyles:
|
||||
self.reqArr.append('timingDiagram {')
|
||||
@ -96,13 +80,9 @@ class Request:
|
||||
self.reqArr.append('</style>')
|
||||
|
||||
def _addVariables(self):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
for var in self.variables: self.reqArr.append(str(var))
|
||||
|
||||
def _compileUML(self, name):
|
||||
# TODO: Единый стиль именования методов! (snake_case / lowercase).
|
||||
# TODO: Метод и строку собирает, и в файл пишет. Разделить логику формирования строки и записи в файл
|
||||
# по разным методам.
|
||||
self._addHeader()
|
||||
self._addVariables()
|
||||
|
||||
@ -111,8 +91,6 @@ class Request:
|
||||
self._addTimestamp(key, item)
|
||||
|
||||
self._endUML()
|
||||
# TODO: Зачем делать из self.stringUML атрибут класса?
|
||||
# Используется только тут - достаточно локальной переменной.
|
||||
self.stringUML = [line + '\n' for line in self.reqArr]
|
||||
with open(f'{name}.txt', 'w', encoding='utf-8') as file:
|
||||
file.writelines(self.stringUML)
|
||||
|
||||
BIN
src/utils/__pycache__/__init__.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/__init__.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/base.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/base_widgets.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/base_widgets.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/diagram_parser.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/diagram_parser.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/json_tools.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/json_tools.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/__pycache__/qt_settings.cpython-310.pyc
Normal file
BIN
src/utils/__pycache__/qt_settings.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/base/__pycache__/base.cpython-310.pyc
Normal file
BIN
src/utils/base/__pycache__/base.cpython-310.pyc
Normal file
Binary file not shown.
BIN
src/utils/base/__pycache__/base_widgets.cpython-310.pyc
Normal file
BIN
src/utils/base/__pycache__/base_widgets.cpython-310.pyc
Normal file
Binary file not shown.
613
src/utils/base/base.py
Normal file
613
src/utils/base/base.py
Normal file
@ -0,0 +1,613 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Optional, Union, Any
|
||||
from cachetools import LRUCache
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
||||
import pandas as pd
|
||||
from PyQt5.QtCore import QObject, QTimer
|
||||
from PyQt5.QtWidgets import QWidget, QTabWidget, QMainWindow, QVBoxLayout
|
||||
from OptAlgorithm import OptAlgorithm
|
||||
from utils.qt_settings import dark_style
|
||||
|
||||
|
||||
|
||||
|
||||
class BaseMediator:
|
||||
def __init__(self,
|
||||
monitor: BaseDirectoryMonitor,
|
||||
converter: BaseDataConverter,
|
||||
passportFormer: BasePointPassportFormer,
|
||||
plot: BasePlotWidget,
|
||||
controller: BaseController):
|
||||
self._monitor = monitor
|
||||
self._monitor.mediator = self
|
||||
self._converter = converter
|
||||
self._converter.mediator = self
|
||||
self._passportFormer = passportFormer
|
||||
self._passportFormer.mediator = self
|
||||
self._plot = plot
|
||||
self._plot.mediator = self
|
||||
self._controller = controller
|
||||
self._controller.mediator = self
|
||||
|
||||
def notify(self,
|
||||
source: Union[BaseDirectoryMonitor, BaseDataConverter, BasePointPassportFormer, BasePlotWidget],
|
||||
data: Union[list[str], list[pd.DataFrame], list[list], list[QWidget]]):
|
||||
...
|
||||
def update_settings (self, data: list[dict]):
|
||||
...
|
||||
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
...
|
||||
|
||||
class BaseDirectoryMonitor:
|
||||
|
||||
update_timer = QTimer()
|
||||
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
super().__init__()
|
||||
self._directory_path = None
|
||||
self._update_time = None
|
||||
self.isActive = False
|
||||
self._files = []
|
||||
self._mediator = mediator
|
||||
|
||||
|
||||
@property
|
||||
def directory_path(self) -> str:
|
||||
return self._directory_path
|
||||
|
||||
@property
|
||||
def update_time(self) -> int:
|
||||
return self._update_time
|
||||
|
||||
@property
|
||||
def files(self) -> list[str]:
|
||||
return self._files
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def _init_state(self):
|
||||
files = os.listdir(self._directory_path)
|
||||
self._files = files
|
||||
|
||||
def start(self):
|
||||
self.isActive = True
|
||||
self.update_timer.start(int(self._update_time))
|
||||
|
||||
def stop(self):
|
||||
self.isActive = False
|
||||
self.update_timer.stop()
|
||||
|
||||
def update_settings(self, data: list[dict]) -> None:
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
...
|
||||
|
||||
def force_all_dir(self):
|
||||
...
|
||||
|
||||
class BaseDataConverter:
|
||||
def __init__(self, mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
def convert_data(self, files: list[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
class BasePlotWidget:
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
super().__init__()
|
||||
self._mediator = mediator
|
||||
|
||||
self._stage_colors = {
|
||||
"Closing": [220, 20, 60, 100], # Crimson
|
||||
"Squeeze": [30, 144, 255, 100], # Dodger Blue
|
||||
"Welding": [128, 128, 128, 100], # Gray
|
||||
"Relief": [34, 139, 34, 100], # Forest Green
|
||||
"Oncomming": [255, 165, 0, 100] # Orange
|
||||
}
|
||||
self._plt_channels = {
|
||||
"Electrode Force, N & Welding Current, kA": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": True,
|
||||
"ideals": True,
|
||||
"mirror ME": False,
|
||||
"workpiece": False,
|
||||
"force compensation FE": False,
|
||||
"force accuracy":True
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Electrode Force, N ME",
|
||||
"pen": 'r',
|
||||
},
|
||||
{
|
||||
"name": "Electrode Force, N FE",
|
||||
"pen": 'w',
|
||||
},
|
||||
{
|
||||
"name": "Welding Current ME",
|
||||
"pen": "y",
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Force",
|
||||
"pen": {'color': 'g', 'width':3},
|
||||
}
|
||||
]
|
||||
},
|
||||
"Electrode Position, mm": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": False,
|
||||
"ideals": True,
|
||||
"mirror ME": True,
|
||||
"workpiece": True,
|
||||
"force compensation FE": True,
|
||||
"force accuracy":False
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Position, mm ME",
|
||||
"pen": {'color': 'r', 'width':2},
|
||||
},
|
||||
{
|
||||
"name": "Rotor Position, mm FE",
|
||||
"pen": {'color': 'w', 'width':2},
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Position ME",
|
||||
"pen": {'color': 'g', 'width':4},
|
||||
},
|
||||
{
|
||||
"name": "Position FE",
|
||||
"pen": {'color': 'b', 'width':4},
|
||||
}
|
||||
]
|
||||
},
|
||||
"Electrode Speed, mm/s": {
|
||||
"Settings": {
|
||||
"zoom": False,
|
||||
"stages": True,
|
||||
"performance": False,
|
||||
"ideals": True,
|
||||
"mirror ME": False,
|
||||
"workpiece": False,
|
||||
"force compensation FE": False,
|
||||
"force accuracy":False
|
||||
},
|
||||
"Real_signals": [
|
||||
{
|
||||
"name": "Rotor Speed, mm/s ME",
|
||||
"pen": 'r',
|
||||
"zoom": False
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed, mm/s FE",
|
||||
"pen": 'w',
|
||||
"zoom": False
|
||||
}
|
||||
],
|
||||
"Ideal_signals": [
|
||||
{
|
||||
"name": "Rotor Speed ME",
|
||||
"pen": {'color': 'g', 'width':3},
|
||||
"zoom": False
|
||||
},
|
||||
{
|
||||
"name": "Rotor Speed FE",
|
||||
"pen": {'color': 'b', 'width':3},
|
||||
"zoom": False
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
def set_style(self, object: Union[QTabWidget, QWidget]) -> None:
|
||||
object.setStyleSheet(
|
||||
"""QLabel {
|
||||
color: #ffffff;
|
||||
font-size: 26px;
|
||||
font-weight: bold;
|
||||
font-family: "Segoe UI", sans-serif;
|
||||
}""")
|
||||
|
||||
def _downsample_data(self, x, y, max_points=5000):
|
||||
"""
|
||||
Понижает разрешение данных до заданного количества точек для улучшения производительности навигатора.
|
||||
"""
|
||||
if len(x) > max_points:
|
||||
factor = len(x) // max_points
|
||||
x_downsampled = x[::factor]
|
||||
y_downsampled = y[::factor]
|
||||
return x_downsampled, y_downsampled
|
||||
return x, y
|
||||
|
||||
def _create_navigator(self,
|
||||
time_region:tuple[float, float],
|
||||
main_plot: pg.PlotItem) -> list[pg.PlotWidget, pg.LinearRegionItem]:
|
||||
"""
|
||||
Создаёт график-навигатор, отображающий все данные в уменьшенном масштабе.
|
||||
"""
|
||||
navigator = pg.PlotItem(title="Navigator")
|
||||
navigator.setFixedHeight(100)
|
||||
# Получение кривых из main_plot
|
||||
for curve in main_plot.listDataItems():
|
||||
# Извлекаем данные из кривой
|
||||
x, y = curve.getData()
|
||||
curve_name = curve.opts.get("name", None)
|
||||
signal_pen = curve.opts.get("pen", None)
|
||||
x_downsampled, y_downsampled = self._downsample_data(x, y, max_points=1000)
|
||||
navigator.plot(x_downsampled, y_downsampled, pen=signal_pen, name=curve_name)
|
||||
|
||||
ROI_region = pg.LinearRegionItem(values=time_region, movable=True, brush=pg.mkBrush(0, 0, 255, 100), pen=pg.mkPen(width=4))
|
||||
ROI_region.setBounds([0, x[-1]])
|
||||
navigator.addItem(ROI_region)
|
||||
navigator.getViewBox().setLimits(xMin=0, xMax=x[-1])
|
||||
|
||||
# Связываем изменение региона навигатора с обновлением области просмотра основного графика
|
||||
ROI_region.sigRegionChanged.connect(lambda: self._sync_main_plot_with_navigator(main_plot, ROI_region))
|
||||
|
||||
return navigator, ROI_region
|
||||
|
||||
def _sync_main_plot_with_navigator(self,
|
||||
main_plot: pg.PlotItem,
|
||||
region: pg.LinearRegionItem):
|
||||
"""
|
||||
Синхронизирует область просмотра основного графика с регионом навигатора.
|
||||
"""
|
||||
x_min, x_max = region.getRegion()
|
||||
if main_plot:
|
||||
main_plot.blockSignals(True)
|
||||
main_plot.setXRange(x_min, x_max, padding=0)
|
||||
main_plot.blockSignals(False)
|
||||
|
||||
def _mirror_shift_data(self,
|
||||
valid_str: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if valid_str in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: shift-x)
|
||||
return dataframe
|
||||
|
||||
def _mirror_shift_ideal(self,
|
||||
name_ME: str, name_FE: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float,
|
||||
P1: float, P2: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if name_ME in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: -x+P1)
|
||||
|
||||
if name_FE in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x+P2)
|
||||
|
||||
return dataframe
|
||||
|
||||
def _shift_data(self,
|
||||
valid_str: str,
|
||||
signals: list[dict],
|
||||
dataframe: pd.DataFrame,
|
||||
shift: float) -> pd.DataFrame:
|
||||
keys = dataframe.keys()
|
||||
for signal in signals:
|
||||
if valid_str in signal["name"] and signal["name"] in keys:
|
||||
dataframe[signal["name"]] = dataframe[signal["name"]].apply(lambda x: x + shift)
|
||||
return dataframe
|
||||
|
||||
def _sync_navigator_with_main(self, main_plot: pg.PlotItem, region:pg.LinearRegionItem):
|
||||
"""
|
||||
Синхронизирует регион навигатора с областью просмотра основного графика.
|
||||
"""
|
||||
if region:
|
||||
x_min, x_max = main_plot
|
||||
region.blockSignals(True) # Предотвращаем рекурсию
|
||||
region.setRegion([x_min, x_max])
|
||||
region.blockSignals(False)
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
|
||||
@property
|
||||
def opt(self) -> BaseIdealDataBuilder:
|
||||
return self._opt
|
||||
|
||||
@opt.setter
|
||||
def opt(self, opt: BaseIdealDataBuilder):
|
||||
self._opt = opt
|
||||
|
||||
def build(self, data: list[pd.DataFrame]) -> list[QWidget]:
|
||||
...
|
||||
|
||||
|
||||
class BaseController(QObject):
|
||||
|
||||
def send_widgets(self, widgets: list[QWidget]) -> None:
|
||||
...
|
||||
|
||||
def update_settings(self, settings: list[dict]) -> None:
|
||||
...
|
||||
|
||||
def raport_mode (self) -> None:
|
||||
...
|
||||
|
||||
def seeking_mode(self) -> None:
|
||||
...
|
||||
|
||||
def open_file(self, filepath: str) -> None:
|
||||
...
|
||||
|
||||
def update_plots(self) -> None:
|
||||
...
|
||||
|
||||
def update_status(self, msg: Union[str, float]) -> None:
|
||||
...
|
||||
|
||||
class BaseIdealDataBuilder(OptAlgorithm):
|
||||
|
||||
def __init__(self, params: list[dict]):
|
||||
operator_params, system_params = params
|
||||
self.mul = system_params['time_capture']
|
||||
self.welding_time = operator_params['time_wielding']
|
||||
|
||||
|
||||
|
||||
super().__init__(operator_params, system_params)
|
||||
|
||||
|
||||
def _get_data(self, end_timestamp:float, func:function) -> pd.DataFrame:
|
||||
data = []
|
||||
for i in range (0, int(end_timestamp*self.mul)+1):
|
||||
time = i/self.mul
|
||||
X1, X2, V1, V2, F = func(time)
|
||||
data.append({"time":time, "Position FE":X1*1000,"Position ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
|
||||
X1, X2, V1, V2, F = func(end_timestamp)
|
||||
data.append({"time":end_timestamp, "Position FE":X1*1000,"Position ME":X2*1000, "Rotor Speed FE":V1*1000, "Rotor Speed ME":V2*1000, "Force":F})
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def get_closingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_compressionDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_openingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_tmovementDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_weldingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_oncomingDF(self) -> pd.DataFrame:
|
||||
...
|
||||
|
||||
def get_ideal_timings(self) -> list[float, float, float, float]:
|
||||
...
|
||||
|
||||
def get_cycle_time(self) -> float:
|
||||
result = sum(self.get_ideal_timings())
|
||||
return result
|
||||
|
||||
class BaseMainWindow(QMainWindow):
|
||||
def __init__(self,
|
||||
controller: Optional[BaseController] = None):
|
||||
super().__init__()
|
||||
self.resize(200,200)
|
||||
self._controller = controller
|
||||
# Создаем центральный виджет и устанавливаем его
|
||||
self._central_widget = QWidget()
|
||||
self.setCentralWidget(self._central_widget)
|
||||
|
||||
# Устанавливаем основной вертикальный макет для центрального виджета
|
||||
self._central_layout = QVBoxLayout()
|
||||
self._central_widget.setLayout(self._central_layout)
|
||||
self.set_style(self)
|
||||
...
|
||||
|
||||
@property
|
||||
def controller(self) -> BaseController:
|
||||
return self._controller
|
||||
|
||||
@controller.setter
|
||||
def controller(self, controller: BaseController) -> None:
|
||||
self._controller = controller
|
||||
|
||||
def set_style(self, object: Union[QTabWidget, QWidget, QMainWindow]) -> None:
|
||||
object.setStyleSheet(dark_style)
|
||||
|
||||
class BasePointPassportFormer:
|
||||
|
||||
def __init__(self,
|
||||
mediator: Optional[BaseMediator] = None):
|
||||
self._mediator = mediator
|
||||
self._clear_stage = "Welding"
|
||||
self._stages = [
|
||||
"Closing",
|
||||
"Squeeze",
|
||||
"Welding",
|
||||
"Relief",
|
||||
"Oncomming"
|
||||
]
|
||||
self._tesla_stages = [
|
||||
"Tesla squeeze",
|
||||
"Tesla closing",
|
||||
"Tesla welding",
|
||||
"Tesla oncomming_relief"
|
||||
]
|
||||
self._params = []
|
||||
self._ideal_data_cashe = LRUCache(maxsize=1000)
|
||||
self._OptAlgorithm_operator_params = [
|
||||
"dist_open_start_1",
|
||||
"dist_open_start_2",
|
||||
"dist_open_after_1",
|
||||
"dist_open_after_2",
|
||||
"dist_open_end_1",
|
||||
"dist_open_end_2",
|
||||
"dist_close_end_1",
|
||||
"dist_close_end_2",
|
||||
"time_command",
|
||||
"time_robot_movement",
|
||||
"object_thickness",
|
||||
"object_position",
|
||||
"distance_h_end1",
|
||||
"distance_h_end2",
|
||||
"distance_l_1",
|
||||
"distance_l_2",
|
||||
"force_target",
|
||||
"force_capture",
|
||||
"time_wielding"]
|
||||
self._OptAlgorithm_system_params = [
|
||||
"a_max_1",
|
||||
"v_max_1",
|
||||
"a_max_2",
|
||||
"v_max_2",
|
||||
"mass_1",
|
||||
"mass_2",
|
||||
"k_hardness_1",
|
||||
"k_hardness_2",
|
||||
"torque_max_1",
|
||||
"torque_max_2",
|
||||
"transmission_ratio_1",
|
||||
"transmission_ratio_2",
|
||||
"position_start_1",
|
||||
"position_start_2",
|
||||
"k_prop",
|
||||
"time_capture"]
|
||||
|
||||
def _find_indexes(self,
|
||||
signal: str,
|
||||
dataframe: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
|
||||
stage_diff = np.diff(dataframe[signal])
|
||||
start_idx = np.where(stage_diff == 1)
|
||||
finish_idx = np.where(stage_diff == -1)
|
||||
return start_idx[0], finish_idx[0]
|
||||
|
||||
def _find_events(self,
|
||||
signal: str,
|
||||
times:pd.Series,
|
||||
dataframe: pd.DataFrame) -> tuple[list[float], list[float]]:
|
||||
start_idx, finish_idx = self._find_indexes(signal, dataframe)
|
||||
|
||||
if len(start_idx) > 0 and len(finish_idx) > 0 and start_idx[0] > finish_idx[0]:
|
||||
start_idx = np.insert(start_idx, 0, 0)
|
||||
start_list = times.iloc[start_idx].tolist() if len(start_idx) > 0 else []
|
||||
end_list = times.iloc[finish_idx].tolist() if len(finish_idx) > 0 else []
|
||||
if len(start_list) - len(end_list) == 1:
|
||||
end_list.append(float(times.iloc[-1]))
|
||||
|
||||
return start_list, end_list
|
||||
|
||||
def _filter_events(self,
|
||||
times: pd.Series,
|
||||
dataframe: pd.DataFrame) -> tuple[dict[str, list[list[float]]], int]:
|
||||
events = {}
|
||||
point_quantity = 0
|
||||
if self._clear_stage in self._stages:
|
||||
start_list, end_list = self._find_events(self._clear_stage, times, dataframe)
|
||||
point_quantity = len(start_list)
|
||||
if point_quantity == 0:
|
||||
#TODO: добавить обработку исключения
|
||||
return []
|
||||
|
||||
for stage in self._stages:
|
||||
start_list, end_list = self._find_events(stage, times, dataframe)
|
||||
temp = min([len(start_list), len(end_list)])
|
||||
if temp < point_quantity:
|
||||
print ("cant find enough", stage)
|
||||
start_list += [0]*(point_quantity - temp)
|
||||
end_list += [1]*(point_quantity - temp)
|
||||
|
||||
events[stage] = [start_list, end_list]
|
||||
return events, point_quantity
|
||||
|
||||
def _build_ideal_data(self,
|
||||
idealDataBuilder: Optional[BaseIdealDataBuilder] = None,
|
||||
params: list[dict] = None) -> dict:
|
||||
self.opt = idealDataBuilder(params)
|
||||
stage_ideals = {
|
||||
"Closing": self._opt.get_closingDF(),
|
||||
"Squeeze": self._opt.get_compressionDF(),
|
||||
"Welding": self._opt.get_weldingDF(),
|
||||
"Relief": self._opt.get_openingDF(),
|
||||
"Oncomming": self._opt.get_oncomingDF(),
|
||||
"Ideal cycle": self._opt.get_cycle_time(),
|
||||
"Ideal timings": self._opt.get_ideal_timings()
|
||||
}
|
||||
return stage_ideals
|
||||
|
||||
def form_passports(self) -> list[list[pd.DataFrame, dict, list]]:
|
||||
...
|
||||
|
||||
def update_settings(self, params: list) -> None:
|
||||
...
|
||||
|
||||
def _generate_cache_key(self,
|
||||
params_list: list[dict[str, Any]]) -> tuple:
|
||||
"""
|
||||
Преобразует params_list в хешируемый ключ для кэша.
|
||||
"""
|
||||
operator_settings, system_settings = params_list
|
||||
|
||||
# Преобразуем словари в отсортированные кортежи пар ключ-значение
|
||||
operator_tuple = frozenset((key, value)
|
||||
for key, value in operator_settings.items()
|
||||
if str(key) in self._OptAlgorithm_operator_params)
|
||||
|
||||
system_tuple = frozenset((key, value)
|
||||
for key, value in system_settings.items()
|
||||
if str(key) in self._OptAlgorithm_system_params)
|
||||
return (operator_tuple, system_tuple)
|
||||
|
||||
@property
|
||||
def opt(self) -> BaseIdealDataBuilder:
|
||||
return self._opt
|
||||
|
||||
@opt.setter
|
||||
def opt(self, opt: BaseIdealDataBuilder):
|
||||
self._opt = opt
|
||||
|
||||
@property
|
||||
def mediator(self) -> BaseMediator:
|
||||
return self._mediator
|
||||
|
||||
@mediator.setter
|
||||
def mediator(self, mediator: BaseMediator) -> None:
|
||||
self._mediator = mediator
|
||||
@ -9,34 +9,27 @@ class DiagramParser:
|
||||
system_config["Welding_signal"],
|
||||
system_config["Release_signal"],
|
||||
system_config["Oncomming_signal"]]
|
||||
# TODO: Переменные в Python именуются в lowercase либо snake_case.
|
||||
|
||||
self.boolDict = {}
|
||||
self.floatDict = {}
|
||||
self.timingsDict = {}
|
||||
self.theor_mode = False
|
||||
|
||||
def setData(self, path):
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
# TODO: Данный метод устанавливает какой-то режим, читает CSV, наполняет словари некими данными.
|
||||
# Декомпозировать на разные методы, каждый из которых будет решать какую-то свою задачу.
|
||||
if not path:
|
||||
self.theor_mode = True
|
||||
else:
|
||||
self.data = pd.read_csv(path)
|
||||
|
||||
# TODO: 2 цикла ниже: дублирование кода. Устранить.
|
||||
|
||||
for signalName in self.data.columns:
|
||||
# TODO: if isinstance...
|
||||
if type (self.data[signalName].iloc[0]) == np.bool:
|
||||
self.boolDict[signalName] = self._getBoolChanges(signalName)
|
||||
|
||||
for signalName in self.data.columns:
|
||||
# TODO: if isinstance...
|
||||
if type (self.data[signalName].iloc[0]) == np.float64:
|
||||
self.floatDict[signalName] = self._getFloatChanges(signalName)
|
||||
|
||||
for key, items in self.boolDict.items():
|
||||
# TODO: Писать так условия - моветон. Как вариант, можно применить конструкцию match - case.
|
||||
if key == self.signals[0]: name = "closure"
|
||||
elif key == self.signals[1]: name = "compression"
|
||||
elif key == self.signals[2]: name = "welding"
|
||||
@ -53,34 +46,22 @@ class DiagramParser:
|
||||
self.timingsDict[name].append([items[i][0], items[i][0]+0.01])
|
||||
|
||||
def getBoolDict (self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# @property
|
||||
# def bool_dict(self) -> dict:
|
||||
# return self._bool_dict
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.boolDict
|
||||
|
||||
def getFloatDict (self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.floatDict
|
||||
|
||||
def getRealTimings(self) -> dict:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.timingsDict
|
||||
|
||||
def getMode(self) -> bool:
|
||||
# TODO: сделать через декоратор @property:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
return self.theor_mode
|
||||
|
||||
def _getBoolChanges(self, signalName) -> list:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
# TODO: if len(signal_values)...
|
||||
|
||||
if len(signal_values) > 0:
|
||||
changes.append([float(timeCode.iloc[0]), 'high' if bool(signal_values.iloc[0]) else 'low'])
|
||||
|
||||
@ -92,7 +73,6 @@ class DiagramParser:
|
||||
|
||||
|
||||
def _getFloatChanges(self, signalName) -> list:
|
||||
# TODO: Методы в Python именуются в lowercase либо snake_case.
|
||||
timeCode = self.data['time']
|
||||
signal_values = self.data[signalName]
|
||||
changes = []
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
# TODO: Было бы логично разместить данный модуль в каталоге gui.
|
||||
|
||||
dark_style = """
|
||||
/*
|
||||
------------------------------------------------------
|
||||
@ -285,10 +283,6 @@ QTableView {
|
||||
border: 1px solid #424242;
|
||||
selection-background-color: #FFCC00; /* Жёлтый при выделении */
|
||||
selection-color: #121212; /* Тёмный цвет текста при выделении */
|
||||
font-size: 14px;
|
||||
}
|
||||
QTableView QLineEdit {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Горизонтальный заголовок в таблицах */
|
||||
@ -455,16 +449,6 @@ QLabel {
|
||||
}"""
|
||||
|
||||
colors = [
|
||||
'#FF6F61', # яркий коралловый
|
||||
'#6B5B95', # приглушенный фиолетовый
|
||||
'#88B04B', # яркий зеленый
|
||||
'#F7CAC9', # светлый розовый
|
||||
'#92A8D1', # светло-голубой
|
||||
'#955251', # теплый терракотовый
|
||||
'#B565A7', # лавандовый
|
||||
'#009B77', # глубокий бирюзовый
|
||||
'#DD4124', # ярко-красный
|
||||
'#45B8AC', # мягкий мятный
|
||||
'#FF6F61', # яркий коралловый
|
||||
'#6B5B95', # приглушенный фиолетовый
|
||||
'#88B04B', # яркий зеленый
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user