버전 업그레이드

This commit is contained in:
2026-06-18 13:38:27 +09:00
parent a48a4b5fe5
commit ba33a78fec
37 changed files with 3355 additions and 1165 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
_archive/
*.py[cod]
*$py.class

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

View File

@@ -0,0 +1 @@
2 0.264192 0.312749 0.362445 0.298805

View 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.

View File

@@ -1,2 +0,0 @@
0 0.227074 0.195219 0.401747 0.215139
1 0.853712 0.252988 0.257642 0.330677

View File

@@ -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
1 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
2 1 0.744601 2.04808 2.4784 2.95921 4.2576 4.23808 2.37292 2.74536 0.00581 0.01389 0.5 1 0.00599 0.03554 0.00178 0.00853 2.08106 3.23067 4.5092 4.67253 2.3941 4.0769 0 0 0
3 2 1.17594 2.83517 3.54636 3.25155 4.89413 4.15149 2.76008 2.98379 0.00588 0.01408 0.5 1 0.00622 0.02369 0.00124 0.00948 2.10159 3.18473 4.51392 5.26193 2.40401 4.00145 1.23763e-05 1.23763e-05 1.23763e-05
4 3 1.4434 3.18815 3.2212 2.59377 4.47996 4.62523 3.91873 2.6647 0.01514 0.01351 1 0.01568 0.02689 0.0022 0.01173 2.24221 2.96806 4.37976 5.66861 2.4629 3.96969 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

Binary file not shown.

Binary file not shown.

View File

@@ -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 파일이 없음")

View File

@@ -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"
]
}
}

View File

@@ -1,2 +1 @@
# db 패키지 — MySQLClient 노출
from .mysql_client import MySQLClient
# db 패키지

View File

@@ -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

View File

@@ -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:
"""검사 결과 저장 — 테이블 확정 후 구현."""

View File

@@ -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()

View File

@@ -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;

View File

@@ -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

View File

@@ -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)

View File

@@ -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;"
)

View File

@@ -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))
# ================================================================== #
# 헬퍼

View File

@@ -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;"
"}"
)

View File

@@ -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)

View File

@@ -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"

View File

