버전 업그레이드
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
_archive/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
BIN
ai/dataset/images/train/스크린샷 2026-05-07 095340.png
Normal file
BIN
ai/dataset/images/train/스크린샷 2026-05-07 095340.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Binary file not shown.
1
ai/dataset/labels/train/스크린샷 2026-04-27 175231.txt
Normal file
1
ai/dataset/labels/train/스크린샷 2026-04-27 175231.txt
Normal file
@@ -0,0 +1 @@
|
||||
2 0.264192 0.312749 0.362445 0.298805
|
||||
2
ai/dataset/labels/train/스크린샷 2026-05-07 095340.txt
Normal file
2
ai/dataset/labels/train/스크린샷 2026-05-07 095340.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
0 0.356148 0.438479 0.378190 0.290828
|
||||
0 0.814385 0.224832 0.306265 0.252796
|
||||
Binary file not shown.
@@ -1,2 +0,0 @@
|
||||
0 0.227074 0.195219 0.401747 0.215139
|
||||
1 0.853712 0.252988 0.257642 0.330677
|
||||
@@ -1,101 +1,4 @@
|
||||
epoch,time,train/box_loss,train/cls_loss,train/dfl_loss,metrics/precision(B),metrics/recall(B),metrics/mAP50(B),metrics/mAP50-95(B),val/box_loss,val/cls_loss,val/dfl_loss,lr/pg0,lr/pg1,lr/pg2
|
||||
1,0.744601,2.4784,4.2576,2.37292,0.00581,0.5,0.00599,0.00178,2.08106,4.5092,2.3941,0,0,0
|
||||
2,1.17594,3.54636,4.89413,2.76008,0.00588,0.5,0.00622,0.00124,2.10159,4.51392,2.40401,1.23763e-05,1.23763e-05,1.23763e-05
|
||||
3,1.4434,3.2212,4.47996,3.91873,0.01514,1,0.01568,0.0022,2.24221,4.37976,2.4629,2.4505e-05,2.4505e-05,2.4505e-05
|
||||
4,1.81097,3.23832,4.18544,3.38558,0.01459,1,0.01534,0.0037,2.32948,4.47892,2.46147,3.63862e-05,3.63862e-05,3.63862e-05
|
||||
5,2.17724,2.76973,4.43589,2.74067,0.00556,0.5,0.00711,0.00195,2.33679,4.46132,2.45867,4.802e-05,4.802e-05,4.802e-05
|
||||
6,2.47758,2.93916,4.25681,2.81574,0.00562,0.5,0.00711,0.00197,2.23356,4.4439,2.55513,5.94062e-05,5.94062e-05,5.94062e-05
|
||||
7,2.77856,2.95798,3.80506,2.41629,0.01433,1,0.0167,0.00167,2.13047,4.47684,2.46296,7.0545e-05,7.0545e-05,7.0545e-05
|
||||
8,3.08538,2.97272,3.8706,2.66583,0.00847,0.5,0.00939,0.00094,1.93983,4.4964,2.44096,8.14362e-05,8.14362e-05,8.14362e-05
|
||||
9,3.38628,1.96121,4.27233,2.04591,0,0,0,0,1.67434,4.64554,2.3051,9.208e-05,9.208e-05,9.208e-05
|
||||
10,3.68733,2.62766,3.67184,2.50334,0,0,0,0,1.44683,4.71852,2.19216,0.000102476,0.000102476,0.000102476
|
||||
11,3.97817,2.60241,4.1424,2.70991,0.00556,0.5,0.01605,0.0016,1.42181,4.70011,2.16441,0.000112625,0.000112625,0.000112625
|
||||
12,4.28608,3.28551,4.52089,2.67545,0.00588,0.5,0.01345,0.00269,1.52292,4.67835,2.13383,0.000122526,0.000122526,0.000122526
|
||||
13,4.58546,2.2033,4.2669,2.49512,0.00562,0.5,0.01157,0.00231,1.68387,4.61194,2.17338,0.00013218,0.00013218,0.00013218
|
||||
14,4.88485,2.87394,4.40826,2.70916,0.00568,0.5,0.00975,0.00195,1.89075,4.58147,2.30027,0.000141586,0.000141586,0.000141586
|
||||
15,5.19968,2.71982,4.27341,2.72914,0,0,0,0,1.96866,4.44855,2.33624,0.000150745,0.000150745,0.000150745
|
||||
16,5.49215,1.68605,4.24646,1.87531,0,0,0,0,2.00553,4.48707,2.29285,0.000159656,0.000159656,0.000159656
|
||||
17,5.79325,2.00138,3.94773,2.26758,0,0,0,0,2.09648,4.63246,2.4007,0.00016832,0.00016832,0.00016832
|
||||
18,6.09155,3.05671,5.53908,2.66707,0,0,0,0,2.09648,4.63246,2.4007,0.000176736,0.000176736,0.000176736
|
||||
19,6.39052,2.20901,3.52372,2.04393,0.00806,0.5,0.02369,0.00237,2.20865,4.54601,2.56285,0.000184905,0.000184905,0.000184905
|
||||
20,6.66892,2.4483,3.70065,2.23467,0.00806,0.5,0.02369,0.00237,2.20865,4.54601,2.56285,0.000192826,0.000192826,0.000192826
|
||||
21,6.9332,2.19448,3.86943,2.10117,0.00463,0.5,0.03317,0.00663,2.32559,4.48125,2.62548,0.0002005,0.0002005,0.0002005
|
||||
22,7.30519,2.9479,4.3599,2.76934,0.00463,0.5,0.03317,0.00663,2.32559,4.48125,2.62548,0.000207926,0.000207926,0.000207926
|
||||
23,7.68986,1.39714,3.40426,1.6151,0.01578,1,0.08837,0.01146,2.44041,4.6826,2.72339,0.000215105,0.000215105,0.000215105
|
||||
24,7.99967,2.46841,3.48691,2.13383,0.01578,1,0.08837,0.01146,2.44041,4.6826,2.72339,0.000222036,0.000222036,0.000222036
|
||||
25,8.31396,2.56835,4.10281,2.20972,0.00481,0.5,0.01777,0.00355,2.61255,4.61058,2.86581,0.00022872,0.00022872,0.00022872
|
||||
26,8.55453,1.65391,3.70866,1.88456,0.00481,0.5,0.01777,0.00355,2.61255,4.61058,2.86581,0.000235156,0.000235156,0.000235156
|
||||
27,8.80235,2.00119,3.45089,1.89926,0.01604,1,0.05212,0.00521,2.74287,4.6437,2.91424,0.000241345,0.000241345,0.000241345
|
||||
28,9.04528,1.89956,3.68555,2.16165,0.01604,1,0.05212,0.00521,2.74287,4.6437,2.91424,0.000247286,0.000247286,0.000247286
|
||||
29,9.29273,1.42472,2.92227,1.75125,0.01111,0.5,0.12437,0.01244,3.06977,4.5736,3.06093,0.00025298,0.00025298,0.00025298
|
||||
30,9.60359,1.35767,2.62078,1.55004,0.01111,0.5,0.12437,0.01244,3.06977,4.5736,3.06093,0.000258426,0.000258426,0.000258426
|
||||
31,9.94422,2.37035,4.66417,2.27559,0.01316,0.5,0.16583,0.01658,3.36893,4.24859,3.23425,0.000263625,0.000263625,0.000263625
|
||||
32,10.2594,1.61322,2.86179,1.53237,0.01316,0.5,0.16583,0.01658,3.36893,4.24859,3.23425,0.000268576,0.000268576,0.000268576
|
||||
33,10.6412,1.85856,3.15064,1.88045,0.02083,0.5,0.16583,0.01658,3.44544,4.47775,3.34489,0.00027328,0.00027328,0.00027328
|
||||
34,10.9661,1.62452,2.88937,1.72429,0.02083,0.5,0.16583,0.01658,3.44544,4.47775,3.34489,0.000277736,0.000277736,0.000277736
|
||||
35,11.2888,1.21431,3.51628,1.62717,0,0,0,0,3.47481,4.57555,3.33539,0.000281945,0.000281945,0.000281945
|
||||
36,11.5318,1.39337,2.94668,1.47319,0,0,0,0,3.47481,4.57555,3.33539,0.000285906,0.000285906,0.000285906
|
||||
37,11.7887,2.14196,3.0238,1.80533,0,0,0,0,3.7452,4.84862,3.44227,0.00028962,0.00028962,0.00028962
|
||||
38,12.0408,1.81846,2.33607,1.79617,0,0,0,0,3.7452,4.84862,3.44227,0.000293086,0.000293086,0.000293086
|
||||
39,12.2951,1.17175,1.92086,1.3652,0,0,0,0,4.06912,4.91101,3.62994,0.000296305,0.000296305,0.000296305
|
||||
40,12.5315,1.62593,2.48208,1.50529,0,0,0,0,4.06912,4.91101,3.62994,0.000299276,0.000299276,0.000299276
|
||||
41,12.8727,2.11165,2.67321,1.7814,0,0,0,0,4.39582,4.54093,3.81935,0.000302,0.000302,0.000302
|
||||
42,13.1287,1.58216,2.68408,1.63978,0,0,0,0,4.39582,4.54093,3.81935,0.000304476,0.000304476,0.000304476
|
||||
43,13.4027,1.4236,2.75829,1.527,0.01351,0.5,0.02163,0.00216,4.3829,4.34724,3.73475,0.000306705,0.000306705,0.000306705
|
||||
44,13.6611,1.58086,3.04931,1.97095,0.01351,0.5,0.02163,0.00216,4.3829,4.34724,3.73475,0.000308686,0.000308686,0.000308686
|
||||
45,13.94,1.70552,2.1028,1.7847,0.0119,0.5,0.03109,0.00311,4.02893,4.68709,3.46274,0.00031042,0.00031042,0.00031042
|
||||
46,14.2063,1.17287,2.30212,1.21687,0.0119,0.5,0.03109,0.00311,4.02893,4.68709,3.46274,0.000311906,0.000311906,0.000311906
|
||||
47,14.4787,0.75953,1.79745,0.99198,0.01111,0.5,0.04523,0.00452,4.16401,4.4644,3.55846,0.000313145,0.000313145,0.000313145
|
||||
48,14.7356,2.12559,4.16293,2.21951,0.01111,0.5,0.04523,0.00452,4.16401,4.4644,3.55846,0.000314136,0.000314136,0.000314136
|
||||
49,15.0753,0.92121,2.05049,1.10199,0.00962,0.5,0.04146,0.00829,3.90365,4.64333,3.48688,0.00031488,0.00031488,0.00031488
|
||||
50,15.3345,1.19831,1.99876,1.28056,0.00962,0.5,0.04146,0.00829,3.90365,4.64333,3.48688,0.000315376,0.000315376,0.000315376
|
||||
51,15.6062,2.135,2.40625,1.81904,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.000315625,0.000315625,0.000315625
|
||||
52,15.9429,1.72253,3.24847,1.71661,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.000315626,0.000315626,0.000315626
|
||||
53,16.2756,0.98827,2.52014,1.26921,0.01042,0.5,0.05528,0.0229,3.18088,5.77238,3.2612,0.00031538,0.00031538,0.00031538
|
||||
54,16.6349,1.8328,2.53368,1.66716,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000314886,0.000314886,0.000314886
|
||||
55,16.8944,1.1818,1.78296,1.15008,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000314145,0.000314145,0.000314145
|
||||
56,17.2351,1.26713,2.38825,1.40594,0.01316,0.5,0.12437,0.01244,3.8633,4.65938,3.23911,0.000313156,0.000313156,0.000313156
|
||||
57,17.5174,1.32827,1.94378,1.44297,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.00031192,0.00031192,0.00031192
|
||||
58,17.7998,0.93866,1.97455,1.19817,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.000310436,0.000310436,0.000310436
|
||||
59,18.0565,1.34164,4.69334,1.52493,0.01562,0.5,0.02073,0.00207,3.86529,4.77191,3.13775,0.000308705,0.000308705,0.000308705
|
||||
60,18.3262,1.70827,3.61104,1.67179,0,0,0,0,3.93656,4.99738,3.11248,0.000306726,0.000306726,0.000306726
|
||||
61,18.6133,0.82569,1.96032,1.1105,0,0,0,0,3.93656,4.99738,3.11248,0.0003045,0.0003045,0.0003045
|
||||
62,18.8669,1.33918,2.16787,1.37237,0,0,0,0,3.93656,4.99738,3.11248,0.000302026,0.000302026,0.000302026
|
||||
63,19.133,0.9404,2.00419,1.09295,0,0,0,0,3.54929,5.50927,3.04358,0.000299305,0.000299305,0.000299305
|
||||
64,19.4517,0.83957,1.71834,1.04536,0,0,0,0,3.54929,5.50927,3.04358,0.000296336,0.000296336,0.000296336
|
||||
65,19.7046,1.05975,1.98836,1.25573,0,0,0,0,3.54929,5.50927,3.04358,0.00029312,0.00029312,0.00029312
|
||||
66,19.966,0.79495,1.71511,1.07986,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000289656,0.000289656,0.000289656
|
||||
67,20.2315,0.89674,1.76647,1.12805,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000285945,0.000285945,0.000285945
|
||||
68,20.4951,1.27186,2.05522,1.24621,0.01111,0.5,0.02073,0.00415,3.48677,6.07562,2.94905,0.000281986,0.000281986,0.000281986
|
||||
69,20.763,0.72455,1.86789,1.0569,0,0,0,0,4.16796,5.98914,3.4865,0.00027778,0.00027778,0.00027778
|
||||
70,21.0206,0.94838,1.5672,1.15049,0,0,0,0,4.16796,5.98914,3.4865,0.000273326,0.000273326,0.000273326
|
||||
71,21.2798,1.62956,2.8345,1.75908,0,0,0,0,4.16796,5.98914,3.4865,0.000268625,0.000268625,0.000268625
|
||||
72,21.6185,0.82159,1.62683,0.94171,0,0,0,0,4.70051,5.39397,3.87655,0.000263676,0.000263676,0.000263676
|
||||
73,21.8885,0.84998,2.02611,1.30797,0,0,0,0,4.70051,5.39397,3.87655,0.00025848,0.00025848,0.00025848
|
||||
74,22.1454,0.85087,1.42472,1.15593,0,0,0,0,4.70051,5.39397,3.87655,0.000253036,0.000253036,0.000253036
|
||||
75,22.4095,1.1786,1.83474,1.21092,0,0,0,0,4.86689,6.24371,3.95955,0.000247345,0.000247345,0.000247345
|
||||
76,22.6694,0.78564,1.66187,0.97539,0,0,0,0,4.86689,6.24371,3.95955,0.000241406,0.000241406,0.000241406
|
||||
77,22.9286,0.79013,1.96926,1.26206,0,0,0,0,4.86689,6.24371,3.95955,0.00023522,0.00023522,0.00023522
|
||||
78,23.1943,0.8152,1.59465,1.07906,0,0,0,0,4.81344,5.97521,4.01022,0.000228786,0.000228786,0.000228786
|
||||
79,23.4546,0.61829,1.29337,0.98868,0,0,0,0,4.81344,5.97521,4.01022,0.000222105,0.000222105,0.000222105
|
||||
80,23.7685,0.95007,1.79471,1.1416,0,0,0,0,4.81344,5.97521,4.01022,0.000215176,0.000215176,0.000215176
|
||||
81,24.1034,0.93272,2.22583,1.20421,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000208,0.000208,0.000208
|
||||
82,24.3629,0.63241,1.21512,1.08097,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000200576,0.000200576,0.000200576
|
||||
83,24.6222,1.39649,2.39204,1.52958,0.00617,0.5,0.00905,0.0009,4.60774,5.39816,3.81369,0.000192905,0.000192905,0.000192905
|
||||
84,24.9101,0.63485,1.24139,0.96726,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000184986,0.000184986,0.000184986
|
||||
85,25.1903,0.88995,1.76109,1.04753,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.00017682,0.00017682,0.00017682
|
||||
86,25.4597,1.16455,2.35159,1.15176,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000168406,0.000168406,0.000168406
|
||||
87,25.761,0.86574,1.60094,1.1271,0.00641,0.5,0.01913,0.00459,4.14874,5.15654,3.60735,0.000159745,0.000159745,0.000159745
|
||||
88,26.1232,0.57931,1.17361,0.95885,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000150836,0.000150836,0.000150836
|
||||
89,26.4022,0.88857,1.97356,1.07497,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.00014168,0.00014168,0.00014168
|
||||
90,26.7071,0.71015,1.22162,1.07789,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000132276,0.000132276,0.000132276
|
||||
91,27.0615,0.80611,2.15097,0.95926,0.00704,0.5,0.01658,0.00332,3.66565,5.88071,3.25055,0.000122625,0.000122625,0.000122625
|
||||
92,27.3798,0.75395,1.9817,1.18844,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,0.000112726,0.000112726,0.000112726
|
||||
93,27.6734,0.52999,2.16673,0.94249,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,0.00010258,0.00010258,0.00010258
|
||||
94,27.9692,0.29519,1.34975,0.94508,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,9.21862e-05,9.21862e-05,9.21862e-05
|
||||
95,28.2525,0.533,1.80971,0.96369,0.00758,0.5,0.01463,0.00368,3.64756,5.57493,3.04703,8.1545e-05,8.1545e-05,8.1545e-05
|
||||
96,28.5584,0.37619,1.67301,0.82287,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,7.06563e-05,7.06563e-05,7.06563e-05
|
||||
97,28.8522,0.58993,1.73032,0.93823,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,5.952e-05,5.952e-05,5.952e-05
|
||||
98,29.2014,1.19689,2.95317,1.27242,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,4.81363e-05,4.81363e-05,4.81363e-05
|
||||
99,29.5194,0.44641,1.50801,0.93925,0.00714,0.5,0.01309,0.00361,3.45585,5.23022,2.909,3.6505e-05,3.6505e-05,3.6505e-05
|
||||
100,29.8906,0.52149,1.81476,0.89577,0.0061,0.5,0.01157,0.00231,3.45621,5.51821,2.93443,2.46263e-05,2.46263e-05,2.46263e-05
|
||||
1,2.04808,2.95921,4.23808,2.74536,0.01389,1,0.03554,0.00853,3.23067,4.67253,4.0769,0,0,0
|
||||
2,2.83517,3.25155,4.15149,2.98379,0.01408,1,0.02369,0.00948,3.18473,5.26193,4.00145,1.23763e-05,1.23763e-05,1.23763e-05
|
||||
3,3.18815,2.59377,4.62523,2.6647,0.01351,1,0.02689,0.01173,2.96806,5.66861,3.96969,2.4505e-05,2.4505e-05,2.4505e-05
|
||||
|
||||
|
Binary file not shown.
Binary file not shown.
@@ -1,9 +0,0 @@
|
||||
import os
|
||||
|
||||
dist_path = "E:/ANT/dist/reflector_inspector.exe"
|
||||
if os.path.exists(dist_path):
|
||||
size_mb = os.path.getsize(dist_path) / (1024 * 1024)
|
||||
print(f"빌드 성공: {dist_path}")
|
||||
print(f"파일 크기: {size_mb:.1f} MB")
|
||||
else:
|
||||
print("빌드 실패: exe 파일이 없음")
|
||||
24
config.json
24
config.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"cognex": {
|
||||
"ip": "169.254.0.1",
|
||||
"ip": "192.168.1.2",
|
||||
"port": 23
|
||||
},
|
||||
"basler": {
|
||||
@@ -12,7 +12,7 @@
|
||||
"speed_cms": 30.0
|
||||
},
|
||||
"db": {
|
||||
"server": "Wizis.iptime.org,20220",
|
||||
"server": "tcp:Wizis.iptime.org,20220",
|
||||
"database": "MES_ANT",
|
||||
"username": "AIUser",
|
||||
"password": "AIUser"
|
||||
@@ -23,5 +23,25 @@
|
||||
},
|
||||
"ai": {
|
||||
"model_path": "ai/models/best.pt"
|
||||
},
|
||||
"mes": {
|
||||
"selected_article_ids": [
|
||||
"C1A1100001",
|
||||
"C1A1100002",
|
||||
"C1A1100003",
|
||||
"C1A1200001",
|
||||
"C1A1200002",
|
||||
"C1A1200003",
|
||||
"C1A1200005",
|
||||
"D1A1100008",
|
||||
"D1A1100009",
|
||||
"D1A1100010",
|
||||
"D1A1100011",
|
||||
"D1A1200006",
|
||||
"D1A1200007",
|
||||
"D1A1200008",
|
||||
"D1A1200009",
|
||||
"D1I1100019"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
# db 패키지 — MySQLClient 노출
|
||||
from .mysql_client import MySQLClient
|
||||
# db 패키지
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# DB 클라이언트 — MySQL 연결 및 리플렉터 데이터 조회
|
||||
import pymysql
|
||||
|
||||
|
||||
class MySQLClient:
|
||||
def __init__(self):
|
||||
self._conn = None
|
||||
|
||||
def connect(self, host: str, port: int, user: str, password: str, database: str):
|
||||
self._conn = pymysql.connect(
|
||||
host=host, port=port, user=user,
|
||||
password=password, database=database,
|
||||
charset="utf8mb4", autocommit=True,
|
||||
)
|
||||
print(f"[DB] 연결 성공: {host}:{port}/{database}")
|
||||
|
||||
def disconnect(self):
|
||||
if self._conn:
|
||||
self._conn.close()
|
||||
self._conn = None
|
||||
print("[DB] 연결 종료")
|
||||
|
||||
def is_connected(self) -> bool:
|
||||
return self._conn is not None
|
||||
|
||||
def get_reflector_list(self) -> list[dict]:
|
||||
"""리플렉터 목록 조회 — 반환: [{"id": ..., "name": ..., "type": ...}, ...]"""
|
||||
pass
|
||||
|
||||
def save_reflector(self, name: str, type_lr: str, pattern_data: bytes):
|
||||
"""리플렉터 등록/갱신 — 미구현"""
|
||||
pass
|
||||
|
||||
def save_inspection_result(self, product_id: int, result: str, defects: list):
|
||||
"""검사 결과 저장 — 미구현"""
|
||||
pass
|
||||
222
db/sql_client.py
222
db/sql_client.py
@@ -1,5 +1,27 @@
|
||||
import pyodbc
|
||||
|
||||
# 선호 순서 — 앞쪽일수록 우선. 설치 환경마다 다를 수 있어 자동 탐지 후 선택.
|
||||
_PREFERRED_DRIVERS = (
|
||||
"ODBC Driver 18 for SQL Server",
|
||||
"ODBC Driver 17 for SQL Server",
|
||||
"ODBC Driver 13 for SQL Server",
|
||||
"SQL Server Native Client 11.0",
|
||||
"SQL Server",
|
||||
)
|
||||
|
||||
|
||||
def _pick_driver() -> "str | None":
|
||||
"""이 PC에 설치된 ODBC 드라이버 중 사용할 SQL Server 드라이버를 선택.
|
||||
선호 순서대로 먼저 찾고, 없으면 이름에 'SQL Server'가 포함된 아무 드라이버라도 사용."""
|
||||
available = pyodbc.drivers()
|
||||
for name in _PREFERRED_DRIVERS:
|
||||
if name in available:
|
||||
return name
|
||||
for name in available:
|
||||
if "SQL Server" in name:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
class SQLClient:
|
||||
def __init__(self):
|
||||
@@ -8,9 +30,15 @@ class SQLClient:
|
||||
|
||||
def connect(self, server: str, database: str,
|
||||
username: str, password: str) -> bool:
|
||||
driver = _pick_driver()
|
||||
if driver is None:
|
||||
print("[DB] 연결 실패: 설치된 SQL Server ODBC 드라이버가 없습니다. "
|
||||
"'ODBC Driver 18 for SQL Server'를 설치하세요.")
|
||||
self.conn = None
|
||||
return False
|
||||
try:
|
||||
conn_str = (
|
||||
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
||||
f"DRIVER={{{driver}}};"
|
||||
f"SERVER={server};"
|
||||
f"DATABASE={database};"
|
||||
f"UID={username};"
|
||||
@@ -20,10 +48,10 @@ class SQLClient:
|
||||
)
|
||||
self.conn = pyodbc.connect(conn_str, timeout=10)
|
||||
self.cursor = self.conn.cursor()
|
||||
print(f"[DB] 연결 성공: {server}/{database}")
|
||||
print(f"[DB] 연결 성공: {server}/{database} (드라이버: {driver})")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[DB] 연결 실패: {e}")
|
||||
print(f"[DB] 연결 실패: {e} (드라이버: {driver})")
|
||||
self.conn = None
|
||||
return False
|
||||
|
||||
@@ -39,9 +67,145 @@ class SQLClient:
|
||||
def is_connected(self) -> bool:
|
||||
return self.conn is not None
|
||||
|
||||
def get_reflector_list(self) -> list:
|
||||
@staticmethod
|
||||
def _norm_id(value) -> str:
|
||||
return str(value).strip() if value is not None else ""
|
||||
|
||||
def get_wk_results(self) -> list:
|
||||
"""
|
||||
vi_AI_mt_Article 뷰에서 리플렉터 제품 목록 조회.
|
||||
vi_AI_WK_Result 뷰 조회.
|
||||
반환: [{
|
||||
"article_id": ..., "machine_id": ..., "machine": ...,
|
||||
"work_start_date": ..., "work_start_time": ...,
|
||||
}, ...]
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return []
|
||||
try:
|
||||
self.cursor.execute("""
|
||||
SELECT ArticleID, MachineID, Machine,
|
||||
WorkStartDate, WorkStartTime
|
||||
FROM vi_AI_WK_Result
|
||||
WHERE ArticleID IS NOT NULL
|
||||
ORDER BY ArticleID
|
||||
""")
|
||||
rows = self.cursor.fetchall()
|
||||
return [self._row_to_wk_result(row) for row in rows]
|
||||
except Exception as e:
|
||||
print(f"[DB] WK_Result 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
def get_wk_result_article_ids(self) -> list:
|
||||
"""vi_AI_WK_Result에 있는 ArticleID 목록 (중복 제거, 순서 유지)."""
|
||||
seen = set()
|
||||
ids = []
|
||||
for row in self.get_wk_results():
|
||||
norm = self._norm_id(row["article_id"])
|
||||
if norm and norm not in seen:
|
||||
seen.add(norm)
|
||||
ids.append(row["article_id"])
|
||||
return ids
|
||||
|
||||
def get_wk_result_map(self) -> dict:
|
||||
"""ArticleID(정규화) → WK_Result 행. 동일 ID가 여러 행이면 마지막 행 사용."""
|
||||
result = {}
|
||||
for row in self.get_wk_results():
|
||||
norm = self._norm_id(row["article_id"])
|
||||
if norm:
|
||||
result[norm] = row
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _row_to_wk_result(row) -> dict:
|
||||
return {
|
||||
"article_id": row[0],
|
||||
"machine_id": row[1],
|
||||
"machine": row[2],
|
||||
"work_start_date": row[3],
|
||||
"work_start_time": row[4],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def format_db_value(value) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
if hasattr(value, "strftime"):
|
||||
if hasattr(value, "hour"):
|
||||
return value.strftime("%H:%M:%S")
|
||||
return value.strftime("%Y-%m-%d")
|
||||
text = str(value).strip()
|
||||
return text if text else "—"
|
||||
|
||||
def get_reflector_list_ordered(self, article_ids: list) -> list:
|
||||
"""article_ids 순서를 유지한 제품 목록 (PatMax 슬롯 순서용)."""
|
||||
if not article_ids:
|
||||
return []
|
||||
by_norm = {
|
||||
self._norm_id(item["article_id"]): item
|
||||
for item in self.get_reflector_list(article_ids=article_ids)
|
||||
}
|
||||
ordered = []
|
||||
for article_id in article_ids:
|
||||
item = by_norm.get(self._norm_id(article_id))
|
||||
if item is not None:
|
||||
ordered.append(item)
|
||||
return ordered
|
||||
|
||||
def split_articles_by_wk(self, mes_selected_ids: "list | None" = None) -> tuple:
|
||||
"""
|
||||
vi_AI_mt_Article 목록을 WK_Result 작업 대상 / 기타로 분류.
|
||||
반환: (active_list, inactive_list)
|
||||
"""
|
||||
if mes_selected_ids is not None:
|
||||
if len(mes_selected_ids) == 0:
|
||||
return [], []
|
||||
all_items = self.get_reflector_list_ordered(mes_selected_ids)
|
||||
else:
|
||||
all_items = self.get_reflector_list()
|
||||
|
||||
if not all_items:
|
||||
return [], []
|
||||
|
||||
wk_norm = set(self.get_wk_result_map().keys())
|
||||
active = [
|
||||
item for item in all_items
|
||||
if self._norm_id(item["article_id"]) in wk_norm
|
||||
]
|
||||
inactive = [
|
||||
item for item in all_items
|
||||
if self._norm_id(item["article_id"]) not in wk_norm
|
||||
]
|
||||
return active, inactive
|
||||
|
||||
def get_inspectable_articles(self, mes_selected_ids: "list | None" = None) -> list:
|
||||
"""
|
||||
vi_AI_mt_Article ∩ vi_AI_WK_Result.
|
||||
mes_selected_ids 지정 시 관리자 MES 선택과도 교집합.
|
||||
"""
|
||||
wk_ids = self.get_wk_result_article_ids()
|
||||
if not wk_ids:
|
||||
return []
|
||||
|
||||
wk_by_norm = {self._norm_id(article_id): article_id for article_id in wk_ids}
|
||||
|
||||
if mes_selected_ids is not None:
|
||||
if len(mes_selected_ids) == 0:
|
||||
return []
|
||||
filter_ids = [
|
||||
wk_by_norm[self._norm_id(article_id)]
|
||||
for article_id in mes_selected_ids
|
||||
if self._norm_id(article_id) in wk_by_norm
|
||||
]
|
||||
else:
|
||||
filter_ids = wk_ids
|
||||
|
||||
if not filter_ids:
|
||||
return []
|
||||
return self.get_reflector_list(article_ids=filter_ids)
|
||||
|
||||
def get_all_articles(self) -> list:
|
||||
"""
|
||||
vi_AI_mt_Article 뷰 전체 조회 (관리자 MES 제품 선택용).
|
||||
반환: [{"article_id": ..., "article": ..., "buyer_article_no": ...}, ...]
|
||||
"""
|
||||
if not self.is_connected():
|
||||
@@ -50,22 +214,52 @@ class SQLClient:
|
||||
self.cursor.execute("""
|
||||
SELECT ArticleID, Article, BuyerArticleNo
|
||||
FROM vi_AI_mt_Article
|
||||
WHERE Article LIKE '%REF%'
|
||||
ORDER BY ArticleID
|
||||
""")
|
||||
rows = self.cursor.fetchall()
|
||||
return [
|
||||
{
|
||||
"article_id": row[0],
|
||||
"article": row[1],
|
||||
"buyer_article_no": row[2],
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
return [self._row_to_article(row) for row in rows]
|
||||
except Exception as e:
|
||||
print(f"[DB] 전체 제품 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
def get_reflector_list(self, article_ids: "list | None" = None) -> list:
|
||||
"""
|
||||
vi_AI_mt_Article 뷰에서 제품 목록 조회.
|
||||
article_ids 지정 시 해당 ID만, 미지정 시 REF 포함 제품 전체.
|
||||
반환: [{"article_id": ..., "article": ..., "buyer_article_no": ...}, ...]
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return []
|
||||
try:
|
||||
if article_ids:
|
||||
placeholders = ",".join("?" * len(article_ids))
|
||||
self.cursor.execute(f"""
|
||||
SELECT ArticleID, Article, BuyerArticleNo
|
||||
FROM vi_AI_mt_Article
|
||||
WHERE ArticleID IN ({placeholders})
|
||||
ORDER BY ArticleID
|
||||
""", article_ids)
|
||||
else:
|
||||
self.cursor.execute("""
|
||||
SELECT ArticleID, Article, BuyerArticleNo
|
||||
FROM vi_AI_mt_Article
|
||||
WHERE Article LIKE '%REF%'
|
||||
ORDER BY ArticleID
|
||||
""")
|
||||
rows = self.cursor.fetchall()
|
||||
return [self._row_to_article(row) for row in rows]
|
||||
except Exception as e:
|
||||
print(f"[DB] 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _row_to_article(row) -> dict:
|
||||
return {
|
||||
"article_id": row[0],
|
||||
"article": row[1],
|
||||
"buyer_article_no": row[2],
|
||||
}
|
||||
|
||||
def save_inspection_result(self, article_id: str,
|
||||
result: str, score: float) -> bool:
|
||||
"""검사 결과 저장 — 테이블 확정 후 구현."""
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
"""
|
||||
In-Sight 2001C-353 트리거 후 전체 셀 스캔 스크립트
|
||||
SE8 트리거 실행 → 결과 갱신 완료 후 A0~Z50 범위 GV 스캔.
|
||||
결과는 콘솔 출력과 find_cells_trigger_result.txt 에 동시 저장.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import string
|
||||
import time
|
||||
|
||||
IP = "169.254.0.1"
|
||||
PORT = 23
|
||||
COLS = list(string.ascii_uppercase) # A ~ Z (26개)
|
||||
ROWS = range(0, 251) # 0 ~ 250 (251개) → 총 6526셀
|
||||
RESULT_FILE = "find_cells_trigger_result.txt"
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 저수준 소켓 헬퍼 (insight.py 방식 동일)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _read_line(sock: socket.socket, buf: bytearray) -> str:
|
||||
"""버퍼에서 \\r\\n까지 읽어 문자열로 반환. 남은 데이터는 buf에 보존."""
|
||||
while b"\r\n" not in buf:
|
||||
try:
|
||||
chunk = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
|
||||
if b"\r\n" in buf:
|
||||
idx = buf.index(b"\r\n")
|
||||
line = bytes(buf[:idx])
|
||||
del buf[:idx + 2]
|
||||
else:
|
||||
line = bytes(buf)
|
||||
buf.clear()
|
||||
|
||||
for encoding in ["euc-kr", "cp949", "utf-8"]:
|
||||
try:
|
||||
return line.decode(encoding).strip()
|
||||
except Exception:
|
||||
continue
|
||||
return line.decode(errors="ignore").strip()
|
||||
|
||||
|
||||
def _send(sock: socket.socket, cmd: str):
|
||||
sock.sendall((cmd + "\r\n").encode())
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 로그인 (insight.py connect() 시퀀스와 동일)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _login(sock: socket.socket, buf: bytearray):
|
||||
banner = _read_line(sock, buf)
|
||||
print(f"[연결] 배너: {banner!r}")
|
||||
|
||||
_send(sock, "admin")
|
||||
while b"Password:" not in buf:
|
||||
try:
|
||||
chunk = sock.recv(4096)
|
||||
except socket.timeout:
|
||||
break
|
||||
if not chunk:
|
||||
break
|
||||
buf += chunk
|
||||
prompt = buf.decode(errors="ignore").strip()
|
||||
buf.clear()
|
||||
print(f"[연결] 프롬프트: {prompt!r}")
|
||||
|
||||
sock.sendall(b"\r\n") # 빈 비밀번호
|
||||
result = _read_line(sock, buf)
|
||||
print(f"[연결] 로그인: {result!r}")
|
||||
|
||||
if "Logged In" not in result:
|
||||
raise RuntimeError(f"로그인 실패: {result!r}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# SE8 트리거
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _trigger(sock: socket.socket, buf: bytearray):
|
||||
"""SE8 소프트웨어 트리거 전송 후 응답 수신."""
|
||||
_send(sock, "SE8")
|
||||
response = _read_line(sock, buf)
|
||||
print(f"[트리거] SE8 응답: {response!r}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 셀 스캔
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _scan(sock: socket.socket, buf: bytearray) -> dict:
|
||||
"""GV[열][행] 전체 스캔 → {셀주소: 값문자열}"""
|
||||
total = len(COLS) * len(ROWS)
|
||||
found = {}
|
||||
|
||||
for col in COLS:
|
||||
for row in ROWS:
|
||||
cell = f"{col}{row}"
|
||||
_send(sock, f"GV{cell}")
|
||||
|
||||
status = _read_line(sock, buf) # "1" 또는 "0"
|
||||
|
||||
if status == "1":
|
||||
value = _read_line(sock, buf)
|
||||
if not value.strip():
|
||||
pass
|
||||
else:
|
||||
found[cell] = value
|
||||
try:
|
||||
print(f"\r[{cell:<4}] = {value:<40}")
|
||||
except UnicodeEncodeError:
|
||||
print(f"\r[{cell:<4}] = {value.encode('utf-8', errors='replace')!r}")
|
||||
|
||||
done = (COLS.index(col)) * len(ROWS) + row + 1
|
||||
print(
|
||||
f"스캔 중... [{cell} / Z{ROWS[-1]}] ({done}/{total})",
|
||||
end="\r", flush=True,
|
||||
)
|
||||
|
||||
print() # 진행바 줄 정리
|
||||
return found
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 결과 저장
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _save(found: dict, path: str = RESULT_FILE):
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(f"스캔 범위: A0 ~ Z{ROWS[-1]} / 유효 셀: {len(found)}개\n")
|
||||
f.write("=" * 40 + "\n")
|
||||
for cell, val in found.items():
|
||||
f.write(f"[{cell:<4}] = {val}\n")
|
||||
print(f"결과 저장 완료 → {path}")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 값 검색
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _search(found: dict, query: str):
|
||||
matches = {c: v for c, v in found.items() if query.lower() in v.lower()}
|
||||
if matches:
|
||||
print(f"\n[검색] '{query}' 포함 셀 {len(matches)}개:")
|
||||
for cell, val in matches.items():
|
||||
print(f" [{cell:<4}] = {val}")
|
||||
else:
|
||||
print(f"[검색] '{query}'를 포함한 셀이 없습니다.")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 메인
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def main():
|
||||
print(f"[연결] {IP}:{PORT} 접속 중...")
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(3.0)
|
||||
sock.connect((IP, PORT))
|
||||
buf = bytearray()
|
||||
|
||||
try:
|
||||
_login(sock, buf)
|
||||
|
||||
input("\n준비됐으면 엔터를 누르세요 (리플렉터를 카메라 앞에 올려두세요): ")
|
||||
|
||||
print("[트리거] SE8 전송 중...")
|
||||
_trigger(sock, buf)
|
||||
|
||||
print("[대기] 카메라 처리 완료 대기 (1초)...")
|
||||
time.sleep(1)
|
||||
|
||||
print(f"\n스캔 시작: A0 ~ Z{ROWS[-1]} (총 {len(COLS) * len(ROWS)}셀)\n")
|
||||
found = _scan(sock, buf)
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
print(f"\n스캔 완료. 총 {len(found)}개 셀 발견")
|
||||
print("=" * 40)
|
||||
for cell, val in found.items():
|
||||
try:
|
||||
print(f" [{cell:<4}] = {val}")
|
||||
except UnicodeEncodeError:
|
||||
print(f" [{cell:<4}] = {val.encode('utf-8', errors='replace')!r}")
|
||||
|
||||
_save(found)
|
||||
|
||||
print()
|
||||
while True:
|
||||
query = input("찾는 값 입력 (엔터 시 종료): ").strip()
|
||||
if not query:
|
||||
break
|
||||
_search(found, query)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -34,9 +34,15 @@ QDoubleSpinBox, QSpinBox {
|
||||
padding: 4px 8px;
|
||||
min-height: 38px;
|
||||
}
|
||||
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button,
|
||||
QSpinBox::up-button, QSpinBox::down-button {
|
||||
width: 20px;
|
||||
QDoubleSpinBox::up-button, QSpinBox::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 30px;
|
||||
}
|
||||
QDoubleSpinBox::down-button, QSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
width: 30px;
|
||||
}
|
||||
QPushButton {
|
||||
background: #2e2e2e;
|
||||
|
||||
@@ -1,195 +0,0 @@
|
||||
from PyQt5.QtWidgets import (
|
||||
QDialog, QFormLayout, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QDoubleSpinBox, QSpinBox, QMessageBox, QLabel
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
|
||||
# (레이블, 셀주소, 위젯종류, min, max, 기본값, 소수점자리)
|
||||
# D3(노출)은 AcqExposure() 출력 셀 — 읽기 전용, 목록에서 제외
|
||||
_PARAMS = [
|
||||
("최대 노출 시간", "F3", "double", 0.01, 1000.0, 950.0, 3),
|
||||
("목표 이미지 밝기", "G3", "double", 0.0, 255.0, 50.0, 1),
|
||||
("조명 강도", "D6", "int", 0, 100, 70, 0),
|
||||
("초점 위치", "D14", "int", 0, 999, 139, 0),
|
||||
]
|
||||
|
||||
|
||||
class ImageSettingsDialog(QDialog):
|
||||
def __init__(self, insight_cam, parent=None):
|
||||
super().__init__(parent)
|
||||
self._cam = insight_cam
|
||||
self.setWindowTitle("이미지 설정 — In-Sight 2000C")
|
||||
self.setMinimumWidth(360)
|
||||
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
|
||||
|
||||
self._widgets = {} # 셀주소 → 위젯
|
||||
self._originals = {} # 셀주소 → 로드 시 원본값
|
||||
|
||||
self._build_ui()
|
||||
self._load_values()
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# UI 구성
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _build_ui(self):
|
||||
form = QFormLayout()
|
||||
form.setLabelAlignment(Qt.AlignRight)
|
||||
form.setRowWrapPolicy(QFormLayout.DontWrapRows)
|
||||
form.setHorizontalSpacing(16)
|
||||
form.setVerticalSpacing(10)
|
||||
|
||||
# 노출(D3)은 읽기 전용 표시
|
||||
self._exposure_label = QLabel("—")
|
||||
self._exposure_label.setStyleSheet("color: gray;")
|
||||
form.addRow("노출 (밀리초, 읽기 전용):", self._exposure_label)
|
||||
|
||||
for label, cell, kind, lo, hi, default, decimals in _PARAMS:
|
||||
if kind == "double":
|
||||
w = QDoubleSpinBox()
|
||||
w.setRange(lo, hi)
|
||||
w.setDecimals(decimals)
|
||||
w.setValue(default)
|
||||
w.setSingleStep(0.1)
|
||||
else:
|
||||
w = QSpinBox()
|
||||
w.setRange(int(lo), int(hi))
|
||||
w.setValue(int(default))
|
||||
|
||||
w.setMinimumWidth(120)
|
||||
self._widgets[cell] = w
|
||||
form.addRow(label, w)
|
||||
|
||||
btn_ok = QPushButton("확인")
|
||||
btn_ok.setDefault(True)
|
||||
btn_ok.clicked.connect(self._on_ok)
|
||||
|
||||
btn_cancel = QPushButton("취소")
|
||||
btn_cancel.clicked.connect(self.reject)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.addStretch()
|
||||
btn_row.addWidget(btn_ok)
|
||||
btn_row.addWidget(btn_cancel)
|
||||
|
||||
root = QVBoxLayout(self)
|
||||
root.addLayout(form)
|
||||
root.addSpacing(8)
|
||||
root.addLayout(btn_row)
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 값 로드 — GV{cell} 명령, 응답 2줄(상태코드 + 값)
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _load_values(self):
|
||||
# D3: 읽기 전용 표시
|
||||
exp = self._gv("D3")
|
||||
print(f"[설정창] GVD3 → {exp!r}")
|
||||
if exp is not None:
|
||||
self._exposure_label.setText(f"{exp:.3f} ms")
|
||||
|
||||
for _, cell, kind, _, _, _, _ in _PARAMS:
|
||||
val = self._gv(cell)
|
||||
print(f"[설정창] GV{cell} → {val!r}")
|
||||
w = self._widgets[cell]
|
||||
if val is not None:
|
||||
if kind == "double":
|
||||
w.setValue(val)
|
||||
else:
|
||||
w.setValue(int(round(val)))
|
||||
self._originals[cell] = val
|
||||
else:
|
||||
self._originals[cell] = None
|
||||
|
||||
def _gv(self, cell: str):
|
||||
"""GV{cell} 전송 → 숫자 반환, 실패 시 None"""
|
||||
try:
|
||||
self._cam._send(f"GV{cell}")
|
||||
status = self._cam._read_line() # "1" 또는 "0"
|
||||
if status != "1":
|
||||
print(f"[설정창] GV{cell} 상태코드: {status!r}")
|
||||
return None
|
||||
raw = self._cam._read_line() # 실제 값
|
||||
return float(raw)
|
||||
except Exception as e:
|
||||
print(f"[설정창] GV{cell} 오류: {e}")
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# 확인 버튼 — SV{cell} {value} 명령
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
def _on_ok(self):
|
||||
import time
|
||||
|
||||
# 변경 항목 수집
|
||||
changes = []
|
||||
for _, cell, kind, _, _, _, _ in _PARAMS:
|
||||
w = self._widgets[cell]
|
||||
current = float(w.value())
|
||||
original = self._originals.get(cell)
|
||||
if original is None or abs(current - original) >= 1e-9:
|
||||
changes.append((cell, current, kind))
|
||||
|
||||
if not changes:
|
||||
self.accept()
|
||||
return
|
||||
|
||||
# 오프라인 전환
|
||||
self._cam._send("SO0")
|
||||
resp = self._cam._read_line()
|
||||
print(f"[설정창] SO0 (오프라인) → {resp!r}")
|
||||
if resp.strip() != "1":
|
||||
QMessageBox.critical(self, "오류", f"오프라인 전환 실패 (응답: {resp!r})")
|
||||
return
|
||||
|
||||
time.sleep(0.3) # 카메라가 오프라인 전환 완료 대기
|
||||
|
||||
# 값 설정
|
||||
errors = []
|
||||
for cell, value, kind in changes:
|
||||
if not self._sv(cell, value, kind):
|
||||
errors.append(cell)
|
||||
|
||||
time.sleep(0.2) # SV 처리 완료 대기
|
||||
|
||||
# 온라인 복귀 — 최대 3회 재시도
|
||||
online_ok = False
|
||||
for attempt in range(3):
|
||||
self._cam._send("SO1")
|
||||
resp = self._cam._read_line()
|
||||
print(f"[설정창] SO1 (온라인) 시도 {attempt+1} → {resp!r}")
|
||||
if resp.strip() == "1":
|
||||
online_ok = True
|
||||
break
|
||||
time.sleep(0.5)
|
||||
|
||||
if not online_ok:
|
||||
QMessageBox.warning(
|
||||
self, "온라인 복귀 실패",
|
||||
f"SO1 명령이 실패했습니다 (마지막 응답: {resp!r})\n"
|
||||
"카메라를 수동으로 재시작하거나 In-Sight Explorer에서 Online으로 전환하세요."
|
||||
)
|
||||
|
||||
if errors:
|
||||
QMessageBox.critical(
|
||||
self, "설정 실패",
|
||||
f"다음 셀 설정 실패:\n{chr(10).join(errors)}\n\n"
|
||||
"셀이 읽기 전용이거나 값 범위를 벗어났을 수 있습니다."
|
||||
)
|
||||
return
|
||||
|
||||
self.accept()
|
||||
|
||||
def _sv(self, cell: str, value: float, kind: str) -> bool:
|
||||
"""SV{cell} {value} 전송 → 응답 '1'이면 True"""
|
||||
try:
|
||||
formatted = str(int(round(value))) if kind == "int" else f"{value:.6g}"
|
||||
self._cam._send(f"SV{cell} {formatted}")
|
||||
resp = self._cam._read_line()
|
||||
print(f"[설정창] SV{cell} {formatted} → {resp!r}")
|
||||
return resp.strip() == "1"
|
||||
except Exception as e:
|
||||
print(f"[설정창] SV{cell} 오류: {e}")
|
||||
return False
|
||||
@@ -110,7 +110,7 @@ class MainWindow(QMainWindow):
|
||||
self.db_client = SQLClient()
|
||||
self.plc_client = plc_client
|
||||
|
||||
self.setWindowTitle("비전 검사 시스템")
|
||||
self.setWindowTitle("리플렉터 검사 시스템")
|
||||
self.showFullScreen()
|
||||
|
||||
# 재학습 탭 연속 클릭(창 최소화 단축) 감지용
|
||||
@@ -189,12 +189,15 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
|
||||
self._register_page = RegisterPage(
|
||||
self.insight, matcher=self.matcher, db_client=self.db_client
|
||||
self.insight, matcher=self.matcher,
|
||||
db_client=self.db_client, config=self.config,
|
||||
)
|
||||
self._inspect_page = InspectPage(
|
||||
self.insight, self.basler,
|
||||
detector=self.detector,
|
||||
belt_delay=_belt_delay,
|
||||
db_client=self.db_client,
|
||||
config=self.config,
|
||||
)
|
||||
self._inspect_page.update_matcher(self.matcher)
|
||||
self._settings_page.belt_settings_changed.connect(
|
||||
@@ -271,6 +274,10 @@ class MainWindow(QMainWindow):
|
||||
btn.setActive(i == idx)
|
||||
if idx == 0:
|
||||
self._settings_page._sync_connection_status()
|
||||
elif idx == 1:
|
||||
self._register_page.load_products()
|
||||
elif idx == 2:
|
||||
self._inspect_page.refresh_wk_results()
|
||||
self.update_connection_status()
|
||||
|
||||
# ================================================================== #
|
||||
@@ -296,6 +303,8 @@ class MainWindow(QMainWindow):
|
||||
connected = db_client is not None and db_client.is_connected()
|
||||
if hasattr(self, "_register_page"):
|
||||
self._register_page.update_db(db_client)
|
||||
if hasattr(self, "_inspect_page"):
|
||||
self._inspect_page.update_db(db_client)
|
||||
if hasattr(self, "_settings_page"):
|
||||
self._settings_page._db_client = db_client
|
||||
self._settings_page._set_db_connected(connected)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 검사 페이지 — 코그넥스/Basler 영상, 그룹 A/B 설정, Pass/Fail 표시
|
||||
# 검사 페이지 — 코그넥스/Basler 영상, Pass/Fail 표시
|
||||
import time
|
||||
import threading
|
||||
import itertools
|
||||
@@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
|
||||
QPushButton, QLabel, QCheckBox, QFrame,
|
||||
QGridLayout, QSizePolicy,
|
||||
QPushButton, QLabel, QFrame,
|
||||
QGridLayout, QSizePolicy, QScrollArea, QScroller, QMessageBox,
|
||||
)
|
||||
|
||||
from logic.inspector import Inspector
|
||||
from logic.group_manager import GroupManager
|
||||
from logic.products import build_patmax_cells, article_label, MAX_PATMAX_SLOTS
|
||||
from db.sql_client import SQLClient
|
||||
from logger import log_inspect_result, log_camera_timing, log_action, log_defect_image
|
||||
|
||||
_DEFECT_COLORS = {
|
||||
@@ -40,33 +42,6 @@ def _draw_detections(frame: np.ndarray, detections: list) -> np.ndarray:
|
||||
)
|
||||
return img
|
||||
|
||||
# 검사 그룹 A/B 선택용 모델 목록
|
||||
_MODELS = [
|
||||
"LOW REF / LX3 / RH",
|
||||
"LOW REF / LX3 / LH",
|
||||
"LOW REF NAS / LX3 / RH",
|
||||
"LOW REF NAS / LX3 / LH",
|
||||
"LOW REF NAS / MX5a 2.0TH / RH",
|
||||
"LOW REF NAS / MX5a 2.0TH / LH",
|
||||
"HIGH REF / LX3 / RH",
|
||||
"HIGH REF / LX3 / LH",
|
||||
"LOW REF NAS 1.5 GEN / CN7 PE / RH",
|
||||
"LOW REF DOM 1.5 GEN / CN7 PE / LH",
|
||||
]
|
||||
|
||||
_MODEL_ID_MAP = {
|
||||
"LOW REF / LX3 / RH": 1,
|
||||
"LOW REF / LX3 / LH": 2,
|
||||
"LOW REF NAS / LX3 / RH": 3,
|
||||
"LOW REF NAS / LX3 / LH": 4,
|
||||
"LOW REF NAS / MX5a 2.0TH / RH": 5,
|
||||
"LOW REF NAS / MX5a 2.0TH / LH": 6,
|
||||
"HIGH REF / LX3 / RH": 7,
|
||||
"HIGH REF / LX3 / LH": 8,
|
||||
"LOW REF NAS 1.5 GEN / CN7 PE / RH": 9,
|
||||
"LOW REF DOM 1.5 GEN / CN7 PE / LH": 10,
|
||||
}
|
||||
|
||||
|
||||
# ================================================================== #
|
||||
# 백그라운드 워커 — 파이프라인 방식
|
||||
@@ -97,6 +72,18 @@ class InspectWorker(QThread):
|
||||
self._stop_flag = False
|
||||
self._pause_flag = False
|
||||
self._seq = itertools.count(1)
|
||||
self._wk_result_ids: set = set()
|
||||
self._wk_check_enabled = False
|
||||
self._allowed_article_ids: set = set()
|
||||
|
||||
def set_work_targets(self, allowed_article_ids: "set | None"):
|
||||
"""WK_Result 작업 대상 ArticleID (정규화된 set). None이면 WK 검사 비활성."""
|
||||
if allowed_article_ids is None:
|
||||
self._wk_check_enabled = False
|
||||
self._allowed_article_ids = set()
|
||||
return
|
||||
self._wk_check_enabled = True
|
||||
self._allowed_article_ids = allowed_article_ids
|
||||
|
||||
# ── 외부 제어 ──────────────────────────────────────────────────── #
|
||||
|
||||
@@ -207,18 +194,25 @@ class InspectWorker(QThread):
|
||||
ct.join(timeout=10.0)
|
||||
log_camera_timing(seq, "cognex_join_done", _ms())
|
||||
|
||||
# ── 모델 판별 ──
|
||||
results = cognex_out.get("results", {})
|
||||
active_names = self._groups.get_active_group()
|
||||
allowed_ids = [_MODEL_ID_MAP[n] for n in active_names if n in _MODEL_ID_MAP]
|
||||
result_info = {
|
||||
# ── 모델 판별 (WK_Result 작업 대상만 허용) ──
|
||||
results = cognex_out.get("results", {})
|
||||
result_info = {
|
||||
"matched": False, "in_allowed": False,
|
||||
"model": None, "score": 0.0,
|
||||
"cognex_pass": False, "status": "인식 불가",
|
||||
}
|
||||
try:
|
||||
if results:
|
||||
result_info = self._inspector.identify_model(results, allowed_ids)
|
||||
if self._wk_check_enabled:
|
||||
result_info = self._inspector.identify_model(
|
||||
results, allowed_article_ids=self._allowed_article_ids,
|
||||
)
|
||||
else:
|
||||
result_info = self._inspector.identify_model(
|
||||
results, allowed_model_ids=self._groups.get_allowed_ids(),
|
||||
)
|
||||
if result_info["matched"] and self._wk_check_enabled:
|
||||
result_info["in_wk_result"] = result_info["in_allowed"]
|
||||
except Exception as e:
|
||||
print(f"[워커 오류] 모델 판별: {e}")
|
||||
|
||||
@@ -258,18 +252,17 @@ class InspectWorker(QThread):
|
||||
|
||||
class InspectPage(QWidget):
|
||||
def __init__(self, insight_cam, basler_cam, detector=None,
|
||||
belt_delay: float = 3.33, parent=None):
|
||||
belt_delay: float = 3.33, db_client=None, config=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._insight = insight_cam
|
||||
self._basler = basler_cam
|
||||
self._db_client = db_client
|
||||
self._config = config or {}
|
||||
self.detector = detector
|
||||
self._inspector = Inspector()
|
||||
self._groups = GroupManager()
|
||||
|
||||
self._counts = {
|
||||
"A": {"total": 0, "pass": 0, "fail": 0, "unknown": 0},
|
||||
"B": {"total": 0, "pass": 0, "fail": 0, "unknown": 0},
|
||||
}
|
||||
self._counts = {"total": 0, "pass": 0, "fail": 0, "unknown": 0}
|
||||
|
||||
self._matcher = None
|
||||
|
||||
@@ -284,6 +277,7 @@ class InspectPage(QWidget):
|
||||
self._worker.result_ready.connect(self._on_result)
|
||||
|
||||
self._build_ui()
|
||||
self.refresh_wk_results()
|
||||
|
||||
# ================================================================== #
|
||||
# 최상위 레이아웃
|
||||
@@ -347,68 +341,48 @@ class InspectPage(QWidget):
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
layout.setSpacing(0)
|
||||
|
||||
layout.addWidget(self._build_col_groups(), stretch=5)
|
||||
layout.addWidget(self._build_col_products(), stretch=5)
|
||||
layout.addWidget(_vline())
|
||||
layout.addWidget(self._build_col_controls(), stretch=3)
|
||||
layout.addWidget(_vline())
|
||||
layout.addWidget(self._build_col_counters(), stretch=4)
|
||||
return w
|
||||
|
||||
def _build_col_groups(self) -> QWidget:
|
||||
def _build_col_products(self) -> QWidget:
|
||||
w = QWidget()
|
||||
layout = QVBoxLayout(w)
|
||||
layout.setContentsMargins(4, 4, 8, 4)
|
||||
layout.setSpacing(6)
|
||||
|
||||
checks_row = QHBoxLayout()
|
||||
checks_row.setSpacing(8)
|
||||
checks_row.addWidget(self._build_group_section("A"), stretch=1)
|
||||
checks_row.addWidget(self._build_group_section("B"), stretch=1)
|
||||
layout.addLayout(checks_row, stretch=1)
|
||||
|
||||
self._switch_btn = QPushButton("현재: 그룹 A 활성 → B로 전환")
|
||||
self._switch_btn.setFixedHeight(56)
|
||||
self._switch_btn.setStyleSheet(
|
||||
"background:#1a3a5c; color:#ffffff; border:none; border-radius:4px;"
|
||||
"font-size:14px; font-weight:bold;"
|
||||
)
|
||||
self._switch_btn.clicked.connect(self._on_switch)
|
||||
layout.addWidget(self._switch_btn)
|
||||
return w
|
||||
|
||||
def _build_group_section(self, name: str) -> QGroupBox:
|
||||
active_color = "#4488ff" if name == "A" else "#cc8844"
|
||||
g = QGroupBox(f"그룹 {name} (최대 4종)")
|
||||
g = QGroupBox("검사 대상")
|
||||
self._products_group = g
|
||||
g.setStyleSheet(
|
||||
f"QGroupBox {{ background:#222222; border:1px solid #333333; border-radius:6px;"
|
||||
f" margin-top:12px; padding:6px 4px 4px 4px; }}"
|
||||
f"QGroupBox::title {{ color:{active_color}; subcontrol-origin:margin;"
|
||||
f" left:8px; font-size:13px; font-weight:bold; }}"
|
||||
"QGroupBox { background:#222222; border:1px solid #333333; border-radius:6px;"
|
||||
" margin-top:12px; padding:6px 4px 4px 4px; }"
|
||||
"QGroupBox::title { color:#4488ff; subcontrol-origin:margin;"
|
||||
" left:8px; font-size:13px; font-weight:bold; }"
|
||||
)
|
||||
layout = QVBoxLayout(g)
|
||||
layout.setSpacing(1)
|
||||
layout.setContentsMargins(4, 2, 4, 2)
|
||||
outer = QVBoxLayout(g)
|
||||
outer.setSpacing(0)
|
||||
outer.setContentsMargins(4, 2, 4, 2)
|
||||
|
||||
checks = []
|
||||
for model in _MODELS:
|
||||
cb = QCheckBox(model)
|
||||
cb.setStyleSheet(
|
||||
"QCheckBox { font-size:12px; min-height:24px; color:#cccccc; }"
|
||||
"QCheckBox::indicator { width:16px; height:16px; }"
|
||||
)
|
||||
checks.append(cb)
|
||||
layout.addWidget(cb)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setStyleSheet("background:transparent;")
|
||||
QScroller.grabGesture(scroll.viewport(), QScroller.LeftMouseButtonGesture)
|
||||
|
||||
if name == "A":
|
||||
self._group_a_checks = checks
|
||||
for cb in checks:
|
||||
cb.clicked.connect(lambda checked, c=cb: self._on_group_changed("A", c, checked))
|
||||
else:
|
||||
self._group_b_checks = checks
|
||||
for cb in checks:
|
||||
cb.clicked.connect(lambda checked, c=cb: self._on_group_changed("B", c, checked))
|
||||
inner = QWidget()
|
||||
inner.setStyleSheet("background:transparent;")
|
||||
self._products_inner_layout = QVBoxLayout(inner)
|
||||
self._products_inner_layout.setSpacing(2)
|
||||
self._products_inner_layout.setContentsMargins(2, 2, 2, 2)
|
||||
|
||||
return g
|
||||
scroll.setWidget(inner)
|
||||
outer.addWidget(scroll)
|
||||
layout.addWidget(g, stretch=1)
|
||||
return w
|
||||
|
||||
def _build_col_controls(self) -> QWidget:
|
||||
w = QWidget()
|
||||
@@ -433,9 +407,9 @@ class InspectPage(QWidget):
|
||||
)
|
||||
self._pause_btn.clicked.connect(self._on_pause)
|
||||
|
||||
self._active_lbl = QLabel("활성 그룹: A")
|
||||
self._active_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._active_lbl.setStyleSheet("font-size:14px; color:#aaaaaa;")
|
||||
self._scope_lbl = QLabel("검사 범위: —")
|
||||
self._scope_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._scope_lbl.setStyleSheet("font-size:14px; color:#aaaaaa;")
|
||||
|
||||
self._model_lbl = QLabel("인식 모델: —")
|
||||
self._model_lbl.setAlignment(Qt.AlignCenter)
|
||||
@@ -446,19 +420,19 @@ class InspectPage(QWidget):
|
||||
f"벨트 딜레이: {self._worker._belt_delay:.2f}s"
|
||||
)
|
||||
self._belt_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._belt_lbl.setStyleSheet("font-size:12px; color:#666666;")
|
||||
self._belt_lbl.setStyleSheet("font-size:13px; color:#999999;")
|
||||
|
||||
self._result_lbl = QLabel("대기 중")
|
||||
self._result_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._result_lbl.setStyleSheet(
|
||||
"font-size:48px; font-weight:bold; background:#2a2a2a;"
|
||||
"color:#666666; border-radius:8px;"
|
||||
"font-size:64px; font-weight:bold; background:#2a2a2a;"
|
||||
"color:#888888; border-radius:8px;"
|
||||
)
|
||||
self._result_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
layout.addWidget(self._start_btn)
|
||||
layout.addWidget(self._pause_btn)
|
||||
layout.addWidget(self._active_lbl)
|
||||
layout.addWidget(self._scope_lbl)
|
||||
layout.addWidget(self._model_lbl)
|
||||
layout.addWidget(self._belt_lbl)
|
||||
layout.addWidget(self._result_lbl, stretch=1)
|
||||
@@ -473,7 +447,7 @@ class InspectPage(QWidget):
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(4)
|
||||
|
||||
for col, text in enumerate(["", "그룹 A", "그룹 B"]):
|
||||
for col, text in enumerate(["", "집계"]):
|
||||
lbl = QLabel(text)
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet("font-size:13px; color:#888888; font-weight:bold;")
|
||||
@@ -491,16 +465,14 @@ class InspectPage(QWidget):
|
||||
row_lbl.setStyleSheet(f"font-size:14px; color:{color}; font-weight:bold;")
|
||||
grid.addWidget(row_lbl, r, 0)
|
||||
|
||||
self._cnt_lbls[key] = {}
|
||||
for g_col, group in enumerate(["A", "B"], start=1):
|
||||
lbl = QLabel("0")
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:36px; font-weight:bold; color:{color};"
|
||||
"background:#222222; border-radius:4px; padding:2px 6px;"
|
||||
)
|
||||
grid.addWidget(lbl, r, g_col)
|
||||
self._cnt_lbls[key][group] = lbl
|
||||
lbl = QLabel("0")
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:36px; font-weight:bold; color:{color};"
|
||||
"background:#222222; border-radius:4px; padding:2px 6px;"
|
||||
)
|
||||
grid.addWidget(lbl, r, 1)
|
||||
self._cnt_lbls[key] = lbl
|
||||
|
||||
layout.addLayout(grid)
|
||||
|
||||
@@ -518,24 +490,6 @@ class InspectPage(QWidget):
|
||||
# 슬롯 — UI 이벤트
|
||||
# ================================================================== #
|
||||
|
||||
def _on_group_changed(self, group: str, changed_cb: QCheckBox, is_checked: bool):
|
||||
checks = self._group_a_checks if group == "A" else self._group_b_checks
|
||||
if is_checked and sum(1 for c in checks if c.isChecked()) > GroupManager.MAX_PER_GROUP:
|
||||
changed_cb.setChecked(False)
|
||||
return
|
||||
models = [c.text() for c in checks if c.isChecked()]
|
||||
if group == "A":
|
||||
self._groups.set_group_a(models)
|
||||
else:
|
||||
self._groups.set_group_b(models)
|
||||
|
||||
def _on_switch(self):
|
||||
active = self._groups.switch_group()
|
||||
other = "B" if active == "A" else "A"
|
||||
self._switch_btn.setText(f"현재: 그룹 {active} 활성 → {other}로 전환")
|
||||
self._active_lbl.setText(f"활성 그룹: {active}")
|
||||
print(f"[검사] 그룹 전환 → 활성 그룹 {active}")
|
||||
|
||||
def _on_start(self):
|
||||
if self._worker.isRunning():
|
||||
return
|
||||
@@ -558,11 +512,18 @@ class InspectPage(QWidget):
|
||||
log_action("[검사] 검사 재개")
|
||||
|
||||
def _on_reset(self):
|
||||
reply = QMessageBox.question(
|
||||
self, "카운터 초기화",
|
||||
"검사 카운트를 0으로 초기화할까요?\n"
|
||||
"초기화한 집계는 되돌릴 수 없습니다.",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No,
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
log_action("[검사] 카운트 리셋")
|
||||
for key in ("total", "pass", "fail", "unknown"):
|
||||
for g in ("A", "B"):
|
||||
self._counts[g][key] = 0
|
||||
self._cnt_lbls[key][g].setText("0")
|
||||
self._counts[key] = 0
|
||||
self._cnt_lbls[key].setText("0")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._worker.stop()
|
||||
@@ -598,6 +559,106 @@ class InspectPage(QWidget):
|
||||
self._worker.set_belt_delay(delay)
|
||||
self._belt_lbl.setText(f"벨트 딜레이: {delay:.2f}s")
|
||||
|
||||
def update_db(self, db_client):
|
||||
"""MainWindow에서 DB 연결/해제 시 호출."""
|
||||
self._db_client = db_client
|
||||
self.refresh_wk_results()
|
||||
|
||||
def refresh_wk_results(self):
|
||||
"""vi_AI_WK_Result 기준 작업 대상을 UI·PatMax 매핑·워커에 반영."""
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
self._inspector.set_pattern_cells({})
|
||||
self._worker.set_work_targets(None)
|
||||
self._update_product_list([], [])
|
||||
self._scope_lbl.setText("검사 범위: DB 미연결")
|
||||
self._products_group.setTitle("검사 대상")
|
||||
print("[검사] WK_Result 비활성 — DB 미연결")
|
||||
return
|
||||
|
||||
mes_selected = self._config.get("mes", {}).get("selected_article_ids")
|
||||
if mes_selected is not None and len(mes_selected) == 0:
|
||||
self._inspector.set_pattern_cells({})
|
||||
self._worker.set_work_targets(set())
|
||||
self._update_product_list([], [])
|
||||
self._scope_lbl.setText("검사 범위: MES 제품 미선택")
|
||||
self._products_group.setTitle("검사 대상 (0종)")
|
||||
return
|
||||
|
||||
if mes_selected is not None:
|
||||
all_items = self._db_client.get_reflector_list_ordered(mes_selected)
|
||||
else:
|
||||
all_items = self._db_client.get_reflector_list()
|
||||
|
||||
self._inspector.set_pattern_cells(build_patmax_cells(all_items))
|
||||
|
||||
active, inactive = self._db_client.split_articles_by_wk(mes_selected)
|
||||
allowed = {SQLClient._norm_id(a["article_id"]) for a in active}
|
||||
|
||||
self._worker.set_work_targets(allowed)
|
||||
self._update_product_list(active, inactive)
|
||||
|
||||
total = len(active) + len(inactive)
|
||||
if active:
|
||||
self._scope_lbl.setText(f"검사 범위: 작업 대상 {len(active)}종")
|
||||
else:
|
||||
self._scope_lbl.setText("검사 범위: 작업 대상 없음")
|
||||
self._products_group.setTitle(
|
||||
f"검사 대상 (작업 {len(active)}종 / 전체 {total}종)"
|
||||
)
|
||||
print(
|
||||
f"[검사] WK_Result 작업 대상: {len(active)}종 (전체 {total}종, "
|
||||
f"PatMax 슬롯 {min(len(all_items), MAX_PATMAX_SLOTS)}개)"
|
||||
)
|
||||
|
||||
def _update_product_list(self, active: list, inactive: list):
|
||||
if not hasattr(self, "_products_inner_layout"):
|
||||
return
|
||||
|
||||
layout = self._products_inner_layout
|
||||
while layout.count():
|
||||
item = layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if active:
|
||||
layout.addWidget(self._make_section_label(
|
||||
f"작업 대상 — WK_Result ({len(active)})", active=True
|
||||
))
|
||||
for i, article in enumerate(active, 1):
|
||||
layout.addWidget(self._make_article_label(article, i, active=True))
|
||||
|
||||
if inactive:
|
||||
layout.addWidget(self._make_section_label(
|
||||
f"기타 ({len(inactive)})", active=False
|
||||
))
|
||||
for i, article in enumerate(inactive, 1):
|
||||
layout.addWidget(self._make_article_label(article, i, active=False))
|
||||
|
||||
if not active and not inactive:
|
||||
layout.addWidget(self._make_section_label("작업 대상 없음", active=False))
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
@staticmethod
|
||||
def _make_section_label(text: str, active: bool) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
color = "#aaaaaa" if active else "#666666"
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:12px; color:{color}; font-weight:bold;"
|
||||
"padding:6px 4px 2px 4px; background:#2a2a2a;"
|
||||
)
|
||||
return lbl
|
||||
|
||||
@staticmethod
|
||||
def _make_article_label(article: dict, index: int, active: bool) -> QLabel:
|
||||
lbl = QLabel(f"{index:2d}. {article_label(article)}")
|
||||
if active:
|
||||
style = "font-size:13px; min-height:32px; color:#eeeeee; padding:2px 4px;"
|
||||
else:
|
||||
style = "font-size:13px; min-height:32px; color:#555555; padding:2px 4px;"
|
||||
lbl.setStyleSheet(style)
|
||||
return lbl
|
||||
|
||||
# ================================================================== #
|
||||
# 워커 signal 슬롯 (메인 스레드)
|
||||
# ================================================================== #
|
||||
@@ -606,24 +667,21 @@ class InspectPage(QWidget):
|
||||
self._display_basler_image(frame, detections=detections)
|
||||
|
||||
def _on_result(self, data: dict):
|
||||
group = data["group"]
|
||||
matched = data["matched"]
|
||||
result = data["result"]
|
||||
cognex_pass = data["cognex_pass"]
|
||||
basler_pass = data["basler_pass"]
|
||||
result_info = data["result_info"]
|
||||
|
||||
self._model_lbl.setText(result_info["status"])
|
||||
|
||||
self._counts[group]["total"] += 1
|
||||
self._counts["total"] += 1
|
||||
if not matched:
|
||||
self._counts[group]["unknown"] += 1
|
||||
self._counts["unknown"] += 1
|
||||
elif result == "PASS":
|
||||
self._counts[group]["pass"] += 1
|
||||
self._counts["pass"] += 1
|
||||
else:
|
||||
self._counts[group]["fail"] += 1
|
||||
self._counts["fail"] += 1
|
||||
for key in ("total", "pass", "fail", "unknown"):
|
||||
self._cnt_lbls[key][group].setText(str(self._counts[group][key]))
|
||||
self._cnt_lbls[key].setText(str(self._counts[key]))
|
||||
|
||||
if not matched:
|
||||
self._set_result("미인식", "#332200", "#ff9900")
|
||||
@@ -639,7 +697,7 @@ class InspectPage(QWidget):
|
||||
def _set_result(self, text: str, bg: str, fg: str):
|
||||
self._result_lbl.setText(text)
|
||||
self._result_lbl.setStyleSheet(
|
||||
f"font-size:48px; font-weight:bold; background:{bg};"
|
||||
f"font-size:64px; font-weight:bold; background:{bg};"
|
||||
f"color:{fg}; border-radius:8px;"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
from PyQt5.QtGui import QImage, QPixmap, QColor
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
|
||||
QPushButton, QListWidget, QListWidgetItem, QLabel,
|
||||
QMessageBox, QScrollArea, QFrame,
|
||||
)
|
||||
|
||||
from db.sql_client import SQLClient
|
||||
|
||||
|
||||
_GRP_STYLE = (
|
||||
"QGroupBox {"
|
||||
@@ -20,11 +22,13 @@ _GRP_STYLE = (
|
||||
|
||||
|
||||
class RegisterPage(QWidget):
|
||||
def __init__(self, insight_cam, matcher=None, db_client=None, parent=None):
|
||||
def __init__(self, insight_cam, matcher=None, db_client=None, config=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._insight = insight_cam
|
||||
self._db_client = db_client
|
||||
self._config = config or {}
|
||||
self._db_items = []
|
||||
self._wk_map = {}
|
||||
self._selected = None
|
||||
self._captured_img = None
|
||||
|
||||
@@ -49,12 +53,8 @@ class RegisterPage(QWidget):
|
||||
layout.setSpacing(10)
|
||||
|
||||
self._btn_mes = QPushButton("MES 불러오기")
|
||||
self._btn_mes.setVisible(False)
|
||||
self._btn_mes.setEnabled(False)
|
||||
self._btn_mes.setFixedHeight(56)
|
||||
self._btn_mes.setToolTip("DB 연결 후 사용 가능")
|
||||
self._btn_mes.setStyleSheet(
|
||||
"background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;"
|
||||
)
|
||||
self._btn_mes.clicked.connect(self._on_load_from_db)
|
||||
layout.addWidget(self._btn_mes)
|
||||
|
||||
@@ -69,10 +69,8 @@ class RegisterPage(QWidget):
|
||||
border-radius:4px; outline:none; font-size:15px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding:0px 14px; border-bottom:1px solid #2a2a2a; color:#dddddd;
|
||||
padding:0px 14px; border-bottom:1px solid #2a2a2a;
|
||||
}
|
||||
QListWidget::item:selected { background:#185FA5; color:#ffffff; }
|
||||
QListWidget::item:hover:!selected { background:#2d2d2d; }
|
||||
""")
|
||||
self._list.currentRowChanged.connect(self._on_select)
|
||||
layout.addWidget(self._list, stretch=1)
|
||||
@@ -109,9 +107,19 @@ class RegisterPage(QWidget):
|
||||
self._lbl_name = _info_value("—")
|
||||
self._lbl_model = _info_value("—")
|
||||
self._lbl_type = _info_value("—")
|
||||
self._lbl_wk = _info_value("—")
|
||||
self._lbl_machine_id = _info_value("—")
|
||||
self._lbl_machine = _info_value("—")
|
||||
self._lbl_work_date = _info_value("—")
|
||||
self._lbl_work_time = _info_value("—")
|
||||
layout.addLayout(_info_row("카테고리", self._lbl_name))
|
||||
layout.addLayout(_info_row("모델명", self._lbl_model))
|
||||
layout.addLayout(_info_row("Type", self._lbl_type))
|
||||
layout.addLayout(_info_row("작업상태", self._lbl_wk))
|
||||
layout.addLayout(_info_row("설비 ID", self._lbl_machine_id))
|
||||
layout.addLayout(_info_row("설비명", self._lbl_machine))
|
||||
layout.addLayout(_info_row("작업시작일", self._lbl_work_date))
|
||||
layout.addLayout(_info_row("작업시작", self._lbl_work_time))
|
||||
|
||||
self._arrow_lbl = QLabel("")
|
||||
self._arrow_lbl.setAlignment(Qt.AlignCenter)
|
||||
@@ -157,21 +165,28 @@ class RegisterPage(QWidget):
|
||||
if row < 0:
|
||||
return
|
||||
item = self._list.item(row)
|
||||
if item is None:
|
||||
if item is None or not (item.flags() & Qt.ItemIsSelectable):
|
||||
return
|
||||
|
||||
article_id = item.data(Qt.UserRole)
|
||||
if article_id is None:
|
||||
return
|
||||
|
||||
self._refresh_list_styles(row)
|
||||
|
||||
db_item = next(
|
||||
(x for x in self._db_items if x["article_id"] == article_id), None
|
||||
(x for x in self._db_items
|
||||
if SQLClient._norm_id(x["article_id"]) == SQLClient._norm_id(article_id)),
|
||||
None,
|
||||
)
|
||||
name = item.data(Qt.UserRole + 1) or item.text()
|
||||
in_wk = bool(item.data(Qt.UserRole + 2))
|
||||
r = {
|
||||
"id": article_id,
|
||||
"name": item.text(),
|
||||
"name": name,
|
||||
"model": db_item.get("buyer_article_no", "") if db_item else "",
|
||||
"type": "",
|
||||
"in_wk": in_wk,
|
||||
}
|
||||
|
||||
self._selected = r
|
||||
@@ -181,6 +196,20 @@ class RegisterPage(QWidget):
|
||||
self._lbl_model.setText(r["model"])
|
||||
t = r.get("type", "")
|
||||
self._lbl_type.setText(t if t else "—")
|
||||
if in_wk:
|
||||
self._lbl_wk.setText("작업 대상")
|
||||
self._lbl_wk.setStyleSheet("color:#cccccc; font-size:16px; font-weight:bold;")
|
||||
wk = self._wk_map.get(SQLClient._norm_id(article_id), {})
|
||||
self._lbl_machine_id.setText(SQLClient.format_db_value(wk.get("machine_id")))
|
||||
self._lbl_machine.setText(SQLClient.format_db_value(wk.get("machine")))
|
||||
self._lbl_work_date.setText(SQLClient.format_db_value(wk.get("work_start_date")))
|
||||
self._lbl_work_time.setText(SQLClient.format_db_value(wk.get("work_start_time")))
|
||||
else:
|
||||
self._lbl_wk.setText("작업 대외")
|
||||
self._lbl_wk.setStyleSheet("color:#888888; font-size:16px; font-weight:bold;")
|
||||
for lbl in (self._lbl_machine_id, self._lbl_machine,
|
||||
self._lbl_work_date, self._lbl_work_time):
|
||||
lbl.setText("—")
|
||||
|
||||
if t == "RH":
|
||||
self._arrow_lbl.setText("→")
|
||||
@@ -219,35 +248,111 @@ class RegisterPage(QWidget):
|
||||
def update_db(self, db_client):
|
||||
"""MainWindow에서 DB 연결/해제 시 호출."""
|
||||
self._db_client = db_client
|
||||
enabled = db_client is not None and db_client.is_connected()
|
||||
self._btn_mes.setEnabled(enabled)
|
||||
self._btn_mes.setStyleSheet(
|
||||
"background:#1a3a5c; color:#ffffff; border:none; border-radius:4px; font-size:15px;"
|
||||
if enabled else
|
||||
"background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;"
|
||||
self.load_products()
|
||||
|
||||
def load_products(self):
|
||||
"""관리자 설정에서 선택한 MES 제품 목록을 자동으로 불러온다."""
|
||||
self._list.clear()
|
||||
self._db_items = []
|
||||
self._wk_map = {}
|
||||
self._selected = None
|
||||
self._lbl_name.setText("—")
|
||||
self._lbl_model.setText("—")
|
||||
self._lbl_type.setText("—")
|
||||
self._lbl_wk.setText("—")
|
||||
self._lbl_wk.setStyleSheet("color:#ffffff; font-size:16px; font-weight:bold;")
|
||||
for lbl in (self._lbl_machine_id, self._lbl_machine,
|
||||
self._lbl_work_date, self._lbl_work_time):
|
||||
lbl.setText("—")
|
||||
self._arrow_lbl.setText("")
|
||||
self._reset_preview()
|
||||
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
return
|
||||
|
||||
selected_ids = self._config.get("mes", {}).get("selected_article_ids")
|
||||
if selected_ids is not None and len(selected_ids) == 0:
|
||||
return
|
||||
|
||||
all_items = self._db_client.get_reflector_list(
|
||||
article_ids=selected_ids if selected_ids is not None else None
|
||||
)
|
||||
if not all_items:
|
||||
return
|
||||
|
||||
self._wk_map = self._db_client.get_wk_result_map()
|
||||
active, inactive = self._db_client.split_articles_by_wk(selected_ids)
|
||||
|
||||
self._db_items = all_items
|
||||
|
||||
if active:
|
||||
self._add_section_header(f"작업 대상 — WK_Result ({len(active)})")
|
||||
for item in active:
|
||||
self._add_product_item(item, in_wk=True)
|
||||
|
||||
if inactive:
|
||||
self._add_section_header(f"기타 ({len(inactive)})")
|
||||
for item in inactive:
|
||||
self._add_product_item(item, in_wk=False)
|
||||
|
||||
self._refresh_list_styles()
|
||||
|
||||
def _on_load_from_db(self):
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
QMessageBox.warning(self, "경고", "DB를 먼저 연결해주세요.")
|
||||
return
|
||||
pass
|
||||
|
||||
items = self._db_client.get_reflector_list()
|
||||
if not items:
|
||||
QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
|
||||
return
|
||||
def _add_section_header(self, text: str):
|
||||
hi = QListWidgetItem(text)
|
||||
hi.setFlags(Qt.NoItemFlags)
|
||||
hi.setForeground(QColor("#aaaaaa"))
|
||||
hi.setBackground(QColor("#2a2a2a"))
|
||||
hi.setSizeHint(QSize(0, 40))
|
||||
self._list.addItem(hi)
|
||||
|
||||
self._list.clear()
|
||||
self._db_items = items
|
||||
for item in items:
|
||||
li = QListWidgetItem(item['article'])
|
||||
li.setSizeHint(QSize(0, 52))
|
||||
li.setData(Qt.UserRole, item["article_id"])
|
||||
self._list.addItem(li)
|
||||
def _add_product_item(self, item: dict, in_wk: bool):
|
||||
li = QListWidgetItem(item["article"])
|
||||
li.setSizeHint(QSize(0, 52))
|
||||
li.setData(Qt.UserRole, item["article_id"])
|
||||
li.setData(Qt.UserRole + 1, item["article"])
|
||||
li.setData(Qt.UserRole + 2, in_wk)
|
||||
if in_wk:
|
||||
wk = self._wk_map.get(SQLClient._norm_id(item["article_id"]), {})
|
||||
tips = []
|
||||
if wk.get("machine_id") is not None:
|
||||
tips.append(f"설비 ID: {SQLClient.format_db_value(wk.get('machine_id'))}")
|
||||
if wk.get("machine") is not None:
|
||||
tips.append(f"설비: {SQLClient.format_db_value(wk.get('machine'))}")
|
||||
if wk.get("work_start_date") is not None:
|
||||
tips.append(f"시작일: {SQLClient.format_db_value(wk.get('work_start_date'))}")
|
||||
if wk.get("work_start_time") is not None:
|
||||
tips.append(f"시작: {SQLClient.format_db_value(wk.get('work_start_time'))}")
|
||||
if tips:
|
||||
li.setToolTip("\n".join(tips))
|
||||
self._list.addItem(li)
|
||||
self._style_product_item(li, in_wk, selected=False)
|
||||
|
||||
QMessageBox.information(
|
||||
self, "완료", f"{len(items)}개 제품을 불러왔습니다."
|
||||
)
|
||||
@staticmethod
|
||||
def _style_product_item(item: QListWidgetItem, in_wk: bool, selected: bool):
|
||||
if in_wk:
|
||||
if selected:
|
||||
bg, fg = "#777777", "#ffffff"
|
||||
else:
|
||||
bg, fg = "#555555", "#eeeeee"
|
||||
elif selected:
|
||||
bg, fg = "#3a3a3a", "#aaaaaa"
|
||||
else:
|
||||
bg, fg = "#1e1e1e", "#666666"
|
||||
item.setBackground(QColor(bg))
|
||||
item.setForeground(QColor(fg))
|
||||
|
||||
def _refresh_list_styles(self, selected_row: int = -1):
|
||||
if selected_row < 0:
|
||||
selected_row = self._list.currentRow()
|
||||
for i in range(self._list.count()):
|
||||
item = self._list.item(i)
|
||||
if not (item.flags() & Qt.ItemIsSelectable):
|
||||
continue
|
||||
in_wk = bool(item.data(Qt.UserRole + 2))
|
||||
self._style_product_item(item, in_wk, selected=(i == selected_row))
|
||||
|
||||
# ================================================================== #
|
||||
# 헬퍼
|
||||
|
||||
@@ -754,7 +754,7 @@ class RetrainPage(QWidget):
|
||||
"color:#aaaaaa; font-size:12px; min-width:55px;"
|
||||
)
|
||||
hint = QLabel("더블클릭: fit | 휠: 줌 | Space+드래그: 패닝 | Del: 박스삭제 | Ctrl+Z: 실행취소")
|
||||
hint.setStyleSheet("color:#555555; font-size:11px;")
|
||||
hint.setStyleSheet("color:#888888; font-size:12px;")
|
||||
btn_fit = QPushButton("초기화")
|
||||
btn_fit.setFixedHeight(28)
|
||||
btn_fit.setStyleSheet(_btn_style("#333333", font_size=12))
|
||||
@@ -1174,6 +1174,12 @@ def _spinbox_style() -> str:
|
||||
" background:#2a2a2a; color:#ffffff; border:1px solid #555555;"
|
||||
" border-radius:4px; padding:4px 8px; font-size:14px; min-height:38px;"
|
||||
"}"
|
||||
"QSpinBox::up-button {"
|
||||
" subcontrol-origin:border; subcontrol-position:top right; width:30px;"
|
||||
"}"
|
||||
"QSpinBox::down-button {"
|
||||
" subcontrol-origin:border; subcontrol-position:bottom right; width:30px;"
|
||||
"}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
|
||||
from PyQt5.QtGui import QColor
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout,
|
||||
QPushButton, QLineEdit, QSpinBox, QDoubleSpinBox,
|
||||
QLabel, QMessageBox, QApplication, QFileDialog,
|
||||
QDialog, QTabWidget, QFrame,
|
||||
QDialog, QTabWidget, QFrame, QListWidget, QListWidgetItem,
|
||||
)
|
||||
|
||||
from camera.insight import InSightCamera
|
||||
@@ -16,6 +18,7 @@ from db.sql_client import SQLClient
|
||||
from plc.plc_client import PLCClient
|
||||
from paths import resolve_path, to_project_relative
|
||||
from utils.path_helper import get_path
|
||||
from utils.touch_keyboard import show_touch_keyboard, hide_touch_keyboard
|
||||
from logger import log_action
|
||||
|
||||
ADMIN_PASSWORD = "1234"
|
||||
@@ -225,6 +228,19 @@ class PasswordDialog(QDialog):
|
||||
# AdminSettingsDialog
|
||||
# ══════════════════════════════════════════════════════════════════════ #
|
||||
|
||||
class _TouchCheckList(QListWidget):
|
||||
"""행 전체 탭으로 체크 토글 (터치 화면용)."""
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
item = self.itemAt(event.pos())
|
||||
if item is not None:
|
||||
item.setCheckState(
|
||||
Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
|
||||
)
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
|
||||
class AdminSettingsDialog(QDialog):
|
||||
def __init__(self, settings_page, parent=None):
|
||||
super().__init__(parent)
|
||||
@@ -258,6 +274,7 @@ class AdminSettingsDialog(QDialog):
|
||||
self._tabs.addTab(self._build_tab_cognex(), "코그넥스")
|
||||
self._tabs.addTab(self._build_tab_basler(), "Basler")
|
||||
self._tabs.addTab(self._build_tab_db(), "DB")
|
||||
self._tabs.addTab(self._build_tab_mes(), "MES 제품")
|
||||
self._tabs.addTab(self._build_tab_ai(), "AI 모델")
|
||||
self._tabs.addTab(self._build_tab_conveyor(), "컨베이어")
|
||||
self._tabs.addTab(self._build_tab_plc(), "PLC")
|
||||
@@ -387,6 +404,74 @@ class AdminSettingsDialog(QDialog):
|
||||
form.addRow("", self._btn_pair(btn_connect, btn_save))
|
||||
return self._tab_wrap(g)
|
||||
|
||||
# ── 탭 3b — MES 제품 선택 ───────────────────────────────────────── #
|
||||
|
||||
def _build_tab_mes(self) -> QWidget:
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background:#1a1a1a;")
|
||||
layout = QVBoxLayout(w)
|
||||
layout.setContentsMargins(32, 24, 32, 20)
|
||||
layout.setSpacing(12)
|
||||
|
||||
hint = QLabel(
|
||||
"제품 등록 탭의 'MES 불러오기'에 표시할 제품을 선택합니다.\n"
|
||||
"DB 연결 후 목록을 불러온 뒤 체크하고 저장하세요."
|
||||
)
|
||||
hint.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
|
||||
hint.setWordWrap(True)
|
||||
layout.addWidget(hint)
|
||||
|
||||
btn_row = QHBoxLayout()
|
||||
btn_row.setSpacing(8)
|
||||
|
||||
btn_load = QPushButton("목록 불러오기")
|
||||
btn_load.setFixedHeight(42)
|
||||
btn_load.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
|
||||
btn_load.clicked.connect(self._on_mes_load)
|
||||
|
||||
btn_all = QPushButton("전체 선택")
|
||||
btn_all.setFixedHeight(42)
|
||||
btn_all.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
|
||||
btn_all.clicked.connect(lambda: self._on_mes_set_all(True))
|
||||
|
||||
btn_none = QPushButton("전체 해제")
|
||||
btn_none.setFixedHeight(42)
|
||||
btn_none.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
|
||||
btn_none.clicked.connect(lambda: self._on_mes_set_all(False))
|
||||
|
||||
btn_row.addWidget(btn_load, stretch=2)
|
||||
btn_row.addWidget(btn_all, stretch=1)
|
||||
btn_row.addWidget(btn_none, stretch=1)
|
||||
layout.addLayout(btn_row)
|
||||
|
||||
self._mes_list = _TouchCheckList()
|
||||
self._mes_list.setSelectionMode(QListWidget.NoSelection)
|
||||
self._mes_list.setMinimumHeight(380)
|
||||
self._mes_list.setStyleSheet("""
|
||||
QListWidget {
|
||||
background:#1a1a1a; border:1px solid #333333;
|
||||
border-radius:4px; outline:none; font-size:15px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding:12px 16px; border-bottom:1px solid #2a2a2a;
|
||||
}
|
||||
QListWidget::indicator { width:0px; height:0px; }
|
||||
""")
|
||||
self._mes_list.itemChanged.connect(self._on_mes_item_changed)
|
||||
layout.addWidget(self._mes_list, stretch=1)
|
||||
|
||||
self._mes_count_lbl = QLabel("선택: 0 / 0")
|
||||
self._mes_count_lbl.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
|
||||
layout.addWidget(self._mes_count_lbl)
|
||||
|
||||
btn_save = QPushButton("선택 저장")
|
||||
btn_save.setFixedHeight(56)
|
||||
btn_save.setStyleSheet(_BTN_DLG_PRIMARY)
|
||||
btn_save.clicked.connect(self._on_mes_save)
|
||||
layout.addWidget(btn_save)
|
||||
|
||||
return w
|
||||
|
||||
# ── 탭 4 — AI 모델 ──────────────────────────────────────────────── #
|
||||
|
||||
def _build_tab_ai(self) -> QWidget:
|
||||
@@ -538,10 +623,60 @@ class AdminSettingsDialog(QDialog):
|
||||
btn_close.setStyleSheet(_BTN_DLG)
|
||||
btn_close.clicked.connect(self.accept)
|
||||
|
||||
# 터치 키보드 표시/숨김 토글 버튼 (물리 키보드 없는 터치 모니터용)
|
||||
self._kb_btn = QPushButton("\u2328") # ⌨ 키보드 글리프
|
||||
self._kb_btn.setCheckable(True)
|
||||
self._kb_btn.setFixedSize(56, 56)
|
||||
self._kb_btn.setToolTip("터치 키보드 표시 / 숨기기")
|
||||
self._kb_btn.setStyleSheet(
|
||||
"QPushButton {"
|
||||
" background:#2a2a2a; color:#cccccc; border:1px solid #555555;"
|
||||
" border-radius:4px; font-size:24px;"
|
||||
"}"
|
||||
"QPushButton:hover { background:#333333; color:#ffffff; }"
|
||||
"QPushButton:checked { background:#1D9E75; color:#ffffff; border:none; }"
|
||||
)
|
||||
self._kb_btn.toggled.connect(self._on_toggle_keyboard)
|
||||
|
||||
row.addWidget(self._kb_btn)
|
||||
row.addWidget(btn_save_all, stretch=2)
|
||||
row.addWidget(btn_close, stretch=1)
|
||||
return bar
|
||||
|
||||
# ── 터치 키보드 토글 ─────────────────────────────────────────────── #
|
||||
|
||||
def _on_toggle_keyboard(self, checked: bool):
|
||||
if checked:
|
||||
if self._show_touch_keyboard():
|
||||
log_action("[설정] 터치 키보드 표시")
|
||||
else:
|
||||
# 실패 시 토글 상태 원복 (시그널 재발생 방지)
|
||||
self._kb_btn.blockSignals(True)
|
||||
self._kb_btn.setChecked(False)
|
||||
self._kb_btn.blockSignals(False)
|
||||
QMessageBox.warning(
|
||||
self, "터치 키보드",
|
||||
"터치 키보드를 열 수 없습니다.\n"
|
||||
"Windows 터치 키보드 또는 화상 키보드를 사용할 수 있는지 확인하세요.",
|
||||
)
|
||||
else:
|
||||
self._hide_touch_keyboard()
|
||||
log_action("[설정] 터치 키보드 숨김")
|
||||
|
||||
def _show_touch_keyboard(self) -> bool:
|
||||
"""Windows 터치 키보드(TabTip)를 ITipInvocation COM으로 표시한다."""
|
||||
return show_touch_keyboard()
|
||||
|
||||
def _hide_touch_keyboard(self):
|
||||
"""표시 중인 터치 키보드를 숨긴다."""
|
||||
hide_touch_keyboard()
|
||||
|
||||
def done(self, result: int):
|
||||
# 키보드를 켠 상태에서만 숨김 (Toggle은 반전이라 미사용 시 닫으면 오히려 켜짐)
|
||||
if self._kb_btn.isChecked():
|
||||
self._hide_touch_keyboard()
|
||||
super().done(result)
|
||||
|
||||
@staticmethod
|
||||
def _make_group(title: str):
|
||||
from PyQt5.QtWidgets import QGroupBox
|
||||
@@ -688,6 +823,95 @@ class AdminSettingsDialog(QDialog):
|
||||
self._sp._config.setdefault("db", {}).update(cfg)
|
||||
QMessageBox.information(self, "저장", "DB 설정이 저장되었습니다.")
|
||||
|
||||
# ── MES 제품 탭 슬롯 ─────────────────────────────────────────────── #
|
||||
|
||||
def _get_mes_selected_ids(self) -> list:
|
||||
ids = []
|
||||
for i in range(self._mes_list.count()):
|
||||
item = self._mes_list.item(i)
|
||||
if item.checkState() == Qt.Checked:
|
||||
ids.append(item.data(Qt.UserRole))
|
||||
return ids
|
||||
|
||||
def _update_mes_count(self):
|
||||
total = self._mes_list.count()
|
||||
selected = len(self._get_mes_selected_ids())
|
||||
self._mes_count_lbl.setText(f"선택: {selected} / {total}")
|
||||
|
||||
@staticmethod
|
||||
def _style_mes_item(item):
|
||||
name = item.data(Qt.UserRole + 1) or item.text().lstrip("✓ ").lstrip(" ")
|
||||
item.setData(Qt.UserRole + 1, name)
|
||||
if item.checkState() == Qt.Checked:
|
||||
item.setText(f"✓ {name}")
|
||||
item.setBackground(QColor("#666666"))
|
||||
item.setForeground(QColor("#ffffff"))
|
||||
else:
|
||||
item.setText(f" {name}")
|
||||
item.setBackground(QColor("#1e1e1e"))
|
||||
item.setForeground(QColor("#888888"))
|
||||
|
||||
def _on_mes_item_changed(self, item):
|
||||
self._style_mes_item(item)
|
||||
self._update_mes_count()
|
||||
|
||||
def _on_mes_set_all(self, checked: bool):
|
||||
state = Qt.Checked if checked else Qt.Unchecked
|
||||
self._mes_list.blockSignals(True)
|
||||
for i in range(self._mes_list.count()):
|
||||
item = self._mes_list.item(i)
|
||||
item.setCheckState(state)
|
||||
self._style_mes_item(item)
|
||||
self._mes_list.blockSignals(False)
|
||||
self._update_mes_count()
|
||||
|
||||
def _on_mes_load(self):
|
||||
client = self._sp._db_client
|
||||
if not client or not client.is_connected():
|
||||
QMessageBox.warning(
|
||||
self, "경고",
|
||||
"DB가 연결되어 있지 않습니다.\n"
|
||||
"DB 탭에서 먼저 연결해주세요.",
|
||||
)
|
||||
return
|
||||
|
||||
items = client.get_all_articles()
|
||||
if not items:
|
||||
QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
|
||||
return
|
||||
|
||||
saved_ids = set(self._sp._config.get("mes", {}).get("selected_article_ids", []))
|
||||
|
||||
self._mes_list.blockSignals(True)
|
||||
self._mes_list.clear()
|
||||
for item in items:
|
||||
li = QListWidgetItem(item["article"])
|
||||
li.setSizeHint(QSize(0, 52))
|
||||
li.setFlags(li.flags() | Qt.ItemIsUserCheckable)
|
||||
li.setData(Qt.UserRole, item["article_id"])
|
||||
li.setData(Qt.UserRole + 1, item["article"])
|
||||
li.setToolTip(
|
||||
f"ID: {item['article_id']} | 모델: {item.get('buyer_article_no', '')}"
|
||||
)
|
||||
li.setCheckState(
|
||||
Qt.Checked if item["article_id"] in saved_ids else Qt.Unchecked
|
||||
)
|
||||
self._style_mes_item(li)
|
||||
self._mes_list.addItem(li)
|
||||
self._mes_list.blockSignals(False)
|
||||
self._update_mes_count()
|
||||
log_action(f"[설정] MES 제품 목록 불러오기: {len(items)}개")
|
||||
|
||||
def _on_mes_save(self):
|
||||
selected_ids = self._get_mes_selected_ids()
|
||||
log_action(f"[설정] MES 제품 선택 저장: {len(selected_ids)}개")
|
||||
self._sp._save_config({"mes": {"selected_article_ids": selected_ids}})
|
||||
self._sp._config.setdefault("mes", {})["selected_article_ids"] = selected_ids
|
||||
QMessageBox.information(
|
||||
self, "저장",
|
||||
f"{len(selected_ids)}개 제품이 MES 불러오기 목록에 저장되었습니다.",
|
||||
)
|
||||
|
||||
# ── AI 탭 슬롯 ───────────────────────────────────────────────────── #
|
||||
|
||||
def _on_ai_browse(self):
|
||||
@@ -778,6 +1002,13 @@ class AdminSettingsDialog(QDialog):
|
||||
"ip": self._plc_ip.text().strip(),
|
||||
"port": self._plc_port.value(),
|
||||
},
|
||||
"mes": {
|
||||
"selected_article_ids": (
|
||||
self._get_mes_selected_ids()
|
||||
if self._mes_list.count() > 0
|
||||
else self._sp._config.get("mes", {}).get("selected_article_ids", [])
|
||||
),
|
||||
},
|
||||
}
|
||||
try:
|
||||
self._sp._save_config(data)
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
# 그룹 관리 — A/B 모델 그룹 수동 전환 (최대 4종 per group)
|
||||
# 검사 대상 관리 — DB 미연결 시 PatMax 슬롯 1~16 허용 (레거시 호환)
|
||||
from logic.products import MAX_PATMAX_SLOTS
|
||||
|
||||
|
||||
class GroupManager:
|
||||
MAX_PER_GROUP = 4
|
||||
|
||||
def __init__(self):
|
||||
self._group_a: list = []
|
||||
self._group_b: list = []
|
||||
self._active: str = "A"
|
||||
|
||||
def set_group_a(self, model_list: list):
|
||||
self._group_a = model_list[: self.MAX_PER_GROUP]
|
||||
|
||||
def set_group_b(self, model_list: list):
|
||||
self._group_b = model_list[: self.MAX_PER_GROUP]
|
||||
"""레거시 호환용. 그룹 A/B 전환·선택 제한은 비활성화."""
|
||||
|
||||
def get_active_group(self) -> list:
|
||||
return self._group_a if self._active == "A" else self._group_b
|
||||
return []
|
||||
|
||||
def get_active_name(self) -> str:
|
||||
return self._active
|
||||
return "ALL"
|
||||
|
||||
def get_allowed_ids(self) -> list:
|
||||
return list(range(1, MAX_PATMAX_SLOTS + 1))
|
||||
|
||||
def switch_group(self) -> str:
|
||||
"""A↔B 전환 후 활성 그룹 이름 반환"""
|
||||
self._active = "B" if self._active == "A" else "A"
|
||||
return self._active
|
||||
return "ALL"
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# Cognex 카메라 셀 매핑 (GV 방식 fallback용으로 유지)
|
||||
PATTERN_RESULT_CELLS = {
|
||||
"A27": {"id": 1, "name": "LOW REF", "model": "LX3", "type": "RH"},
|
||||
"A77": {"id": 2, "name": "LOW REF", "model": "LX3", "type": "LH"},
|
||||
"A127": {"id": 3, "name": "LOW REF NAS", "model": "LX3", "type": "RH"},
|
||||
"A177": {"id": 4, "name": "LOW REF NAS", "model": "LX3", "type": "LH"},
|
||||
}
|
||||
from db.sql_client import SQLClient
|
||||
from logic.products import model_display_label
|
||||
|
||||
|
||||
class Inspector:
|
||||
|
||||
def __init__(self):
|
||||
self._pattern_cells: dict = {}
|
||||
|
||||
def set_pattern_cells(self, cells: dict):
|
||||
"""DB 기반 PatMax 셀 매핑 (refresh_wk_results 시 갱신)."""
|
||||
self._pattern_cells = cells or {}
|
||||
|
||||
# ── Python PatMax 매칭 (주 경로) ─────────────────────────────────── #
|
||||
|
||||
def match_image(self, image_bytes: bytes, matcher: "PatternMatcher") -> dict:
|
||||
@@ -47,9 +49,9 @@ class Inspector:
|
||||
# ── Cognex GV 셀 방식 (fallback) ────────────────────────────────── #
|
||||
|
||||
def read_patmax_results(self, insight) -> dict:
|
||||
"""A27/A77/A127/A177 셀 조회 → #ERR이면 실패, 그 외 점수 파싱."""
|
||||
"""PatMax 결과 셀 조회 → #ERR이면 실패, 그 외 점수 파싱."""
|
||||
results = {}
|
||||
for cell, model_info in PATTERN_RESULT_CELLS.items():
|
||||
for cell, model_info in self._pattern_cells.items():
|
||||
try:
|
||||
insight._send(f"GV{cell}")
|
||||
code = insight._read_line()
|
||||
@@ -85,8 +87,10 @@ class Inspector:
|
||||
|
||||
# ── 공통: 모델 판별 + 판정 ──────────────────────────────────────── #
|
||||
|
||||
def identify_model(self, results: dict, allowed_model_ids: list) -> dict:
|
||||
"""매칭된 패턴 중 점수가 가장 높은 것을 선택해 허용 모델 여부 판별."""
|
||||
def identify_model(self, results: dict,
|
||||
allowed_model_ids: "list | None" = None,
|
||||
allowed_article_ids: "set | None" = None) -> dict:
|
||||
"""매칭된 패턴 중 점수가 가장 높은 것을 선택해 허용 여부 판별."""
|
||||
matched_patterns = [
|
||||
(cell, info) for cell, info in results.items()
|
||||
if info["matched"]
|
||||
@@ -100,19 +104,26 @@ class Inspector:
|
||||
}
|
||||
|
||||
_best_cell, best_info = max(matched_patterns, key=lambda x: x[1]["score"])
|
||||
model = best_info["model"]
|
||||
in_allowed = model["id"] in allowed_model_ids
|
||||
model = best_info["model"]
|
||||
label = model_display_label(model)
|
||||
|
||||
if allowed_article_ids is not None:
|
||||
in_allowed = (
|
||||
SQLClient._norm_id(model.get("article_id")) in allowed_article_ids
|
||||
)
|
||||
else:
|
||||
in_allowed = model["id"] in (allowed_model_ids or [])
|
||||
|
||||
return {
|
||||
"matched": True,
|
||||
"in_allowed": in_allowed,
|
||||
"model": model,
|
||||
"score": best_info["score"],
|
||||
"matched": True,
|
||||
"in_allowed": in_allowed,
|
||||
"model": model,
|
||||
"score": best_info["score"],
|
||||
"cognex_pass": in_allowed,
|
||||
"status": (
|
||||
f"{model['name']} {model['model']} {model['type']} ({best_info['score']:.1f}점)"
|
||||
f"{label} ({best_info['score']:.1f}점)"
|
||||
if in_allowed
|
||||
else f"허용 외 모델: {model['name']} {model['model']} {model['type']}"
|
||||
else f"작업 대상 외: {label}"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
56
logic/products.py
Normal file
56
logic/products.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Cognex PatMax GV 셀 주소 + DB 제품 기반 런타임 매핑
|
||||
|
||||
MAX_PATMAX_SLOTS = 16
|
||||
|
||||
# Cognex job: PatMax 블록마다 50행 간격, 첫 결과 셀 = A27
|
||||
_PATMAX_FIRST_ROW = 27
|
||||
_PATMAX_ROW_STEP = 50
|
||||
|
||||
|
||||
def patmax_cell_address(slot_index: int) -> str:
|
||||
"""0-based PatMax 슬롯 → GV 셀 주소 (A27, A77, …)."""
|
||||
row = _PATMAX_FIRST_ROW + slot_index * _PATMAX_ROW_STEP
|
||||
return f"A{row}"
|
||||
|
||||
|
||||
def build_patmax_cells(articles: list) -> dict:
|
||||
"""
|
||||
DB 제품 목록(관리자 MES 선택 순서)으로 PatMax 셀 → 제품 정보 매핑 생성.
|
||||
Cognex job 슬롯 순서와 mes.selected_article_ids 순서가 일치해야 함.
|
||||
"""
|
||||
cells = {}
|
||||
for i, article in enumerate(articles[:MAX_PATMAX_SLOTS]):
|
||||
cells[patmax_cell_address(i)] = article_to_model(article, slot_id=i + 1)
|
||||
return cells
|
||||
|
||||
|
||||
def article_to_model(article: dict, slot_id: int) -> dict:
|
||||
"""DB article 행 → PatMax/검사용 model dict."""
|
||||
return {
|
||||
"id": slot_id,
|
||||
"article_id": article["article_id"],
|
||||
"article": article.get("article", ""),
|
||||
"name": article.get("article", ""),
|
||||
"model": article.get("buyer_article_no", "") or "",
|
||||
"type": "",
|
||||
}
|
||||
|
||||
|
||||
def article_label(article: dict) -> str:
|
||||
"""DB article / model dict UI 표시용."""
|
||||
if article.get("article"):
|
||||
return article["article"]
|
||||
if article.get("name"):
|
||||
return article["name"]
|
||||
return str(article.get("article_id", ""))
|
||||
|
||||
|
||||
def model_display_label(model: dict) -> str:
|
||||
"""PatMax 인식 결과 model dict 표시용."""
|
||||
if model.get("article"):
|
||||
return model["article"]
|
||||
parts = [model.get("name", ""), model.get("model", "")]
|
||||
parts = [p for p in parts if p]
|
||||
if model.get("type"):
|
||||
parts.append(model["type"])
|
||||
return " / ".join(parts) if parts else str(model.get("article_id", ""))
|
||||
110
logs/inspect/2026-06-12.csv
Normal file
110
logs/inspect/2026-06-12.csv
Normal file
@@ -0,0 +1,110 @@
|
||||
timestamp,group,result,cognex_pass,basler_pass,detected_models
|
||||
2026-06-12 15:06:10,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:13,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:17,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:21,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:24,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:28,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:31,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:35,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:38,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:42,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:45,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:49,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:53,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:06:56,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:00,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:04,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:07,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:11,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:14,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:18,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:22,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:25,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:29,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:33,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:36,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:40,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:43,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:47,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:07:51,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:24:53,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:24:57,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:00,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:04,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:07,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:11,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:15,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:18,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:22,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:26,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:29,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:33,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:36,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:40,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:44,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:47,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:51,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:55,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:25:58,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:02,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:05,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:09,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:13,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:16,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:20,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:24,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:27,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:31,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:35,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:38,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:42,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:45,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:49,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:53,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:26:56,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:00,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:04,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:07,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:11,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:14,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:18,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:22,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:27:25,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:36:50,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:36:53,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:36:57,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:37:00,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:37:03,A,UNKNOWN,N,Y,
|
||||
2026-06-12 15:37:07,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:20,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:23,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:27,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:30,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:33,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:40,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:43,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:47,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:53,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:09:57,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:10:00,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:10:03,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:10:07,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:10:10,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:10:14,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:50:10,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:50:13,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:53:55,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:12,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:22,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:25,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:29,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:32,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:35,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:39,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:42,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:45,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:49,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:52,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:55,A,UNKNOWN,N,Y,
|
||||
2026-06-12 16:54:59,A,UNKNOWN,N,Y,
|
||||
|
20
logs/inspect/2026-06-15.csv
Normal file
20
logs/inspect/2026-06-15.csv
Normal file
@@ -0,0 +1,20 @@
|
||||
timestamp,group,result,cognex_pass,basler_pass,detected_models
|
||||
2026-06-15 15:36:58,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:02,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:05,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:08,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:12,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:15,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:18,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:22,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:25,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:28,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:32,A,UNKNOWN,N,Y,
|
||||
2026-06-15 15:37:35,A,UNKNOWN,N,Y,
|
||||
2026-06-15 17:20:49,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:20:53,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:20:56,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:20:59,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:21:03,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:21:06,ALL,UNKNOWN,N,Y,
|
||||
2026-06-15 17:21:10,ALL,UNKNOWN,N,Y,
|
||||
|
1777
logs/timing/2026-06-12.csv
Normal file
1777
logs/timing/2026-06-12.csv
Normal file
File diff suppressed because it is too large
Load Diff
385
logs/timing/2026-06-15.csv
Normal file
385
logs/timing/2026-06-15.csv
Normal file
@@ -0,0 +1,385 @@
|
||||
timestamp,seq,event,elapsed_ms,detail
|
||||
15:36:55.351,1,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:36:55.352,1,cognex_trigger_send,2.0,
|
||||
15:36:55.387,1,cognex_trigger_ok,38.4,
|
||||
15:36:56.392,1,cognex_ftp_start,1040.9,
|
||||
15:36:57.153,1,cognex_ftp_done,1805.0,3686454bytes
|
||||
15:36:57.153,1,cognex_patmax_start,1806.5,
|
||||
15:36:57.153,1,cognex_patmax_done,1816.9,
|
||||
15:36:58.685,1,basler_capture_start,3333.7,
|
||||
15:36:58.685,1,basler_capture_done,3336.7,failed
|
||||
15:36:58.685,1,cognex_join_wait,3338.7,
|
||||
15:36:58.685,1,cognex_join_done,3340.6,
|
||||
15:36:58.685,1,cycle_done,3342.5,result=FAIL cognex=FAIL basler=PASS
|
||||
15:36:58.697,2,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:36:58.697,2,cognex_trigger_send,2.4,
|
||||
15:36:58.729,2,cognex_trigger_ok,36.1,
|
||||
15:36:59.736,2,cognex_ftp_start,1038.3,
|
||||
15:37:00.502,2,cognex_ftp_done,1803.7,3686454bytes
|
||||
15:37:00.503,2,cognex_patmax_start,1805.2,
|
||||
15:37:00.503,2,cognex_patmax_done,1816.3,
|
||||
15:37:02.032,2,basler_capture_start,3334.1,
|
||||
15:37:02.035,2,basler_capture_done,3337.6,failed
|
||||
15:37:02.037,2,cognex_join_wait,3339.7,
|
||||
15:37:02.037,2,cognex_join_done,3341.7,
|
||||
15:37:02.037,2,cycle_done,3343.6,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:02.037,3,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:02.037,3,cognex_trigger_send,2.3,
|
||||
15:37:02.068,3,cognex_trigger_ok,36.7,
|
||||
15:37:03.084,3,cognex_ftp_start,1039.2,
|
||||
15:37:03.821,3,cognex_ftp_done,1789.4,3686454bytes
|
||||
15:37:03.821,3,cognex_patmax_start,1790.8,
|
||||
15:37:03.837,3,cognex_patmax_done,1800.6,
|
||||
15:37:05.379,3,basler_capture_start,3333.8,
|
||||
15:37:05.381,3,basler_capture_done,3337.0,failed
|
||||
15:37:05.384,3,cognex_join_wait,3339.1,
|
||||
15:37:05.386,3,cognex_join_done,3341.1,
|
||||
15:37:05.388,3,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:05.391,4,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:05.394,4,cognex_trigger_send,2.3,
|
||||
15:37:05.427,4,cognex_trigger_ok,36.3,
|
||||
15:37:06.431,4,cognex_ftp_start,1038.7,
|
||||
15:37:07.192,4,cognex_ftp_done,1799.9,3686454bytes
|
||||
15:37:07.193,4,cognex_patmax_start,1801.4,
|
||||
15:37:07.204,4,cognex_patmax_done,1812.6,
|
||||
15:37:08.725,4,basler_capture_start,3333.7,
|
||||
15:37:08.727,4,basler_capture_done,3336.9,failed
|
||||
15:37:08.727,4,cognex_join_wait,3339.0,
|
||||
15:37:08.727,4,cognex_join_done,3341.1,
|
||||
15:37:08.727,4,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:08.727,5,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:08.727,5,cognex_trigger_send,2.3,
|
||||
15:37:08.759,5,cognex_trigger_ok,36.0,
|
||||
15:37:09.777,5,cognex_ftp_start,1038.5,
|
||||
15:37:10.503,5,cognex_ftp_done,1776.7,3686454bytes
|
||||
15:37:10.503,5,cognex_patmax_start,1778.2,
|
||||
15:37:10.520,5,cognex_patmax_done,1789.3,
|
||||
15:37:12.072,5,basler_capture_start,3333.4,
|
||||
15:37:12.072,5,basler_capture_done,3338.1,failed
|
||||
15:37:12.072,5,cognex_join_wait,3340.1,
|
||||
15:37:12.072,5,cognex_join_done,3341.9,
|
||||
15:37:12.072,5,cycle_done,3343.9,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:12.084,6,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:12.086,6,cognex_trigger_send,2.3,
|
||||
15:37:12.120,6,cognex_trigger_ok,36.3,
|
||||
15:37:13.124,6,cognex_ftp_start,1038.6,
|
||||
15:37:13.853,6,cognex_ftp_done,1782.4,3686454bytes
|
||||
15:37:13.869,6,cognex_patmax_start,1783.9,
|
||||
15:37:13.869,6,cognex_patmax_done,1795.0,
|
||||
15:37:15.419,6,basler_capture_start,3333.8,
|
||||
15:37:15.423,6,basler_capture_done,3337.2,failed
|
||||
15:37:15.424,6,cognex_join_wait,3339.3,
|
||||
15:37:15.427,6,cognex_join_done,3341.3,
|
||||
15:37:15.429,6,cycle_done,3343.3,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:15.431,7,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:15.431,7,cognex_trigger_send,2.3,
|
||||
15:37:15.462,7,cognex_trigger_ok,36.8,
|
||||
15:37:16.472,7,cognex_ftp_start,1039.1,
|
||||
15:37:17.236,7,cognex_ftp_done,1806.2,3686454bytes
|
||||
15:37:17.236,7,cognex_patmax_start,1807.8,
|
||||
15:37:17.236,7,cognex_patmax_done,1816.9,
|
||||
15:37:18.766,7,basler_capture_start,3333.4,
|
||||
15:37:18.766,7,basler_capture_done,3336.4,failed
|
||||
15:37:18.766,7,cognex_join_wait,3337.7,
|
||||
15:37:18.766,7,cognex_join_done,3339.0,
|
||||
15:37:18.766,7,cycle_done,3340.3,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:18.775,8,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:18.776,8,cognex_trigger_send,1.7,
|
||||
15:37:18.806,8,cognex_trigger_ok,34.6,
|
||||
15:37:19.813,8,cognex_ftp_start,1037.2,
|
||||
15:37:20.569,8,cognex_ftp_done,1798.4,3686454bytes
|
||||
15:37:20.569,8,cognex_patmax_start,1799.9,
|
||||
15:37:20.586,8,cognex_patmax_done,1811.0,
|
||||
15:37:22.109,8,basler_capture_start,3333.9,
|
||||
15:37:22.109,8,basler_capture_done,3337.2,failed
|
||||
15:37:22.109,8,cognex_join_wait,3340.0,
|
||||
15:37:22.109,8,cognex_join_done,3341.9,
|
||||
15:37:22.119,8,cycle_done,3343.8,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:22.122,9,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:22.125,9,cognex_trigger_send,2.3,
|
||||
15:37:22.152,9,cognex_trigger_ok,35.8,
|
||||
15:37:23.161,9,cognex_ftp_start,1038.0,
|
||||
15:37:23.903,9,cognex_ftp_done,1793.8,3686454bytes
|
||||
15:37:23.918,9,cognex_patmax_start,1795.3,
|
||||
15:37:23.919,9,cognex_patmax_done,1806.5,
|
||||
15:37:25.457,9,basler_capture_start,3333.8,
|
||||
15:37:25.457,9,basler_capture_done,3337.3,failed
|
||||
15:37:25.462,9,cognex_join_wait,3339.3,
|
||||
15:37:25.464,9,cognex_join_done,3341.4,
|
||||
15:37:25.466,9,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:25.470,10,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:25.472,10,cognex_trigger_send,2.3,
|
||||
15:37:25.496,10,cognex_trigger_ok,36.5,
|
||||
15:37:26.509,10,cognex_ftp_start,1039.1,
|
||||
15:37:27.253,10,cognex_ftp_done,1786.2,3686454bytes
|
||||
15:37:27.253,10,cognex_patmax_start,1787.7,
|
||||
15:37:27.268,10,cognex_patmax_done,1798.8,
|
||||
15:37:28.804,10,basler_capture_start,3334.1,
|
||||
15:37:28.807,10,basler_capture_done,3337.3,failed
|
||||
15:37:28.807,10,cognex_join_wait,3339.4,
|
||||
15:37:28.807,10,cognex_join_done,3341.4,
|
||||
15:37:28.807,10,cycle_done,3343.4,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:28.807,11,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:28.807,11,cognex_trigger_send,2.2,
|
||||
15:37:28.852,11,cognex_trigger_ok,36.8,
|
||||
15:37:29.856,11,cognex_ftp_start,1039.2,
|
||||
15:37:30.589,11,cognex_ftp_done,1772.6,3686454bytes
|
||||
15:37:30.591,11,cognex_patmax_start,1774.0,
|
||||
15:37:30.602,11,cognex_patmax_done,1785.1,
|
||||
15:37:32.151,11,basler_capture_start,3334.3,
|
||||
15:37:32.152,11,basler_capture_done,3337.5,failed
|
||||
15:37:32.152,11,cognex_join_wait,3339.5,
|
||||
15:37:32.152,11,cognex_join_done,3341.4,
|
||||
15:37:32.152,11,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:32.152,12,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:32.152,12,cognex_trigger_send,2.2,
|
||||
15:37:32.199,12,cognex_trigger_ok,36.8,
|
||||
15:37:33.203,12,cognex_ftp_start,1039.5,
|
||||
15:37:33.941,12,cognex_ftp_done,1777.1,3686454bytes
|
||||
15:37:33.942,12,cognex_patmax_start,1778.6,
|
||||
15:37:33.953,12,cognex_patmax_done,1789.8,
|
||||
15:37:35.497,12,basler_capture_start,3333.7,
|
||||
15:37:35.499,12,basler_capture_done,3336.5,failed
|
||||
15:37:35.499,12,cognex_join_wait,3338.4,
|
||||
15:37:35.499,12,cognex_join_done,3340.7,
|
||||
15:37:35.499,12,cycle_done,3342.8,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:35.510,13,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:35.510,13,cognex_trigger_send,2.3,
|
||||
15:37:35.541,13,cognex_trigger_ok,36.2,
|
||||
15:37:36.549,13,cognex_ftp_start,1038.7,
|
||||
15:37:37.303,13,cognex_ftp_done,1793.3,3686454bytes
|
||||
15:37:37.303,13,cognex_patmax_start,1794.9,
|
||||
15:37:37.303,13,cognex_patmax_done,1806.0,
|
||||
15:37:38.844,13,basler_capture_start,3333.9,
|
||||
15:37:38.844,13,basler_capture_done,3337.3,failed
|
||||
15:37:38.844,13,cognex_join_wait,3339.3,
|
||||
15:37:38.844,13,cognex_join_done,3341.2,
|
||||
15:37:38.853,13,cycle_done,3343.3,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:38.853,14,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:38.853,14,cognex_trigger_send,2.2,
|
||||
15:37:38.886,14,cognex_trigger_ok,35.9,
|
||||
15:37:39.896,14,cognex_ftp_start,1040.6,
|
||||
15:37:40.636,14,cognex_ftp_done,1795.1,3686454bytes
|
||||
15:37:40.652,14,cognex_patmax_start,1796.6,
|
||||
15:37:40.653,14,cognex_patmax_done,1805.6,
|
||||
15:37:42.189,14,basler_capture_start,3333.7,
|
||||
15:37:42.192,14,basler_capture_done,3336.9,failed
|
||||
15:37:42.194,14,cognex_join_wait,3338.4,
|
||||
15:37:42.195,14,cognex_join_done,3339.8,
|
||||
15:37:42.197,14,cycle_done,3341.0,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:42.198,15,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:42.200,15,cognex_trigger_send,1.6,
|
||||
15:37:42.233,15,cognex_trigger_ok,35.0,
|
||||
15:37:43.235,15,cognex_ftp_start,1036.7,
|
||||
15:37:43.970,15,cognex_ftp_done,1785.7,3686454bytes
|
||||
15:37:43.986,15,cognex_patmax_start,1787.2,
|
||||
15:37:43.988,15,cognex_patmax_done,1797.1,
|
||||
15:37:45.532,15,basler_capture_start,3333.4,
|
||||
15:37:45.534,15,basler_capture_done,3336.6,failed
|
||||
15:37:45.537,15,cognex_join_wait,3338.5,
|
||||
15:37:45.538,15,cognex_join_done,3340.5,
|
||||
15:37:45.541,15,cycle_done,3342.5,result=FAIL cognex=FAIL basler=PASS
|
||||
15:37:51.367,16,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:51.369,16,cognex_trigger_send,2.2,
|
||||
15:37:51.403,16,cognex_trigger_ok,36.5,
|
||||
15:37:52.406,16,cognex_ftp_start,1038.7,
|
||||
15:37:53.153,16,cognex_ftp_done,1801.2,3686454bytes
|
||||
15:37:53.169,16,cognex_patmax_start,1802.8,
|
||||
15:37:53.181,16,cognex_patmax_done,1813.9,
|
||||
15:37:54.701,16,basler_capture_start,3333.7,
|
||||
15:37:54.701,16,basler_capture_done,3336.9,failed
|
||||
15:37:54.701,16,cognex_join_wait,3338.9,
|
||||
15:37:54.701,16,cognex_join_done,3340.8,
|
||||
15:37:54.701,16,cycle_done,3342.8,result=PASS cognex=PASS basler=PASS
|
||||
15:37:54.701,17,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:54.714,17,cognex_trigger_send,2.5,
|
||||
15:37:54.736,17,cognex_trigger_ok,36.3,
|
||||
15:37:55.752,17,cognex_ftp_start,1039.4,
|
||||
15:37:56.503,17,cognex_ftp_done,1790.7,3686454bytes
|
||||
15:37:56.503,17,cognex_patmax_start,1792.3,
|
||||
15:37:56.507,17,cognex_patmax_done,1803.6,
|
||||
15:37:58.046,17,basler_capture_start,3333.8,
|
||||
15:37:58.046,17,basler_capture_done,3337.0,failed
|
||||
15:37:58.046,17,cognex_join_wait,3339.1,
|
||||
15:37:58.052,17,cognex_join_done,3341.2,
|
||||
15:37:58.052,17,cycle_done,3343.2,result=PASS cognex=PASS basler=PASS
|
||||
15:37:58.052,18,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:37:58.052,18,cognex_trigger_send,2.4,
|
||||
15:37:58.089,18,cognex_trigger_ok,36.4,
|
||||
15:37:59.096,18,cognex_ftp_start,1038.4,
|
||||
15:37:59.837,18,cognex_ftp_done,1793.0,3686454bytes
|
||||
15:37:59.837,18,cognex_patmax_start,1794.4,
|
||||
15:37:59.853,18,cognex_patmax_done,1805.6,
|
||||
15:38:01.392,18,basler_capture_start,3334.1,
|
||||
15:38:01.394,18,basler_capture_done,3337.4,failed
|
||||
15:38:01.394,18,cognex_join_wait,3339.7,
|
||||
15:38:01.394,18,cognex_join_done,3341.6,
|
||||
15:38:01.394,18,cycle_done,3343.5,result=PASS cognex=PASS basler=PASS
|
||||
15:38:01.394,19,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:01.394,19,cognex_trigger_send,2.4,
|
||||
15:38:01.439,19,cognex_trigger_ok,36.0,
|
||||
15:38:02.442,19,cognex_ftp_start,1038.0,
|
||||
15:38:03.203,19,cognex_ftp_done,1799.0,3686454bytes
|
||||
15:38:03.203,19,cognex_patmax_start,1800.6,
|
||||
15:38:03.203,19,cognex_patmax_done,1811.9,
|
||||
15:38:04.738,19,basler_capture_start,3334.2,
|
||||
15:38:04.738,19,basler_capture_done,3337.3,failed
|
||||
15:38:04.738,19,cognex_join_wait,3339.3,
|
||||
15:38:04.744,19,cognex_join_done,3341.2,
|
||||
15:38:04.744,19,cycle_done,3343.1,result=PASS cognex=PASS basler=PASS
|
||||
15:38:04.744,20,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:04.744,20,cognex_trigger_send,2.4,
|
||||
15:38:04.785,20,cognex_trigger_ok,36.1,
|
||||
15:38:05.788,20,cognex_ftp_start,1038.2,
|
||||
15:38:06.534,20,cognex_ftp_done,1784.6,3686454bytes
|
||||
15:38:06.535,20,cognex_patmax_start,1786.0,
|
||||
15:38:06.545,20,cognex_patmax_done,1795.8,
|
||||
15:38:08.083,20,basler_capture_start,3333.4,
|
||||
15:38:08.085,20,basler_capture_done,3336.5,failed
|
||||
15:38:08.088,20,cognex_join_wait,3338.5,
|
||||
15:38:08.090,20,cognex_join_done,3340.4,
|
||||
15:38:08.092,20,cycle_done,3342.3,result=PASS cognex=PASS basler=PASS
|
||||
15:38:14.311,21,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:14.311,21,cognex_trigger_send,1.9,
|
||||
15:38:14.339,21,cognex_trigger_ok,37.5,
|
||||
15:38:15.351,21,cognex_ftp_start,1040.0,
|
||||
15:38:16.099,21,cognex_ftp_done,1789.0,3686454bytes
|
||||
15:38:16.101,21,cognex_patmax_start,1790.5,
|
||||
15:38:16.113,21,cognex_patmax_done,1802.1,
|
||||
15:38:17.645,21,basler_capture_start,3334.2,
|
||||
15:38:17.645,21,basler_capture_done,3337.4,failed
|
||||
15:38:17.645,21,cognex_join_wait,3338.7,
|
||||
15:38:17.650,21,cognex_join_done,3340.0,
|
||||
15:38:17.650,21,cycle_done,3341.3,result=FAIL cognex=FAIL basler=PASS
|
||||
15:38:17.650,22,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:17.650,22,cognex_trigger_send,1.7,
|
||||
15:38:17.688,22,cognex_trigger_ok,35.2,
|
||||
15:38:18.691,22,cognex_ftp_start,1037.0,
|
||||
15:38:19.451,22,cognex_ftp_done,1797.7,3686454bytes
|
||||
15:38:19.453,22,cognex_patmax_start,1799.3,
|
||||
15:38:19.464,22,cognex_patmax_done,1810.7,
|
||||
15:38:20.987,22,basler_capture_start,3333.5,
|
||||
15:38:20.987,22,basler_capture_done,3336.5,failed
|
||||
15:38:20.987,22,cognex_join_wait,3338.4,
|
||||
15:38:20.987,22,cognex_join_done,3340.3,
|
||||
15:38:20.987,22,cycle_done,3342.1,result=FAIL cognex=FAIL basler=PASS
|
||||
15:38:20.987,23,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:20.987,23,cognex_trigger_send,2.3,
|
||||
15:38:21.034,23,cognex_trigger_ok,36.0,
|
||||
15:38:22.036,23,cognex_ftp_start,1038.3,
|
||||
15:38:22.769,23,cognex_ftp_done,1771.4,3686454bytes
|
||||
15:38:22.769,23,cognex_patmax_start,1772.9,
|
||||
15:38:22.769,23,cognex_patmax_done,1784.0,
|
||||
15:38:24.332,23,basler_capture_start,3333.7,
|
||||
15:38:24.334,23,basler_capture_done,3337.0,failed
|
||||
15:38:24.337,23,cognex_join_wait,3339.0,
|
||||
15:38:24.339,23,cognex_join_done,3340.9,
|
||||
15:38:24.341,23,cycle_done,3342.8,result=FAIL cognex=FAIL basler=PASS
|
||||
15:38:24.343,24,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:38:24.345,24,cognex_trigger_send,2.3,
|
||||
15:38:24.379,24,cognex_trigger_ok,36.5,
|
||||
15:38:25.382,24,cognex_ftp_start,1038.8,
|
||||
15:38:26.140,24,cognex_ftp_done,1797.1,3686454bytes
|
||||
15:38:26.142,24,cognex_patmax_start,1798.8,
|
||||
15:38:26.153,24,cognex_patmax_done,1809.8,
|
||||
15:38:27.677,24,basler_capture_start,3333.4,
|
||||
15:38:27.679,24,basler_capture_done,3336.4,failed
|
||||
15:38:27.681,24,cognex_join_wait,3338.4,
|
||||
15:38:27.681,24,cognex_join_done,3340.3,
|
||||
15:38:27.681,24,cycle_done,3342.2,result=PASS cognex=PASS basler=PASS
|
||||
15:39:54.449,25,cycle_start,0.0,group=A belt_delay=3.33s
|
||||
15:39:54.449,25,cognex_trigger_send,2.0,
|
||||
15:39:54.486,25,cognex_trigger_ok,36.2,
|
||||
15:39:55.489,25,cognex_ftp_start,1039.4,
|
||||
15:39:56.243,25,cognex_ftp_done,1802.6,3686454bytes
|
||||
15:39:56.243,25,cognex_patmax_start,1804.2,
|
||||
15:39:56.258,25,cognex_patmax_done,1816.6,
|
||||
15:39:57.783,25,basler_capture_start,3333.8,
|
||||
15:39:57.786,25,basler_capture_done,3337.0,failed
|
||||
15:39:57.788,25,cognex_join_wait,3339.0,
|
||||
15:39:57.790,25,cognex_join_done,3340.9,
|
||||
15:39:57.790,25,cycle_done,3342.9,result=PASS cognex=PASS basler=PASS
|
||||
17:20:46.605,1,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:20:46.605,1,cognex_trigger_send,1.7,
|
||||
17:20:46.699,1,cognex_trigger_ok,101.9,
|
||||
17:20:47.710,1,cognex_ftp_start,1103.5,
|
||||
17:20:48.828,1,cognex_ftp_done,2223.2,3686454bytes
|
||||
17:20:48.828,1,cognex_patmax_start,2224.8,
|
||||
17:20:48.859,1,cognex_patmax_done,2257.7,
|
||||
17:20:49.940,1,basler_capture_start,3333.7,
|
||||
17:20:49.940,1,basler_capture_done,3337.4,failed
|
||||
17:20:49.940,1,cognex_join_wait,3339.3,
|
||||
17:20:49.940,1,cognex_join_done,3341.2,
|
||||
17:20:49.940,1,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
|
||||
17:20:49.940,2,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:20:49.955,2,cognex_trigger_send,2.3,
|
||||
17:20:50.050,2,cognex_trigger_ok,99.7,
|
||||
17:20:51.055,2,cognex_ftp_start,1101.5,
|
||||
17:20:52.165,2,cognex_ftp_done,2221.0,3686454bytes
|
||||
17:20:52.165,2,cognex_patmax_start,2222.7,
|
||||
17:20:52.196,2,cognex_patmax_done,2255.7,
|
||||
17:20:53.288,2,basler_capture_start,3334.2,
|
||||
17:20:53.288,2,basler_capture_done,3337.9,failed
|
||||
17:20:53.293,2,cognex_join_wait,3339.8,
|
||||
17:20:53.293,2,cognex_join_done,3341.6,
|
||||
17:20:53.293,2,cycle_done,3343.5,result=FAIL cognex=FAIL basler=PASS
|
||||
17:20:53.293,3,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:20:53.293,3,cognex_trigger_send,2.2,
|
||||
17:20:53.386,3,cognex_trigger_ok,98.9,
|
||||
17:20:54.401,3,cognex_ftp_start,1100.8,
|
||||
17:20:55.515,3,cognex_ftp_done,2218.9,3686454bytes
|
||||
17:20:55.515,3,cognex_patmax_start,2220.4,
|
||||
17:20:55.547,3,cognex_patmax_done,2253.3,
|
||||
17:20:56.634,3,basler_capture_start,3334.1,
|
||||
17:20:56.634,3,basler_capture_done,3337.5,failed
|
||||
17:20:56.634,3,cognex_join_wait,3338.8,
|
||||
17:20:56.634,3,cognex_join_done,3340.1,
|
||||
17:20:56.634,3,cycle_done,3341.3,result=FAIL cognex=FAIL basler=PASS
|
||||
17:20:56.643,4,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:20:56.643,4,cognex_trigger_send,1.6,
|
||||
17:20:56.738,4,cognex_trigger_ok,98.6,
|
||||
17:20:57.745,4,cognex_ftp_start,1100.1,
|
||||
17:20:58.788,4,cognex_ftp_done,2158.4,3686454bytes
|
||||
17:20:58.804,4,cognex_patmax_start,2160.0,
|
||||
17:20:58.835,4,cognex_patmax_done,2191.1,
|
||||
17:20:59.978,4,basler_capture_start,3333.8,
|
||||
17:20:59.978,4,basler_capture_done,3337.3,failed
|
||||
17:20:59.978,4,cognex_join_wait,3339.3,
|
||||
17:20:59.978,4,cognex_join_done,3341.2,
|
||||
17:20:59.978,4,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
|
||||
17:20:59.978,5,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:20:59.994,5,cognex_trigger_send,2.3,
|
||||
17:21:00.089,5,cognex_trigger_ok,99.7,
|
||||
17:21:01.093,5,cognex_ftp_start,1101.6,
|
||||
17:21:02.206,5,cognex_ftp_done,2219.2,3686454bytes
|
||||
17:21:02.206,5,cognex_patmax_start,2220.7,
|
||||
17:21:02.237,5,cognex_patmax_done,2254.0,
|
||||
17:21:03.325,5,basler_capture_start,3334.0,
|
||||
17:21:03.325,5,basler_capture_done,3337.6,failed
|
||||
17:21:03.325,5,cognex_join_wait,3340.1,
|
||||
17:21:03.333,5,cognex_join_done,3342.0,
|
||||
17:21:03.333,5,cycle_done,3344.0,result=FAIL cognex=FAIL basler=PASS
|
||||
17:21:03.333,6,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:21:03.333,6,cognex_trigger_send,2.3,
|
||||
17:21:03.427,6,cognex_trigger_ok,99.6,
|
||||
17:21:04.440,6,cognex_ftp_start,1101.3,
|
||||
17:21:05.557,6,cognex_ftp_done,2224.7,3686454bytes
|
||||
17:21:05.557,6,cognex_patmax_start,2226.3,
|
||||
17:21:05.588,6,cognex_patmax_done,2259.0,
|
||||
17:21:06.673,6,basler_capture_start,3334.1,
|
||||
17:21:06.673,6,basler_capture_done,3337.4,failed
|
||||
17:21:06.673,6,cognex_join_wait,3339.4,
|
||||
17:21:06.673,6,cognex_join_done,3341.4,
|
||||
17:21:06.673,6,cycle_done,3343.4,result=FAIL cognex=FAIL basler=PASS
|
||||
17:21:06.684,7,cycle_start,0.0,group=ALL belt_delay=3.33s
|
||||
17:21:06.684,7,cognex_trigger_send,2.4,
|
||||
17:21:06.778,7,cognex_trigger_ok,99.3,
|
||||
17:21:07.787,7,cognex_ftp_start,1101.1,
|
||||
17:21:08.908,7,cognex_ftp_done,2223.3,3686454bytes
|
||||
17:21:08.908,7,cognex_patmax_start,2224.7,
|
||||
17:21:08.940,7,cognex_patmax_done,2257.9,
|
||||
17:21:10.020,7,basler_capture_start,3333.8,
|
||||
17:21:10.020,7,basler_capture_done,3336.9,failed
|
||||
17:21:10.020,7,cognex_join_wait,3338.9,
|
||||
17:21:10.020,7,cognex_join_done,3340.8,
|
||||
17:21:10.020,7,cycle_done,3342.7,result=FAIL cognex=FAIL basler=PASS
|
||||
|
11
main.py
11
main.py
@@ -119,6 +119,17 @@ QLineEdit, QSpinBox, QDoubleSpinBox {
|
||||
padding: 6px 8px;
|
||||
min-height: 38px;
|
||||
}
|
||||
/* 터치 화면에서 누르기 쉽도록 스핀박스 ▲▼ 버튼 폭 확대 */
|
||||
QSpinBox::up-button, QDoubleSpinBox::up-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: top right;
|
||||
width: 30px;
|
||||
}
|
||||
QSpinBox::down-button, QDoubleSpinBox::down-button {
|
||||
subcontrol-origin: border;
|
||||
subcontrol-position: bottom right;
|
||||
width: 30px;
|
||||
}
|
||||
QLabel { color: #ffffff; }
|
||||
QListWidget {
|
||||
background-color: #222222;
|
||||
|
||||
372
plc_test_gui.py
372
plc_test_gui.py
@@ -1,372 +0,0 @@
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QTextEdit, QFrame,
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
|
||||
from pymelsec import Type3E
|
||||
from pymelsec.constants import DT
|
||||
|
||||
PLC_IP = "192.168.3.39"
|
||||
PLC_PORT = 5010
|
||||
|
||||
# ── 스타일 상수 ─────────────────────────────────────────────────────────── #
|
||||
_PANEL = (
|
||||
"QFrame {"
|
||||
" background:#222222; border:1px solid #333333; border-radius:6px;"
|
||||
"}"
|
||||
)
|
||||
_BTN_GREEN = (
|
||||
"QPushButton {"
|
||||
" background:#1D9E75; color:#ffffff; border:none; border-radius:4px;"
|
||||
" min-height:38px; font-size:13px;"
|
||||
"}"
|
||||
"QPushButton:hover { background:#20b585; }"
|
||||
"QPushButton:disabled { background:#145f48; color:#5a9e7e; }"
|
||||
)
|
||||
_BTN_RED = (
|
||||
"QPushButton {"
|
||||
" background:#8B2020; color:#ffffff; border:none; border-radius:4px;"
|
||||
" min-height:38px; font-size:13px;"
|
||||
"}"
|
||||
"QPushButton:hover { background:#a02828; }"
|
||||
"QPushButton:disabled { background:#4a1515; color:#9e5a5a; }"
|
||||
)
|
||||
_BTN_GRAY = (
|
||||
"QPushButton {"
|
||||
" background:#333333; color:#aaaaaa; border:none; border-radius:4px;"
|
||||
" min-height:38px; font-size:13px;"
|
||||
"}"
|
||||
"QPushButton:hover { background:#444444; color:#ffffff; }"
|
||||
"QPushButton:disabled { background:#2a2a2a; color:#555555; }"
|
||||
)
|
||||
_BTN_RED_OUTLINE = (
|
||||
"QPushButton {"
|
||||
" background:#3D1515; color:#F09595; border:none; border-radius:4px;"
|
||||
" min-height:34px; font-size:13px;"
|
||||
"}"
|
||||
"QPushButton:hover { background:#4a1818; }"
|
||||
"QPushButton:disabled { background:#222222; color:#555555; }"
|
||||
)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════ #
|
||||
# PLCMonitor — D500 폴링 스레드
|
||||
# ══════════════════════════════════════════════════════════════════════════ #
|
||||
|
||||
class PLCMonitor(QThread):
|
||||
signal_received = pyqtSignal(int)
|
||||
error_occurred = pyqtSignal(str)
|
||||
|
||||
def __init__(self, plc: Type3E):
|
||||
super().__init__()
|
||||
self.plc = plc
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
while self.running:
|
||||
try:
|
||||
result = self.plc.batch_read(
|
||||
ref_device="D500",
|
||||
read_size=1,
|
||||
data_type=DT.SWORD,
|
||||
)
|
||||
raw = result[0]
|
||||
value = int(raw.value if hasattr(raw, "value") else raw)
|
||||
self.signal_received.emit(value)
|
||||
except Exception as e:
|
||||
self.error_occurred.emit(str(e))
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
self.wait(2000)
|
||||
if self.isRunning():
|
||||
self.terminate()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════ #
|
||||
# PLCTestGUI
|
||||
# ══════════════════════════════════════════════════════════════════════════ #
|
||||
|
||||
class PLCTestGUI(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.plc = None
|
||||
self._connected = False
|
||||
self._monitor = None
|
||||
self._last_m100 = -1
|
||||
|
||||
self.setWindowTitle("PLC 신호 테스트")
|
||||
self.setFixedSize(600, 400)
|
||||
self.setStyleSheet("background:#1a1a1a; color:#ffffff; font-size:13px;")
|
||||
|
||||
self._build_ui()
|
||||
self._connect_plc()
|
||||
|
||||
# ── UI 구성 ────────────────────────────────────────────────────────── #
|
||||
|
||||
def _build_ui(self):
|
||||
root = QVBoxLayout(self)
|
||||
root.setContentsMargins(16, 12, 16, 12)
|
||||
root.setSpacing(8)
|
||||
|
||||
root.addWidget(self._build_header())
|
||||
root.addWidget(self._separator())
|
||||
|
||||
center = QHBoxLayout()
|
||||
center.setSpacing(10)
|
||||
center.addWidget(self._build_send_panel(), stretch=1)
|
||||
center.addWidget(self._build_recv_panel(), stretch=1)
|
||||
root.addLayout(center, stretch=1)
|
||||
|
||||
root.addWidget(self._separator())
|
||||
root.addWidget(self._build_log())
|
||||
|
||||
def _build_header(self) -> QWidget:
|
||||
w = QWidget()
|
||||
w.setStyleSheet("background:transparent;")
|
||||
row = QHBoxLayout(w)
|
||||
row.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
lbl_ip = QLabel(f"PLC IP: {PLC_IP} : {PLC_PORT}")
|
||||
lbl_ip.setStyleSheet("color:#888888; font-size:13px;")
|
||||
|
||||
self._dot = QLabel("●")
|
||||
self._dot.setStyleSheet("color:#cc2222; font-size:16px;")
|
||||
|
||||
self._lbl_status = QLabel("연결 안됨")
|
||||
self._lbl_status.setStyleSheet("color:#cc2222; font-size:13px;")
|
||||
|
||||
row.addWidget(lbl_ip)
|
||||
row.addStretch()
|
||||
row.addWidget(self._dot)
|
||||
row.addSpacing(4)
|
||||
row.addWidget(self._lbl_status)
|
||||
return w
|
||||
|
||||
def _build_send_panel(self) -> QFrame:
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet(_PANEL)
|
||||
v = QVBoxLayout(frame)
|
||||
v.setContentsMargins(12, 10, 12, 10)
|
||||
v.setSpacing(6)
|
||||
|
||||
title = QLabel("PC → PLC 신호 전송")
|
||||
title.setStyleSheet(
|
||||
"color:#aaaaaa; font-size:12px; background:transparent; border:none;"
|
||||
)
|
||||
v.addWidget(title)
|
||||
|
||||
self._btn_pass = QPushButton("PASS 신호 전송 (M200 = 1)")
|
||||
self._btn_pass.setStyleSheet(_BTN_GREEN)
|
||||
self._btn_pass.clicked.connect(self._send_pass)
|
||||
|
||||
self._btn_fail = QPushButton("FAIL 신호 전송 (M201 = 1)")
|
||||
self._btn_fail.setStyleSheet(_BTN_RED)
|
||||
self._btn_fail.clicked.connect(self._send_fail)
|
||||
|
||||
self._btn_reset = QPushButton("신호 초기화 (M200 = M201 = D100 = 0)")
|
||||
self._btn_reset.setStyleSheet(_BTN_GRAY)
|
||||
self._btn_reset.clicked.connect(self._send_reset)
|
||||
|
||||
self._send_btns = [self._btn_pass, self._btn_fail, self._btn_reset]
|
||||
v.addWidget(self._btn_pass)
|
||||
v.addWidget(self._btn_fail)
|
||||
v.addWidget(self._btn_reset)
|
||||
v.addStretch()
|
||||
return frame
|
||||
|
||||
def _build_recv_panel(self) -> QFrame:
|
||||
frame = QFrame()
|
||||
frame.setStyleSheet(_PANEL)
|
||||
v = QVBoxLayout(frame)
|
||||
v.setContentsMargins(12, 10, 12, 10)
|
||||
v.setSpacing(6)
|
||||
|
||||
title = QLabel("PLC → PC 신호 수신")
|
||||
title.setStyleSheet(
|
||||
"color:#aaaaaa; font-size:12px; background:transparent; border:none;"
|
||||
)
|
||||
v.addWidget(title)
|
||||
|
||||
self._lbl_d500 = QLabel("● 대기 중")
|
||||
self._lbl_d500.setAlignment(Qt.AlignCenter)
|
||||
self._lbl_d500.setStyleSheet(
|
||||
"color:#555555; font-size:15px; font-weight:normal;"
|
||||
"background:transparent; border:none;"
|
||||
)
|
||||
v.addWidget(self._lbl_d500)
|
||||
|
||||
self._btn_mon_start = QPushButton("신호 감지 시작")
|
||||
self._btn_mon_start.setStyleSheet(_BTN_GREEN.replace("min-height:38px", "min-height:34px"))
|
||||
self._btn_mon_start.clicked.connect(self._start_monitor)
|
||||
|
||||
self._btn_mon_stop = QPushButton("신호 감지 중지")
|
||||
self._btn_mon_stop.setEnabled(False)
|
||||
self._btn_mon_stop.setStyleSheet(_BTN_RED_OUTLINE)
|
||||
self._btn_mon_stop.clicked.connect(self._stop_monitor)
|
||||
|
||||
v.addWidget(self._btn_mon_start)
|
||||
v.addWidget(self._btn_mon_stop)
|
||||
v.addStretch()
|
||||
return frame
|
||||
|
||||
@staticmethod
|
||||
def _separator() -> QFrame:
|
||||
line = QFrame()
|
||||
line.setFrameShape(QFrame.HLine)
|
||||
line.setStyleSheet("border:none; background:#2a2a2a; max-height:1px;")
|
||||
return line
|
||||
|
||||
def _build_log(self) -> QTextEdit:
|
||||
self._log = QTextEdit()
|
||||
self._log.setReadOnly(True)
|
||||
self._log.setFixedHeight(108)
|
||||
self._log.setStyleSheet(
|
||||
"background:#111111; color:#888888;"
|
||||
"border:1px solid #2a2a2a; border-radius:4px;"
|
||||
"font-family: Consolas, monospace; font-size:12px;"
|
||||
)
|
||||
return self._log
|
||||
|
||||
# ── PLC 연결 ───────────────────────────────────────────────────────── #
|
||||
|
||||
def _connect_plc(self):
|
||||
try:
|
||||
self.plc = Type3E(host=PLC_IP, port=PLC_PORT, plc_type="Q")
|
||||
self.plc.connect(PLC_IP, PLC_PORT)
|
||||
self._connected = True
|
||||
self._dot.setStyleSheet("color:#1D9E75; font-size:16px;")
|
||||
self._lbl_status.setStyleSheet("color:#1D9E75; font-size:13px;")
|
||||
self._lbl_status.setText("연결됨")
|
||||
self._log_msg(f"PLC 연결 성공: {PLC_IP}:{PLC_PORT}")
|
||||
except Exception as e:
|
||||
self._connected = False
|
||||
self._log_msg(f"PLC 연결 실패: {e}")
|
||||
for btn in self._send_btns:
|
||||
btn.setEnabled(False)
|
||||
self._btn_mon_start.setEnabled(False)
|
||||
|
||||
# ── 신호 전송 ──────────────────────────────────────────────────────── #
|
||||
|
||||
def _send_pass(self):
|
||||
if not self._connected:
|
||||
return
|
||||
try:
|
||||
self.plc.batch_write(ref_device="M200", values=[1], data_type=DT.BIT)
|
||||
self._log_msg("PC → PLC: M200 = 1 (PASS)")
|
||||
except Exception as e:
|
||||
self._log_msg(f"전송 오류: {e}")
|
||||
|
||||
def _send_fail(self):
|
||||
if not self._connected:
|
||||
return
|
||||
try:
|
||||
self.plc.batch_write(ref_device="M201", values=[1], data_type=DT.BIT)
|
||||
self._log_msg("PC → PLC: M201 = 1 (FAIL)")
|
||||
except Exception as e:
|
||||
self._log_msg(f"전송 오류: {e}")
|
||||
|
||||
def _send_reset(self):
|
||||
if not self._connected:
|
||||
return
|
||||
try:
|
||||
self.plc.batch_write(ref_device="M200", values=[0], data_type=DT.BIT)
|
||||
self.plc.batch_write(ref_device="M201", values=[0], data_type=DT.BIT)
|
||||
self.plc.batch_write(ref_device="D100", values=[0], data_type=DT.SWORD)
|
||||
self._log_msg("PC → PLC: M200=0, M201=0, D100=0 (초기화)")
|
||||
except Exception as e:
|
||||
self._log_msg(f"초기화 오류: {e}")
|
||||
|
||||
# ── 신호 수신 폴링 ─────────────────────────────────────────────────── #
|
||||
|
||||
def _start_monitor(self):
|
||||
if not self._connected or self._monitor:
|
||||
return
|
||||
self._last_m100 = -1
|
||||
self._monitor = PLCMonitor(self.plc)
|
||||
self._monitor.signal_received.connect(self._on_signal)
|
||||
self._monitor.error_occurred.connect(self._on_poll_error)
|
||||
self._monitor.start()
|
||||
self._btn_mon_start.setEnabled(False)
|
||||
self._btn_mon_stop.setEnabled(True)
|
||||
self._log_msg("D500 폴링 시작 (100ms 간격)")
|
||||
|
||||
def _stop_monitor(self):
|
||||
if self._monitor:
|
||||
self._monitor.stop()
|
||||
self._monitor = None
|
||||
self._btn_mon_start.setEnabled(True)
|
||||
self._btn_mon_stop.setEnabled(False)
|
||||
self._lbl_d500.setText("● 대기 중")
|
||||
self._lbl_d500.setStyleSheet(
|
||||
"color:#555555; font-size:15px; font-weight:normal;"
|
||||
"background:transparent; border:none;"
|
||||
)
|
||||
self._log_msg("D500 폴링 중지")
|
||||
|
||||
def _on_signal(self, value: int):
|
||||
prev = self._last_m100
|
||||
self._last_m100 = value
|
||||
|
||||
if value >= 1:
|
||||
self._lbl_d500.setText(f"● 신호 수신! ({value})")
|
||||
self._lbl_d500.setStyleSheet(
|
||||
"color:#1D9E75; font-size:15px; font-weight:bold;"
|
||||
"background:transparent; border:none;"
|
||||
)
|
||||
if prev < 1:
|
||||
self._log_msg(f"PLC → PC: D500 = {value} 수신!")
|
||||
else:
|
||||
self._lbl_d500.setText("● 대기 중")
|
||||
self._lbl_d500.setStyleSheet(
|
||||
"color:#555555; font-size:15px; font-weight:normal;"
|
||||
"background:transparent; border:none;"
|
||||
)
|
||||
if prev >= 1:
|
||||
self._log_msg(f"PLC → PC: D500 = {value} (신호 해제)")
|
||||
|
||||
def _on_poll_error(self, msg: str):
|
||||
self._log_msg(f"폴링 오류: {msg}")
|
||||
|
||||
# ── 로그 ───────────────────────────────────────────────────────────── #
|
||||
|
||||
def _log_msg(self, text: str):
|
||||
ts = datetime.now().strftime("%H:%M:%S")
|
||||
self._log.append(f"[{ts}] {text}")
|
||||
|
||||
# ── 종료 ───────────────────────────────────────────────────────────── #
|
||||
|
||||
def closeEvent(self, event):
|
||||
if self._monitor:
|
||||
self._monitor.stop()
|
||||
if self.plc and self._connected:
|
||||
try:
|
||||
self.plc.close()
|
||||
except Exception:
|
||||
pass
|
||||
event.accept()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyleSheet("""
|
||||
QWidget { background:#1a1a1a; color:#ffffff; font-size:13px; }
|
||||
QScrollBar:vertical {
|
||||
background:#2a2a2a; width:8px; border-radius:4px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background:#444444; border-radius:4px; min-height:20px;
|
||||
}
|
||||
QScrollBar::add-line:vertical,
|
||||
QScrollBar::sub-line:vertical { height:0; }
|
||||
""")
|
||||
window = PLCTestGUI()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
||||
14
test.py
14
test.py
@@ -1,14 +0,0 @@
|
||||
import re
|
||||
from pymelsec import Type3E
|
||||
from pymelsec.constants import DT
|
||||
|
||||
plc = Type3E(host="192.168.3.39", port=5010, plc_type="Q")
|
||||
plc.connect("192.168.3.39", 5010)
|
||||
|
||||
# D500에 값 전송
|
||||
plc.batch_write(ref_device="B5F", values=[1], data_type=DT.BIT)
|
||||
|
||||
re = plc.batch_read(ref_device="B52", read_size=1, data_type=DT.BIT)
|
||||
print(re)
|
||||
|
||||
plc.close()
|
||||
123
utils/touch_keyboard.py
Normal file
123
utils/touch_keyboard.py
Normal file
@@ -0,0 +1,123 @@
|
||||
"""Windows 터치 키보드(TabTip) 표시/숨김 — ITipInvocation COM API 사용."""
|
||||
import ctypes
|
||||
import os
|
||||
import sys
|
||||
from ctypes import wintypes
|
||||
|
||||
_TABTIP_PATHS = (
|
||||
r"C:\Program Files\Common Files\microsoft shared\ink\TabTip.exe",
|
||||
r"C:\Program Files (x86)\Common Files\microsoft shared\ink\TabTip.exe",
|
||||
)
|
||||
|
||||
CLSID_UIHostNoLaunch = (
|
||||
0x4CE576FA,
|
||||
0x83DC,
|
||||
0x4F88,
|
||||
(0x95, 0x1C, 0x9D, 0x07, 0x82, 0xB4, 0xE3, 0x76),
|
||||
)
|
||||
IID_ITipInvocation = (
|
||||
0x37C994E7,
|
||||
0x432B,
|
||||
0x4834,
|
||||
(0xA2, 0xF7, 0xDC, 0xE1, 0xF1, 0x3B, 0x83, 0x4B),
|
||||
)
|
||||
|
||||
REGDB_E_CLASSNOTREG = -2147221164 # 0x80040154
|
||||
SW_SHOW = 5
|
||||
CLSCTX_LOCAL_SERVER = 4
|
||||
|
||||
|
||||
class GUID(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("Data1", wintypes.DWORD),
|
||||
("Data2", wintypes.WORD),
|
||||
("Data3", wintypes.WORD),
|
||||
("Data4", wintypes.BYTE * 8),
|
||||
]
|
||||
|
||||
def __init__(self, guid_tuple):
|
||||
super().__init__()
|
||||
self.Data1 = guid_tuple[0]
|
||||
self.Data2 = guid_tuple[1]
|
||||
self.Data3 = guid_tuple[2]
|
||||
self.Data4[:] = guid_tuple[3]
|
||||
|
||||
|
||||
def _tabtip_path() -> str | None:
|
||||
for path in _TABTIP_PATHS:
|
||||
if os.path.exists(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
|
||||
def _launch_tabtip() -> bool:
|
||||
path = _tabtip_path()
|
||||
if not path:
|
||||
return False
|
||||
ret = ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "open", path, None, None, SW_SHOW,
|
||||
)
|
||||
return ret > 32
|
||||
|
||||
|
||||
def _create_tip_invocation(ole32):
|
||||
clsid = GUID(CLSID_UIHostNoLaunch)
|
||||
iid = GUID(IID_ITipInvocation)
|
||||
obj = ctypes.c_void_p()
|
||||
hr = ole32.CoCreateInstance(
|
||||
ctypes.byref(clsid),
|
||||
None,
|
||||
CLSCTX_LOCAL_SERVER,
|
||||
ctypes.byref(iid),
|
||||
ctypes.byref(obj),
|
||||
)
|
||||
if hr == REGDB_E_CLASSNOTREG:
|
||||
if not _launch_tabtip():
|
||||
return None
|
||||
hr = ole32.CoCreateInstance(
|
||||
ctypes.byref(clsid),
|
||||
None,
|
||||
CLSCTX_LOCAL_SERVER,
|
||||
ctypes.byref(iid),
|
||||
ctypes.byref(obj),
|
||||
)
|
||||
if hr != 0:
|
||||
return None
|
||||
return obj
|
||||
|
||||
|
||||
def _toggle_touch_keyboard() -> bool:
|
||||
ole32 = ctypes.windll.ole32
|
||||
user32 = ctypes.windll.user32
|
||||
|
||||
obj = _create_tip_invocation(ole32)
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
vtable_ptr = ctypes.cast(obj, ctypes.POINTER(ctypes.c_void_p)).contents.value
|
||||
vtable = ctypes.cast(vtable_ptr, ctypes.POINTER(ctypes.c_void_p))
|
||||
toggle_fn = ctypes.WINFUNCTYPE(
|
||||
ctypes.c_long, ctypes.c_void_p, wintypes.HWND,
|
||||
)(vtable[3])
|
||||
|
||||
hr = toggle_fn(obj, user32.GetDesktopWindow())
|
||||
release_fn = ctypes.WINFUNCTYPE(ctypes.c_ulong, ctypes.c_void_p)(vtable[2])
|
||||
release_fn(obj)
|
||||
return hr == 0
|
||||
|
||||
|
||||
def show_touch_keyboard() -> bool:
|
||||
"""터치 키보드를 화면에 표시한다."""
|
||||
if sys.platform != "win32":
|
||||
return False
|
||||
ole32 = ctypes.windll.ole32
|
||||
ole32.CoInitialize(None)
|
||||
try:
|
||||
return _toggle_touch_keyboard()
|
||||
finally:
|
||||
ole32.CoUninitialize()
|
||||
|
||||
|
||||
def hide_touch_keyboard() -> bool:
|
||||
"""표시 중인 터치 키보드를 숨긴다 (Toggle)."""
|
||||
return show_touch_keyboard()
|
||||
Reference in New Issue
Block a user