@@ -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
View 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
View 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,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-06-12 15:06:10 A UNKNOWN N Y
3 2026-06-12 15:06:13 A UNKNOWN N Y
4 2026-06-12 15:06:17 A UNKNOWN N Y
5 2026-06-12 15:06:21 A UNKNOWN N Y
6 2026-06-12 15:06:24 A UNKNOWN N Y
7 2026-06-12 15:06:28 A UNKNOWN N Y
8 2026-06-12 15:06:31 A UNKNOWN N Y
9 2026-06-12 15:06:35 A UNKNOWN N Y
10 2026-06-12 15:06:38 A UNKNOWN N Y
11 2026-06-12 15:06:42 A UNKNOWN N Y
12 2026-06-12 15:06:45 A UNKNOWN N Y
13 2026-06-12 15:06:49 A UNKNOWN N Y
14 2026-06-12 15:06:53 A UNKNOWN N Y
15 2026-06-12 15:06:56 A UNKNOWN N Y
16 2026-06-12 15:07:00 A UNKNOWN N Y
17 2026-06-12 15:07:04 A UNKNOWN N Y
18 2026-06-12 15:07:07 A UNKNOWN N Y
19 2026-06-12 15:07:11 A UNKNOWN N Y
20 2026-06-12 15:07:14 A UNKNOWN N Y
21 2026-06-12 15:07:18 A UNKNOWN N Y
22 2026-06-12 15:07:22 A UNKNOWN N Y
23 2026-06-12 15:07:25 A UNKNOWN N Y
24 2026-06-12 15:07:29 A UNKNOWN N Y
25 2026-06-12 15:07:33 A UNKNOWN N Y
26 2026-06-12 15:07:36 A UNKNOWN N Y
27 2026-06-12 15:07:40 A UNKNOWN N Y
28 2026-06-12 15:07:43 A UNKNOWN N Y
29 2026-06-12 15:07:47 A UNKNOWN N Y
30 2026-06-12 15:07:51 A UNKNOWN N Y
31 2026-06-12 15:24:53 A UNKNOWN N Y
32 2026-06-12 15:24:57 A UNKNOWN N Y
33 2026-06-12 15:25:00 A UNKNOWN N Y
34 2026-06-12 15:25:04 A UNKNOWN N Y
35 2026-06-12 15:25:07 A UNKNOWN N Y
36 2026-06-12 15:25:11 A UNKNOWN N Y
37 2026-06-12 15:25:15 A UNKNOWN N Y
38 2026-06-12 15:25:18 A UNKNOWN N Y
39 2026-06-12 15:25:22 A UNKNOWN N Y
40 2026-06-12 15:25:26 A UNKNOWN N Y
41 2026-06-12 15:25:29 A UNKNOWN N Y
42 2026-06-12 15:25:33 A UNKNOWN N Y
43 2026-06-12 15:25:36 A UNKNOWN N Y
44 2026-06-12 15:25:40 A UNKNOWN N Y
45 2026-06-12 15:25:44 A UNKNOWN N Y
46 2026-06-12 15:25:47 A UNKNOWN N Y
47 2026-06-12 15:25:51 A UNKNOWN N Y
48 2026-06-12 15:25:55 A UNKNOWN N Y
49 2026-06-12 15:25:58 A UNKNOWN N Y
50 2026-06-12 15:26:02 A UNKNOWN N Y
51 2026-06-12 15:26:05 A UNKNOWN N Y
52 2026-06-12 15:26:09 A UNKNOWN N Y
53 2026-06-12 15:26:13 A UNKNOWN N Y
54 2026-06-12 15:26:16 A UNKNOWN N Y
55 2026-06-12 15:26:20 A UNKNOWN N Y
56 2026-06-12 15:26:24 A UNKNOWN N Y
57 2026-06-12 15:26:27 A UNKNOWN N Y
58 2026-06-12 15:26:31 A UNKNOWN N Y
59 2026-06-12 15:26:35 A UNKNOWN N Y
60 2026-06-12 15:26:38 A UNKNOWN N Y
61 2026-06-12 15:26:42 A UNKNOWN N Y
62 2026-06-12 15:26:45 A UNKNOWN N Y
63 2026-06-12 15:26:49 A UNKNOWN N Y
64 2026-06-12 15:26:53 A UNKNOWN N Y
65 2026-06-12 15:26:56 A UNKNOWN N Y
66 2026-06-12 15:27:00 A UNKNOWN N Y
67 2026-06-12 15:27:04 A UNKNOWN N Y
68 2026-06-12 15:27:07 A UNKNOWN N Y
69 2026-06-12 15:27:11 A UNKNOWN N Y
70 2026-06-12 15:27:14 A UNKNOWN N Y
71 2026-06-12 15:27:18 A UNKNOWN N Y
72 2026-06-12 15:27:22 A UNKNOWN N Y
73 2026-06-12 15:27:25 A UNKNOWN N Y
74 2026-06-12 15:36:50 A UNKNOWN N Y
75 2026-06-12 15:36:53 A UNKNOWN N Y
76 2026-06-12 15:36:57 A UNKNOWN N Y
77 2026-06-12 15:37:00 A UNKNOWN N Y
78 2026-06-12 15:37:03 A UNKNOWN N Y
79 2026-06-12 15:37:07 A UNKNOWN N Y
80 2026-06-12 16:09:20 A UNKNOWN N Y
81 2026-06-12 16:09:23 A UNKNOWN N Y
82 2026-06-12 16:09:27 A UNKNOWN N Y
83 2026-06-12 16:09:30 A UNKNOWN N Y
84 2026-06-12 16:09:33 A UNKNOWN N Y
85 2026-06-12 16:09:40 A UNKNOWN N Y
86 2026-06-12 16:09:43 A UNKNOWN N Y
87 2026-06-12 16:09:47 A UNKNOWN N Y
88 2026-06-12 16:09:53 A UNKNOWN N Y
89 2026-06-12 16:09:57 A UNKNOWN N Y
90 2026-06-12 16:10:00 A UNKNOWN N Y
91 2026-06-12 16:10:03 A UNKNOWN N Y
92 2026-06-12 16:10:07 A UNKNOWN N Y
93 2026-06-12 16:10:10 A UNKNOWN N Y
94 2026-06-12 16:10:14 A UNKNOWN N Y
95 2026-06-12 16:50:10 A UNKNOWN N Y
96 2026-06-12 16:50:13 A UNKNOWN N Y
97 2026-06-12 16:53:55 A UNKNOWN N Y
98 2026-06-12 16:54:12 A UNKNOWN N Y
99 2026-06-12 16:54:22 A UNKNOWN N Y
100 2026-06-12 16:54:25 A UNKNOWN N Y
101 2026-06-12 16:54:29 A UNKNOWN N Y
102 2026-06-12 16:54:32 A UNKNOWN N Y
103 2026-06-12 16:54:35 A UNKNOWN N Y
104 2026-06-12 16:54:39 A UNKNOWN N Y
105 2026-06-12 16:54:42 A UNKNOWN N Y
106 2026-06-12 16:54:45 A UNKNOWN N Y
107 2026-06-12 16:54:49 A UNKNOWN N Y
108 2026-06-12 16:54:52 A UNKNOWN N Y
109 2026-06-12 16:54:55 A UNKNOWN N Y
110 2026-06-12 16:54:59 A UNKNOWN N Y

View 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,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-06-15 15:36:58 A UNKNOWN N Y
3 2026-06-15 15:37:02 A UNKNOWN N Y
4 2026-06-15 15:37:05 A UNKNOWN N Y
5 2026-06-15 15:37:08 A UNKNOWN N Y
6 2026-06-15 15:37:12 A UNKNOWN N Y
7 2026-06-15 15:37:15 A UNKNOWN N Y
8 2026-06-15 15:37:18 A UNKNOWN N Y
9 2026-06-15 15:37:22 A UNKNOWN N Y
10 2026-06-15 15:37:25 A UNKNOWN N Y
11 2026-06-15 15:37:28 A UNKNOWN N Y
12 2026-06-15 15:37:32 A UNKNOWN N Y
13 2026-06-15 15:37:35 A UNKNOWN N Y
14 2026-06-15 17:20:49 ALL UNKNOWN N Y
15 2026-06-15 17:20:53 ALL UNKNOWN N Y
16 2026-06-15 17:20:56 ALL UNKNOWN N Y
17 2026-06-15 17:20:59 ALL UNKNOWN N Y
18 2026-06-15 17:21:03 ALL UNKNOWN N Y
19 2026-06-15 17:21:06 ALL UNKNOWN N Y
20 2026-06-15 17:21:10 ALL UNKNOWN N Y

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
View 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
1 timestamp seq event elapsed_ms detail
2 15:36:55.351 1 cycle_start 0.0 group=A belt_delay=3.33s
3 15:36:55.352 1 cognex_trigger_send 2.0
4 15:36:55.387 1 cognex_trigger_ok 38.4
5 15:36:56.392 1 cognex_ftp_start 1040.9
6 15:36:57.153 1 cognex_ftp_done 1805.0 3686454bytes
7 15:36:57.153 1 cognex_patmax_start 1806.5
8 15:36:57.153 1 cognex_patmax_done 1816.9
9 15:36:58.685 1 basler_capture_start 3333.7
10 15:36:58.685 1 basler_capture_done 3336.7 failed
11 15:36:58.685 1 cognex_join_wait 3338.7
12 15:36:58.685 1 cognex_join_done 3340.6
13 15:36:58.685 1 cycle_done 3342.5 result=FAIL cognex=FAIL basler=PASS
14 15:36:58.697 2 cycle_start 0.0 group=A belt_delay=3.33s
15 15:36:58.697 2 cognex_trigger_send 2.4
16 15:36:58.729 2 cognex_trigger_ok 36.1
17 15:36:59.736 2 cognex_ftp_start 1038.3
18 15:37:00.502 2 cognex_ftp_done 1803.7 3686454bytes
19 15:37:00.503 2 cognex_patmax_start 1805.2
20 15:37:00.503 2 cognex_patmax_done 1816.3
21 15:37:02.032 2 basler_capture_start 3334.1
22 15:37:02.035 2 basler_capture_done 3337.6 failed
23 15:37:02.037 2 cognex_join_wait 3339.7
24 15:37:02.037 2 cognex_join_done 3341.7
25 15:37:02.037 2 cycle_done 3343.6 result=FAIL cognex=FAIL basler=PASS
26 15:37:02.037 3 cycle_start 0.0 group=A belt_delay=3.33s
27 15:37:02.037 3 cognex_trigger_send 2.3
28 15:37:02.068 3 cognex_trigger_ok 36.7
29 15:37:03.084 3 cognex_ftp_start 1039.2
30 15:37:03.821 3 cognex_ftp_done 1789.4 3686454bytes
31 15:37:03.821 3 cognex_patmax_start 1790.8
32 15:37:03.837 3 cognex_patmax_done 1800.6
33 15:37:05.379 3 basler_capture_start 3333.8
34 15:37:05.381 3 basler_capture_done 3337.0 failed
35 15:37:05.384 3 cognex_join_wait 3339.1
36 15:37:05.386 3 cognex_join_done 3341.1
37 15:37:05.388 3 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
38 15:37:05.391 4 cycle_start 0.0 group=A belt_delay=3.33s
39 15:37:05.394 4 cognex_trigger_send 2.3
40 15:37:05.427 4 cognex_trigger_ok 36.3
41 15:37:06.431 4 cognex_ftp_start 1038.7
42 15:37:07.192 4 cognex_ftp_done 1799.9 3686454bytes
43 15:37:07.193 4 cognex_patmax_start 1801.4
44 15:37:07.204 4 cognex_patmax_done 1812.6
45 15:37:08.725 4 basler_capture_start 3333.7
46 15:37:08.727 4 basler_capture_done 3336.9 failed
47 15:37:08.727 4 cognex_join_wait 3339.0
48 15:37:08.727 4 cognex_join_done 3341.1
49 15:37:08.727 4 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
50 15:37:08.727 5 cycle_start 0.0 group=A belt_delay=3.33s
51 15:37:08.727 5 cognex_trigger_send 2.3
52 15:37:08.759 5 cognex_trigger_ok 36.0
53 15:37:09.777 5 cognex_ftp_start 1038.5
54 15:37:10.503 5 cognex_ftp_done 1776.7 3686454bytes
55 15:37:10.503 5 cognex_patmax_start 1778.2
56 15:37:10.520 5 cognex_patmax_done 1789.3
57 15:37:12.072 5 basler_capture_start 3333.4
58 15:37:12.072 5 basler_capture_done 3338.1 failed
59 15:37:12.072 5 cognex_join_wait 3340.1
60 15:37:12.072 5 cognex_join_done 3341.9
61 15:37:12.072 5 cycle_done 3343.9 result=FAIL cognex=FAIL basler=PASS
62 15:37:12.084 6 cycle_start 0.0 group=A belt_delay=3.33s
63 15:37:12.086 6 cognex_trigger_send 2.3
64 15:37:12.120 6 cognex_trigger_ok 36.3
65 15:37:13.124 6 cognex_ftp_start 1038.6
66 15:37:13.853 6 cognex_ftp_done 1782.4 3686454bytes
67 15:37:13.869 6 cognex_patmax_start 1783.9
68 15:37:13.869 6 cognex_patmax_done 1795.0
69 15:37:15.419 6 basler_capture_start 3333.8
70 15:37:15.423 6 basler_capture_done 3337.2 failed
71 15:37:15.424 6 cognex_join_wait 3339.3
72 15:37:15.427 6 cognex_join_done 3341.3
73 15:37:15.429 6 cycle_done 3343.3 result=FAIL cognex=FAIL basler=PASS
74 15:37:15.431 7 cycle_start 0.0 group=A belt_delay=3.33s
75 15:37:15.431 7 cognex_trigger_send 2.3
76 15:37:15.462 7 cognex_trigger_ok 36.8
77 15:37:16.472 7 cognex_ftp_start 1039.1
78 15:37:17.236 7 cognex_ftp_done 1806.2 3686454bytes
79 15:37:17.236 7 cognex_patmax_start 1807.8
80 15:37:17.236 7 cognex_patmax_done 1816.9
81 15:37:18.766 7 basler_capture_start 3333.4
82 15:37:18.766 7 basler_capture_done 3336.4 failed
83 15:37:18.766 7 cognex_join_wait 3337.7
84 15:37:18.766 7 cognex_join_done 3339.0
85 15:37:18.766 7 cycle_done 3340.3 result=FAIL cognex=FAIL basler=PASS
86 15:37:18.775 8 cycle_start 0.0 group=A belt_delay=3.33s
87 15:37:18.776 8 cognex_trigger_send 1.7
88 15:37:18.806 8 cognex_trigger_ok 34.6
89 15:37:19.813 8 cognex_ftp_start 1037.2
90 15:37:20.569 8 cognex_ftp_done 1798.4 3686454bytes
91 15:37:20.569 8 cognex_patmax_start 1799.9
92 15:37:20.586 8 cognex_patmax_done 1811.0
93 15:37:22.109 8 basler_capture_start 3333.9
94 15:37:22.109 8 basler_capture_done 3337.2 failed
95 15:37:22.109 8 cognex_join_wait 3340.0
96 15:37:22.109 8 cognex_join_done 3341.9
97 15:37:22.119 8 cycle_done 3343.8 result=FAIL cognex=FAIL basler=PASS
98 15:37:22.122 9 cycle_start 0.0 group=A belt_delay=3.33s
99 15:37:22.125 9 cognex_trigger_send 2.3
100 15:37:22.152 9 cognex_trigger_ok 35.8
101 15:37:23.161 9 cognex_ftp_start 1038.0
102 15:37:23.903 9 cognex_ftp_done 1793.8 3686454bytes
103 15:37:23.918 9 cognex_patmax_start 1795.3
104 15:37:23.919 9 cognex_patmax_done 1806.5
105 15:37:25.457 9 basler_capture_start 3333.8
106 15:37:25.457 9 basler_capture_done 3337.3 failed
107 15:37:25.462 9 cognex_join_wait 3339.3
108 15:37:25.464 9 cognex_join_done 3341.4
109 15:37:25.466 9 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS
110 15:37:25.470 10 cycle_start 0.0 group=A belt_delay=3.33s
111 15:37:25.472 10 cognex_trigger_send 2.3
112 15:37:25.496 10 cognex_trigger_ok 36.5
113 15:37:26.509 10 cognex_ftp_start 1039.1
114 15:37:27.253 10 cognex_ftp_done 1786.2 3686454bytes
115 15:37:27.253 10 cognex_patmax_start 1787.7
116 15:37:27.268 10 cognex_patmax_done 1798.8
117 15:37:28.804 10 basler_capture_start 3334.1
118 15:37:28.807 10 basler_capture_done 3337.3 failed
119 15:37:28.807 10 cognex_join_wait 3339.4
120 15:37:28.807 10 cognex_join_done 3341.4
121 15:37:28.807 10 cycle_done 3343.4 result=FAIL cognex=FAIL basler=PASS
122 15:37:28.807 11 cycle_start 0.0 group=A belt_delay=3.33s
123 15:37:28.807 11 cognex_trigger_send 2.2
124 15:37:28.852 11 cognex_trigger_ok 36.8
125 15:37:29.856 11 cognex_ftp_start 1039.2
126 15:37:30.589 11 cognex_ftp_done 1772.6 3686454bytes
127 15:37:30.591 11 cognex_patmax_start 1774.0
128 15:37:30.602 11 cognex_patmax_done 1785.1
129 15:37:32.151 11 basler_capture_start 3334.3
130 15:37:32.152 11 basler_capture_done 3337.5 failed
131 15:37:32.152 11 cognex_join_wait 3339.5
132 15:37:32.152 11 cognex_join_done 3341.4
133 15:37:32.152 11 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS
134 15:37:32.152 12 cycle_start 0.0 group=A belt_delay=3.33s
135 15:37:32.152 12 cognex_trigger_send 2.2
136 15:37:32.199 12 cognex_trigger_ok 36.8
137 15:37:33.203 12 cognex_ftp_start 1039.5
138 15:37:33.941 12 cognex_ftp_done 1777.1 3686454bytes
139 15:37:33.942 12 cognex_patmax_start 1778.6
140 15:37:33.953 12 cognex_patmax_done 1789.8
141 15:37:35.497 12 basler_capture_start 3333.7
142 15:37:35.499 12 basler_capture_done 3336.5 failed
143 15:37:35.499 12 cognex_join_wait 3338.4
144 15:37:35.499 12 cognex_join_done 3340.7
145 15:37:35.499 12 cycle_done 3342.8 result=FAIL cognex=FAIL basler=PASS
146 15:37:35.510 13 cycle_start 0.0 group=A belt_delay=3.33s
147 15:37:35.510 13 cognex_trigger_send 2.3
148 15:37:35.541 13 cognex_trigger_ok 36.2
149 15:37:36.549 13 cognex_ftp_start 1038.7
150 15:37:37.303 13 cognex_ftp_done 1793.3 3686454bytes
151 15:37:37.303 13 cognex_patmax_start 1794.9
152 15:37:37.303 13 cognex_patmax_done 1806.0
153 15:37:38.844 13 basler_capture_start 3333.9
154 15:37:38.844 13 basler_capture_done 3337.3 failed
155 15:37:38.844 13 cognex_join_wait 3339.3
156 15:37:38.844 13 cognex_join_done 3341.2
157 15:37:38.853 13 cycle_done 3343.3 result=FAIL cognex=FAIL basler=PASS
158 15:37:38.853 14 cycle_start 0.0 group=A belt_delay=3.33s
159 15:37:38.853 14 cognex_trigger_send 2.2
160 15:37:38.886 14 cognex_trigger_ok 35.9
161 15:37:39.896 14 cognex_ftp_start 1040.6
162 15:37:40.636 14 cognex_ftp_done 1795.1 3686454bytes
163 15:37:40.652 14 cognex_patmax_start 1796.6
164 15:37:40.653 14 cognex_patmax_done 1805.6
165 15:37:42.189 14 basler_capture_start 3333.7
166 15:37:42.192 14 basler_capture_done 3336.9 failed
167 15:37:42.194 14 cognex_join_wait 3338.4
168 15:37:42.195 14 cognex_join_done 3339.8
169 15:37:42.197 14 cycle_done 3341.0 result=FAIL cognex=FAIL basler=PASS
170 15:37:42.198 15 cycle_start 0.0 group=A belt_delay=3.33s
171 15:37:42.200 15 cognex_trigger_send 1.6
172 15:37:42.233 15 cognex_trigger_ok 35.0
173 15:37:43.235 15 cognex_ftp_start 1036.7
174 15:37:43.970 15 cognex_ftp_done 1785.7 3686454bytes
175 15:37:43.986 15 cognex_patmax_start 1787.2
176 15:37:43.988 15 cognex_patmax_done 1797.1
177 15:37:45.532 15 basler_capture_start 3333.4
178 15:37:45.534 15 basler_capture_done 3336.6 failed
179 15:37:45.537 15 cognex_join_wait 3338.5
180 15:37:45.538 15 cognex_join_done 3340.5
181 15:37:45.541 15 cycle_done 3342.5 result=FAIL cognex=FAIL basler=PASS
182 15:37:51.367 16 cycle_start 0.0 group=A belt_delay=3.33s
183 15:37:51.369 16 cognex_trigger_send 2.2
184 15:37:51.403 16 cognex_trigger_ok 36.5
185 15:37:52.406 16 cognex_ftp_start 1038.7
186 15:37:53.153 16 cognex_ftp_done 1801.2 3686454bytes
187 15:37:53.169 16 cognex_patmax_start 1802.8
188 15:37:53.181 16 cognex_patmax_done 1813.9
189 15:37:54.701 16 basler_capture_start 3333.7
190 15:37:54.701 16 basler_capture_done 3336.9 failed
191 15:37:54.701 16 cognex_join_wait 3338.9
192 15:37:54.701 16 cognex_join_done 3340.8
193 15:37:54.701 16 cycle_done 3342.8 result=PASS cognex=PASS basler=PASS
194 15:37:54.701 17 cycle_start 0.0 group=A belt_delay=3.33s
195 15:37:54.714 17 cognex_trigger_send 2.5
196 15:37:54.736 17 cognex_trigger_ok 36.3
197 15:37:55.752 17 cognex_ftp_start 1039.4
198 15:37:56.503 17 cognex_ftp_done 1790.7 3686454bytes
199 15:37:56.503 17 cognex_patmax_start 1792.3
200 15:37:56.507 17 cognex_patmax_done 1803.6
201 15:37:58.046 17 basler_capture_start 3333.8
202 15:37:58.046 17 basler_capture_done 3337.0 failed
203 15:37:58.046 17 cognex_join_wait 3339.1
204 15:37:58.052 17 cognex_join_done 3341.2
205 15:37:58.052 17 cycle_done 3343.2 result=PASS cognex=PASS basler=PASS
206 15:37:58.052 18 cycle_start 0.0 group=A belt_delay=3.33s
207 15:37:58.052 18 cognex_trigger_send 2.4
208 15:37:58.089 18 cognex_trigger_ok 36.4
209 15:37:59.096 18 cognex_ftp_start 1038.4
210 15:37:59.837 18 cognex_ftp_done 1793.0 3686454bytes
211 15:37:59.837 18 cognex_patmax_start 1794.4
212 15:37:59.853 18 cognex_patmax_done 1805.6
213 15:38:01.392 18 basler_capture_start 3334.1
214 15:38:01.394 18 basler_capture_done 3337.4 failed
215 15:38:01.394 18 cognex_join_wait 3339.7
216 15:38:01.394 18 cognex_join_done 3341.6
217 15:38:01.394 18 cycle_done 3343.5 result=PASS cognex=PASS basler=PASS
218 15:38:01.394 19 cycle_start 0.0 group=A belt_delay=3.33s
219 15:38:01.394 19 cognex_trigger_send 2.4
220 15:38:01.439 19 cognex_trigger_ok 36.0
221 15:38:02.442 19 cognex_ftp_start 1038.0
222 15:38:03.203 19 cognex_ftp_done 1799.0 3686454bytes
223 15:38:03.203 19 cognex_patmax_start 1800.6
224 15:38:03.203 19 cognex_patmax_done 1811.9
225 15:38:04.738 19 basler_capture_start 3334.2
226 15:38:04.738 19 basler_capture_done 3337.3 failed
227 15:38:04.738 19 cognex_join_wait 3339.3
228 15:38:04.744 19 cognex_join_done 3341.2
229 15:38:04.744 19 cycle_done 3343.1 result=PASS cognex=PASS basler=PASS
230 15:38:04.744 20 cycle_start 0.0 group=A belt_delay=3.33s
231 15:38:04.744 20 cognex_trigger_send 2.4
232 15:38:04.785 20 cognex_trigger_ok 36.1
233 15:38:05.788 20 cognex_ftp_start 1038.2
234 15:38:06.534 20 cognex_ftp_done 1784.6 3686454bytes
235 15:38:06.535 20 cognex_patmax_start 1786.0
236 15:38:06.545 20 cognex_patmax_done 1795.8
237 15:38:08.083 20 basler_capture_start 3333.4
238 15:38:08.085 20 basler_capture_done 3336.5 failed
239 15:38:08.088 20 cognex_join_wait 3338.5
240 15:38:08.090 20 cognex_join_done 3340.4
241 15:38:08.092 20 cycle_done 3342.3 result=PASS cognex=PASS basler=PASS
242 15:38:14.311 21 cycle_start 0.0 group=A belt_delay=3.33s
243 15:38:14.311 21 cognex_trigger_send 1.9
244 15:38:14.339 21 cognex_trigger_ok 37.5
245 15:38:15.351 21 cognex_ftp_start 1040.0
246 15:38:16.099 21 cognex_ftp_done 1789.0 3686454bytes
247 15:38:16.101 21 cognex_patmax_start 1790.5
248 15:38:16.113 21 cognex_patmax_done 1802.1
249 15:38:17.645 21 basler_capture_start 3334.2
250 15:38:17.645 21 basler_capture_done 3337.4 failed
251 15:38:17.645 21 cognex_join_wait 3338.7
252 15:38:17.650 21 cognex_join_done 3340.0
253 15:38:17.650 21 cycle_done 3341.3 result=FAIL cognex=FAIL basler=PASS
254 15:38:17.650 22 cycle_start 0.0 group=A belt_delay=3.33s
255 15:38:17.650 22 cognex_trigger_send 1.7
256 15:38:17.688 22 cognex_trigger_ok 35.2
257 15:38:18.691 22 cognex_ftp_start 1037.0
258 15:38:19.451 22 cognex_ftp_done 1797.7 3686454bytes
259 15:38:19.453 22 cognex_patmax_start 1799.3
260 15:38:19.464 22 cognex_patmax_done 1810.7
261 15:38:20.987 22 basler_capture_start 3333.5
262 15:38:20.987 22 basler_capture_done 3336.5 failed
263 15:38:20.987 22 cognex_join_wait 3338.4
264 15:38:20.987 22 cognex_join_done 3340.3
265 15:38:20.987 22 cycle_done 3342.1 result=FAIL cognex=FAIL basler=PASS
266 15:38:20.987 23 cycle_start 0.0 group=A belt_delay=3.33s
267 15:38:20.987 23 cognex_trigger_send 2.3
268 15:38:21.034 23 cognex_trigger_ok 36.0
269 15:38:22.036 23 cognex_ftp_start 1038.3
270 15:38:22.769 23 cognex_ftp_done 1771.4 3686454bytes
271 15:38:22.769 23 cognex_patmax_start 1772.9
272 15:38:22.769 23 cognex_patmax_done 1784.0
273 15:38:24.332 23 basler_capture_start 3333.7
274 15:38:24.334 23 basler_capture_done 3337.0 failed
275 15:38:24.337 23 cognex_join_wait 3339.0
276 15:38:24.339 23 cognex_join_done 3340.9
277 15:38:24.341 23 cycle_done 3342.8 result=FAIL cognex=FAIL basler=PASS
278 15:38:24.343 24 cycle_start 0.0 group=A belt_delay=3.33s
279 15:38:24.345 24 cognex_trigger_send 2.3
280 15:38:24.379 24 cognex_trigger_ok 36.5
281 15:38:25.382 24 cognex_ftp_start 1038.8
282 15:38:26.140 24 cognex_ftp_done 1797.1 3686454bytes
283 15:38:26.142 24 cognex_patmax_start 1798.8
284 15:38:26.153 24 cognex_patmax_done 1809.8
285 15:38:27.677 24 basler_capture_start 3333.4
286 15:38:27.679 24 basler_capture_done 3336.4 failed
287 15:38:27.681 24 cognex_join_wait 3338.4
288 15:38:27.681 24 cognex_join_done 3340.3
289 15:38:27.681 24 cycle_done 3342.2 result=PASS cognex=PASS basler=PASS
290 15:39:54.449 25 cycle_start 0.0 group=A belt_delay=3.33s
291 15:39:54.449 25 cognex_trigger_send 2.0
292 15:39:54.486 25 cognex_trigger_ok 36.2
293 15:39:55.489 25 cognex_ftp_start 1039.4
294 15:39:56.243 25 cognex_ftp_done 1802.6 3686454bytes
295 15:39:56.243 25 cognex_patmax_start 1804.2
296 15:39:56.258 25 cognex_patmax_done 1816.6
297 15:39:57.783 25 basler_capture_start 3333.8
298 15:39:57.786 25 basler_capture_done 3337.0 failed
299 15:39:57.788 25 cognex_join_wait 3339.0
300 15:39:57.790 25 cognex_join_done 3340.9
301 15:39:57.790 25 cycle_done 3342.9 result=PASS cognex=PASS basler=PASS
302 17:20:46.605 1 cycle_start 0.0 group=ALL belt_delay=3.33s
303 17:20:46.605 1 cognex_trigger_send 1.7
304 17:20:46.699 1 cognex_trigger_ok 101.9
305 17:20:47.710 1 cognex_ftp_start 1103.5
306 17:20:48.828 1 cognex_ftp_done 2223.2 3686454bytes
307 17:20:48.828 1 cognex_patmax_start 2224.8
308 17:20:48.859 1 cognex_patmax_done 2257.7
309 17:20:49.940 1 basler_capture_start 3333.7
310 17:20:49.940 1 basler_capture_done 3337.4 failed
311 17:20:49.940 1 cognex_join_wait 3339.3
312 17:20:49.940 1 cognex_join_done 3341.2
313 17:20:49.940 1 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
314 17:20:49.940 2 cycle_start 0.0 group=ALL belt_delay=3.33s
315 17:20:49.955 2 cognex_trigger_send 2.3
316 17:20:50.050 2 cognex_trigger_ok 99.7
317 17:20:51.055 2 cognex_ftp_start 1101.5
318 17:20:52.165 2 cognex_ftp_done 2221.0 3686454bytes
319 17:20:52.165 2 cognex_patmax_start 2222.7
320 17:20:52.196 2 cognex_patmax_done 2255.7
321 17:20:53.288 2 basler_capture_start 3334.2
322 17:20:53.288 2 basler_capture_done 3337.9 failed
323 17:20:53.293 2 cognex_join_wait 3339.8
324 17:20:53.293 2 cognex_join_done 3341.6
325 17:20:53.293 2 cycle_done 3343.5 result=FAIL cognex=FAIL basler=PASS
326 17:20:53.293 3 cycle_start 0.0 group=ALL belt_delay=3.33s
327 17:20:53.293 3 cognex_trigger_send 2.2
328 17:20:53.386 3 cognex_trigger_ok 98.9
329 17:20:54.401 3 cognex_ftp_start 1100.8
330 17:20:55.515 3 cognex_ftp_done 2218.9 3686454bytes
331 17:20:55.515 3 cognex_patmax_start 2220.4
332 17:20:55.547 3 cognex_patmax_done 2253.3
333 17:20:56.634 3 basler_capture_start 3334.1
334 17:20:56.634 3 basler_capture_done 3337.5 failed
335 17:20:56.634 3 cognex_join_wait 3338.8
336 17:20:56.634 3 cognex_join_done 3340.1
337 17:20:56.634 3 cycle_done 3341.3 result=FAIL cognex=FAIL basler=PASS
338 17:20:56.643 4 cycle_start 0.0 group=ALL belt_delay=3.33s
339 17:20:56.643 4 cognex_trigger_send 1.6
340 17:20:56.738 4 cognex_trigger_ok 98.6
341 17:20:57.745 4 cognex_ftp_start 1100.1
342 17:20:58.788 4 cognex_ftp_done 2158.4 3686454bytes
343 17:20:58.804 4 cognex_patmax_start 2160.0
344 17:20:58.835 4 cognex_patmax_done 2191.1
345 17:20:59.978 4 basler_capture_start 3333.8
346 17:20:59.978 4 basler_capture_done 3337.3 failed
347 17:20:59.978 4 cognex_join_wait 3339.3
348 17:20:59.978 4 cognex_join_done 3341.2
349 17:20:59.978 4 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
350 17:20:59.978 5 cycle_start 0.0 group=ALL belt_delay=3.33s
351 17:20:59.994 5 cognex_trigger_send 2.3
352 17:21:00.089 5 cognex_trigger_ok 99.7
353 17:21:01.093 5 cognex_ftp_start 1101.6
354 17:21:02.206 5 cognex_ftp_done 2219.2 3686454bytes
355 17:21:02.206 5 cognex_patmax_start 2220.7
356 17:21:02.237 5 cognex_patmax_done 2254.0
357 17:21:03.325 5 basler_capture_start 3334.0
358 17:21:03.325 5 basler_capture_done 3337.6 failed
359 17:21:03.325 5 cognex_join_wait 3340.1
360 17:21:03.333 5 cognex_join_done 3342.0
361 17:21:03.333 5 cycle_done 3344.0 result=FAIL cognex=FAIL basler=PASS
362 17:21:03.333 6 cycle_start 0.0 group=ALL belt_delay=3.33s
363 17:21:03.333 6 cognex_trigger_send 2.3
364 17:21:03.427 6 cognex_trigger_ok 99.6
365 17:21:04.440 6 cognex_ftp_start 1101.3
366 17:21:05.557 6 cognex_ftp_done 2224.7 3686454bytes
367 17:21:05.557 6 cognex_patmax_start 2226.3
368 17:21:05.588 6 cognex_patmax_done 2259.0
369 17:21:06.673 6 basler_capture_start 3334.1
370 17:21:06.673 6 basler_capture_done 3337.4 failed
371 17:21:06.673 6 cognex_join_wait 3339.4
372 17:21:06.673 6 cognex_join_done 3341.4
373 17:21:06.673 6 cycle_done 3343.4 result=FAIL cognex=FAIL basler=PASS
374 17:21:06.684 7 cycle_start 0.0 group=ALL belt_delay=3.33s
375 17:21:06.684 7 cognex_trigger_send 2.4
376 17:21:06.778 7 cognex_trigger_ok 99.3
377 17:21:07.787 7 cognex_ftp_start 1101.1
378 17:21:08.908 7 cognex_ftp_done 2223.3 3686454bytes
379 17:21:08.908 7 cognex_patmax_start 2224.7
380 17:21:08.940 7 cognex_patmax_done 2257.9
381 17:21:10.020 7 basler_capture_start 3333.8
382 17:21:10.020 7 basler_capture_done 3336.9 failed
383 17:21:10.020 7 cognex_join_wait 3338.9
384 17:21:10.020 7 cognex_join_done 3340.8
385 17:21:10.020 7 cycle_done 3342.7 result=FAIL cognex=FAIL basler=PASS

11
main.py
View File

@@ -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;

View File

@@ -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
View File

@@ -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
View 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()