feat: 초기 프로젝트 구조 추가

This commit is contained in:
Kim Min Jae
2026-06-10 16:18:41 +09:00
parent 5d985560c5
commit a48a4b5fe5
100 changed files with 10530 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
{
"permissions": {
"allow": [
"Bash(C:/Users/Administrator/AppData/Local/Programs/Python/Python314/python.exe *)",
"Bash(python *)",
"Bash(.venv\\\\Scripts\\\\python.exe *)",
"PowerShell(& \".\\\\.venv\\\\Scripts\\\\python.exe\" -c \"import ast; ast.parse\\(open\\('gui/pages/settings_page.py', encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")",
"PowerShell(& \".\\\\.venv\\\\Scripts\\\\python.exe\" -c \"import ast; ast.parse\\(open\\('logger.py', encoding='utf-8'\\).read\\(\\)\\); print\\('logger OK'\\)\")",
"PowerShell(& \".\\\\.venv\\\\Scripts\\\\python.exe\" -c \"import ast; ast.parse\\(open\\('gui/pages/inspect_page.py', encoding='utf-8'\\).read\\(\\)\\); print\\('inspect_page OK'\\)\")",
"WebSearch",
"PowerShell(& \".\\\\.venv\\\\Scripts\\\\python.exe\" -c \"import ast; ast.parse\\(open\\('gui/pages/register_page.py', encoding='utf-8'\\).read\\(\\)\\); print\\('OK'\\)\")",
"Bash(dir E:\\\\ANT\\\\*.py)",
"PowerShell(E:\\\\ANT\\\\.venv\\\\Scripts\\\\pip install pymelsec 2>&1 | Select-Object -Last 5)",
"Bash(.venv/Scripts/python -c ' *)"
]
}
}

263
PROJECT_CONTEXT.md Normal file
View File

@@ -0,0 +1,263 @@
# ANT 비전 검사 시스템 — 프로젝트 컨텍스트
> 최종 갱신: 2026-05-13
---
## 1. 프로젝트 구조 및 파일 목록
```
E:\ANT\
├── main.py # 엔트리포인트 (스플래시 → 백그라운드 초기화 → MainWindow)
├── logger.py # 로그 시스템 (app/inspect/timing/train CSV)
├── find_cells_trigger.py # Cognex 셀 전체 스캔 유틸 (진단용)
├── config.json # 런타임 설정 저장 (cognex/basler/db/ai/conveyor/plc)
├── camera/
│ ├── insight.py # InSightCamera — Telnet + FTP 영구세션
│ └── basler.py # BaslerCamera — pypylon SDK
├── db/
│ ├── sql_client.py # SQLClient — MS SQL Server (pyodbc, ODBC Driver 18)
│ └── mysql_client.py # MySQLClient — 미사용 레거시 (pymysql)
├── ai/
│ ├── detector.py # Detector — YOLOv8 추론 (ultralytics)
│ └── trainer.py # Trainer + TrainWorker — 재학습 (별도 subprocess)
├── logic/
│ ├── inspector.py # Inspector — PatMax 결과 판독 + 모델 판별 + Pass/Fail
│ ├── group_manager.py # GroupManager — A/B 그룹 수동 전환 (최대 4종)
│ └── pattern_matcher.py # PatternMatcher — ORB 특징점 + 엣지 NCC fallback
├── plc/
│ └── plc_client.py # PLCClient — 인터페이스만 정의 (미구현)
├── gui/
│ ├── main_window.py # MainWindow — 4탭 네비게이션, 상태바
│ ├── splash_screen.py # SplashScreen + InitWorker
│ ├── image_settings_dialog.py # 이미지 설정 다이얼로그
│ └── pages/
│ ├── settings_page.py # 환경설정 탭 (연결카드 + 관리자 설정 다이얼로그)
│ ├── register_page.py # 제품 등록 탭 (MES 불러오기 + 캡처)
│ ├── inspect_page.py # 검사 탭 (파이프라인 워커 + 결과 표시)
│ └── retrain_page.py # 재학습 탭 (YOLOv8 재학습 UI)
├── assets/
│ ├── images/ant_logo.png
│ └── patterns.pkl # PatternMatcher 등록 패턴 저장 파일
└── logs/
├── app/YYYY-MM-DD.log # 전체 앱 로그 (print 가로채기)
├── inspect/YYYY-MM-DD.csv # 검사 결과 CSV
└── timing/YYYY-MM-DD.csv # Cognex/Basler 타이밍 CSV
```
---
## 2. 확정된 셀 주소 (Cognex GV 방식)
`logic/inspector.py` `PATTERN_RESULT_CELLS` 딕셔너리에 확정:
| 셀 주소 | ID | 제품명 | 모델 | Type |
|---------|-----|--------|------|------|
| **A27** | 1 | LOW REF | LX3 | RH |
| **A77** | 2 | LOW REF | LX3 | LH |
| **A127** | 3 | LOW REF NAS | LX3 | RH |
| **A177** | 4 | LOW REF NAS | LX3 | LH |
- 읽기 방식: `GV{셀주소}` 전송 → 응답 `"1"` + 값 라인 수신
- 값 파싱: `"(736.1,742.0) -1.8 = 82.9"` 형식에서 `=` 뒤 점수 추출
- `#ERR` 포함 시 → matched=False, score=0.0
> Cognex 인식 외 추가 제품은 `PatternMatcher` (Python ORB/NCC) 경로로 처리
---
## 3. DB 접속 정보
**드라이버**: `ODBC Driver 18 for SQL Server` (pyodbc)
**연결 문자열 구조** (`db/sql_client.py`):
```
DRIVER={ODBC Driver 18 for SQL Server};
SERVER=<서버주소,포트>;
DATABASE=<DB명>;
UID=<사용자명>;
PWD=<비밀번호>;
TrustServerCertificate=yes;
Encrypt=optional;
```
**설정 UI 예시 서버 주소**: `Wizis.iptime.org,20220`
**사용 뷰/테이블**:
- `vi_AI_mt_Article` — 제품 등록 탭에서 리플렉터 목록 조회
```sql
SELECT ArticleID, Article, BuyerArticleNo
FROM vi_AI_mt_Article
WHERE Article LIKE '%REF%'
ORDER BY ArticleID
```
**설정 저장 위치**: `config.json` → `"db"` 키 (server/database/username/password)
**자동 연결**: 앱 시작 시 `MainWindow._auto_connect_db()` 가 config.json 값으로 자동 시도
---
## 4. 구현 완료된 기능
### 카메라
- [x] Cognex In-Sight — Telnet 로그인 (admin/빈 비밀번호)
- [x] Cognex In-Sight — FTP 영구세션 (NOOP keepalive + 자동 재연결)
- [x] Cognex In-Sight — SE8 소프트웨어 트리거
- [x] Cognex In-Sight — FTP BMP 우선 / JPG fallback 이미지 수신
- [x] Basler USB — pypylon GrabOne 단일 캡처
- [x] Basler USB — ExposureAuto=Continuous 연결
- [x] Basler USB — 연속 grab (start_continuous / grab_latest)
### 검사 로직
- [x] Cognex GV 방식 PatMax 결과 조회 (A27/A77/A127/A177)
- [x] Python PatternMatcher — ORB 특징점 매칭 (Lowe's ratio test 0.75)
- [x] Python PatternMatcher — 엣지 NCC fallback (특징점 10개 미만 시)
- [x] PatternMatcher — ORB+NCC 혼합 결과 병합
- [x] 모델 판별 (최고 점수 패턴 선택 → 허용 그룹 여부 확인)
- [x] Pass/Fail 최종 판정 (cognex_pass AND basler_pass)
- [x] 그룹 A/B 수동 전환 (최대 4종 per 그룹)
### 검사 파이프라인 (InspectWorker)
- [x] Cognex 트리거 → 1초 대기 → FTP 수신 → PatMax 병렬 처리 (서브스레드)
- [x] Basler 캡처 — 트리거 기준 belt_delay 후 촬영 (동시 진행)
- [x] YOLOv8 추론 — 불량 감지 (confidence ≥ 0.5)
- [x] 검사 결과 카운터 — 그룹별 전체/양품/불량/미인식
### AI
- [x] YOLOv8 모델 로드/해제 (지연 로딩 — torch DLL 충돌 방지)
- [x] 4클래스 추론: 스크래치/이물/흑점/변형
- [x] 재학습 — 데이터셋 준비 (80/20 train/val 자동 분할)
- [x] 재학습 — YOLOv8n 기반, 별도 subprocess 격리 (Qt 충돌 방지)
- [x] 재학습 — 에포크별 진행 콜백 → UI ProgressBar 연동
### GUI
- [x] 1920×1080 전체화면, 다크 테마
- [x] ChevronTabButton — breadcrumb 스타일 4탭 (환경설정/제품 등록/검사/재학습)
- [x] 재학습 탭 더블클릭(600ms) → 창 최소화 단축
- [x] 환경설정 탭 — 연결 카드 (코그넥스/Basler/DB/AI)
- [x] 관리자 설정 다이얼로그 — 6탭 (코그넥스/Basler/DB/AI모델/컨베이어/PLC)
- [x] 관리자 인증 — 4자리 PIN 키패드 (기본값: 1234)
- [x] 제품 등록 탭 — MES 불러오기 (DB 연결 시 활성화)
- [x] 제품 등록 탭 — In-Sight 트리거 캡처 + 미리보기
- [x] 검사 탭 — Cognex/Basler 듀얼 영상 표시
- [x] 검사 탭 — Basler 이미지에 불량 BBox 오버레이 (클래스별 색상)
- [x] 상태바 — 코그넥스/Basler/DB 연결 상태 실시간 표시
- [x] 컨베이어 딜레이 설정 — 거리(cm) ÷ 속도(cm/s) 자동 계산
### 로그
- [x] print 가로채기 — 타임스탬프 자동 prefix + 파일 저장
- [x] 세션 시작/종료 헤더 (비정상 종료 감지 포함)
- [x] 검사 결과 CSV (`logs/inspect/`)
- [x] 카메라 타이밍 CSV (`logs/timing/`)
- [x] 사용자 액션 강조 로그
---
## 5. 미구현 / 블로킹 기능
### 블로킹 (핵심 미완성)
| 항목 | 파일 | 상태 |
|------|------|------|
| 검사 결과 DB 저장 | `db/sql_client.py:save_inspection_result()` | TODO — 저장 테이블 미확정 |
| PatternMatcher 등록 UI 연동 | `gui/pages/register_page.py` | 캡처만 구현, `matcher.train()` 호출 없음 |
| PatternMatcher 등록 저장 버튼 | `gui/pages/register_page.py` | 미구현 |
### 미구현 (인터페이스만 정의)
| 항목 | 파일 | 상태 |
|------|------|------|
| PLC 통신 전체 | `plc/plc_client.py` | 통신 방식 미확정 (Modbus TCP / MC프로토콜 / OPC-UA 중 선택 필요) |
| PLC 연결 버튼 | `gui/pages/settings_page.py:_build_tab_plc()` | print만 출력 |
| DB 리플렉터 등록/갱신 | `db/mysql_client.py:save_reflector()` | `pass` |
| 재학습 후 모델 자동 교체 | `gui/pages/retrain_page.py` | 미확인 |
### 부분 구현
| 항목 | 상태 |
|------|------|
| Basler 노출/게인 설정 | UI만 있음, 연결 후 `ExposureAuto=Continuous` 고정 |
| 제품 등록 탭 RH/LH 방향 표시 | DB `type` 필드가 비어있어 화살표 미표시 |
---
## 6. 시스템 흐름 요약
### 앱 시작
```
main.py
└─ SplashScreen + InitWorker (백그라운드)
├─ InSightCamera.connect() → Telnet + FTP 세션
├─ BaslerCamera.connect() → pypylon
├─ config.json 로드
└─ MainWindow(insight, basler, config)
├─ PatternMatcher.load() → assets/patterns.pkl
├─ SQLClient 자동 연결 → config.json db 설정
└─ Detector (모델 미로드 상태)
```
### 검사 1사이클 (InspectWorker.run)
```
[메인 루프]
├─ [Cognex 서브스레드] ─────────────────────────────────────┐
│ SE8 트리거 전송 │
│ → sleep(1.0) │
│ → FTP get_image() (BMP/JPG) │
│ → cognex_image_ready.emit(raw) ← GUI 표시 │
│ → inspector.read_patmax_results() ← GV 셀 조회 │
│ → inspector.match_image() ← Python ORB/NCC │
│ → cognex_out["results"] 저장 │
│ │
├─ [워커 메인] belt_delay 대기 후 │
│ Basler.capture() │
│ → detector.detect() ← YOLOv8 추론 │
│ → basler_image_ready.emit(frame, detections) ← GUI │
│ │
├─ ct.join(timeout=10.0) ← Cognex 서브스레드 완료 대기 ───┘
├─ inspector.identify_model(results, allowed_ids)
│ → 최고 점수 패턴 선택 → 허용 그룹 여부 확인
├─ inspector.judge(cognex_pass, basler_pass)
│ → "PASS" or "FAIL"
└─ result_ready.emit(dict) → UI 카운터/결과 표시 + CSV 기록
```
### 모델 판별 우선순위
1. **Cognex GV 셀** (A27/A77/A127/A177) — job 파일 내 PatMax 결과
2. **Python PatternMatcher** (ORB 특징점 → NCC fallback) — 추가 등록 제품
두 결과를 병합하여 가장 높은 점수의 패턴을 최종 선택.
### 컨베이어 딜레이 계산
```
belt_delay (초) = 카메라 간 거리(cm) ÷ 벨트 속도(cm/s)
기본값: 100cm ÷ 30cm/s = 3.33초
```
### 설정 저장
- 모든 설정은 `config.json` (프로젝트 루트)에 JSON으로 저장
- 앱 재시작 시 자동 로드 및 카메라/DB 자동 연결 시도
---
## 7. 주요 의존성
| 패키지 | 용도 |
|--------|------|
| PyQt5 | GUI 프레임워크 |
| pypylon | Basler 카메라 SDK |
| ultralytics | YOLOv8 추론/학습 |
| opencv-python | 이미지 처리, ORB, NCC |
| pyodbc | MS SQL Server 연결 |
| pymysql | MySQL (레거시, 미사용) |
| numpy | 배열 처리 |
| pyyaml | 학습 데이터셋 yaml 생성 |

3
ai/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# ai 패키지 — Detector, Trainer 노출
from .detector import Detector
from .trainer import Trainer

9
ai/dataset/data.yaml Normal file
View File

@@ -0,0 +1,9 @@
names:
- 스크래치
- 이물
- 흑점
- 변형
nc: 4
path: e:\ANT\ai\dataset
train: images/train
val: images/val

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

View File

@@ -0,0 +1 @@
0 0.233974 0.409524 0.339744 0.590476

View File

@@ -0,0 +1 @@
0 0.318598 0.542781 0.472561 0.754011

BIN
ai/dataset/labels/val.cache Normal file

Binary file not shown.

View File

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

59
ai/detector.py Normal file
View File

@@ -0,0 +1,59 @@
# AI 추론 — YOLOv8 기반 불량(스크래치/이물/흑점/변형) 검출
import os
import numpy as np
from utils.path_helper import BASE_PATH
class Detector:
class_names = ["스크래치", "이물", "흑점", "변형"]
def __init__(self):
self._model = None
self.model_path = None
def load_model(self, model_path: str) -> bool:
if model_path and not os.path.isabs(model_path):
model_path = os.path.join(BASE_PATH, model_path)
try:
from ultralytics import YOLO # 지연 로딩 — 앱 시작 시 torch DLL 오류 방지
self._model = YOLO(model_path)
self.model_path = model_path
print(f"[AI] 모델 로드 성공: {model_path}")
return True
except Exception as e:
print(f"[AI] 모델 로드 실패: {e}")
self._model = None
return False
def is_loaded(self) -> bool:
return self._model is not None
def detect(self, image: np.ndarray) -> list:
"""
image: numpy array (BGR)
반환: [{"class_id": int, "class_name": str,
"confidence": float, "bbox": [x1,y1,x2,y2]}, ...]
"""
if self._model is None:
return []
try:
results = self._model(image, verbose=False)
detections = []
for result in results:
for box in result.boxes:
class_id = int(box.cls[0])
detections.append({
"class_id": class_id,
"class_name": (
self.class_names[class_id]
if class_id < len(self.class_names)
else str(class_id)
),
"confidence": float(box.conf[0]),
"bbox": box.xyxy[0].tolist(),
})
return detections
except Exception as e:
print(f"[AI] 추론 오류: {e}")
return []

BIN
ai/models/best.pt Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

110
ai/runs/train/args.yaml Normal file
View File

@@ -0,0 +1,110 @@
task: detect
mode: train
model: yolov8n.pt
data: e:\ANT\ai\dataset\data.yaml
epochs: 100
time: null
patience: 100
batch: 16
imgsz: 640
save: true
save_period: -1
cache: false
device: null
workers: 0
project: e:\ANT\ai\runs
name: train
exist_ok: true
pretrained: true
optimizer: auto
verbose: true
seed: 0
deterministic: true
single_cls: false
rect: false
cos_lr: false
close_mosaic: 10
resume: false
amp: false
fraction: 1.0
profile: false
freeze: null
multi_scale: 0.0
compile: false
overlap_mask: true
mask_ratio: 4
dropout: 0.0
val: true
split: val
save_json: false
conf: null
iou: 0.7
max_det: 300
half: false
dnn: false
plots: false
end2end: null
source: null
vid_stride: 1
stream_buffer: false
visualize: false
augment: false
agnostic_nms: false
classes: null
retina_masks: false
embed: null
show: false
save_frames: false
save_txt: false
save_conf: false
save_crop: false
show_labels: true
show_conf: true
show_boxes: true
line_width: null
format: torchscript
keras: false
optimize: false
int8: false
dynamic: false
simplify: true
opset: null
workspace: null
nms: false
lr0: 0.01
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3.0
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 7.5
cls: 0.5
cls_pw: 0.0
dfl: 1.5
pose: 12.0
kobj: 1.0
rle: 1.0
angle: 1.0
nbs: 64
hsv_h: 0.015
hsv_s: 0.7
hsv_v: 0.4
degrees: 0.0
translate: 0.1
scale: 0.5
shear: 0.0
perspective: 0.0
flipud: 0.0
fliplr: 0.5
bgr: 0.0
mosaic: 1.0
mixup: 0.0
cutmix: 0.0
copy_paste: 0.0
copy_paste_mode: flip
auto_augment: randaugment
erasing: 0.4
cfg: null
tracker: botsort.yaml
save_dir: E:\ANT\ai\runs\train

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

101
ai/runs/train/results.csv Normal file
View File

@@ -0,0 +1,101 @@
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 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.4784 4.2576 2.37292 0.00581 0.5 0.00599 0.00178 2.08106 4.5092 2.3941 0 0 0
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 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
29 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
30 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
31 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
32 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
33 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
34 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
35 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
36 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
37 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
38 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
39 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
40 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
41 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
42 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
43 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
44 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
45 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
46 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
47 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
48 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
49 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
50 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
51 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
52 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
53 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
54 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
55 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
56 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
57 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
58 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
59 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
60 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
61 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
62 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
63 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
64 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
65 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
66 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
67 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
68 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
69 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
70 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
71 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
72 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
73 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
74 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
75 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
76 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
77 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
78 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
79 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
80 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
81 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
82 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
83 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
84 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
85 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
86 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
87 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
88 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
89 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
90 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
91 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
92 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
93 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
94 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
95 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
96 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
97 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
98 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
99 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
100 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
101 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

BIN
ai/runs/train/results.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Binary file not shown.

269
ai/trainer.py Normal file
View File

@@ -0,0 +1,269 @@
# AI 학습 — YOLOv8 재학습 및 모델 저장
import multiprocessing
import os
import random
import shutil
from utils.path_helper import get_path
import yaml
from PyQt5.QtCore import QThread, pyqtSignal
class Trainer:
def __init__(self):
self.model = None
self.is_training = False
# ------------------------------------------------------------------ #
def prepare_dataset(self, image_folder: str) -> str:
dataset_dir = get_path("ai", "dataset")
if os.path.exists(dataset_dir):
shutil.rmtree(dataset_dir)
for split in ("train", "val"):
os.makedirs(os.path.join(dataset_dir, "images", split), exist_ok=True)
os.makedirs(os.path.join(dataset_dir, "labels", split), exist_ok=True)
pairs = []
for f in os.listdir(image_folder):
if f.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")):
img_path = os.path.join(image_folder, f)
txt_path = os.path.join(
image_folder, os.path.splitext(f)[0] + ".txt"
)
if os.path.exists(txt_path):
pairs.append((img_path, txt_path))
random.shuffle(pairs)
split_idx = int(len(pairs) * 0.8)
train_pairs = pairs[:split_idx]
val_pairs = pairs[split_idx:]
for img, lbl in train_pairs:
shutil.copy(img, os.path.join(dataset_dir, "images", "train"))
shutil.copy(lbl, os.path.join(dataset_dir, "labels", "train"))
for img, lbl in val_pairs:
shutil.copy(img, os.path.join(dataset_dir, "images", "val"))
shutil.copy(lbl, os.path.join(dataset_dir, "labels", "val"))
yaml_content = {
"path": dataset_dir,
"train": "images/train",
"val": "images/val",
"nc": 4,
"names": ["스크래치", "이물", "흑점", "변형"],
}
yaml_path = os.path.join(dataset_dir, "data.yaml")
with open(yaml_path, "w", encoding="utf-8") as fh:
yaml.dump(yaml_content, fh, allow_unicode=True)
return yaml_path
# ------------------------------------------------------------------ #
def train(
self,
image_folder: str,
epochs: int,
batch: int,
save_path: str,
log_callback=None,
progress_callback=None,
):
self.is_training = True
try:
if log_callback:
log_callback("데이터셋 준비 중...")
yaml_path = self.prepare_dataset(image_folder)
if log_callback:
log_callback(f"데이터셋 준비 완료: {yaml_path}")
if log_callback:
log_callback("YOLOv8 모델 로드 중...")
from ultralytics import YOLO # 지연 로딩 — 앱 시작 시 torch DLL 오류 방지
self.model = YOLO("yolov8n.pt")
if log_callback:
log_callback(f"학습 시작 (epoch={epochs}, batch={batch})")
def _on_epoch_end(trainer):
ep = trainer.epoch + 1
try:
loss_val = float(trainer.loss)
loss_str = f"{loss_val:.4f}"
except Exception:
loss_str = "?"
if log_callback:
log_callback(f"Epoch {ep}/{epochs} loss={loss_str}")
if progress_callback:
progress_callback(int(ep / epochs * 100))
self.model.add_callback("on_train_epoch_end", _on_epoch_end)
self.model.train(
data=yaml_path,
epochs=epochs,
batch=batch,
imgsz=640,
project=get_path("ai", "runs"),
name="train",
exist_ok=True,
verbose=True,
workers=0, # disable DataLoader multiprocessing inside subprocess
amp=False, # AMP check also spawns a subprocess on Windows
plots=False, # matplotlib can interfere with Qt event loop
)
# best.pt 복사
best_pt = get_path("ai", "runs", "train", "weights", "best.pt")
if os.path.exists(best_pt):
os.makedirs(os.path.dirname(save_path), exist_ok=True)
shutil.copy(best_pt, save_path)
if log_callback:
log_callback(f"모델 저장 완료: {save_path}")
if progress_callback:
progress_callback(100)
if log_callback:
log_callback("학습 완료!")
except BaseException as e:
import traceback
if log_callback:
try:
log_callback(f"학습 오류: {e}")
log_callback(traceback.format_exc())
except Exception:
pass
finally:
self.is_training = False
# ------------------------------------------------------------------ #
def stop(self):
self.is_training = False
# ====================================================================== #
# Subprocess entry point — defined at module level so it is picklable.
# ultralytics training can call sys.exit() internally; running it in a
# separate process completely isolates the Qt application from that.
# ====================================================================== #
def _train_subprocess_main(queue, image_folder, epochs, batch, save_path):
"""Entry point for the isolated training subprocess."""
try:
trainer = Trainer()
def _log(msg):
try:
queue.put(("log", msg))
except Exception:
pass
def _progress(pct):
try:
queue.put(("progress", int(pct)))
except Exception:
pass
trainer.train(
image_folder=image_folder,
epochs=epochs,
batch=batch,
save_path=save_path,
log_callback=_log,
progress_callback=_progress,
)
queue.put(("done", True))
except BaseException as e:
import traceback
try:
queue.put(("log", f"학습 오류: {e}"))
queue.put(("log", traceback.format_exc()))
except Exception:
pass
try:
queue.put(("done", False))
except Exception:
pass
# ====================================================================== #
class TrainWorker(QThread):
log_signal = pyqtSignal(str)
progress_signal = pyqtSignal(int)
finished_signal = pyqtSignal(bool)
def __init__(self, trainer, image_folder, epochs, batch, save_path):
super().__init__()
self.trainer = trainer
self.image_folder = image_folder
self.epochs = epochs
self.batch = batch
self.save_path = save_path
self._proc = None # training subprocess handle
def run(self):
# Spawn an isolated subprocess so that any sys.exit() call inside
# ultralytics does not reach PyQt5's QThread handler and trigger
# QApplication.exit() in the main process.
ctx = multiprocessing.get_context("spawn")
q = ctx.Queue()
self._proc = ctx.Process(
target=_train_subprocess_main,
args=(q, self.image_folder, self.epochs, self.batch, self.save_path),
daemon=True,
)
self._proc.start()
success = False
while True:
proc_alive = self._proc.is_alive()
try:
msg_type, msg_data = q.get(timeout=0.3)
except Exception:
# queue.Empty — check if subprocess died unexpectedly
if not proc_alive:
# Give one last chance to read remaining messages
while True:
try:
msg_type, msg_data = q.get_nowait()
except Exception:
break
if msg_type == "log":
self.log_signal.emit(str(msg_data))
elif msg_type == "progress":
self.progress_signal.emit(int(msg_data))
elif msg_type == "done":
success = bool(msg_data)
break
continue
if msg_type == "log":
self.log_signal.emit(str(msg_data))
elif msg_type == "progress":
self.progress_signal.emit(int(msg_data))
elif msg_type == "done":
success = bool(msg_data)
break
self._proc.join(timeout=30)
if self._proc.is_alive():
self._proc.terminate()
self._proc.join(timeout=5)
self.finished_signal.emit(success)
def stop_subprocess(self):
"""Call from the main thread to forcefully stop training."""
if self._proc and self._proc.is_alive():
self._proc.terminate()

BIN
assets/images/ant_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
assets/images/product_1.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

BIN
assets/patterns.pkl Normal file

Binary file not shown.

3
assets/products.json Normal file
View File

@@ -0,0 +1,3 @@
{
"products": []
}

31
build.py Normal file
View File

@@ -0,0 +1,31 @@
import PyInstaller.__main__
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
PyInstaller.__main__.run([
"main.py",
"--onefile",
"--windowed",
"--name=reflector_inspector",
"--add-data=config.json;.",
"--add-data=assets;assets",
"--add-data=ai/models;ai/models",
"--hidden-import=PyQt5",
"--hidden-import=PyQt5.QtCore",
"--hidden-import=PyQt5.QtGui",
"--hidden-import=PyQt5.QtWidgets",
"--hidden-import=cv2",
"--hidden-import=numpy",
"--hidden-import=pymysql",
"--hidden-import=ultralytics",
"--hidden-import=pypylon.pylon",
"--hidden-import=ftplib",
"--hidden-import=socket",
"--collect-all=ultralytics",
"--collect-all=pypylon",
"--distpath=E:/ANT/dist",
"--workpath=E:/ANT/build",
"--specpath=E:/ANT",
"--noconfirm",
])

3
camera/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# camera 패키지 — InSightCamera, BaslerCamera 노출
from .insight import InSightCamera
from .basler import BaslerCamera

67
camera/basler.py Normal file
View File

@@ -0,0 +1,67 @@
# Basler pylon SDK를 별도로 설치해야 합니다.
# 설치 방법: https://www.baslerweb.com/en/downloads/software-downloads/
# SDK 설치 후: pip install pypylon
import numpy as np
from pypylon import pylon
class BaslerCamera:
def __init__(self):
self.camera = None
def connect(self):
"""첫 번째 Basler 장치에 연결, ExposureAuto=Continuous"""
try:
self.camera = pylon.InstantCamera(
pylon.TlFactory.GetInstance().CreateFirstDevice()
)
self.camera.Open()
self.camera.ExposureAuto.SetValue("Continuous")
print("[Basler] 연결 성공")
except Exception as e:
print(f"[Basler] 연결 실패: {e}")
self.camera = None
def disconnect(self):
if self.camera and self.camera.IsOpen():
try:
self.camera.Close()
except Exception:
pass
print("[Basler] 연결 종료")
def is_connected(self) -> bool:
return self.camera is not None and self.camera.IsOpen()
def capture(self) -> np.ndarray:
"""단일 프레임 촬영 — GrabOne() 사용 (모든 카메라 모델 호환)"""
if not self.camera:
return None
try:
with self.camera.GrabOne(5000) as result:
if result.GrabSucceeded():
return result.Array.copy()
except Exception as e:
print(f"[Basler] capture 오류: {e}")
return None
def start_continuous(self):
"""GrabStrategy_LatestImageOnly로 연속 grab 시작"""
if self.camera:
self.camera.StartGrabbing(pylon.GrabStrategy_LatestImageOnly)
def grab_latest(self) -> np.ndarray:
"""최신 프레임 반환"""
if not self.camera or not self.camera.IsGrabbing():
return None
try:
with self.camera.RetrieveResult(1000) as result:
if result.GrabSucceeded():
return result.Array.copy()
except Exception as e:
print(f"[Basler] grab_latest 오류: {e}")
return None
def stop_continuous(self):
if self.camera:
self.camera.StopGrabbing()

189
camera/insight.py Normal file
View File

@@ -0,0 +1,189 @@
import socket
import ftplib
import io
import time
def _ts():
return time.perf_counter()
class InSightCamera:
def __init__(self):
self._sock = None
self._buf = b""
self._ip = None
self._ftp: "ftplib.FTP | None" = None # 영구 FTP 세션
# ------------------------------------------------------------------ #
# 연결 / 해제
# ------------------------------------------------------------------ #
def connect(self, ip: str, port: int = 23):
"""Telnet 연결 후 FTP 영구 세션도 함께 수립."""
self._ip = ip
try:
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024)
self._sock.settimeout(5)
self._sock.connect((ip, port))
self._buf = b""
banner = self._read_line()
print(f"[InSight] 배너: {banner!r}")
self._send("admin")
pwd_prompt = self._read_line()
print(f"[InSight] 프롬프트: {pwd_prompt!r}")
self._sock.sendall(b"\r\n")
login_result = self._read_line()
print(f"[InSight] 로그인: {login_result!r}")
print(f"[InSight] Telnet 연결 성공: {ip}:{port}")
except Exception as e:
print(f"[InSight] Telnet 연결 실패: {e}")
self._sock = None
return
# Telnet 성공 후 FTP 영구 연결
self.connect_ftp()
def connect_ftp(self):
"""FTP 영구 세션 수립. 실패해도 나중에 _ensure_ftp()가 재시도."""
if not self._ip:
return
try:
ftp = ftplib.FTP()
ftp.connect(self._ip, 21, timeout=10)
ftp.login("admin", "")
self._ftp = ftp
print("[InSight FTP] 영구 세션 수립")
except Exception as e:
print(f"[InSight FTP] 세션 수립 실패 (자동 재연결 대기): {e}")
self._ftp = None
def disconnect(self):
self._close_ftp()
if self._sock:
try:
self._sock.close()
except Exception:
pass
self._sock = None
print("[InSight] 연결 종료")
def _close_ftp(self):
if self._ftp:
try:
self._ftp.quit()
except Exception:
pass
self._ftp = None
def is_connected(self) -> bool:
return self._sock is not None
# ------------------------------------------------------------------ #
# 트리거 / 이미지
# ------------------------------------------------------------------ #
def software_trigger(self) -> bool:
"""SE8 전송 → 응답이 '1'이면 True"""
t0 = _ts()
self._send("SE8")
resp = self._read_line()
print(f"[InSight][시간] SE8 응답: {resp!r} ({_ts()-t0:.3f}s)")
return resp == "1"
def trigger_and_get_image(self) -> bytes:
"""SE8 트리거 → 1초 대기 → FTP 이미지 수신."""
self.software_trigger()
time.sleep(1.0)
return self.get_image()
def get_image(self) -> bytes:
"""영구 FTP 세션으로 이미지 수신. 끊겼으면 자동 재연결 후 1회 재시도."""
if not self._ip:
return b""
ftp = self._ensure_ftp()
if ftp is None:
return b""
t0 = _ts()
try:
target = self._find_bmp_first(ftp)
print(f"[InSight FTP][시간] 파일 탐색: {_ts()-t0:.3f}s → {target}")
buf = io.BytesIO()
ftp.retrbinary(f"RETR {target}", buf.write)
data = buf.getvalue()
print(f"[InSight FTP] 수신 완료: {len(data)} bytes ({_ts()-t0:.3f}s)")
return data
except Exception as e:
print(f"[InSight FTP] 오류: {e} — 세션 재연결 후 재시도")
self._ftp = None
ftp2 = self._ensure_ftp()
if ftp2 is None:
return b""
try:
target = self._find_bmp_first(ftp2)
buf = io.BytesIO()
ftp2.retrbinary(f"RETR {target}", buf.write)
return buf.getvalue()
except Exception as e2:
print(f"[InSight FTP] 재시도 실패: {e2}")
return b""
def _ensure_ftp(self) -> "ftplib.FTP | None":
"""FTP 세션이 살아있으면 재사용, 끊겼으면 재연결."""
if self._ftp is not None:
try:
self._ftp.voidcmd("NOOP")
return self._ftp
except Exception:
self._ftp = None
self.connect_ftp()
return self._ftp
# ------------------------------------------------------------------ #
# 내부 헬퍼
# ------------------------------------------------------------------ #
def _find_bmp_first(self, ftp: ftplib.FTP) -> str:
"""BMP 우선, 없으면 JPG. 기본값 /image.bmp."""
try:
files = ftp.nlst()
print(f"[InSight FTP] 파일 목록: {files}")
for f in files:
if f.lower().endswith(".bmp"):
return f"/{f}" if not f.startswith("/") else f
for f in files:
if f.lower().endswith(".jpg") or f.lower().endswith(".jpeg"):
return f"/{f}" if not f.startswith("/") else f
except Exception as e:
print(f"[InSight FTP] 파일 탐색 오류: {e}")
return "/image.bmp"
def _send(self, cmd: str):
self._sock.sendall((cmd + "\r\n").encode())
def _read_line(self) -> str:
while b"\r\n" not in self._buf:
try:
chunk = self._sock.recv(4096)
except socket.timeout:
break
if not chunk:
break
self._buf += chunk
if b"\r\n" in self._buf:
idx = self._buf.index(b"\r\n")
line = self._buf[:idx]
self._buf = self._buf[idx + 2:]
else:
line = self._buf
self._buf = b""
return line.decode(errors="ignore").strip()

9
check_build.py Normal file
View File

@@ -0,0 +1,9 @@
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 파일이 없음")

27
config.json Normal file
View File

@@ -0,0 +1,27 @@
{
"cognex": {
"ip": "169.254.0.1",
"port": 23
},
"basler": {
"exposure": 10000,
"gain": 20
},
"conveyor": {
"distance_cm": 100.0,
"speed_cms": 30.0
},
"db": {
"server": "Wizis.iptime.org,20220",
"database": "MES_ANT",
"username": "AIUser",
"password": "AIUser"
},
"plc": {
"ip": "192.168.3.39",
"port": 5010
},
"ai": {
"model_path": "ai/models/best.pt"
}
}

2
db/__init__.py Normal file
View File

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

36
db/mysql_client.py Normal file
View File

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

74
db/sql_client.py Normal file
View File

@@ -0,0 +1,74 @@
import pyodbc
class SQLClient:
def __init__(self):
self.conn = None
self.cursor = None
def connect(self, server: str, database: str,
username: str, password: str) -> bool:
try:
conn_str = (
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
f"SERVER={server};"
f"DATABASE={database};"
f"UID={username};"
f"PWD={password};"
f"TrustServerCertificate=yes;"
f"Encrypt=optional;"
)
self.conn = pyodbc.connect(conn_str, timeout=10)
self.cursor = self.conn.cursor()
print(f"[DB] 연결 성공: {server}/{database}")
return True
except Exception as e:
print(f"[DB] 연결 실패: {e}")
self.conn = None
return False
def disconnect(self):
if self.cursor:
self.cursor.close()
if self.conn:
self.conn.close()
self.conn = None
self.cursor = None
print("[DB] 연결 해제")
def is_connected(self) -> bool:
return self.conn is not None
def get_reflector_list(self) -> list:
"""
vi_AI_mt_Article 뷰에서 리플렉터 제품 목록 조회.
반환: [{"article_id": ..., "article": ..., "buyer_article_no": ...}, ...]
"""
if not self.is_connected():
return []
try:
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
]
except Exception as e:
print(f"[DB] 조회 실패: {e}")
return []
def save_inspection_result(self, article_id: str,
result: str, score: float) -> bool:
"""검사 결과 저장 — 테이블 확정 후 구현."""
# TODO: 결과 저장 테이블 확정 후 쿼리 구현
print(f"[DB] 검사 결과 저장: {article_id} {result} {score}")
return True

203
find_cells_trigger.py Normal file
View File

@@ -0,0 +1,203 @@
"""
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()

File diff suppressed because it is too large Load Diff

1
gui/__init__.py Normal file
View File

@@ -0,0 +1 @@
# gui 패키지

1
gui/dialogs/__init__.py Normal file
View File

@@ -0,0 +1 @@
# gui/dialogs 패키지

View File

@@ -0,0 +1,245 @@
# 이미지 설정 다이얼로그 — In-Sight 2000C Telnet 셀 직접 GV/SV
from PyQt5.QtWidgets import (
QDialog, QFormLayout, QVBoxLayout, QHBoxLayout,
QPushButton, QDoubleSpinBox, QSpinBox, QMessageBox, QLabel,
)
from PyQt5.QtCore import Qt
# (레이블, 셀주소, 위젯종류, min, max, 기본값, 소수점자리)
_PARAMS = [
("노출 (밀리초)", "D3", "double", 0.01, 1000.0, 30.0, 3),
("최대 노출 시간", "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),
]
# 스캔할 셀 범위 (A1~J20)
_SCAN_COLS = list("ABCDEFGHIJ")
_SCAN_ROWS = range(1, 21)
_STYLE = """
QDialog, QWidget {
background: #1a1a1a;
color: #ffffff;
font-size: 14px;
}
QLabel { color: #cccccc; }
QDoubleSpinBox, QSpinBox {
background: #2a2a2a;
color: #ffffff;
border: 1px solid #555555;
border-radius: 4px;
padding: 4px 8px;
min-height: 38px;
}
QDoubleSpinBox::up-button, QDoubleSpinBox::down-button,
QSpinBox::up-button, QSpinBox::down-button {
width: 20px;
}
QPushButton {
background: #2e2e2e;
color: #ffffff;
border: 1px solid #555555;
border-radius: 4px;
min-height: 56px;
padding: 0 24px;
font-size: 15px;
}
QPushButton:hover { background: #3a3a3a; }
QPushButton:pressed { background: #1e1e1e; }
QPushButton#scan {
min-height: 38px;
font-size: 13px;
color: #aaaaaa;
}
"""
class ImageSettingsDialog(QDialog):
def __init__(self, insight_cam, parent=None):
super().__init__(parent)
self._cam = insight_cam
self._widgets = {} # 셀주소 → 위젯
self._originals = {} # 셀주소 → 로드 시 원본값
self.setWindowTitle("이미지 설정 — In-Sight 2000C")
self.setMinimumWidth(440)
self.setWindowFlags(self.windowFlags() & ~Qt.WindowContextHelpButtonHint)
self.setStyleSheet(_STYLE)
self._build_ui()
self._load_values()
# ================================================================== #
# UI 구성
# ================================================================== #
def _build_ui(self):
form = QFormLayout()
form.setLabelAlignment(Qt.AlignRight)
form.setRowWrapPolicy(QFormLayout.DontWrapRows)
form.setHorizontalSpacing(20)
form.setVerticalSpacing(12)
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 if decimals > 0 else 1.0)
else:
w = QSpinBox()
w.setRange(int(lo), int(hi))
w.setValue(int(default))
w.setMinimumWidth(140)
self._widgets[cell] = w
form.addRow(label, w)
btn_scan = QPushButton("쓰기 가능한 셀 자동 탐지…")
btn_scan.setObjectName("scan")
btn_scan.clicked.connect(self._on_scan)
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.addWidget(btn_scan)
btn_row.addStretch()
btn_row.addWidget(btn_ok)
btn_row.addWidget(btn_cancel)
root = QVBoxLayout(self)
root.setContentsMargins(24, 20, 24, 20)
root.setSpacing(16)
root.addLayout(form)
root.addLayout(btn_row)
# ================================================================== #
# 값 로드 — GV{cell}: 응답 "1" + 값
# ================================================================== #
def _load_values(self):
for _, cell, kind, _, _, _, _ in _PARAMS:
val = self._gv(cell)
print(f"[설정창] GV{cell}{val!r}")
w = self._widgets[cell]
if val is not None:
w.setValue(val if kind == "double" else int(round(val)))
self._originals[cell] = val
else:
self._originals[cell] = None # 조회 실패 → 기본값 유지
def _gv(self, cell: str):
"""GV{cell} 전송 → float 반환, 실패 시 None"""
try:
self._cam._send(f"GV{cell}")
status = self._cam._read_line()
if status != "1":
return None
return float(self._cam._read_line())
except Exception:
return None
# ================================================================== #
# 셀 자동 탐지 — GV로 읽히고 SV로 쓸 수 있는 셀 목록
# ================================================================== #
def _on_scan(self):
"""A1~J20 범위를 스캔해 Online(쓰기 가능) 셀을 찾아 표시."""
readable = [] # [(cell, value)]
writable = [] # [(cell, value)]
for col in _SCAN_COLS:
for row in _SCAN_ROWS:
cell = f"{col}{row}"
val = self._gv(cell)
if val is None:
continue
readable.append((cell, val))
ok, _ = self._sv(cell, val, "double") # 같은 값 재기입 → 실질적 변경 없음
if ok:
writable.append((cell, val))
if writable:
lines = "\n".join(f" {c} = {v}" for c, v in writable)
msg = (
f"쓰기 가능한 셀 {len(writable)}개 발견:\n\n"
f"{lines}\n\n"
f"image_settings_dialog.py 상단 _PARAMS의\n"
f"셀 주소를 위 주소로 변경하세요."
)
elif readable:
lines = "\n".join(f" {c} = {v}" for c, v in readable[:20])
msg = (
f"읽기 전용 셀 {len(readable)}개 발견 (쓰기 가능 셀 없음):\n\n"
f"{lines}\n\n"
f"─────────────────────────────\n"
f"Cognex In-Sight Explorer에서\n"
f"제어하려는 셀을 우클릭 →\n"
f"[Cell Properties] → [Online] 체크 후\n"
f"job을 카메라에 업로드하세요.\n\n"
f"이후 이 버튼을 다시 누르면\n"
f"쓰기 가능한 셀 주소를 알 수 있습니다."
)
else:
msg = (
f"A1~J20 범위에서 읽을 수 있는 셀이 없습니다.\n\n"
f"카메라 연결 상태 또는 job 파일을 확인하세요."
)
QMessageBox.information(self, "셀 탐지 결과", msg)
# ================================================================== #
# 확인 버튼 — 변경된 항목만 SV{cell} {value}
# ================================================================== #
def _on_ok(self):
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
errors = []
for cell, value, kind in changes:
ok, resp = self._sv(cell, value, kind)
if not ok:
errors.append((cell, resp))
if errors:
detail = "\n".join(f"{c} (카메라 응답: {r!r})" for c, r in errors)
QMessageBox.critical(
self, "설정 실패",
f"다음 셀 설정에 실패했습니다:\n{detail}\n\n"
f"'쓰기 가능한 셀 자동 탐지' 버튼으로\n"
f"현재 Online 셀 주소를 확인하세요.",
)
return
self.accept()
def _sv(self, cell: str, value: float, kind: str):
"""SV{cell} {value} 전송 → (성공여부, 응답코드 문자열)"""
try:
fmt = str(int(round(value))) if kind == "int" else f"{value:.6g}"
self._cam._send(f"SV{cell} {fmt}")
resp = self._cam._read_line()
print(f"[설정창] SV{cell} {fmt}{resp!r}")
return resp.strip() == "1", resp.strip()
except Exception as e:
print(f"[설정창] SV{cell} 오류: {e}")
return False, str(e)

View File

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

361
gui/main_window.py Normal file
View File

@@ -0,0 +1,361 @@
# 메인 윈도우 — 1920x1080 전체화면, 다크 테마, 4탭 네비게이션
from PyQt5.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QStackedWidget, QLabel, QSizePolicy,
)
from PyQt5.QtCore import Qt, QPoint, QRect, QElapsedTimer
from PyQt5.QtGui import QPainter, QPolygon, QColor, QFont
from gui.pages.settings_page import SettingsPage
from gui.pages.register_page import RegisterPage
from gui.pages.inspect_page import InspectPage
from gui.pages.retrain_page import RetrainPage
from ai.detector import Detector
from logic.pattern_matcher import PatternMatcher
from db.sql_client import SQLClient
_DOT_OK = ("background:#22cc55; border-radius:7px;"
"min-width:14px; max-width:14px; min-height:14px; max-height:14px;")
_DOT_FAIL = ("background:#cc2222; border-radius:7px;"
"min-width:14px; max-width:14px; min-height:14px; max-height:14px;")
class ChevronTabButton(QPushButton):
# 우측이 ">" 화살표 모양인 breadcrumb/stepper 스타일 탭 버튼
CHEVRON_W = 26 # 화살표 뾰족한 부분 너비(px)
COLOR_ACTIVE_BG = QColor("#0055cc")
COLOR_ACTIVE_FG = QColor("#ffffff")
COLOR_HOVER_BG = QColor("#2e2e2e")
COLOR_HOVER_FG = QColor("#ffffff")
COLOR_IDLE_BG = QColor("#222222")
COLOR_IDLE_FG = QColor("#aaaaaa")
def __init__(self, text: str, is_first: bool = False,
is_last: bool = False, parent=None):
super().__init__(text, parent)
self._is_first = is_first
self._is_last = is_last
self._active = False
self._hover = False
self.setMinimumHeight(60)
self.setCursor(Qt.PointingHandCursor)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.setFlat(True)
self.setAttribute(Qt.WA_Hover, True)
def setActive(self, active: bool):
self._active = active
self.update()
def enterEvent(self, event):
self._hover = True
self.update()
super().enterEvent(event)
def leaveEvent(self, event):
self._hover = False
self.update()
super().leaveEvent(event)
def paintEvent(self, event):
if self._active:
bg, fg = self.COLOR_ACTIVE_BG, self.COLOR_ACTIVE_FG
elif self._hover:
bg, fg = self.COLOR_HOVER_BG, self.COLOR_HOVER_FG
else:
bg, fg = self.COLOR_IDLE_BG, self.COLOR_IDLE_FG
w, h = self.width(), self.height()
cw = self.CHEVRON_W
left_cw = 0 if self._is_first else cw
right_cw = 0 if self._is_last else cw
p = QPainter(self)
p.setRenderHint(QPainter.Antialiasing)
p.setPen(Qt.NoPen)
p.setBrush(bg)
# 시계방향: 좌상 → 우상 → (우측 ▶) → 우하 → 좌하 → (좌측 V)
pts = [QPoint(0, 0)]
if self._is_last:
pts += [QPoint(w, 0), QPoint(w, h)]
else:
pts += [QPoint(w - cw, 0), QPoint(w, h // 2), QPoint(w - cw, h)]
pts.append(QPoint(0, h))
if not self._is_first:
pts.append(QPoint(cw, h // 2))
p.drawPolygon(QPolygon(pts))
p.setPen(fg)
font = QFont(self.font())
font.setPointSize(18)
font.setBold(self._active)
p.setFont(font)
text_rect = QRect(left_cw, 0, w - left_cw - right_cw, h)
p.drawText(text_rect, Qt.AlignCenter, self.text())
class MainWindow(QMainWindow):
def __init__(self, insight_cam, basler_cam, config: dict, plc_client=None):
super().__init__()
self.insight = insight_cam
self.basler = basler_cam
self.config = config
self.detector = Detector()
self.matcher = PatternMatcher()
self.matcher.load() # 앱 시작 시 저장된 패턴 자동 로드
self.db_client = SQLClient()
self.plc_client = plc_client
self.setWindowTitle("비전 검사 시스템")
self.showFullScreen()
# 재학습 탭 연속 클릭(창 최소화 단축) 감지용
self._retrain_click_timer = QElapsedTimer()
self._retrain_click_timer.start()
self._build_ui()
self._switch_tab(0)
self._auto_connect_db()
self.update_connection_status()
# ================================================================== #
# UI 구성
# ================================================================== #
def _build_ui(self):
root = QWidget()
self.setCentralWidget(root)
layout = QVBoxLayout(root)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(self._build_tab_bar())
layout.addWidget(self._build_stack(), stretch=1)
layout.addWidget(self._build_status_bar())
def _build_tab_bar(self) -> QWidget:
bar = QWidget()
bar.setFixedHeight(60)
# 화살표 모양 사이의 빈 삼각형 영역에서 보일 배경
bar.setStyleSheet("background:#1a1a1a;")
row = QHBoxLayout(bar)
row.setContentsMargins(0, 0, 0, 0)
row.setSpacing(0)
labels = ["환경설정", "제품 등록", "검사", "재학습"]
self._tab_btns = []
for i, label in enumerate(labels):
btn = ChevronTabButton(
label,
is_first=(i == 0),
is_last=(i == len(labels) - 1),
)
btn.clicked.connect(lambda _, idx=i: self._switch_tab(idx))
self._tab_btns.append(btn)
row.addWidget(btn)
return bar
def _build_stack(self) -> QStackedWidget:
self._stack = QStackedWidget()
# belt_delay = 거리 / 속도 (config 기본값 사용)
_conv = self.config.get("conveyor", {})
_dist = _conv.get("distance_cm", 100.0)
_speed = _conv.get("speed_cms", 30.0)
_belt_delay = _dist / _speed if _speed > 0 else 3.33
self._settings_page = SettingsPage(
self.insight, self.basler, self.config,
detector=self.detector,
update_insight_cb=self.update_insight,
update_basler_cb=self.update_basler,
update_detector_cb=self.update_detector,
update_db_cb=self.update_db,
update_plc_cb=self.update_plc,
plc_client=self.plc_client,
)
self._settings_page.cognex_status_changed.connect(
lambda _: self.update_connection_status()
)
self._settings_page.basler_status_changed.connect(
lambda _: self.update_connection_status()
)
self._settings_page.plc_status_changed.connect(
lambda _: self.update_connection_status()
)
self._register_page = RegisterPage(
self.insight, matcher=self.matcher, db_client=self.db_client
)
self._inspect_page = InspectPage(
self.insight, self.basler,
detector=self.detector,
belt_delay=_belt_delay,
)
self._inspect_page.update_matcher(self.matcher)
self._settings_page.belt_settings_changed.connect(
self._inspect_page.update_belt_delay
)
self._pages = [
self._settings_page,
self._register_page,
self._inspect_page,
RetrainPage(),
]
for page in self._pages:
self._stack.addWidget(page)
return self._stack
def _build_status_bar(self) -> QWidget:
bar = QWidget()
bar.setFixedHeight(36)
bar.setStyleSheet("background:#111111;")
row = QHBoxLayout(bar)
row.setContentsMargins(16, 0, 16, 0)
row.setSpacing(20)
self._dot_cognex = self._make_dot(self.insight.is_connected())
self._dot_basler = self._make_dot(
self.basler.is_connected() if self.basler else False
)
self._dot_db = self._make_dot(False)
self._dot_plc = self._make_dot(
bool(self.plc_client and self.plc_client.is_connected())
)
self._lbl_cognex = QLabel("코그넥스")
self._lbl_basler = QLabel("Basler")
self._lbl_db = QLabel("DB")
self._lbl_plc = QLabel("PLC")
for lbl in (self._lbl_cognex, self._lbl_basler, self._lbl_db, self._lbl_plc):
lbl.setStyleSheet("color:#aaaaaa; font-size:13px;")
row.addWidget(self._dot_cognex)
row.addWidget(self._lbl_cognex)
row.addWidget(self._dot_basler)
row.addWidget(self._lbl_basler)
row.addWidget(self._dot_db)
row.addWidget(self._lbl_db)
row.addWidget(self._dot_plc)
row.addWidget(self._lbl_plc)
row.addStretch()
return bar
@staticmethod
def _make_dot(connected: bool) -> QLabel:
dot = QLabel()
dot.setStyleSheet(_DOT_OK if connected else _DOT_FAIL)
return dot
# ================================================================== #
# 탭 전환
# ================================================================== #
def _switch_tab(self, idx: int):
# 재학습 탭(3)을 600ms 이내에 연속으로 두 번 클릭하면 창 최소화
if idx == 3:
if (self._stack.currentIndex() == 3
and self._retrain_click_timer.elapsed() < 600):
self.showMinimized()
self._retrain_click_timer.restart()
return
self._retrain_click_timer.restart()
self._stack.setCurrentIndex(idx)
for i, btn in enumerate(self._tab_btns):
btn.setActive(i == idx)
if idx == 0:
self._settings_page._sync_connection_status()
self.update_connection_status()
# ================================================================== #
# 코그넥스 인스턴스 교체 (SettingsPage 연결 성공 시 호출)
# ================================================================== #
def update_insight(self, new_insight):
self.insight = new_insight
self._register_page._insight = new_insight
self._inspect_page.update_insight(new_insight)
def update_basler(self, new_basler):
self.basler = new_basler
self._inspect_page.update_basler(new_basler)
def update_detector(self, new_detector):
self.detector = new_detector
if hasattr(self, "_inspect_page"):
self._inspect_page.update_detector(new_detector)
def update_db(self, db_client):
self.db_client = db_client
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, "_settings_page"):
self._settings_page._db_client = db_client
self._settings_page._set_db_connected(connected)
self.update_connection_status()
def update_plc(self, plc_client):
self.plc_client = plc_client
connected = plc_client is not None and plc_client.is_connected()
if hasattr(self, "_settings_page"):
self._settings_page._plc_client = plc_client
self._settings_page._set_plc_connected(connected)
self.update_connection_status()
def _auto_connect_db(self):
"""앱 시작 시 config.json DB 접속 정보로 자동 연결 시도."""
db_cfg = self.config.get("db", {})
server = db_cfg.get("server", "").strip()
database = db_cfg.get("database", "").strip()
username = db_cfg.get("username", "").strip()
password = db_cfg.get("password", "")
if not server or not database:
return
ok = self.db_client.connect(server, database, username, password)
if ok:
self.update_db(self.db_client)
# ================================================================== #
# 실제 연결 상태를 읽어 상태바 전체 갱신
# ================================================================== #
def update_connection_status(self):
cognex_ok = bool(self.insight and self.insight.is_connected())
self._dot_cognex.setStyleSheet(_DOT_OK if cognex_ok else _DOT_FAIL)
self._lbl_cognex.setText("코그넥스 연결됨" if cognex_ok else "코그넥스 연결 안됨")
self._lbl_cognex.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if cognex_ok
else "color:#aaaaaa; font-size:13px;"
)
basler_ok = bool(self.basler and self.basler.is_connected())
self._dot_basler.setStyleSheet(_DOT_OK if basler_ok else _DOT_FAIL)
self._lbl_basler.setText("Basler 연결됨" if basler_ok else "Basler 연결 안됨")
self._lbl_basler.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if basler_ok
else "color:#aaaaaa; font-size:13px;"
)
db_ok = bool(self.db_client and self.db_client.is_connected())
self._dot_db.setStyleSheet(_DOT_OK if db_ok else _DOT_FAIL)
self._lbl_db.setText("DB 연결됨" if db_ok else "DB 연결 안됨")
self._lbl_db.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if db_ok
else "color:#aaaaaa; font-size:13px;"
)
plc_ok = bool(self.plc_client and self.plc_client.is_connected())
self._dot_plc.setStyleSheet(_DOT_OK if plc_ok else _DOT_FAIL)
self._lbl_plc.setText("PLC 연결됨" if plc_ok else "PLC 연결 안됨")
self._lbl_plc.setStyleSheet(
"color:#1D9E75; font-size:13px; font-weight:bold;" if plc_ok
else "color:#aaaaaa; font-size:13px;"
)

1
gui/pages/__init__.py Normal file
View File

@@ -0,0 +1 @@
# gui/pages 패키지

726
gui/pages/inspect_page.py Normal file
View File

@@ -0,0 +1,726 @@
# 검사 페이지 — 코그넥스/Basler 영상, 그룹 A/B 설정, Pass/Fail 표시
import time
import threading
import itertools
import cv2
import numpy as np
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,
)
from logic.inspector import Inspector
from logic.group_manager import GroupManager
from logger import log_inspect_result, log_camera_timing, log_action, log_defect_image
_DEFECT_COLORS = {
"스크래치": (0, 0, 255),
"이물": (0, 165, 255),
"흑점": (128, 0, 128),
"변형": (255, 165, 0 ),
}
def _draw_detections(frame: np.ndarray, detections: list) -> np.ndarray:
"""frame 복사본에 BBox 오버레이를 그려 반환."""
img = frame.copy()
for det in detections:
x1, y1, x2, y2 = [int(v) for v in det["bbox"]]
name = det["class_name"]
conf = det["confidence"]
color = _DEFECT_COLORS.get(name, (0, 255, 0))
cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
cv2.putText(
img, f"{name} {conf:.0%}",
(x1, max(y1 - 8, 0)),
cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2,
)
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,
}
# ================================================================== #
# 백그라운드 워커 — 파이프라인 방식
#
# [Cognex 서브스레드] trigger → sleep(1.0) → FTP(영구세션) → PatMax
# ↕ 병렬 ↕ join
# [워커 메인] sleep(belt_delay) → Basler 캡처 → 판정 → emit
#
# belt_delay = 카메라 간 거리(cm) / 벨트 속도(cm/s)
# 두 작업이 동시에 시작되어 같은 제품을 각 위치에서 촬영함.
# ================================================================== #
class InspectWorker(QThread):
cognex_image_ready = pyqtSignal(bytes) # raw BMP/JPG 바이트
basler_image_ready = pyqtSignal(object, list) # (ndarray, detections)
result_ready = pyqtSignal(dict)
def __init__(self, insight, basler, inspector, groups,
belt_delay: float = 3.33, parent=None):
super().__init__(parent)
self._insight = insight
self._basler = basler
self._inspector = inspector
self._groups = groups
self.detector = None
self.matcher = None # PatternMatcher — InspectPage에서 주입
self._belt_delay = belt_delay
self._stop_flag = False
self._pause_flag = False
self._seq = itertools.count(1)
# ── 외부 제어 ──────────────────────────────────────────────────── #
def stop(self):
self._stop_flag = True
def pause(self):
self._pause_flag = True
def resume(self):
self._pause_flag = False
def set_belt_delay(self, delay: float):
self._belt_delay = delay
# ── 스레드 본체 ────────────────────────────────────────────────── #
def run(self):
self._stop_flag = False
self._pause_flag = False
while not self._stop_flag:
if self._pause_flag:
time.sleep(0.1)
continue
self._do_one_cycle()
# ── 검사 1사이클 ───────────────────────────────────────────────── #
def _do_one_cycle(self):
group = self._groups.get_active_name()
seq = next(self._seq)
trigger_time = time.perf_counter()
def _ms() -> float:
return (time.perf_counter() - trigger_time) * 1000
log_camera_timing(seq, "cycle_start", 0.0, f"group={group} belt_delay={self._belt_delay:.2f}s")
# ── Cognex 작업: 서브 스레드에서 병렬 실행 ──
cognex_out: dict = {}
def _cognex_work():
try:
log_camera_timing(seq, "cognex_trigger_send", _ms())
ok = self._insight.software_trigger()
log_camera_timing(seq, f"cognex_trigger_{'ok' if ok else 'fail'}", _ms())
if not ok:
cognex_out["error"] = "trigger_failed"
return
time.sleep(1.0)
log_camera_timing(seq, "cognex_ftp_start", _ms())
raw = self._insight.get_image()
log_camera_timing(
seq, "cognex_ftp_done", _ms(),
f"{len(raw)}bytes" if raw else "empty",
)
if raw:
self.cognex_image_ready.emit(raw)
log_camera_timing(seq, "cognex_patmax_start", _ms())
# Cognex job 파일 결과 (항상)
gv_results = self._inspector.read_patmax_results(self._insight)
# Python ORB 결과 (추가 등록 제품, 있을 때만)
py_results = {}
if self.matcher and self.matcher.registered_ids and raw:
py_results = self._inspector.match_image(raw, self.matcher)
cognex_out["results"] = {**gv_results, **py_results}
log_camera_timing(seq, "cognex_patmax_done", _ms())
except Exception as e:
print(f"[워커] Cognex 서브스레드 오류: {e}")
cognex_out["error"] = str(e)
log_camera_timing(seq, "cognex_error", _ms(), str(e))
ct = threading.Thread(target=_cognex_work, daemon=True)
ct.start()
# ── Basler: trigger 시점 기준 belt_delay 후 캡처 ──
elapsed = time.perf_counter() - trigger_time
remaining = self._belt_delay - elapsed
if remaining > 0:
time.sleep(remaining)
basler_pass = True
basler_detections = []
try:
log_camera_timing(seq, "basler_capture_start", _ms())
frame = self._basler.capture()
log_camera_timing(
seq, "basler_capture_done", _ms(),
f"{frame.shape}" if frame is not None else "failed",
)
if frame is not None:
if self.detector and self.detector.is_loaded():
detections = self.detector.detect(frame)
defects = [d for d in detections if d["confidence"] >= 0.5]
basler_pass = len(defects) == 0
basler_detections = defects
if defects:
print(f"[워커] 불량 감지: {[d['class_name'] for d in defects]}")
annotated = _draw_detections(frame, defects)
log_defect_image(annotated, defects)
self.basler_image_ready.emit(frame, basler_detections)
except Exception as e:
print(f"[워커 오류] Basler: {e}")
log_camera_timing(seq, "basler_error", _ms(), str(e))
# ── Cognex 서브스레드 완료 대기 ──
log_camera_timing(seq, "cognex_join_wait", _ms())
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 = {
"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)
except Exception as e:
print(f"[워커 오류] 모델 판별: {e}")
cognex_pass = result_info["cognex_pass"]
matched = result_info["matched"]
final = self._inspector.judge(cognex_pass, basler_pass)
self.result_ready.emit({
"group": group,
"matched": matched,
"result": final,
"cognex_pass": cognex_pass,
"basler_pass": basler_pass,
"result_info": result_info,
})
log_camera_timing(
seq, "cycle_done", _ms(),
f"result={final} cognex={'PASS' if cognex_pass else 'FAIL'} basler={'PASS' if basler_pass else 'FAIL'}",
)
try:
log_inspect_result(
group=group,
result=("UNKNOWN" if not matched else final),
cognex_pass=cognex_pass,
basler_pass=basler_pass,
detected=[result_info["model"]] if result_info.get("model") else None,
)
except Exception as e:
print(f"[워커 오류] 로그 기록: {e}")
# ================================================================== #
# 검사 페이지
# ================================================================== #
class InspectPage(QWidget):
def __init__(self, insight_cam, basler_cam, detector=None,
belt_delay: float = 3.33, parent=None):
super().__init__(parent)
self._insight = insight_cam
self._basler = basler_cam
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._matcher = None
self._worker = InspectWorker(
self._insight, self._basler, self._inspector, self._groups,
belt_delay=belt_delay,
)
self._worker.detector = self.detector
self._worker.matcher = self._matcher
self._worker.cognex_image_ready.connect(self._display_cognex_image)
self._worker.basler_image_ready.connect(self._on_basler_ready)
self._worker.result_ready.connect(self._on_result)
self._build_ui()
# ================================================================== #
# 최상위 레이아웃
# ================================================================== #
def _build_ui(self):
root = QVBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
root.addWidget(self._build_top(), stretch=7)
root.addWidget(self._build_bottom(), stretch=3)
# ================================================================== #
# 상단: 코그넥스 (좌 50 %) / Basler (우 50 %)
# ================================================================== #
def _build_top(self) -> QWidget:
w = QWidget()
w.setStyleSheet("background:#0d0d0d;")
layout = QHBoxLayout(w)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addLayout(self._build_cam_col("■ In-Sight 2000C", "#4488ff", "_cognex_label"), stretch=1)
layout.addWidget(_vline())
layout.addLayout(self._build_cam_col("■ Basler USB", "#44cc88", "_basler_label"), stretch=1)
return w
def _build_cam_col(self, title: str, color: str, attr: str) -> QVBoxLayout:
col = QVBoxLayout()
col.setContentsMargins(0, 0, 0, 0)
col.setSpacing(0)
title_lbl = QLabel(title)
title_lbl.setFixedHeight(32)
title_lbl.setStyleSheet(
f"color:{color}; font-size:15px; font-weight:bold;"
"padding-left:10px; background:#111111;"
)
img_lbl = QLabel()
img_lbl.setAlignment(Qt.AlignCenter)
img_lbl.setStyleSheet("background:#0d0d0d;")
img_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
img_lbl.setMinimumSize(640, 480)
img_lbl.setScaledContents(False)
setattr(self, attr, img_lbl)
col.addWidget(title_lbl)
col.addWidget(img_lbl, stretch=1)
return col
# ================================================================== #
# 하단: 3열
# ================================================================== #
def _build_bottom(self) -> QWidget:
w = QWidget()
w.setStyleSheet("background:#1a1a1a;")
layout = QHBoxLayout(w)
layout.setContentsMargins(8, 6, 8, 6)
layout.setSpacing(0)
layout.addWidget(self._build_col_groups(), 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:
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.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; }}"
)
layout = QVBoxLayout(g)
layout.setSpacing(1)
layout.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)
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))
return g
def _build_col_controls(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
layout.setContentsMargins(12, 4, 12, 4)
layout.setSpacing(6)
self._start_btn = QPushButton("검사 시작")
self._start_btn.setFixedHeight(70)
self._start_btn.setStyleSheet(
"background:#1a5c1a; color:#ffffff; border:none; border-radius:4px;"
"font-size:18px; font-weight:bold;"
)
self._start_btn.clicked.connect(self._on_start)
self._pause_btn = QPushButton("일시 정지")
self._pause_btn.setFixedHeight(70)
self._pause_btn.setEnabled(False)
self._pause_btn.setStyleSheet(
"background:#5c5500; color:#ffffff; border:none; border-radius:4px;"
"font-size:18px; font-weight:bold;"
)
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._model_lbl = QLabel("인식 모델: —")
self._model_lbl.setAlignment(Qt.AlignCenter)
self._model_lbl.setWordWrap(True)
self._model_lbl.setStyleSheet("font-size:13px; color:#cccccc;")
self._belt_lbl = QLabel(
f"벨트 딜레이: {self._worker._belt_delay:.2f}s"
)
self._belt_lbl.setAlignment(Qt.AlignCenter)
self._belt_lbl.setStyleSheet("font-size:12px; color:#666666;")
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;"
)
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._model_lbl)
layout.addWidget(self._belt_lbl)
layout.addWidget(self._result_lbl, stretch=1)
return w
def _build_col_counters(self) -> QWidget:
w = QWidget()
layout = QVBoxLayout(w)
layout.setContentsMargins(12, 4, 8, 4)
layout.setSpacing(6)
grid = QGridLayout()
grid.setSpacing(4)
for col, text in enumerate(["", "그룹 A", "그룹 B"]):
lbl = QLabel(text)
lbl.setAlignment(Qt.AlignCenter)
lbl.setStyleSheet("font-size:13px; color:#888888; font-weight:bold;")
grid.addWidget(lbl, 0, col)
row_defs = [
("전체", "total", "#ffffff"),
("양품", "pass", "#22cc55"),
("불량", "fail", "#cc2222"),
("미인식", "unknown", "#ff9900"),
]
self._cnt_lbls = {}
for r, (display, key, color) in enumerate(row_defs, start=1):
row_lbl = QLabel(display)
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
layout.addLayout(grid)
btn_reset = QPushButton("카운터 초기화")
btn_reset.setFixedHeight(56)
btn_reset.setStyleSheet(
"background:#3a1a1a; color:#ffffff; border:none; border-radius:4px; font-size:14px;"
)
btn_reset.clicked.connect(self._on_reset)
layout.addWidget(btn_reset)
layout.addStretch()
return w
# ================================================================== #
# 슬롯 — 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
log_action("[검사] 검사 시작")
self._start_btn.setEnabled(False)
self._pause_btn.setEnabled(True)
self._pause_btn.setText("일시 정지")
self._worker.start()
def _on_pause(self):
if self._worker.isRunning() and not self._worker._pause_flag:
self._worker.pause()
self._pause_btn.setText("재개")
self._start_btn.setEnabled(True)
log_action("[검사] 일시 정지")
else:
self._worker.resume()
self._pause_btn.setText("일시 정지")
self._start_btn.setEnabled(False)
log_action("[검사] 검사 재개")
def _on_reset(self):
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")
def closeEvent(self, event):
self._worker.stop()
self._worker.wait(3000)
super().closeEvent(event)
# ================================================================== #
# 외부에서 호출하는 업데이트 메서드
# ================================================================== #
def update_detector(self, detector):
self.detector = detector
self._worker.detector = detector
print(f"[검사] AI 모델 업데이트: {detector.model_path if detector else None}")
def update_matcher(self, matcher):
self._matcher = matcher
self._worker.matcher = matcher
n = len(matcher.registered_ids) if matcher else 0
print(f"[검사] PatternMatcher 업데이트: {n}개 패턴")
def update_insight(self, new_insight):
"""카메라 재연결 시 워커에도 반영."""
self._insight = new_insight
self._worker._insight = new_insight
def update_basler(self, new_basler):
self._basler = new_basler
self._worker._basler = new_basler
def update_belt_delay(self, delay: float):
"""설정 페이지에서 컨베이어 값 변경 시 호출."""
self._worker.set_belt_delay(delay)
self._belt_lbl.setText(f"벨트 딜레이: {delay:.2f}s")
# ================================================================== #
# 워커 signal 슬롯 (메인 스레드)
# ================================================================== #
def _on_basler_ready(self, frame, detections):
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
if not matched:
self._counts[group]["unknown"] += 1
elif result == "PASS":
self._counts[group]["pass"] += 1
else:
self._counts[group]["fail"] += 1
for key in ("total", "pass", "fail", "unknown"):
self._cnt_lbls[key][group].setText(str(self._counts[group][key]))
if not matched:
self._set_result("미인식", "#332200", "#ff9900")
elif result == "PASS":
self._set_result("PASS", "#003300", "#22ff55")
else:
self._set_result("FAIL", "#330000", "#ff2222")
# ================================================================== #
# 이미지 표시 헬퍼
# ================================================================== #
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"color:{fg}; border-radius:8px;"
)
def _display_cognex_image(self, raw_data: bytes):
try:
arr = np.frombuffer(raw_data, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_COLOR)
if img is None:
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
if img is None:
print("[코그넥스] 이미지 디코딩 실패")
return
if len(img.shape) == 2:
h, w = img.shape
qimg = QImage(img.data.tobytes(), w, h, w, QImage.Format_Grayscale8)
else:
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
qimg = QImage(rgb.data.tobytes(), w, h, w * ch, QImage.Format_RGB888)
self._cognex_label.setPixmap(
QPixmap.fromImage(qimg).scaled(
self._cognex_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
except Exception as e:
print(f"[코그넥스] 이미지 표시 오류: {e}")
def _display_basler_image(self, frame, detections=None):
try:
img = _draw_detections(frame, detections or [])
if len(img.shape) == 2:
h, w = img.shape
qimg = QImage(img.data.tobytes(), w, h, w, QImage.Format_Grayscale8)
else:
rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
h, w, ch = rgb.shape
qimg = QImage(rgb.data.tobytes(), w, h, w * ch, QImage.Format_RGB888)
self._basler_label.setPixmap(
QPixmap.fromImage(qimg).scaled(
self._basler_label.size(),
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
)
except Exception as e:
print(f"[Basler] 이미지 표시 오류: {e}")
# ================================================================== #
# 모듈 수준 유틸리티
# ================================================================== #
def _vline() -> QFrame:
f = QFrame()
f.setFrameShape(QFrame.VLine)
f.setFixedWidth(2)
f.setStyleSheet("background:#333333; border:none;")
return f
def _raw_to_pixmap(raw: bytes, size: QSize) -> "QPixmap | None":
arr = np.frombuffer(raw, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
if img is None:
return None
return _ndarray_to_pixmap(img, size)
def _ndarray_to_pixmap(img: np.ndarray, size: QSize) -> "QPixmap | None":
if img.ndim == 2:
img_c = np.ascontiguousarray(img)
h, w = img_c.shape
qimg = QImage(img_c.data, w, h, w, QImage.Format_Grayscale8)
else:
rgb = np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
h, w, ch = rgb.shape
qimg = QImage(rgb.data, w, h, w * ch, QImage.Format_RGB888)
return QPixmap.fromImage(qimg).scaled(size, Qt.KeepAspectRatio, Qt.SmoothTransformation)

297
gui/pages/register_page.py Normal file
View File

@@ -0,0 +1,297 @@
# 제품 등록 페이지 — 기준 이미지 캡처
import cv2
import numpy as np
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtGui import QImage, QPixmap
from PyQt5.QtWidgets import (
QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
QPushButton, QListWidget, QListWidgetItem, QLabel,
QMessageBox, QScrollArea, QFrame,
)
_GRP_STYLE = (
"QGroupBox {"
" background:#222222; border:1px solid #333333; border-radius:6px;"
" margin-top:14px; padding:14px 12px 12px 12px;"
"}"
"QGroupBox::title { color:#aaaaaa; subcontrol-origin:margin; left:10px; padding:0 4px; }"
)
class RegisterPage(QWidget):
def __init__(self, insight_cam, matcher=None, db_client=None, parent=None):
super().__init__(parent)
self._insight = insight_cam
self._db_client = db_client
self._db_items = []
self._selected = None
self._captured_img = None
self._build_ui()
# ================================================================== #
# UI 구성
# ================================================================== #
def _build_ui(self):
root = QHBoxLayout(self)
root.setContentsMargins(0, 0, 0, 0)
root.setSpacing(0)
root.addWidget(self._build_left_panel(), stretch=2)
root.addWidget(self._build_right_panel(), stretch=3)
def _build_left_panel(self) -> QWidget:
w = QWidget()
w.setStyleSheet("background:#1a1a1a;")
layout = QVBoxLayout(w)
layout.setContentsMargins(16, 16, 8, 16)
layout.setSpacing(10)
self._btn_mes = QPushButton("MES 불러오기")
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)
lbl = QLabel("제품 목록")
lbl.setStyleSheet("font-size:13px; color:#777777;")
layout.addWidget(lbl)
self._list = QListWidget()
self._list.setStyleSheet("""
QListWidget {
background:#222222; border:1px solid #333333;
border-radius:4px; outline:none; font-size:15px;
}
QListWidget::item {
padding:0px 14px; border-bottom:1px solid #2a2a2a; color:#dddddd;
}
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)
return w
def _build_right_panel(self) -> QScrollArea:
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QScrollArea.NoFrame)
scroll.setStyleSheet("background:#1a1a1a;")
inner = QWidget()
inner.setStyleSheet("background:#1a1a1a;")
layout = QVBoxLayout(inner)
layout.setContentsMargins(8, 16, 16, 16)
layout.setSpacing(0)
layout.addWidget(self._build_detail_section())
layout.addWidget(_divider())
layout.addWidget(self._build_capture_section())
layout.addStretch()
scroll.setWidget(inner)
return scroll
# ── 섹션 1: 제품 상세 ──────────────────────────────────────────────
def _build_detail_section(self) -> QGroupBox:
g = QGroupBox("제품 상세 정보")
g.setStyleSheet(_GRP_STYLE)
layout = QVBoxLayout(g)
layout.setSpacing(8)
self._lbl_name = _info_value("")
self._lbl_model = _info_value("")
self._lbl_type = _info_value("")
layout.addLayout(_info_row("카테고리", self._lbl_name))
layout.addLayout(_info_row("모델명", self._lbl_model))
layout.addLayout(_info_row("Type", self._lbl_type))
self._arrow_lbl = QLabel("")
self._arrow_lbl.setAlignment(Qt.AlignCenter)
self._arrow_lbl.setFixedHeight(80)
self._arrow_lbl.setStyleSheet("font-size:60px; background:transparent;")
layout.addWidget(self._arrow_lbl)
return g
# ── 섹션 2: 캡처 ──────────────────────────────────────────────────
def _build_capture_section(self) -> QGroupBox:
g = QGroupBox("기준 이미지 캡처")
g.setStyleSheet(_GRP_STYLE)
layout = QVBoxLayout(g)
layout.setSpacing(12)
btn = QPushButton("캡처 (In-Sight 트리거)")
btn.setFixedHeight(56)
btn.setStyleSheet(
"background:#1a3a5c; color:#ffffff; border:none; border-radius:4px;"
"font-size:15px; font-weight:bold;"
)
btn.clicked.connect(self._on_capture)
self._preview = QLabel()
self._preview.setAlignment(Qt.AlignCenter)
self._preview.setFixedSize(400, 300)
self._preview.setStyleSheet(
"background:#111111; color:#555555; border:1px solid #333333;"
"border-radius:4px; font-size:14px;"
)
self._preview.setText("캡처 이미지 없음")
layout.addWidget(btn)
layout.addWidget(self._preview, alignment=Qt.AlignHCenter)
return g
# ================================================================== #
# 슬롯
# ================================================================== #
def _on_select(self, row: int):
if row < 0:
return
item = self._list.item(row)
if item is None:
return
article_id = item.data(Qt.UserRole)
if article_id is None:
return
db_item = next(
(x for x in self._db_items if x["article_id"] == article_id), None
)
r = {
"id": article_id,
"name": item.text(),
"model": db_item.get("buyer_article_no", "") if db_item else "",
"type": "",
}
self._selected = r
self._captured_img = None
self._lbl_name.setText(r["name"])
self._lbl_model.setText(r["model"])
t = r.get("type", "")
self._lbl_type.setText(t if t else "")
if t == "RH":
self._arrow_lbl.setText("")
self._arrow_lbl.setStyleSheet("font-size:60px; color:#4488ff; background:transparent;")
elif t == "LH":
self._arrow_lbl.setText("")
self._arrow_lbl.setStyleSheet("font-size:60px; color:#ff8844; background:transparent;")
else:
self._arrow_lbl.setText("")
self._reset_preview()
def _on_capture(self):
if self._selected is None:
QMessageBox.warning(self, "경고", "먼저 제품을 선택하세요.")
return
if not self._insight.is_connected():
QMessageBox.critical(self, "오류", "Cognex 카메라가 연결되어 있지 않습니다.")
return
raw = self._insight.trigger_and_get_image()
if not raw:
QMessageBox.critical(self, "캡처 실패",
"이미지를 수신하지 못했습니다.\n카메라 연결 상태를 확인하세요.")
return
arr = np.frombuffer(raw, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
if img is None:
QMessageBox.critical(self, "캡처 실패", "이미지 디코딩에 실패했습니다.")
return
self._captured_img = img
self._show_ndarray(img)
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;"
)
def _on_load_from_db(self):
if not self._db_client or not self._db_client.is_connected():
QMessageBox.warning(self, "경고", "DB를 먼저 연결해주세요.")
return
items = self._db_client.get_reflector_list()
if not items:
QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
return
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)
QMessageBox.information(
self, "완료", f"{len(items)}개 제품을 불러왔습니다."
)
# ================================================================== #
# 헬퍼
# ================================================================== #
def _show_ndarray(self, img: np.ndarray):
h_img, w_img = img.shape[:2]
if img.ndim == 2:
qimg = QImage(img.data, w_img, h_img, w_img, QImage.Format_Grayscale8)
else:
rgb = np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
qimg = QImage(rgb.data, w_img, h_img, w_img * 3, QImage.Format_RGB888)
pix = QPixmap.fromImage(qimg).scaled(400, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self._preview.setPixmap(pix)
self._preview.setText("")
def _reset_preview(self):
self._preview.clear()
self._preview.setText("캡처 이미지 없음")
# ================================================================== #
# 모듈 수준 유틸리티
# ================================================================== #
def _divider() -> QFrame:
f = QFrame()
f.setFrameShape(QFrame.HLine)
f.setFixedHeight(1)
f.setStyleSheet("background:#333333; border:none;")
return f
def _info_value(text: str) -> QLabel:
lbl = QLabel(text)
lbl.setStyleSheet("color:#ffffff; font-size:16px; font-weight:bold;")
return lbl
def _info_row(label: str, value_widget: QLabel) -> QHBoxLayout:
row = QHBoxLayout()
key = QLabel(label + ":")
key.setFixedWidth(80)
key.setStyleSheet("color:#888888; font-size:14px;")
row.addWidget(key)
row.addWidget(value_widget, stretch=1)
return row

1193
gui/pages/retrain_page.py Normal file

File diff suppressed because it is too large Load Diff

1219
gui/pages/settings_page.py Normal file

File diff suppressed because it is too large Load Diff

182
gui/splash_screen.py Normal file
View File

@@ -0,0 +1,182 @@
import json
import os
from PyQt5.QtWidgets import QWidget, QLabel, QProgressBar, QVBoxLayout, QApplication
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QPainter, QPainterPath, QColor
class SplashScreen(QWidget):
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setFixedSize(500, 300)
screen = QApplication.primaryScreen().geometry()
self.move(
(screen.width() - self.width()) // 2,
(screen.height() - self.height()) // 2,
)
self._build_ui()
def _build_ui(self):
layout = QVBoxLayout(self)
layout.setContentsMargins(40, 40, 40, 30)
layout.setSpacing(0)
title = QLabel("리플렉터 검사 시스템")
title.setAlignment(Qt.AlignCenter)
title.setStyleSheet(
"color:#ffffff; font-size:22px; font-weight:bold; background:transparent;"
)
subtitle = QLabel("Reflector Inspection System")
subtitle.setAlignment(Qt.AlignCenter)
subtitle.setStyleSheet("color:#555555; font-size:12px; background:transparent;")
layout.addStretch(2)
layout.addWidget(title)
layout.addSpacing(6)
layout.addWidget(subtitle)
layout.addStretch(3)
self.status_label = QLabel("초기화 중...")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet(
"color:#888888; font-size:13px; background:transparent;"
)
layout.addWidget(self.status_label)
layout.addSpacing(10)
self.progress_bar = QProgressBar()
self.progress_bar.setRange(0, 100)
self.progress_bar.setValue(0)
self.progress_bar.setFixedHeight(6)
self.progress_bar.setTextVisible(False)
self.progress_bar.setStyleSheet("""
QProgressBar {
background: #2a2a2a;
border: none;
border-radius: 3px;
}
QProgressBar::chunk {
background: #1D9E75;
border-radius: 3px;
}
""")
layout.addWidget(self.progress_bar)
layout.addStretch(1)
version = QLabel("v1.0.0")
version.setAlignment(Qt.AlignCenter)
version.setStyleSheet("color:#333333; font-size:11px; background:transparent;")
layout.addWidget(version)
def update_progress(self, value: int, message: str):
self.progress_bar.setValue(value)
self.status_label.setText(message)
QApplication.processEvents()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
path = QPainterPath()
path.addRoundedRect(0.0, 0.0, float(self.width()), float(self.height()), 12.0, 12.0)
painter.fillPath(path, QColor("#0d0d0d"))
class InitWorker(QThread):
progress = pyqtSignal(int, str) # (진행률, 메시지)
finished = pyqtSignal(object) # 초기화 결과 dict
def run(self):
from utils.path_helper import get_path
from camera.insight import InSightCamera
from camera.basler import BaslerCamera
from ai.detector import Detector
from db.sql_client import SQLClient
results = {}
# ── 1단계: 설정 로드 ──────────────────────────────────────────── #
self.progress.emit(10, "설정 불러오는 중...")
try:
with open(get_path("config.json"), encoding="utf-8") as f:
config = json.load(f)
except Exception as e:
print(f"[InitWorker] 설정 로드 실패: {e}")
config = {}
results["config"] = config
# ── 2단계: 코그넥스 카메라 ────────────────────────────────────── #
self.progress.emit(25, "코그넥스 카메라 연결 중...")
insight = InSightCamera()
try:
cognex_cfg = config.get("cognex", {})
ip = cognex_cfg.get("ip", "")
port = cognex_cfg.get("port", 23)
if ip:
insight.connect(ip, port)
except Exception as e:
print(f"[InitWorker] 코그넥스 연결 실패: {e}")
results["insight"] = insight
# ── 3단계: Basler 카메라 ──────────────────────────────────────── #
self.progress.emit(45, "Basler 카메라 연결 중...")
basler = BaslerCamera()
try:
basler.connect()
except Exception as e:
print(f"[InitWorker] Basler 연결 실패: {e}")
results["basler"] = basler
# ── 4단계: AI 모델 로드 ───────────────────────────────────────── #
self.progress.emit(65, "AI 모델 로드 중...")
detector = Detector()
try:
from paths import resolve_path
model_path = config.get("ai", {}).get("model_path", "")
if model_path:
abs_path = resolve_path(model_path)
if abs_path and os.path.exists(abs_path):
detector.load_model(abs_path)
except Exception as e:
print(f"[InitWorker] AI 모델 로드 실패: {e}")
results["detector"] = detector
# ── 5단계: DB 연결 ────────────────────────────────────────────── #
self.progress.emit(80, "DB 연결 중...")
db_client = SQLClient()
try:
db_cfg = config.get("db", {})
if db_cfg.get("server"):
db_client.connect(
db_cfg["server"],
db_cfg["database"],
db_cfg["username"],
db_cfg["password"],
)
except Exception as e:
print(f"[InitWorker] DB 연결 실패: {e}")
results["db"] = db_client
# ── 6단계: PLC 연결 ───────────────────────────────────────────── #
self.progress.emit(92, "PLC 연결 중...")
plc_client = None
try:
from plc.plc_client import PLCClient
plc_cfg = config.get("plc", {})
ip = plc_cfg.get("ip", "").strip()
port = plc_cfg.get("port", 5010)
if ip:
client = PLCClient()
if client.connect(ip, port):
plc_client = client
except Exception as e:
print(f"[InitWorker] PLC 연결 실패: {e}")
results["plc"] = plc_client
# ── 완료 ──────────────────────────────────────────────────────── #
self.progress.emit(100, "시작 중...")
self.finished.emit(results)

237
logger.py Normal file
View File

@@ -0,0 +1,237 @@
# 로그 시스템 — 앱 로그(텍스트) + 검사 결과(CSV) + 학습 이력(텍스트)
# 폴더 구조: logs/<category>/<YYYY-MM-DD>.<ext>
import builtins
import csv
import os
import sys
from datetime import datetime
from pathlib import Path
from paths import PROJECT_ROOT
LOGS_ROOT = Path(PROJECT_ROOT) / "logs"
_BORDER = "=" * 60
_orig_print = builtins.print
_session_start: datetime | None = None
_ACTION_LINE = "" * 64
# ───────────────────────────── 경로 헬퍼 ───────────────────────────── #
def _date_path(category: str, ext: str = "log") -> Path:
"""logs/<category>/<YYYY-MM-DD>.<ext> 경로 (폴더 자동 생성)."""
now = datetime.now()
folder = LOGS_ROOT / category
folder.mkdir(parents=True, exist_ok=True)
return folder / f"{now:%Y-%m-%d}.{ext}"
def _append_text(path: Path, line: str):
with open(path, "a", encoding="utf-8") as f:
if not line.endswith("\n"):
line += "\n"
f.write(line)
def _safe_write_app(line: str):
try:
_append_text(_date_path("app"), line)
except Exception as e:
_orig_print(f"[logger] 파일 쓰기 실패: {e}", file=sys.stderr)
# ───────────────────────────── 세션 헤더 ───────────────────────────── #
def _previous_session_crashed() -> bool:
"""오늘 로그 파일에서 마지막 '세션 시작' 이후 '세션 종료'가 없으면 크래시로 간주."""
today = _date_path("app")
if not today.exists():
return False
try:
with open(today, "rb") as f:
try:
f.seek(-4096, os.SEEK_END)
except OSError:
f.seek(0)
tail = f.read().decode("utf-8", errors="ignore")
last_start = tail.rfind("=== 세션 시작")
last_end = tail.rfind("=== 세션 종료")
return last_start > last_end
except Exception:
return False
def setup_logging():
"""앱 시작 시 호출. print 가로채기 + 세션 시작 헤더 기록."""
global _session_start
crashed = _previous_session_crashed()
_session_start = datetime.now()
pid = os.getpid()
header = [
"",
_BORDER,
f"=== 세션 시작 : {_session_start:%Y-%m-%d %H:%M:%S} PID {pid}",
]
if crashed:
header.append("=== ⚠ 직전 세션 비정상 종료 감지 ===")
header.append(_BORDER)
for line in header:
_safe_write_app(line)
def _ts_print(*args, sep=" ", end="\n", file=None, flush=False, **kwargs):
now = datetime.now()
msg = sep.join(str(a) for a in args)
# 콘솔: [HH:MM:SS]
_orig_print(
f"[{now:%H:%M:%S}]", *args,
sep=sep, end=end, file=file, flush=flush, **kwargs,
)
# 파일: 풀 시각 + 메시지
_safe_write_app(f"{now:%Y-%m-%d %H:%M:%S.%f}"[:-3] + f" {msg}")
builtins.print = _ts_print
def teardown_logging():
"""앱 정상 종료 시 호출. 세션 종료 헤더 기록."""
global _session_start
if _session_start is None:
return
end = datetime.now()
secs = int((end - _session_start).total_seconds())
h, rem = divmod(secs, 3600)
m, s = divmod(rem, 60)
for line in [
_BORDER,
f"=== 세션 종료 : {end:%Y-%m-%d %H:%M:%S} 운영시간 {h:02d}:{m:02d}:{s:02d}",
_BORDER,
"",
]:
_safe_write_app(line)
_session_start = None
# ───────────────────────────── 사용자 액션 ───────────────────────────── #
def log_action(msg: str):
"""버튼 클릭·기능 실행 등 사용자 액션을 구분선으로 강조해 app 로그에 기록."""
_safe_write_app(_ACTION_LINE)
print(f"{msg}")
_safe_write_app(_ACTION_LINE)
# ───────────────────────────── 검사 결과 CSV ───────────────────────────── #
_INSPECT_HEADER = [
"timestamp", "group", "result",
"cognex_pass", "basler_pass", "detected_models",
]
def log_inspect_result(
group: str,
result: str,
cognex_pass: bool | None = None,
basler_pass: bool | None = None,
detected: list[str] | None = None,
):
"""검사 결과 1건을 logs/inspect/YYYY-MM-DD.csv 에 append.
파일 신규일 때만 헤더 행 작성. utf-8-sig 로 Excel 호환."""
csv_path = _date_path("inspect", ext="csv")
is_new = not csv_path.exists()
try:
with open(csv_path, "a", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
if is_new:
w.writerow(_INSPECT_HEADER)
w.writerow([
f"{datetime.now():%Y-%m-%d %H:%M:%S}",
group,
result,
"" if cognex_pass is None else ("Y" if cognex_pass else "N"),
"" if basler_pass is None else ("Y" if basler_pass else "N"),
",".join(detected) if detected else "",
])
except Exception as e:
_orig_print(f"[logger] 검사 CSV 기록 실패: {e}", file=sys.stderr)
# ───────────────────────────── 카메라 타이밍 CSV ───────────────────────────── #
_TIMING_HEADER = ["timestamp", "seq", "event", "elapsed_ms", "detail"]
_timing_lock = __import__("threading").Lock()
def log_camera_timing(seq: int, event: str, elapsed_ms: float, detail: str = ""):
"""카메라 촬영 타이밍 1이벤트를 logs/timing/YYYY-MM-DD.csv 에 append.
Cognex/Basler 서브스레드에서 호출되므로 Lock으로 동시 쓰기 보호."""
csv_path = _date_path("timing", ext="csv")
is_new = not csv_path.exists()
try:
with _timing_lock:
with open(csv_path, "a", encoding="utf-8-sig", newline="") as f:
w = csv.writer(f)
if is_new:
w.writerow(_TIMING_HEADER)
w.writerow([
f"{datetime.now():%H:%M:%S.%f}"[:-3],
seq,
event,
f"{elapsed_ms:.1f}",
detail,
])
except Exception as e:
_orig_print(f"[logger] 타이밍 CSV 기록 실패: {e}", file=sys.stderr)
# ───────────────────────────── 학습 로그 ───────────────────────────── #
def log_train(message: str):
"""재학습 이벤트 1건을 logs/train/YYYY/MM/YYYY-MM-DD.log 에 append."""
line = f"{datetime.now():%Y-%m-%d %H:%M:%S} {message}"
try:
_append_text(_date_path("train"), line)
except Exception as e:
_orig_print(f"[logger] 학습 로그 기록 실패: {e}", file=sys.stderr)
# ───────────────────────────── 불량 이미지 저장 ───────────────────────────── #
def log_defect_image(image: "np.ndarray", defects: list) -> bool:
"""불량 감지 이미지를 logs/defect/YYYY-MM-DD/HHMMSS_SSS_{클래스}.jpg 에 저장.
반환: 저장 성공 여부.
"""
import cv2
import numpy as np # noqa: F401 — type annotation용
now = datetime.now()
folder = LOGS_ROOT / "defect" / now.strftime("%Y-%m-%d")
try:
folder.mkdir(parents=True, exist_ok=True)
except Exception as e:
print(f"[logger] 불량 이미지 폴더 생성 실패: {e} 경로: {folder}")
return False
# 중복 제거 후 순서 유지한 클래스명 목록
seen = set()
names = [
d["class_name"] for d in defects
if not (d["class_name"] in seen or seen.add(d["class_name"]))
]
ms = now.microsecond // 1000
filename = f"{now:%H%M%S}_{ms:03d}_{'_'.join(names)}.jpg"
path = folder / filename
try:
ok = cv2.imwrite(str(path), image)
if not ok:
print(f"[logger] 불량 이미지 저장 실패 (imwrite 반환 False): {path}")
return False
return True
except Exception as e:
print(f"[logger] 불량 이미지 저장 예외: {e} 경로: {path}")
return False

3
logic/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
# logic 패키지 — Inspector, GroupManager 노출
from .inspector import Inspector
from .group_manager import GroupManager

25
logic/group_manager.py Normal file
View File

@@ -0,0 +1,25 @@
# 그룹 관리 — A/B 모델 그룹 수동 전환 (최대 4종 per group)
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]
def get_active_group(self) -> list:
return self._group_a if self._active == "A" else self._group_b
def get_active_name(self) -> str:
return self._active
def switch_group(self) -> str:
"""A↔B 전환 후 활성 그룹 이름 반환"""
self._active = "B" if self._active == "A" else "A"
return self._active

120
logic/inspector.py Normal file
View File

@@ -0,0 +1,120 @@
# 검사 판별 로직 — PatMax 결과 판독 + 모델 판별 + Pass/Fail 판정
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"},
}
class Inspector:
# ── Python PatMax 매칭 (주 경로) ─────────────────────────────────── #
def match_image(self, image_bytes: bytes, matcher: "PatternMatcher") -> dict:
"""
FTP로 받은 이미지 바이트를 Python PatternMatcher로 매칭.
반환 형식은 read_patmax_results와 동일하여 identify_model에서 그대로 사용.
"""
if not image_bytes:
return {}
arr = np.frombuffer(image_bytes, dtype=np.uint8)
img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
if img is None:
return {}
all_scores = matcher.match_all(img)
results = {}
for pid, score in all_scores.items():
info = matcher.get_product_info(pid)
if info is None:
continue
results[f"PY_{pid}"] = {
"matched": score >= matcher.score_threshold,
"score": score,
"model": info,
"raw": f"python_match={score:.1f}",
}
return results
# ── Cognex GV 셀 방식 (fallback) ────────────────────────────────── #
def read_patmax_results(self, insight) -> dict:
"""A27/A77/A127/A177 셀 조회 → #ERR이면 실패, 그 외 점수 파싱."""
results = {}
for cell, model_info in PATTERN_RESULT_CELLS.items():
try:
insight._send(f"GV{cell}")
code = insight._read_line()
if code != "1":
results[cell] = {
"matched": False, "score": 0.0,
"model": model_info, "raw": ""
}
continue
value = insight._read_line()
if "#ERR" in value or value.strip() == "":
results[cell] = {
"matched": False, "score": 0.0,
"model": model_info, "raw": value
}
else:
# "(736.1,742.0) -1.8 = 82.9" 형식에서 = 뒤 값 추출
try:
score = float(value.split("=")[-1].strip())
except Exception:
score = 0.0
results[cell] = {
"matched": True, "score": score,
"model": model_info, "raw": value
}
except Exception as e:
print(f"[PatMax] {cell} 읽기 오류: {e}")
results[cell] = {
"matched": False, "score": 0.0,
"model": model_info, "raw": ""
}
return results
# ── 공통: 모델 판별 + 판정 ──────────────────────────────────────── #
def identify_model(self, results: dict, allowed_model_ids: list) -> dict:
"""매칭된 패턴 중 점수가 가장 높은 것을 선택해 허용 모델 여부 판별."""
matched_patterns = [
(cell, info) for cell, info in results.items()
if info["matched"]
]
if not matched_patterns:
return {
"matched": False, "in_allowed": False,
"model": None, "score": 0.0,
"cognex_pass": False, "status": "인식 불가"
}
_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
return {
"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}점)"
if in_allowed
else f"허용 외 모델: {model['name']} {model['model']} {model['type']}"
),
}
def judge(self, cognex_pass: bool, basler_pass: bool) -> str:
return "PASS" if cognex_pass and basler_pass else "FAIL"

222
logic/pattern_matcher.py Normal file
View File

@@ -0,0 +1,222 @@
# Python PatMax 대체 구현
# ORB 특징점 매칭 (위치·회전 불변) + 엣지 NCC fallback (특징점 부족 시)
import os
import pickle
import cv2
import numpy as np
from typing import Optional
_PATTERNS_PATH = os.path.join("assets", "patterns.pkl")
SCORE_THRESHOLD = 60.0
_MAX_IMG_SIZE = 1200 # ORB 처리 전 긴 변 최대 픽셀 (속도·메모리 제한)
_GOOD_MATCH_REF = 20 # good match 이 수 이상 → 100점 기준
class PatternMatcher:
def __init__(self, threshold: float = SCORE_THRESHOLD):
# {product_id: {"method": "orb"|"ncc", "des": ndarray|None, ...}}
self._patterns: dict = {}
self._threshold = threshold
# ── 학습 ──────────────────────────────────────────────────────────── #
def train(self, image: np.ndarray, product_id: int, product_info: dict,
roi=None):
"""
roi: (x, y, w, h) 픽셀 좌표, None이면 전체 이미지.
ORB로 특징점을 자동 검출해 등록. 특징점 10개 미만이면 엣지 NCC 방식으로 전환.
"""
gray = _to_gray(image)
if roi is not None:
x, y, rw, rh = roi
x, y = max(0, x), max(0, y)
rw = min(rw, gray.shape[1] - x)
rh = min(rh, gray.shape[0] - y)
gray = gray[y:y + rh, x:x + rw].copy()
gray = _resize_if_large(gray)
kp, des = cv2.ORB_create(nfeatures=1000).detectAndCompute(gray, None)
if des is not None and len(kp) >= 10:
method = "orb"
n_kp = len(kp)
edges = None
else:
method = "ncc"
n_kp = 0
des = None
edges = _to_edges(gray)
name = (f"{product_info.get('name')} "
f"{product_info.get('model')} {product_info.get('type')}")
suffix = f" ORB 특징점 {n_kp}" if method == "orb" else " 엣지 NCC (특징점 부족)"
print(f"[PatternMatcher] 등록: id={product_id} {name}{suffix}")
self._patterns[product_id] = {
"method": method,
"gray": gray, # 축소된 그레이 (시각화·fallback용)
"des": des, # ORB 디스크립터 (np.ndarray) or None
"n_kp": n_kp,
"edges": edges, # Canny 엣지 or None
"info": product_info,
"roi": roi,
}
# ── 매칭 ──────────────────────────────────────────────────────────── #
def match_all(self, image: np.ndarray) -> dict:
"""모든 등록 패턴에 대한 점수 반환 {product_id: score(0~100)}"""
gray = _resize_if_large(_to_gray(image))
ncc_edges = None # NCC용 엣지는 필요할 때만 계산
scores = {}
for pid, data in self._patterns.items():
if data.get("method") == "orb" and data.get("des") is not None:
scores[pid] = _orb_score(gray, data["des"])
else:
if ncc_edges is None:
ncc_edges = _to_edges(gray)
tmpl = data.get("edges") or _to_edges(data["gray"])
scores[pid] = _best_rotated_score(ncc_edges, tmpl)
return scores
# ── 저장 / 로드 ────────────────────────────────────────────────────── #
def save(self, path: str = _PATTERNS_PATH):
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "wb") as f:
pickle.dump({"patterns": self._patterns, "threshold": self._threshold}, f)
print(f"[PatternMatcher] 저장: {len(self._patterns)}개 → {path}")
def load(self, path: str = _PATTERNS_PATH) -> bool:
if not os.path.exists(path):
return False
try:
with open(path, "rb") as f:
data = pickle.load(f)
self._patterns = data.get("patterns", {})
self._threshold = data.get("threshold", SCORE_THRESHOLD)
# 구버전 호환: method 키 없으면 NCC로 처리
for pat in self._patterns.values():
if "method" not in pat:
pat["method"] = "ncc"
if not pat.get("edges"):
pat["edges"] = _to_edges(pat["gray"])
print(f"[PatternMatcher] 로드: {len(self._patterns)}개 ← {path}")
return True
except Exception as e:
print(f"[PatternMatcher] 로드 실패: {e}")
return False
# ── 조회 ──────────────────────────────────────────────────────────── #
def has_pattern(self, product_id: int) -> bool:
return product_id in self._patterns
def remove_pattern(self, product_id: int):
self._patterns.pop(product_id, None)
def get_product_info(self, product_id: int) -> Optional[dict]:
data = self._patterns.get(product_id)
return data["info"] if data else None
def get_pattern_summary(self, product_id: int) -> str:
"""패턴 등록 방식 및 특징점 수 요약 문자열."""
data = self._patterns.get(product_id)
if not data:
return ""
if data.get("method") == "orb":
return f"ORB 특징점 {data.get('n_kp', '?')}개 검출됨"
return "엣지 NCC 방식 (특징점 부족)"
@property
def registered_ids(self) -> list:
return list(self._patterns.keys())
@property
def score_threshold(self) -> float:
return self._threshold
# ── 공통 유틸 ──────────────────────────────────────────────────────────── #
def _to_gray(image: np.ndarray) -> np.ndarray:
if len(image.shape) == 3:
return cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
return image.copy()
def _resize_if_large(image: np.ndarray) -> np.ndarray:
"""긴 변이 _MAX_IMG_SIZE 초과 시 비율 유지 축소."""
h, w = image.shape[:2]
if max(h, w) <= _MAX_IMG_SIZE:
return image
scale = _MAX_IMG_SIZE / max(h, w)
return cv2.resize(image, (int(w * scale), int(h * scale)),
interpolation=cv2.INTER_AREA)
# ── ORB 특징점 매칭 ────────────────────────────────────────────────────── #
def _orb_score(image: np.ndarray, template_des: np.ndarray) -> float:
"""
ORB 디스크립터로 유사도 점수 계산 (0~100).
Lowe's ratio test(0.75)로 good match 필터링.
good match _GOOD_MATCH_REF개 이상이면 100점.
"""
orb = cv2.ORB_create(nfeatures=1000)
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False)
kp2, des2 = orb.detectAndCompute(image, None)
if des2 is None or len(kp2) < 2:
return 0.0
try:
matches = bf.knnMatch(template_des, des2, k=2)
except cv2.error:
return 0.0
good = [p[0] for p in matches
if len(p) == 2 and p[0].distance < 0.75 * p[1].distance]
return min(len(good) / _GOOD_MATCH_REF * 100.0, 100.0)
# ── 엣지 NCC fallback (특징점 없는 매끄러운 제품용) ───────────────────── #
_ROTATION_ANGLES = list(range(-15, 16, 5))
def _to_edges(image: np.ndarray) -> np.ndarray:
return cv2.Canny(cv2.GaussianBlur(image, (3, 3), 0), 50, 150)
def _rotate_template(image: np.ndarray, angle_deg: float) -> np.ndarray:
if abs(angle_deg) < 0.1:
return image
h, w = image.shape[:2]
cx, cy = w / 2.0, h / 2.0
M = cv2.getRotationMatrix2D((cx, cy), angle_deg, 1.0)
cos_a, sin_a = abs(M[0, 0]), abs(M[0, 1])
new_w = int(h * sin_a + w * cos_a)
new_h = int(h * cos_a + w * sin_a)
M[0, 2] += (new_w - w) / 2.0
M[1, 2] += (new_h - h) / 2.0
return cv2.warpAffine(image, M, (new_w, new_h))
def _best_rotated_score(search: np.ndarray, template: np.ndarray) -> float:
return max(_ncc_score(search, _rotate_template(template, float(a)))
for a in _ROTATION_ANGLES)
def _ncc_score(image: np.ndarray, template: np.ndarray) -> float:
h, w = image.shape[:2]
th, tw = template.shape[:2]
if th > h or tw > w:
scale = 0.8 * min(h / th, w / tw)
template = cv2.resize(template,
(max(1, int(tw * scale)), max(1, int(th * scale))),
interpolation=cv2.INTER_AREA)
th, tw = template.shape[:2]
if th > h or tw > w:
return 0.0
result = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
_, max_val, _, _ = cv2.minMaxLoc(result)
return float(max(0.0, max_val)) * 100.0

View File

@@ -0,0 +1,65 @@
timestamp,group,result,cognex_pass,basler_pass,detected_models
2026-05-06 09:23:34,A,UNKNOWN,N,Y,
2026-05-06 09:23:37,A,UNKNOWN,N,Y,
2026-05-06 09:23:39,A,UNKNOWN,N,Y,
2026-05-06 09:23:41,A,UNKNOWN,N,Y,
2026-05-06 09:23:44,A,UNKNOWN,N,Y,
2026-05-06 09:23:46,A,UNKNOWN,N,Y,
2026-05-06 09:23:48,A,UNKNOWN,N,Y,
2026-05-06 09:23:51,A,UNKNOWN,N,Y,
2026-05-06 09:23:53,A,UNKNOWN,N,Y,
2026-05-06 09:23:55,A,UNKNOWN,N,Y,
2026-05-06 09:23:58,A,UNKNOWN,N,Y,
2026-05-06 09:24:00,A,UNKNOWN,N,Y,
2026-05-06 09:24:02,A,UNKNOWN,N,Y,
2026-05-06 09:24:05,A,UNKNOWN,N,Y,
2026-05-06 09:24:07,A,UNKNOWN,N,Y,
2026-05-06 09:24:09,A,UNKNOWN,N,Y,
2026-05-06 09:24:12,A,UNKNOWN,N,Y,
2026-05-06 09:24:14,A,UNKNOWN,N,Y,
2026-05-06 09:24:16,A,UNKNOWN,N,Y,
2026-05-06 09:24:19,A,UNKNOWN,N,Y,
2026-05-06 09:24:21,A,UNKNOWN,N,Y,
2026-05-06 09:37:41,A,UNKNOWN,N,Y,
2026-05-06 09:37:44,A,UNKNOWN,N,Y,
2026-05-06 09:37:48,A,UNKNOWN,N,Y,
2026-05-06 09:37:52,A,UNKNOWN,N,Y,
2026-05-06 09:37:55,A,UNKNOWN,N,Y,
2026-05-06 09:37:59,A,UNKNOWN,N,Y,
2026-05-06 09:38:02,A,UNKNOWN,N,Y,
2026-05-06 09:38:06,A,UNKNOWN,N,Y,
2026-05-06 09:38:10,A,UNKNOWN,N,Y,
2026-05-06 09:38:13,A,UNKNOWN,N,Y,
2026-05-06 09:38:17,A,UNKNOWN,N,Y,
2026-05-06 09:38:20,A,UNKNOWN,N,Y,
2026-05-06 09:38:24,A,UNKNOWN,N,Y,
2026-05-06 09:38:28,A,UNKNOWN,N,Y,
2026-05-06 09:38:31,A,UNKNOWN,N,Y,
2026-05-06 09:38:35,A,UNKNOWN,N,Y,
2026-05-06 09:38:38,A,UNKNOWN,N,Y,
2026-05-06 10:29:02,A,UNKNOWN,N,Y,
2026-05-06 10:29:06,A,UNKNOWN,N,Y,
2026-05-06 10:29:09,A,UNKNOWN,N,Y,
2026-05-06 10:29:13,A,UNKNOWN,N,Y,
2026-05-06 10:29:17,A,UNKNOWN,N,Y,
2026-05-06 10:29:20,A,UNKNOWN,N,Y,
2026-05-06 10:29:24,A,UNKNOWN,N,Y,
2026-05-06 10:29:28,A,UNKNOWN,N,Y,
2026-05-06 10:29:31,A,UNKNOWN,N,Y,
2026-05-06 10:29:35,A,UNKNOWN,N,Y,
2026-05-06 10:29:38,A,UNKNOWN,N,Y,
2026-05-06 18:06:40,A,UNKNOWN,N,Y,
2026-05-06 18:06:43,A,UNKNOWN,N,Y,
2026-05-06 18:06:47,A,UNKNOWN,N,Y,
2026-05-06 18:07:01,A,UNKNOWN,N,Y,
2026-05-06 18:07:04,A,UNKNOWN,N,Y,
2026-05-06 18:07:07,A,UNKNOWN,N,Y,
2026-05-06 18:07:11,A,UNKNOWN,N,Y,
2026-05-06 18:07:14,A,UNKNOWN,N,Y,
2026-05-06 18:07:18,A,UNKNOWN,N,Y,
2026-05-06 18:07:21,A,UNKNOWN,N,Y,
2026-05-06 18:07:24,A,UNKNOWN,N,Y,
2026-05-06 18:07:28,A,UNKNOWN,N,Y,
2026-05-06 18:07:31,A,UNKNOWN,N,Y,
2026-05-06 18:07:34,A,UNKNOWN,N,Y,
2026-05-06 18:07:38,A,UNKNOWN,N,Y,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-05-06 09:23:34 A UNKNOWN N Y
3 2026-05-06 09:23:37 A UNKNOWN N Y
4 2026-05-06 09:23:39 A UNKNOWN N Y
5 2026-05-06 09:23:41 A UNKNOWN N Y
6 2026-05-06 09:23:44 A UNKNOWN N Y
7 2026-05-06 09:23:46 A UNKNOWN N Y
8 2026-05-06 09:23:48 A UNKNOWN N Y
9 2026-05-06 09:23:51 A UNKNOWN N Y
10 2026-05-06 09:23:53 A UNKNOWN N Y
11 2026-05-06 09:23:55 A UNKNOWN N Y
12 2026-05-06 09:23:58 A UNKNOWN N Y
13 2026-05-06 09:24:00 A UNKNOWN N Y
14 2026-05-06 09:24:02 A UNKNOWN N Y
15 2026-05-06 09:24:05 A UNKNOWN N Y
16 2026-05-06 09:24:07 A UNKNOWN N Y
17 2026-05-06 09:24:09 A UNKNOWN N Y
18 2026-05-06 09:24:12 A UNKNOWN N Y
19 2026-05-06 09:24:14 A UNKNOWN N Y
20 2026-05-06 09:24:16 A UNKNOWN N Y
21 2026-05-06 09:24:19 A UNKNOWN N Y
22 2026-05-06 09:24:21 A UNKNOWN N Y
23 2026-05-06 09:37:41 A UNKNOWN N Y
24 2026-05-06 09:37:44 A UNKNOWN N Y
25 2026-05-06 09:37:48 A UNKNOWN N Y
26 2026-05-06 09:37:52 A UNKNOWN N Y
27 2026-05-06 09:37:55 A UNKNOWN N Y
28 2026-05-06 09:37:59 A UNKNOWN N Y
29 2026-05-06 09:38:02 A UNKNOWN N Y
30 2026-05-06 09:38:06 A UNKNOWN N Y
31 2026-05-06 09:38:10 A UNKNOWN N Y
32 2026-05-06 09:38:13 A UNKNOWN N Y
33 2026-05-06 09:38:17 A UNKNOWN N Y
34 2026-05-06 09:38:20 A UNKNOWN N Y
35 2026-05-06 09:38:24 A UNKNOWN N Y
36 2026-05-06 09:38:28 A UNKNOWN N Y
37 2026-05-06 09:38:31 A UNKNOWN N Y
38 2026-05-06 09:38:35 A UNKNOWN N Y
39 2026-05-06 09:38:38 A UNKNOWN N Y
40 2026-05-06 10:29:02 A UNKNOWN N Y
41 2026-05-06 10:29:06 A UNKNOWN N Y
42 2026-05-06 10:29:09 A UNKNOWN N Y
43 2026-05-06 10:29:13 A UNKNOWN N Y
44 2026-05-06 10:29:17 A UNKNOWN N Y
45 2026-05-06 10:29:20 A UNKNOWN N Y
46 2026-05-06 10:29:24 A UNKNOWN N Y
47 2026-05-06 10:29:28 A UNKNOWN N Y
48 2026-05-06 10:29:31 A UNKNOWN N Y
49 2026-05-06 10:29:35 A UNKNOWN N Y
50 2026-05-06 10:29:38 A UNKNOWN N Y
51 2026-05-06 18:06:40 A UNKNOWN N Y
52 2026-05-06 18:06:43 A UNKNOWN N Y
53 2026-05-06 18:06:47 A UNKNOWN N Y
54 2026-05-06 18:07:01 A UNKNOWN N Y
55 2026-05-06 18:07:04 A UNKNOWN N Y
56 2026-05-06 18:07:07 A UNKNOWN N Y
57 2026-05-06 18:07:11 A UNKNOWN N Y
58 2026-05-06 18:07:14 A UNKNOWN N Y
59 2026-05-06 18:07:18 A UNKNOWN N Y
60 2026-05-06 18:07:21 A UNKNOWN N Y
61 2026-05-06 18:07:24 A UNKNOWN N Y
62 2026-05-06 18:07:28 A UNKNOWN N Y
63 2026-05-06 18:07:31 A UNKNOWN N Y
64 2026-05-06 18:07:34 A UNKNOWN N Y
65 2026-05-06 18:07:38 A UNKNOWN N Y

View File

@@ -0,0 +1,9 @@
timestamp,group,result,cognex_pass,basler_pass,detected_models
2026-05-07 17:36:51,A,UNKNOWN,N,Y,
2026-05-07 17:36:54,A,UNKNOWN,N,Y,
2026-05-07 17:36:58,A,UNKNOWN,N,Y,
2026-05-07 17:37:01,A,UNKNOWN,N,Y,
2026-05-07 17:37:04,A,UNKNOWN,N,Y,
2026-05-07 17:37:08,A,UNKNOWN,N,Y,
2026-05-07 17:37:11,A,UNKNOWN,N,Y,
2026-05-07 17:37:14,A,UNKNOWN,N,Y,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-05-07 17:36:51 A UNKNOWN N Y
3 2026-05-07 17:36:54 A UNKNOWN N Y
4 2026-05-07 17:36:58 A UNKNOWN N Y
5 2026-05-07 17:37:01 A UNKNOWN N Y
6 2026-05-07 17:37:04 A UNKNOWN N Y
7 2026-05-07 17:37:08 A UNKNOWN N Y
8 2026-05-07 17:37:11 A UNKNOWN N Y
9 2026-05-07 17:37:14 A UNKNOWN N Y

View File

@@ -0,0 +1,10 @@
timestamp,group,result,cognex_pass,basler_pass,detected_models
2026-05-08 14:33:04,A,UNKNOWN,N,Y,
2026-05-08 14:33:07,A,UNKNOWN,N,Y,
2026-05-08 14:33:11,A,UNKNOWN,N,Y,
2026-05-08 14:33:14,A,UNKNOWN,N,Y,
2026-05-08 14:33:18,A,UNKNOWN,N,Y,
2026-05-08 14:33:21,A,UNKNOWN,N,Y,
2026-05-08 14:33:25,A,UNKNOWN,N,Y,
2026-05-08 14:33:29,A,UNKNOWN,N,Y,
2026-05-08 14:33:32,A,UNKNOWN,N,Y,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-05-08 14:33:04 A UNKNOWN N Y
3 2026-05-08 14:33:07 A UNKNOWN N Y
4 2026-05-08 14:33:11 A UNKNOWN N Y
5 2026-05-08 14:33:14 A UNKNOWN N Y
6 2026-05-08 14:33:18 A UNKNOWN N Y
7 2026-05-08 14:33:21 A UNKNOWN N Y
8 2026-05-08 14:33:25 A UNKNOWN N Y
9 2026-05-08 14:33:29 A UNKNOWN N Y
10 2026-05-08 14:33:32 A UNKNOWN N Y

View File

@@ -0,0 +1,14 @@
timestamp,group,result,cognex_pass,basler_pass,detected_models
2026-05-13 09:17:49,A,UNKNOWN,N,Y,
2026-05-13 09:17:52,A,UNKNOWN,N,Y,
2026-05-13 09:17:55,A,UNKNOWN,N,Y,
2026-05-13 09:17:59,A,UNKNOWN,N,Y,
2026-05-13 09:18:02,A,UNKNOWN,N,Y,
2026-05-13 09:18:05,A,UNKNOWN,N,Y,
2026-05-13 09:18:09,A,UNKNOWN,N,Y,
2026-05-13 09:18:12,A,UNKNOWN,N,Y,
2026-05-13 09:18:15,A,UNKNOWN,N,Y,
2026-05-13 09:18:19,A,UNKNOWN,N,Y,
2026-05-13 09:18:22,A,UNKNOWN,N,Y,
2026-05-13 09:18:25,A,UNKNOWN,N,Y,
2026-05-13 09:18:29,A,UNKNOWN,N,Y,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-05-13 09:17:49 A UNKNOWN N Y
3 2026-05-13 09:17:52 A UNKNOWN N Y
4 2026-05-13 09:17:55 A UNKNOWN N Y
5 2026-05-13 09:17:59 A UNKNOWN N Y
6 2026-05-13 09:18:02 A UNKNOWN N Y
7 2026-05-13 09:18:05 A UNKNOWN N Y
8 2026-05-13 09:18:09 A UNKNOWN N Y
9 2026-05-13 09:18:12 A UNKNOWN N Y
10 2026-05-13 09:18:15 A UNKNOWN N Y
11 2026-05-13 09:18:19 A UNKNOWN N Y
12 2026-05-13 09:18:22 A UNKNOWN N Y
13 2026-05-13 09:18:25 A UNKNOWN N Y
14 2026-05-13 09:18:29 A UNKNOWN N Y

View File

@@ -0,0 +1,4 @@
timestamp,group,result,cognex_pass,basler_pass,detected_models
2026-05-20 10:10:41,A,UNKNOWN,N,Y,
2026-05-20 10:10:49,A,UNKNOWN,N,Y,
2026-05-20 10:10:53,A,UNKNOWN,N,Y,
1 timestamp group result cognex_pass basler_pass detected_models
2 2026-05-20 10:10:41 A UNKNOWN N Y
3 2026-05-20 10:10:49 A UNKNOWN N Y
4 2026-05-20 10:10:53 A UNKNOWN N Y

541
logs/timing/2026-05-06.csv Normal file
View File

@@ -0,0 +1,541 @@
timestamp,seq,event,elapsed_ms,detail
10:28:58.280,1,cycle_start,0.0,group=A belt_delay=3.33s
10:28:58.280,1,cognex_trigger_send,22.6,
10:28:58.295,1,cognex_trigger_ok,29.2,
10:28:59.302,1,cognex_ftp_start,1031.6,
10:29:00.048,1,cognex_ftp_done,1777.7,3686454bytes
10:29:00.048,1,cognex_patmax_start,1779.2,
10:29:00.048,1,cognex_patmax_done,1791.1,
10:29:01.605,1,basler_capture_start,3333.9,
10:29:01.854,1,basler_capture_done,3591.2,"(4504, 4504)"
10:29:02.636,1,cognex_join_wait,4376.1,
10:29:02.636,1,cognex_join_done,4377.7,
10:29:02.665,1,cycle_done,4394.5,result=FAIL cognex=FAIL basler=PASS
10:29:02.669,2,cycle_start,0.0,group=A belt_delay=3.33s
10:29:02.671,2,cognex_trigger_send,2.9,
10:29:02.672,2,cognex_trigger_ok,9.4,
10:29:03.681,2,cognex_ftp_start,1011.7,
10:29:04.413,2,cognex_ftp_done,1744.1,3686454bytes
10:29:04.414,2,cognex_patmax_start,1745.6,
10:29:04.414,2,cognex_patmax_done,1757.2,
10:29:06.003,2,basler_capture_start,3333.8,
10:29:06.252,2,basler_capture_done,3590.7,"(4504, 4504)"
10:29:06.252,2,cognex_join_wait,3597.3,
10:29:06.284,2,cognex_join_done,3614.1,
10:29:06.287,2,cycle_done,3617.9,result=FAIL cognex=FAIL basler=PASS
10:29:06.290,3,cycle_start,0.0,group=A belt_delay=3.33s
10:29:06.290,3,cognex_trigger_send,2.5,
10:29:06.299,3,cognex_trigger_ok,9.0,
10:29:07.302,3,cognex_ftp_start,1011.4,
10:29:08.031,3,cognex_ftp_done,1741.0,3686454bytes
10:29:08.031,3,cognex_patmax_start,1742.5,
10:29:08.031,3,cognex_patmax_done,1754.0,
10:29:09.625,3,basler_capture_start,3333.9,
10:29:09.869,3,basler_capture_done,3589.8,"(4504, 4504)"
10:29:09.884,3,cognex_join_wait,3596.7,
10:29:09.903,3,cognex_join_done,3614.2,
10:29:09.903,3,cycle_done,3617.5,result=FAIL cognex=FAIL basler=PASS
10:29:09.903,4,cycle_start,0.0,group=A belt_delay=3.33s
10:29:09.914,4,cognex_trigger_send,2.3,
10:29:09.920,4,cognex_trigger_ok,8.7,
10:29:10.923,4,cognex_ftp_start,1010.9,
10:29:11.648,4,cognex_ftp_done,1736.6,3686454bytes
10:29:11.648,4,cognex_patmax_start,1738.1,
10:29:11.648,4,cognex_patmax_done,1749.6,
10:29:13.246,4,basler_capture_start,3334.3,
10:29:13.500,4,basler_capture_done,3590.8,"(4504, 4504)"
10:29:13.500,4,cognex_join_wait,3597.8,
10:29:13.527,4,cognex_join_done,3608.0,
10:29:13.527,4,cycle_done,3618.5,result=FAIL cognex=FAIL basler=PASS
10:29:13.534,5,cycle_start,0.0,group=A belt_delay=3.33s
10:29:13.536,5,cognex_trigger_send,2.4,
10:29:13.539,5,cognex_trigger_ok,9.2,
10:29:14.546,5,cognex_ftp_start,1011.6,
10:29:15.264,5,cognex_ftp_done,1744.0,3686454bytes
10:29:15.264,5,cognex_patmax_start,1745.6,
10:29:15.281,5,cognex_patmax_done,1757.1,
10:29:16.868,5,basler_capture_start,3334.1,
10:29:17.119,5,basler_capture_done,3591.4,"(4504, 4504)"
10:29:17.119,5,cognex_join_wait,3598.4,
10:29:17.135,5,cognex_join_done,3602.3,
10:29:17.153,5,cycle_done,3619.1,result=FAIL cognex=FAIL basler=PASS
10:29:17.157,6,cycle_start,0.0,group=A belt_delay=3.33s
10:29:17.157,6,cognex_trigger_send,2.4,
10:29:17.166,6,cognex_trigger_ok,9.2,
10:29:18.169,6,cognex_ftp_start,1011.5,
10:29:18.914,6,cognex_ftp_done,1758.7,3686454bytes
10:29:18.914,6,cognex_patmax_start,1760.2,
10:29:18.914,6,cognex_patmax_done,1772.1,
10:29:20.491,6,basler_capture_start,3334.0,
10:29:20.739,6,basler_capture_done,3591.4,"(4504, 4504)"
10:29:20.755,6,cognex_join_wait,3598.5,
10:29:20.774,6,cognex_join_done,3615.4,
10:29:20.776,6,cycle_done,3619.0,result=FAIL cognex=FAIL basler=PASS
10:29:20.777,7,cycle_start,0.0,group=A belt_delay=3.33s
10:29:20.782,7,cognex_trigger_send,2.4,
10:29:20.788,7,cognex_trigger_ok,9.0,
10:29:21.791,7,cognex_ftp_start,1010.8,
10:29:22.514,7,cognex_ftp_done,1749.6,3686454bytes
10:29:22.531,7,cognex_patmax_start,1751.2,
10:29:22.531,7,cognex_patmax_done,1760.6,
10:29:24.113,7,basler_capture_start,3333.5,
10:29:24.368,7,basler_capture_done,3590.6,"(4504, 4504)"
10:29:24.368,7,cognex_join_wait,3597.8,
10:29:24.368,7,cognex_join_done,3599.4,
10:29:24.390,7,cycle_done,3615.9,result=FAIL cognex=FAIL basler=PASS
10:29:24.399,8,cycle_start,0.0,group=A belt_delay=3.33s
10:29:24.401,8,cognex_trigger_send,1.9,
10:29:24.405,8,cognex_trigger_ok,8.4,
10:29:25.410,8,cognex_ftp_start,1010.6,
10:29:26.165,8,cognex_ftp_done,1768.0,3686454bytes
10:29:26.165,8,cognex_patmax_start,1769.5,
10:29:26.180,8,cognex_patmax_done,1780.9,
10:29:27.733,8,basler_capture_start,3333.7,
10:29:27.985,8,basler_capture_done,3591.1,"(4504, 4504)"
10:29:27.985,8,cognex_join_wait,3598.1,
10:29:28.001,8,cognex_join_done,3601.9,
10:29:28.017,8,cycle_done,3618.3,result=FAIL cognex=FAIL basler=PASS
10:29:28.022,9,cycle_start,0.0,group=A belt_delay=3.33s
10:29:28.023,9,cognex_trigger_send,2.4,
10:29:28.031,9,cognex_trigger_ok,9.0,
10:29:29.033,9,cognex_ftp_start,1011.4,
10:29:29.748,9,cognex_ftp_done,1741.5,3686454bytes
10:29:29.765,9,cognex_patmax_start,1743.1,
10:29:29.765,9,cognex_patmax_done,1754.6,
10:29:31.355,9,basler_capture_start,3333.4,
10:29:31.603,9,basler_capture_done,3589.9,"(4504, 4504)"
10:29:31.619,9,cognex_join_wait,3597.0,
10:29:31.638,9,cognex_join_done,3614.9,
10:29:31.640,9,cycle_done,3619.4,result=FAIL cognex=FAIL basler=PASS
10:29:31.640,10,cycle_start,0.0,group=A belt_delay=3.33s
10:29:31.640,10,cognex_trigger_send,2.2,
10:29:31.651,10,cognex_trigger_ok,8.7,
10:29:32.655,10,cognex_ftp_start,1010.9,
10:29:33.381,10,cognex_ftp_done,1737.0,3686454bytes
10:29:33.381,10,cognex_patmax_start,1738.5,
10:29:33.381,10,cognex_patmax_done,1750.2,
10:29:34.978,10,basler_capture_start,3334.0,
10:29:35.234,10,basler_capture_done,3591.0,"(4504, 4504)"
10:29:35.234,10,cognex_join_wait,3598.4,
10:29:35.255,10,cognex_join_done,3608.7,
10:29:35.255,10,cycle_done,3619.3,result=FAIL cognex=FAIL basler=PASS
10:29:35.268,11,cycle_start,0.0,group=A belt_delay=3.33s
10:29:35.268,11,cognex_trigger_send,2.4,
10:29:35.268,11,cognex_trigger_ok,8.9,
10:29:36.279,11,cognex_ftp_start,1011.4,
10:29:37.031,11,cognex_ftp_done,1763.6,3686454bytes
10:29:37.031,11,cognex_patmax_start,1765.1,
10:29:37.031,11,cognex_patmax_done,1776.5,
10:29:38.601,11,basler_capture_start,3333.6,
10:29:38.855,11,basler_capture_done,3590.7,"(4504, 4504)"
10:29:38.855,11,cognex_join_wait,3597.8,
10:29:38.885,11,cognex_join_done,3608.3,
10:29:38.888,11,cycle_done,3619.7,result=FAIL cognex=FAIL basler=PASS
18:06:16.958,1,cycle_start,0.0,group=A belt_delay=3.33s
18:06:16.958,1,cognex_trigger_send,3.0,
18:06:16.974,1,cognex_trigger_ok,9.5,
18:06:17.977,1,cognex_ftp_start,1011.8,
18:06:18.704,1,cognex_ftp_done,1738.6,3686454bytes
18:06:18.705,1,cognex_patmax_start,1739.9,
18:06:18.832,1,cognex_patmax_done,1866.3,
18:06:20.299,1,basler_capture_start,3333.8,
18:06:20.302,1,basler_capture_done,3336.9,failed
18:06:20.304,1,cognex_join_wait,3338.9,
18:06:20.306,1,cognex_join_done,3340.9,
18:06:20.309,1,cycle_done,3343.2,result=PASS cognex=PASS basler=PASS
18:06:20.311,2,cycle_start,0.0,group=A belt_delay=3.33s
18:06:20.313,2,cognex_trigger_send,2.3,
18:06:20.320,2,cognex_trigger_ok,8.8,
18:06:21.323,2,cognex_ftp_start,1011.6,
18:06:22.040,2,cognex_ftp_done,1743.5,3686454bytes
18:06:22.056,2,cognex_patmax_start,1745.1,
18:06:22.162,2,cognex_patmax_done,1850.8,
18:06:23.645,2,basler_capture_start,3333.7,
18:06:23.645,2,basler_capture_done,3336.5,failed
18:06:23.645,2,cognex_join_wait,3338.4,
18:06:23.645,2,cognex_join_done,3340.4,
18:06:23.645,2,cycle_done,3342.3,result=PASS cognex=PASS basler=PASS
18:06:23.645,3,cycle_start,0.0,group=A belt_delay=3.33s
18:06:23.645,3,cognex_trigger_send,2.2,
18:06:23.664,3,cognex_trigger_ok,8.6,
18:06:24.666,3,cognex_ftp_start,1010.5,
18:06:25.406,3,cognex_ftp_done,1758.8,3686454bytes
18:06:25.406,3,cognex_patmax_start,1760.2,
18:06:25.522,3,cognex_patmax_done,1866.4,
18:06:26.990,3,basler_capture_start,3334.1,
18:06:26.992,3,basler_capture_done,3337.1,failed
18:06:26.992,3,cognex_join_wait,3338.5,
18:06:26.992,3,cognex_join_done,3339.7,
18:06:26.992,3,cycle_done,3341.0,result=FAIL cognex=FAIL basler=PASS
18:06:26.992,4,cycle_start,0.0,group=A belt_delay=3.33s
18:06:26.992,4,cognex_trigger_send,1.7,
18:06:27.005,4,cognex_trigger_ok,7.7,
18:06:28.009,4,cognex_ftp_start,1010.2,
18:06:28.740,4,cognex_ftp_done,1756.4,3686454bytes
18:06:28.756,4,cognex_patmax_start,1758.0,
18:06:28.861,4,cognex_patmax_done,1863.5,
18:06:30.332,4,basler_capture_start,3333.5,
18:06:30.332,4,basler_capture_done,3336.2,failed
18:06:30.332,4,cognex_join_wait,3338.1,
18:06:30.332,4,cognex_join_done,3340.0,
18:06:30.332,4,cycle_done,3342.0,result=FAIL cognex=FAIL basler=PASS
18:06:30.332,5,cycle_start,0.0,group=A belt_delay=3.33s
18:06:30.344,5,cognex_trigger_send,2.4,
18:06:30.351,5,cognex_trigger_ok,8.7,
18:06:31.354,5,cognex_ftp_start,1011.5,
18:06:32.106,5,cognex_ftp_done,1764.4,3686454bytes
18:06:32.106,5,cognex_patmax_start,1765.8,
18:06:32.212,5,cognex_patmax_done,1869.6,
18:06:33.676,5,basler_capture_start,3333.6,
18:06:33.678,5,basler_capture_done,3336.7,failed
18:06:33.678,5,cognex_join_wait,3338.6,
18:06:33.678,5,cognex_join_done,3340.5,
18:06:33.678,5,cycle_done,3342.4,result=FAIL cognex=FAIL basler=PASS
18:06:33.678,6,cycle_start,0.0,group=A belt_delay=3.33s
18:06:33.678,6,cognex_trigger_send,2.4,
18:06:33.697,6,cognex_trigger_ok,8.9,
18:06:34.699,6,cognex_ftp_start,1011.8,
18:06:35.456,6,cognex_ftp_done,1769.3,3686454bytes
18:06:35.456,6,cognex_patmax_start,1770.9,
18:06:35.565,6,cognex_patmax_done,1878.1,
18:06:37.021,6,basler_capture_start,3333.9,
18:06:37.021,6,basler_capture_done,3337.3,failed
18:06:37.021,6,cognex_join_wait,3339.3,
18:06:37.027,6,cognex_join_done,3341.2,
18:06:37.027,6,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
18:06:37.027,7,cycle_start,0.0,group=A belt_delay=3.33s
18:06:37.027,7,cognex_trigger_send,2.4,
18:06:37.041,7,cognex_trigger_ok,8.9,
18:06:38.044,7,cognex_ftp_start,1011.4,
18:06:38.772,7,cognex_ftp_done,1739.0,3686454bytes
18:06:38.773,7,cognex_patmax_start,1740.5,
18:06:38.880,7,cognex_patmax_done,1847.0,
18:06:40.367,7,basler_capture_start,3334.3,
18:06:40.367,7,basler_capture_done,3337.4,failed
18:06:40.367,7,cognex_join_wait,3339.3,
18:06:40.367,7,cognex_join_done,3341.3,
18:06:40.375,7,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
18:06:40.375,8,cycle_start,0.0,group=A belt_delay=3.33s
18:06:40.375,8,cognex_trigger_send,2.3,
18:06:40.375,8,cognex_trigger_ok,8.8,
18:06:41.391,8,cognex_ftp_start,1011.2,
18:06:42.139,8,cognex_ftp_done,1759.5,3686454bytes
18:06:42.140,8,cognex_patmax_start,1761.2,
18:06:42.247,8,cognex_patmax_done,1867.4,
18:06:43.713,8,basler_capture_start,3333.6,
18:06:43.713,8,basler_capture_done,3336.5,failed
18:06:43.713,8,cognex_join_wait,3338.5,
18:06:43.713,8,cognex_join_done,3340.5,
18:06:43.713,8,cycle_done,3342.4,result=FAIL cognex=FAIL basler=PASS
18:06:43.713,9,cycle_start,0.0,group=A belt_delay=3.33s
18:06:43.727,9,cognex_trigger_send,2.2,
18:06:43.727,9,cognex_trigger_ok,8.8,
18:06:44.737,9,cognex_ftp_start,1011.0,
18:06:45.473,9,cognex_ftp_done,1760.9,3686454bytes
18:06:45.473,9,cognex_patmax_start,1762.4,
18:06:45.593,9,cognex_patmax_done,1867.3,
18:06:47.060,9,basler_capture_start,3334.0,
18:06:47.060,9,basler_capture_done,3337.1,failed
18:06:47.060,9,cognex_join_wait,3339.1,
18:06:47.060,9,cognex_join_done,3341.0,
18:06:47.060,9,cycle_done,3342.9,result=FAIL cognex=FAIL basler=PASS
18:06:57.940,10,cycle_start,0.0,group=A belt_delay=3.33s
18:06:57.940,10,cognex_trigger_send,3.7,
18:06:57.945,10,cognex_trigger_ok,10.4,
18:06:58.953,10,cognex_ftp_start,1012.9,
18:06:59.690,10,cognex_ftp_done,1765.5,3686454bytes
18:06:59.706,10,cognex_patmax_start,1767.1,
18:06:59.812,10,cognex_patmax_done,1872.7,
18:07:01.274,10,basler_capture_start,3334.1,
18:07:01.274,10,basler_capture_done,3337.2,failed
18:07:01.278,10,cognex_join_wait,3338.5,
18:07:01.278,10,cognex_join_done,3339.8,
18:07:01.278,10,cycle_done,3341.1,result=FAIL cognex=FAIL basler=PASS
18:07:01.278,11,cycle_start,0.0,group=A belt_delay=3.33s
18:07:01.278,11,cognex_trigger_send,1.6,
18:07:01.278,11,cognex_trigger_ok,7.6,
18:07:02.293,11,cognex_ftp_start,1009.0,
18:07:03.033,11,cognex_ftp_done,1749.5,3686454bytes
18:07:03.034,11,cognex_patmax_start,1750.9,
18:07:03.137,11,cognex_patmax_done,1853.5,
18:07:04.618,11,basler_capture_start,3334.1,
18:07:04.618,11,basler_capture_done,3337.6,failed
18:07:04.618,11,cognex_join_wait,3339.5,
18:07:04.618,11,cognex_join_done,3341.5,
18:07:04.618,11,cycle_done,3343.4,result=FAIL cognex=FAIL basler=PASS
18:07:04.628,12,cycle_start,0.0,group=A belt_delay=3.33s
18:07:04.628,12,cognex_trigger_send,2.2,
18:07:04.628,12,cognex_trigger_ok,8.5,
18:07:05.641,12,cognex_ftp_start,1010.7,
18:07:06.373,12,cognex_ftp_done,1744.0,3686454bytes
18:07:06.373,12,cognex_patmax_start,1745.4,
18:07:06.473,12,cognex_patmax_done,1852.2,
18:07:07.964,12,basler_capture_start,3333.9,
18:07:07.964,12,basler_capture_done,3337.2,failed
18:07:07.964,12,cognex_join_wait,3339.2,
18:07:07.964,12,cognex_join_done,3341.0,
18:07:07.964,12,cycle_done,3343.0,result=FAIL cognex=FAIL basler=PASS
18:07:07.976,13,cycle_start,0.0,group=A belt_delay=3.33s
18:07:07.976,13,cognex_trigger_send,2.2,
18:07:07.976,13,cognex_trigger_ok,8.8,
18:07:08.988,13,cognex_ftp_start,1010.9,
18:07:09.723,13,cognex_ftp_done,1761.7,3686454bytes
18:07:09.740,13,cognex_patmax_start,1763.3,
18:07:09.842,13,cognex_patmax_done,1865.8,
18:07:11.311,13,basler_capture_start,3334.1,
18:07:11.312,13,basler_capture_done,3337.4,failed
18:07:11.312,13,cognex_join_wait,3339.4,
18:07:11.312,13,cognex_join_done,3341.2,
18:07:11.312,13,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
18:07:11.312,14,cycle_start,0.0,group=A belt_delay=3.33s
18:07:11.312,14,cognex_trigger_send,2.2,
18:07:11.327,14,cognex_trigger_ok,8.7,
18:07:12.334,14,cognex_ftp_start,1010.9,
18:07:13.090,14,cognex_ftp_done,1767.6,3686454bytes
18:07:13.090,14,cognex_patmax_start,1769.1,
18:07:13.197,14,cognex_patmax_done,1873.5,
18:07:14.657,14,basler_capture_start,3333.9,
18:07:14.657,14,basler_capture_done,3336.8,failed
18:07:14.662,14,cognex_join_wait,3339.6,
18:07:14.662,14,cognex_join_done,3341.5,
18:07:14.662,14,cycle_done,3343.4,result=FAIL cognex=FAIL basler=PASS
18:07:14.662,15,cycle_start,0.0,group=A belt_delay=3.33s
18:07:14.662,15,cognex_trigger_send,2.2,
18:07:14.678,15,cognex_trigger_ok,8.8,
18:07:15.681,15,cognex_ftp_start,1010.9,
18:07:16.423,15,cognex_ftp_done,1754.9,3686454bytes
18:07:16.423,15,cognex_patmax_start,1756.4,
18:07:16.523,15,cognex_patmax_done,1861.8,
18:07:18.004,15,basler_capture_start,3333.8,
18:07:18.004,15,basler_capture_done,3337.1,failed
18:07:18.004,15,cognex_join_wait,3339.1,
18:07:18.004,15,cognex_join_done,3341.0,
18:07:18.011,15,cycle_done,3342.9,result=FAIL cognex=FAIL basler=PASS
18:07:18.011,16,cycle_start,0.0,group=A belt_delay=3.33s
18:07:18.011,16,cognex_trigger_send,2.2,
18:07:18.011,16,cognex_trigger_ok,8.5,
18:07:19.027,16,cognex_ftp_start,1010.8,
18:07:19.757,16,cognex_ftp_done,1755.9,3686454bytes
18:07:19.773,16,cognex_patmax_start,1757.5,
18:07:19.873,16,cognex_patmax_done,1864.6,
18:07:21.350,16,basler_capture_start,3333.7,
18:07:21.350,16,basler_capture_done,3336.8,failed
18:07:21.350,16,cognex_join_wait,3338.8,
18:07:21.350,16,cognex_join_done,3340.7,
18:07:21.350,16,cycle_done,3342.5,result=FAIL cognex=FAIL basler=PASS
18:07:21.360,17,cycle_start,0.0,group=A belt_delay=3.33s
18:07:21.360,17,cognex_trigger_send,2.2,
18:07:21.360,17,cognex_trigger_ok,8.6,
18:07:22.373,17,cognex_ftp_start,1010.8,
18:07:23.123,17,cognex_ftp_done,1761.5,3686454bytes
18:07:23.123,17,cognex_patmax_start,1763.0,
18:07:23.223,17,cognex_patmax_done,1869.6,
18:07:24.696,17,basler_capture_start,3333.7,
18:07:24.696,17,basler_capture_done,3337.0,failed
18:07:24.696,17,cognex_join_wait,3338.4,
18:07:24.696,17,cognex_join_done,3339.6,
18:07:24.696,17,cycle_done,3340.9,result=FAIL cognex=FAIL basler=PASS
18:07:24.696,18,cycle_start,0.0,group=A belt_delay=3.33s
18:07:24.696,18,cognex_trigger_send,1.6,
18:07:24.711,18,cognex_trigger_ok,7.3,
18:07:25.714,18,cognex_ftp_start,1008.8,
18:07:26.440,18,cognex_ftp_done,1735.4,3686454bytes
18:07:26.440,18,cognex_patmax_start,1736.9,
18:07:26.540,18,cognex_patmax_done,1842.2,
18:07:28.040,18,basler_capture_start,3334.1,
18:07:28.042,18,basler_capture_done,3337.6,failed
18:07:28.042,18,cognex_join_wait,3340.2,
18:07:28.042,18,cognex_join_done,3342.2,
18:07:28.042,18,cycle_done,3344.1,result=FAIL cognex=FAIL basler=PASS
18:07:28.042,19,cycle_start,0.0,group=A belt_delay=3.33s
18:07:28.042,19,cognex_trigger_send,2.2,
18:07:28.057,19,cognex_trigger_ok,8.7,
18:07:29.064,19,cognex_ftp_start,1011.0,
18:07:29.823,19,cognex_ftp_done,1771.2,3686454bytes
18:07:29.823,19,cognex_patmax_start,1772.7,
18:07:29.923,19,cognex_patmax_done,1879.1,
18:07:31.387,19,basler_capture_start,3334.3,
18:07:31.387,19,basler_capture_done,3337.4,failed
18:07:31.387,19,cognex_join_wait,3339.9,
18:07:31.395,19,cognex_join_done,3341.9,
18:07:31.395,19,cycle_done,3343.9,result=FAIL cognex=FAIL basler=PASS
18:07:31.395,20,cycle_start,0.0,group=A belt_delay=3.33s
18:07:31.395,20,cognex_trigger_send,2.3,
18:07:31.395,20,cognex_trigger_ok,8.6,
18:07:32.412,20,cognex_ftp_start,1010.9,
18:07:33.123,20,cognex_ftp_done,1737.1,3686454bytes
18:07:33.139,20,cognex_patmax_start,1738.7,
18:07:33.240,20,cognex_patmax_done,1840.1,
18:07:34.734,20,basler_capture_start,3333.5,
18:07:34.734,20,basler_capture_done,3336.8,failed
18:07:34.734,20,cognex_join_wait,3339.1,
18:07:34.742,20,cognex_join_done,3341.1,
18:07:34.742,20,cycle_done,3343.1,result=FAIL cognex=FAIL basler=PASS
18:07:34.742,21,cycle_start,0.0,group=A belt_delay=3.33s
18:07:34.742,21,cognex_trigger_send,2.2,
18:07:34.742,21,cognex_trigger_ok,8.8,
18:07:35.758,21,cognex_ftp_start,1011.1,
18:07:36.490,21,cognex_ftp_done,1756.5,3686454bytes
18:07:36.490,21,cognex_patmax_start,1758.0,
18:07:36.610,21,cognex_patmax_done,1863.5,
18:07:38.081,21,basler_capture_start,3333.8,
18:07:38.081,21,basler_capture_done,3336.6,failed
18:07:38.081,21,cognex_join_wait,3338.6,
18:07:38.081,21,cognex_join_done,3340.5,
18:07:38.081,21,cycle_done,3342.4,result=FAIL cognex=FAIL basler=PASS
18:09:47.668,22,cycle_start,0.0,group=A belt_delay=3.33s
18:09:47.670,22,cognex_trigger_send,3.4,
18:09:47.670,22,cognex_trigger_ok,10.0,
18:09:48.680,22,cognex_ftp_start,1012.4,
18:09:49.416,22,cognex_ftp_done,1754.5,3686454bytes
18:09:49.416,22,cognex_patmax_start,1755.9,
18:09:49.551,22,cognex_patmax_done,1896.3,
18:09:51.002,22,basler_capture_start,3334.0,
18:09:51.002,22,basler_capture_done,3337.1,failed
18:09:51.006,22,cognex_join_wait,3339.1,
18:09:51.006,22,cognex_join_done,3341.1,
18:09:51.006,22,cycle_done,3343.0,result=PASS cognex=PASS basler=PASS
18:09:51.006,23,cycle_start,0.0,group=A belt_delay=3.33s
18:09:51.006,23,cognex_trigger_send,2.4,
18:09:51.022,23,cognex_trigger_ok,9.0,
18:09:52.025,23,cognex_ftp_start,1011.6,
18:09:52.767,23,cognex_ftp_done,1758.5,3686454bytes
18:09:52.767,23,cognex_patmax_start,1760.0,
18:09:52.902,23,cognex_patmax_done,1897.6,
18:09:54.347,23,basler_capture_start,3333.4,
18:09:54.347,23,basler_capture_done,3336.3,failed
18:09:54.347,23,cognex_join_wait,3338.8,
18:09:54.347,23,cognex_join_done,3340.7,
18:09:54.347,23,cycle_done,3342.6,result=PASS cognex=PASS basler=PASS
18:11:41.743,24,cycle_start,0.0,group=A belt_delay=3.33s
18:11:41.745,24,cognex_trigger_send,3.5,
18:11:41.745,24,cognex_trigger_ok,10.3,
18:11:42.757,24,cognex_ftp_start,1013.2,
18:11:43.483,24,cognex_ftp_done,1749.5,3686454bytes
18:11:43.483,24,cognex_patmax_start,1751.0,
18:11:43.621,24,cognex_patmax_done,1885.8,
18:11:45.078,24,basler_capture_start,3334.2,
18:11:45.078,24,basler_capture_done,3337.3,failed
18:11:45.078,24,cognex_join_wait,3338.6,
18:11:45.078,24,cognex_join_done,3339.8,
18:11:45.078,24,cycle_done,3341.4,result=PASS cognex=PASS basler=PASS
18:11:45.078,25,cycle_start,0.0,group=A belt_delay=3.33s
18:11:45.078,25,cognex_trigger_send,1.7,
18:11:45.093,25,cognex_trigger_ok,7.6,
18:11:46.096,25,cognex_ftp_start,1009.6,
18:11:46.832,25,cognex_ftp_done,1755.0,3686454bytes
18:11:46.832,25,cognex_patmax_start,1756.5,
18:11:46.970,25,cognex_patmax_done,1884.8,
18:11:48.421,25,basler_capture_start,3334.0,
18:11:48.421,25,basler_capture_done,3337.0,failed
18:11:48.424,25,cognex_join_wait,3339.0,
18:11:48.424,25,cognex_join_done,3340.9,
18:11:48.424,25,cycle_done,3342.7,result=PASS cognex=PASS basler=PASS
18:11:48.424,26,cycle_start,0.0,group=A belt_delay=3.33s
18:11:48.424,26,cognex_trigger_send,2.3,
18:11:48.440,26,cognex_trigger_ok,8.8,
18:11:49.443,26,cognex_ftp_start,1011.2,
18:11:50.182,26,cognex_ftp_done,1758.9,3686454bytes
18:11:50.182,26,cognex_patmax_start,1760.4,
18:11:50.320,26,cognex_patmax_done,1888.4,
18:11:51.765,26,basler_capture_start,3333.7,
18:11:51.765,26,basler_capture_done,3337.0,failed
18:11:51.769,26,cognex_join_wait,3338.9,
18:11:51.769,26,cognex_join_done,3340.8,
18:11:51.774,26,cycle_done,3342.6,result=PASS cognex=PASS basler=PASS
18:11:51.774,27,cycle_start,0.0,group=A belt_delay=3.33s
18:11:51.774,27,cognex_trigger_send,2.3,
18:11:51.785,27,cognex_trigger_ok,8.8,
18:11:52.788,27,cognex_ftp_start,1011.7,
18:11:53.520,27,cognex_ftp_done,1749.0,3686454bytes
18:11:53.520,27,cognex_patmax_start,1750.6,
18:11:53.640,27,cognex_patmax_done,1878.4,
18:11:55.111,27,basler_capture_start,3334.0,
18:11:55.111,27,basler_capture_done,3337.1,failed
18:11:55.111,27,cognex_join_wait,3339.0,
18:11:55.111,27,cognex_join_done,3340.9,
18:11:55.111,27,cycle_done,3342.8,result=PASS cognex=PASS basler=PASS
18:11:55.111,28,cycle_start,0.0,group=A belt_delay=3.33s
18:11:55.111,28,cognex_trigger_send,2.3,
18:11:55.130,28,cognex_trigger_ok,8.8,
18:11:56.133,28,cognex_ftp_start,1011.6,
18:11:56.850,28,cognex_ftp_done,1741.1,3686454bytes
18:11:56.850,28,cognex_patmax_start,1742.5,
18:11:56.992,28,cognex_patmax_done,1872.0,
18:11:58.456,28,basler_capture_start,3334.1,
18:11:58.456,28,basler_capture_done,3337.1,failed
18:11:58.456,28,cognex_join_wait,3339.0,
18:11:58.461,28,cognex_join_done,3340.8,
18:11:58.461,28,cycle_done,3342.8,result=PASS cognex=PASS basler=PASS
18:11:58.461,29,cycle_start,0.0,group=A belt_delay=3.33s
18:11:58.461,29,cognex_trigger_send,2.3,
18:11:58.475,29,cognex_trigger_ok,8.9,
18:11:59.478,29,cognex_ftp_start,1011.7,
18:12:00.219,29,cognex_ftp_done,1755.7,3686454bytes
18:12:00.219,29,cognex_patmax_start,1757.2,
18:12:00.354,29,cognex_patmax_done,1886.8,
18:12:01.801,29,basler_capture_start,3333.8,
18:12:01.801,29,basler_capture_done,3337.1,failed
18:12:01.801,29,cognex_join_wait,3339.0,
18:12:01.801,29,cognex_join_done,3340.9,
18:12:01.809,29,cycle_done,3342.8,result=PASS cognex=PASS basler=PASS
18:12:01.809,30,cycle_start,0.0,group=A belt_delay=3.33s
18:12:01.809,30,cognex_trigger_send,2.3,
18:12:01.820,30,cognex_trigger_ok,8.8,
18:12:02.823,30,cognex_ftp_start,1011.4,
18:12:03.551,30,cognex_ftp_done,1749.0,3686454bytes
18:12:03.551,30,cognex_patmax_start,1750.5,
18:12:03.686,30,cognex_patmax_done,1876.1,
18:12:05.146,30,basler_capture_start,3334.1,
18:12:05.146,30,basler_capture_done,3337.2,failed
18:12:05.146,30,cognex_join_wait,3339.3,
18:12:05.146,30,cognex_join_done,3341.1,
18:12:05.146,30,cycle_done,3342.9,result=PASS cognex=PASS basler=PASS
18:12:05.157,31,cycle_start,0.0,group=A belt_delay=3.33s
18:12:05.157,31,cognex_trigger_send,2.3,
18:12:05.165,31,cognex_trigger_ok,8.7,
18:12:06.169,31,cognex_ftp_start,1011.4,
18:12:06.886,31,cognex_ftp_done,1739.1,3686454bytes
18:12:06.886,31,cognex_patmax_start,1740.6,
18:12:07.020,31,cognex_patmax_done,1868.9,
18:12:08.491,31,basler_capture_start,3333.6,
18:12:08.491,31,basler_capture_done,3336.5,failed
18:12:08.491,31,cognex_join_wait,3338.3,
18:12:08.491,31,cognex_join_done,3339.5,
18:12:08.491,31,cycle_done,3340.8,result=PASS cognex=PASS basler=PASS
18:12:08.491,32,cycle_start,0.0,group=A belt_delay=3.33s
18:12:08.491,32,cognex_trigger_send,1.7,
18:12:08.507,32,cognex_trigger_ok,7.6,
18:12:09.509,32,cognex_ftp_start,1009.6,
18:12:10.234,32,cognex_ftp_done,1743.2,3686454bytes
18:12:10.234,32,cognex_patmax_start,1744.6,
18:12:10.370,32,cognex_patmax_done,1872.5,
18:12:11.833,32,basler_capture_start,3333.5,
18:12:11.833,32,basler_capture_done,3336.8,failed
18:12:11.833,32,cognex_join_wait,3338.8,
18:12:11.833,32,cognex_join_done,3340.6,
18:12:11.842,32,cycle_done,3342.5,result=PASS cognex=PASS basler=PASS
18:12:11.842,33,cycle_start,0.0,group=A belt_delay=3.33s
18:12:11.842,33,cognex_trigger_send,2.4,
18:12:11.854,33,cognex_trigger_ok,9.2,
18:12:12.856,33,cognex_ftp_start,1011.9,
18:12:13.586,33,cognex_ftp_done,1749.6,3686454bytes
18:12:13.586,33,cognex_patmax_start,1751.1,
18:12:13.704,33,cognex_patmax_done,1869.1,
18:12:15.179,33,basler_capture_start,3334.2,
18:12:15.179,33,basler_capture_done,3337.3,failed
18:12:15.179,33,cognex_join_wait,3339.3,
18:12:15.179,33,cognex_join_done,3341.2,
18:12:15.179,33,cycle_done,3343.1,result=PASS cognex=PASS basler=PASS
18:12:15.179,34,cycle_start,0.0,group=A belt_delay=3.33s
18:12:15.190,34,cognex_trigger_send,2.5,
18:12:15.198,34,cognex_trigger_ok,9.1,
18:12:16.201,34,cognex_ftp_start,1011.3,
18:12:16.920,34,cognex_ftp_done,1738.0,3686454bytes
18:12:16.920,34,cognex_patmax_start,1739.4,
18:12:17.053,34,cognex_patmax_done,1868.0,
18:12:18.524,34,basler_capture_start,3333.6,
18:12:18.524,34,basler_capture_done,3336.8,failed
18:12:18.524,34,cognex_join_wait,3338.7,
18:12:18.524,34,cognex_join_done,3340.6,
18:12:18.524,34,cycle_done,3342.5,result=PASS cognex=PASS basler=PASS
1 timestamp seq event elapsed_ms detail
2 10:28:58.280 1 cycle_start 0.0 group=A belt_delay=3.33s
3 10:28:58.280 1 cognex_trigger_send 22.6
4 10:28:58.295 1 cognex_trigger_ok 29.2
5 10:28:59.302 1 cognex_ftp_start 1031.6
6 10:29:00.048 1 cognex_ftp_done 1777.7 3686454bytes
7 10:29:00.048 1 cognex_patmax_start 1779.2
8 10:29:00.048 1 cognex_patmax_done 1791.1
9 10:29:01.605 1 basler_capture_start 3333.9
10 10:29:01.854 1 basler_capture_done 3591.2 (4504, 4504)
11 10:29:02.636 1 cognex_join_wait 4376.1
12 10:29:02.636 1 cognex_join_done 4377.7
13 10:29:02.665 1 cycle_done 4394.5 result=FAIL cognex=FAIL basler=PASS
14 10:29:02.669 2 cycle_start 0.0 group=A belt_delay=3.33s
15 10:29:02.671 2 cognex_trigger_send 2.9
16 10:29:02.672 2 cognex_trigger_ok 9.4
17 10:29:03.681 2 cognex_ftp_start 1011.7
18 10:29:04.413 2 cognex_ftp_done 1744.1 3686454bytes
19 10:29:04.414 2 cognex_patmax_start 1745.6
20 10:29:04.414 2 cognex_patmax_done 1757.2
21 10:29:06.003 2 basler_capture_start 3333.8
22 10:29:06.252 2 basler_capture_done 3590.7 (4504, 4504)
23 10:29:06.252 2 cognex_join_wait 3597.3
24 10:29:06.284 2 cognex_join_done 3614.1
25 10:29:06.287 2 cycle_done 3617.9 result=FAIL cognex=FAIL basler=PASS
26 10:29:06.290 3 cycle_start 0.0 group=A belt_delay=3.33s
27 10:29:06.290 3 cognex_trigger_send 2.5
28 10:29:06.299 3 cognex_trigger_ok 9.0
29 10:29:07.302 3 cognex_ftp_start 1011.4
30 10:29:08.031 3 cognex_ftp_done 1741.0 3686454bytes
31 10:29:08.031 3 cognex_patmax_start 1742.5
32 10:29:08.031 3 cognex_patmax_done 1754.0
33 10:29:09.625 3 basler_capture_start 3333.9
34 10:29:09.869 3 basler_capture_done 3589.8 (4504, 4504)
35 10:29:09.884 3 cognex_join_wait 3596.7
36 10:29:09.903 3 cognex_join_done 3614.2
37 10:29:09.903 3 cycle_done 3617.5 result=FAIL cognex=FAIL basler=PASS
38 10:29:09.903 4 cycle_start 0.0 group=A belt_delay=3.33s
39 10:29:09.914 4 cognex_trigger_send 2.3
40 10:29:09.920 4 cognex_trigger_ok 8.7
41 10:29:10.923 4 cognex_ftp_start 1010.9
42 10:29:11.648 4 cognex_ftp_done 1736.6 3686454bytes
43 10:29:11.648 4 cognex_patmax_start 1738.1
44 10:29:11.648 4 cognex_patmax_done 1749.6
45 10:29:13.246 4 basler_capture_start 3334.3
46 10:29:13.500 4 basler_capture_done 3590.8 (4504, 4504)
47 10:29:13.500 4 cognex_join_wait 3597.8
48 10:29:13.527 4 cognex_join_done 3608.0
49 10:29:13.527 4 cycle_done 3618.5 result=FAIL cognex=FAIL basler=PASS
50 10:29:13.534 5 cycle_start 0.0 group=A belt_delay=3.33s
51 10:29:13.536 5 cognex_trigger_send 2.4
52 10:29:13.539 5 cognex_trigger_ok 9.2
53 10:29:14.546 5 cognex_ftp_start 1011.6
54 10:29:15.264 5 cognex_ftp_done 1744.0 3686454bytes
55 10:29:15.264 5 cognex_patmax_start 1745.6
56 10:29:15.281 5 cognex_patmax_done 1757.1
57 10:29:16.868 5 basler_capture_start 3334.1
58 10:29:17.119 5 basler_capture_done 3591.4 (4504, 4504)
59 10:29:17.119 5 cognex_join_wait 3598.4
60 10:29:17.135 5 cognex_join_done 3602.3
61 10:29:17.153 5 cycle_done 3619.1 result=FAIL cognex=FAIL basler=PASS
62 10:29:17.157 6 cycle_start 0.0 group=A belt_delay=3.33s
63 10:29:17.157 6 cognex_trigger_send 2.4
64 10:29:17.166 6 cognex_trigger_ok 9.2
65 10:29:18.169 6 cognex_ftp_start 1011.5
66 10:29:18.914 6 cognex_ftp_done 1758.7 3686454bytes
67 10:29:18.914 6 cognex_patmax_start 1760.2
68 10:29:18.914 6 cognex_patmax_done 1772.1
69 10:29:20.491 6 basler_capture_start 3334.0
70 10:29:20.739 6 basler_capture_done 3591.4 (4504, 4504)
71 10:29:20.755 6 cognex_join_wait 3598.5
72 10:29:20.774 6 cognex_join_done 3615.4
73 10:29:20.776 6 cycle_done 3619.0 result=FAIL cognex=FAIL basler=PASS
74 10:29:20.777 7 cycle_start 0.0 group=A belt_delay=3.33s
75 10:29:20.782 7 cognex_trigger_send 2.4
76 10:29:20.788 7 cognex_trigger_ok 9.0
77 10:29:21.791 7 cognex_ftp_start 1010.8
78 10:29:22.514 7 cognex_ftp_done 1749.6 3686454bytes
79 10:29:22.531 7 cognex_patmax_start 1751.2
80 10:29:22.531 7 cognex_patmax_done 1760.6
81 10:29:24.113 7 basler_capture_start 3333.5
82 10:29:24.368 7 basler_capture_done 3590.6 (4504, 4504)
83 10:29:24.368 7 cognex_join_wait 3597.8
84 10:29:24.368 7 cognex_join_done 3599.4
85 10:29:24.390 7 cycle_done 3615.9 result=FAIL cognex=FAIL basler=PASS
86 10:29:24.399 8 cycle_start 0.0 group=A belt_delay=3.33s
87 10:29:24.401 8 cognex_trigger_send 1.9
88 10:29:24.405 8 cognex_trigger_ok 8.4
89 10:29:25.410 8 cognex_ftp_start 1010.6
90 10:29:26.165 8 cognex_ftp_done 1768.0 3686454bytes
91 10:29:26.165 8 cognex_patmax_start 1769.5
92 10:29:26.180 8 cognex_patmax_done 1780.9
93 10:29:27.733 8 basler_capture_start 3333.7
94 10:29:27.985 8 basler_capture_done 3591.1 (4504, 4504)
95 10:29:27.985 8 cognex_join_wait 3598.1
96 10:29:28.001 8 cognex_join_done 3601.9
97 10:29:28.017 8 cycle_done 3618.3 result=FAIL cognex=FAIL basler=PASS
98 10:29:28.022 9 cycle_start 0.0 group=A belt_delay=3.33s
99 10:29:28.023 9 cognex_trigger_send 2.4
100 10:29:28.031 9 cognex_trigger_ok 9.0
101 10:29:29.033 9 cognex_ftp_start 1011.4
102 10:29:29.748 9 cognex_ftp_done 1741.5 3686454bytes
103 10:29:29.765 9 cognex_patmax_start 1743.1
104 10:29:29.765 9 cognex_patmax_done 1754.6
105 10:29:31.355 9 basler_capture_start 3333.4
106 10:29:31.603 9 basler_capture_done 3589.9 (4504, 4504)
107 10:29:31.619 9 cognex_join_wait 3597.0
108 10:29:31.638 9 cognex_join_done 3614.9
109 10:29:31.640 9 cycle_done 3619.4 result=FAIL cognex=FAIL basler=PASS
110 10:29:31.640 10 cycle_start 0.0 group=A belt_delay=3.33s
111 10:29:31.640 10 cognex_trigger_send 2.2
112 10:29:31.651 10 cognex_trigger_ok 8.7
113 10:29:32.655 10 cognex_ftp_start 1010.9
114 10:29:33.381 10 cognex_ftp_done 1737.0 3686454bytes
115 10:29:33.381 10 cognex_patmax_start 1738.5
116 10:29:33.381 10 cognex_patmax_done 1750.2
117 10:29:34.978 10 basler_capture_start 3334.0
118 10:29:35.234 10 basler_capture_done 3591.0 (4504, 4504)
119 10:29:35.234 10 cognex_join_wait 3598.4
120 10:29:35.255 10 cognex_join_done 3608.7
121 10:29:35.255 10 cycle_done 3619.3 result=FAIL cognex=FAIL basler=PASS
122 10:29:35.268 11 cycle_start 0.0 group=A belt_delay=3.33s
123 10:29:35.268 11 cognex_trigger_send 2.4
124 10:29:35.268 11 cognex_trigger_ok 8.9
125 10:29:36.279 11 cognex_ftp_start 1011.4
126 10:29:37.031 11 cognex_ftp_done 1763.6 3686454bytes
127 10:29:37.031 11 cognex_patmax_start 1765.1
128 10:29:37.031 11 cognex_patmax_done 1776.5
129 10:29:38.601 11 basler_capture_start 3333.6
130 10:29:38.855 11 basler_capture_done 3590.7 (4504, 4504)
131 10:29:38.855 11 cognex_join_wait 3597.8
132 10:29:38.885 11 cognex_join_done 3608.3
133 10:29:38.888 11 cycle_done 3619.7 result=FAIL cognex=FAIL basler=PASS
134 18:06:16.958 1 cycle_start 0.0 group=A belt_delay=3.33s
135 18:06:16.958 1 cognex_trigger_send 3.0
136 18:06:16.974 1 cognex_trigger_ok 9.5
137 18:06:17.977 1 cognex_ftp_start 1011.8
138 18:06:18.704 1 cognex_ftp_done 1738.6 3686454bytes
139 18:06:18.705 1 cognex_patmax_start 1739.9
140 18:06:18.832 1 cognex_patmax_done 1866.3
141 18:06:20.299 1 basler_capture_start 3333.8
142 18:06:20.302 1 basler_capture_done 3336.9 failed
143 18:06:20.304 1 cognex_join_wait 3338.9
144 18:06:20.306 1 cognex_join_done 3340.9
145 18:06:20.309 1 cycle_done 3343.2 result=PASS cognex=PASS basler=PASS
146 18:06:20.311 2 cycle_start 0.0 group=A belt_delay=3.33s
147 18:06:20.313 2 cognex_trigger_send 2.3
148 18:06:20.320 2 cognex_trigger_ok 8.8
149 18:06:21.323 2 cognex_ftp_start 1011.6
150 18:06:22.040 2 cognex_ftp_done 1743.5 3686454bytes
151 18:06:22.056 2 cognex_patmax_start 1745.1
152 18:06:22.162 2 cognex_patmax_done 1850.8
153 18:06:23.645 2 basler_capture_start 3333.7
154 18:06:23.645 2 basler_capture_done 3336.5 failed
155 18:06:23.645 2 cognex_join_wait 3338.4
156 18:06:23.645 2 cognex_join_done 3340.4
157 18:06:23.645 2 cycle_done 3342.3 result=PASS cognex=PASS basler=PASS
158 18:06:23.645 3 cycle_start 0.0 group=A belt_delay=3.33s
159 18:06:23.645 3 cognex_trigger_send 2.2
160 18:06:23.664 3 cognex_trigger_ok 8.6
161 18:06:24.666 3 cognex_ftp_start 1010.5
162 18:06:25.406 3 cognex_ftp_done 1758.8 3686454bytes
163 18:06:25.406 3 cognex_patmax_start 1760.2
164 18:06:25.522 3 cognex_patmax_done 1866.4
165 18:06:26.990 3 basler_capture_start 3334.1
166 18:06:26.992 3 basler_capture_done 3337.1 failed
167 18:06:26.992 3 cognex_join_wait 3338.5
168 18:06:26.992 3 cognex_join_done 3339.7
169 18:06:26.992 3 cycle_done 3341.0 result=FAIL cognex=FAIL basler=PASS
170 18:06:26.992 4 cycle_start 0.0 group=A belt_delay=3.33s
171 18:06:26.992 4 cognex_trigger_send 1.7
172 18:06:27.005 4 cognex_trigger_ok 7.7
173 18:06:28.009 4 cognex_ftp_start 1010.2
174 18:06:28.740 4 cognex_ftp_done 1756.4 3686454bytes
175 18:06:28.756 4 cognex_patmax_start 1758.0
176 18:06:28.861 4 cognex_patmax_done 1863.5
177 18:06:30.332 4 basler_capture_start 3333.5
178 18:06:30.332 4 basler_capture_done 3336.2 failed
179 18:06:30.332 4 cognex_join_wait 3338.1
180 18:06:30.332 4 cognex_join_done 3340.0
181 18:06:30.332 4 cycle_done 3342.0 result=FAIL cognex=FAIL basler=PASS
182 18:06:30.332 5 cycle_start 0.0 group=A belt_delay=3.33s
183 18:06:30.344 5 cognex_trigger_send 2.4
184 18:06:30.351 5 cognex_trigger_ok 8.7
185 18:06:31.354 5 cognex_ftp_start 1011.5
186 18:06:32.106 5 cognex_ftp_done 1764.4 3686454bytes
187 18:06:32.106 5 cognex_patmax_start 1765.8
188 18:06:32.212 5 cognex_patmax_done 1869.6
189 18:06:33.676 5 basler_capture_start 3333.6
190 18:06:33.678 5 basler_capture_done 3336.7 failed
191 18:06:33.678 5 cognex_join_wait 3338.6
192 18:06:33.678 5 cognex_join_done 3340.5
193 18:06:33.678 5 cycle_done 3342.4 result=FAIL cognex=FAIL basler=PASS
194 18:06:33.678 6 cycle_start 0.0 group=A belt_delay=3.33s
195 18:06:33.678 6 cognex_trigger_send 2.4
196 18:06:33.697 6 cognex_trigger_ok 8.9
197 18:06:34.699 6 cognex_ftp_start 1011.8
198 18:06:35.456 6 cognex_ftp_done 1769.3 3686454bytes
199 18:06:35.456 6 cognex_patmax_start 1770.9
200 18:06:35.565 6 cognex_patmax_done 1878.1
201 18:06:37.021 6 basler_capture_start 3333.9
202 18:06:37.021 6 basler_capture_done 3337.3 failed
203 18:06:37.021 6 cognex_join_wait 3339.3
204 18:06:37.027 6 cognex_join_done 3341.2
205 18:06:37.027 6 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
206 18:06:37.027 7 cycle_start 0.0 group=A belt_delay=3.33s
207 18:06:37.027 7 cognex_trigger_send 2.4
208 18:06:37.041 7 cognex_trigger_ok 8.9
209 18:06:38.044 7 cognex_ftp_start 1011.4
210 18:06:38.772 7 cognex_ftp_done 1739.0 3686454bytes
211 18:06:38.773 7 cognex_patmax_start 1740.5
212 18:06:38.880 7 cognex_patmax_done 1847.0
213 18:06:40.367 7 basler_capture_start 3334.3
214 18:06:40.367 7 basler_capture_done 3337.4 failed
215 18:06:40.367 7 cognex_join_wait 3339.3
216 18:06:40.367 7 cognex_join_done 3341.3
217 18:06:40.375 7 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS
218 18:06:40.375 8 cycle_start 0.0 group=A belt_delay=3.33s
219 18:06:40.375 8 cognex_trigger_send 2.3
220 18:06:40.375 8 cognex_trigger_ok 8.8
221 18:06:41.391 8 cognex_ftp_start 1011.2
222 18:06:42.139 8 cognex_ftp_done 1759.5 3686454bytes
223 18:06:42.140 8 cognex_patmax_start 1761.2
224 18:06:42.247 8 cognex_patmax_done 1867.4
225 18:06:43.713 8 basler_capture_start 3333.6
226 18:06:43.713 8 basler_capture_done 3336.5 failed
227 18:06:43.713 8 cognex_join_wait 3338.5
228 18:06:43.713 8 cognex_join_done 3340.5
229 18:06:43.713 8 cycle_done 3342.4 result=FAIL cognex=FAIL basler=PASS
230 18:06:43.713 9 cycle_start 0.0 group=A belt_delay=3.33s
231 18:06:43.727 9 cognex_trigger_send 2.2
232 18:06:43.727 9 cognex_trigger_ok 8.8
233 18:06:44.737 9 cognex_ftp_start 1011.0
234 18:06:45.473 9 cognex_ftp_done 1760.9 3686454bytes
235 18:06:45.473 9 cognex_patmax_start 1762.4
236 18:06:45.593 9 cognex_patmax_done 1867.3
237 18:06:47.060 9 basler_capture_start 3334.0
238 18:06:47.060 9 basler_capture_done 3337.1 failed
239 18:06:47.060 9 cognex_join_wait 3339.1
240 18:06:47.060 9 cognex_join_done 3341.0
241 18:06:47.060 9 cycle_done 3342.9 result=FAIL cognex=FAIL basler=PASS
242 18:06:57.940 10 cycle_start 0.0 group=A belt_delay=3.33s
243 18:06:57.940 10 cognex_trigger_send 3.7
244 18:06:57.945 10 cognex_trigger_ok 10.4
245 18:06:58.953 10 cognex_ftp_start 1012.9
246 18:06:59.690 10 cognex_ftp_done 1765.5 3686454bytes
247 18:06:59.706 10 cognex_patmax_start 1767.1
248 18:06:59.812 10 cognex_patmax_done 1872.7
249 18:07:01.274 10 basler_capture_start 3334.1
250 18:07:01.274 10 basler_capture_done 3337.2 failed
251 18:07:01.278 10 cognex_join_wait 3338.5
252 18:07:01.278 10 cognex_join_done 3339.8
253 18:07:01.278 10 cycle_done 3341.1 result=FAIL cognex=FAIL basler=PASS
254 18:07:01.278 11 cycle_start 0.0 group=A belt_delay=3.33s
255 18:07:01.278 11 cognex_trigger_send 1.6
256 18:07:01.278 11 cognex_trigger_ok 7.6
257 18:07:02.293 11 cognex_ftp_start 1009.0
258 18:07:03.033 11 cognex_ftp_done 1749.5 3686454bytes
259 18:07:03.034 11 cognex_patmax_start 1750.9
260 18:07:03.137 11 cognex_patmax_done 1853.5
261 18:07:04.618 11 basler_capture_start 3334.1
262 18:07:04.618 11 basler_capture_done 3337.6 failed
263 18:07:04.618 11 cognex_join_wait 3339.5
264 18:07:04.618 11 cognex_join_done 3341.5
265 18:07:04.618 11 cycle_done 3343.4 result=FAIL cognex=FAIL basler=PASS
266 18:07:04.628 12 cycle_start 0.0 group=A belt_delay=3.33s
267 18:07:04.628 12 cognex_trigger_send 2.2
268 18:07:04.628 12 cognex_trigger_ok 8.5
269 18:07:05.641 12 cognex_ftp_start 1010.7
270 18:07:06.373 12 cognex_ftp_done 1744.0 3686454bytes
271 18:07:06.373 12 cognex_patmax_start 1745.4
272 18:07:06.473 12 cognex_patmax_done 1852.2
273 18:07:07.964 12 basler_capture_start 3333.9
274 18:07:07.964 12 basler_capture_done 3337.2 failed
275 18:07:07.964 12 cognex_join_wait 3339.2
276 18:07:07.964 12 cognex_join_done 3341.0
277 18:07:07.964 12 cycle_done 3343.0 result=FAIL cognex=FAIL basler=PASS
278 18:07:07.976 13 cycle_start 0.0 group=A belt_delay=3.33s
279 18:07:07.976 13 cognex_trigger_send 2.2
280 18:07:07.976 13 cognex_trigger_ok 8.8
281 18:07:08.988 13 cognex_ftp_start 1010.9
282 18:07:09.723 13 cognex_ftp_done 1761.7 3686454bytes
283 18:07:09.740 13 cognex_patmax_start 1763.3
284 18:07:09.842 13 cognex_patmax_done 1865.8
285 18:07:11.311 13 basler_capture_start 3334.1
286 18:07:11.312 13 basler_capture_done 3337.4 failed
287 18:07:11.312 13 cognex_join_wait 3339.4
288 18:07:11.312 13 cognex_join_done 3341.2
289 18:07:11.312 13 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
290 18:07:11.312 14 cycle_start 0.0 group=A belt_delay=3.33s
291 18:07:11.312 14 cognex_trigger_send 2.2
292 18:07:11.327 14 cognex_trigger_ok 8.7
293 18:07:12.334 14 cognex_ftp_start 1010.9
294 18:07:13.090 14 cognex_ftp_done 1767.6 3686454bytes
295 18:07:13.090 14 cognex_patmax_start 1769.1
296 18:07:13.197 14 cognex_patmax_done 1873.5
297 18:07:14.657 14 basler_capture_start 3333.9
298 18:07:14.657 14 basler_capture_done 3336.8 failed
299 18:07:14.662 14 cognex_join_wait 3339.6
300 18:07:14.662 14 cognex_join_done 3341.5
301 18:07:14.662 14 cycle_done 3343.4 result=FAIL cognex=FAIL basler=PASS
302 18:07:14.662 15 cycle_start 0.0 group=A belt_delay=3.33s
303 18:07:14.662 15 cognex_trigger_send 2.2
304 18:07:14.678 15 cognex_trigger_ok 8.8
305 18:07:15.681 15 cognex_ftp_start 1010.9
306 18:07:16.423 15 cognex_ftp_done 1754.9 3686454bytes
307 18:07:16.423 15 cognex_patmax_start 1756.4
308 18:07:16.523 15 cognex_patmax_done 1861.8
309 18:07:18.004 15 basler_capture_start 3333.8
310 18:07:18.004 15 basler_capture_done 3337.1 failed
311 18:07:18.004 15 cognex_join_wait 3339.1
312 18:07:18.004 15 cognex_join_done 3341.0
313 18:07:18.011 15 cycle_done 3342.9 result=FAIL cognex=FAIL basler=PASS
314 18:07:18.011 16 cycle_start 0.0 group=A belt_delay=3.33s
315 18:07:18.011 16 cognex_trigger_send 2.2
316 18:07:18.011 16 cognex_trigger_ok 8.5
317 18:07:19.027 16 cognex_ftp_start 1010.8
318 18:07:19.757 16 cognex_ftp_done 1755.9 3686454bytes
319 18:07:19.773 16 cognex_patmax_start 1757.5
320 18:07:19.873 16 cognex_patmax_done 1864.6
321 18:07:21.350 16 basler_capture_start 3333.7
322 18:07:21.350 16 basler_capture_done 3336.8 failed
323 18:07:21.350 16 cognex_join_wait 3338.8
324 18:07:21.350 16 cognex_join_done 3340.7
325 18:07:21.350 16 cycle_done 3342.5 result=FAIL cognex=FAIL basler=PASS
326 18:07:21.360 17 cycle_start 0.0 group=A belt_delay=3.33s
327 18:07:21.360 17 cognex_trigger_send 2.2
328 18:07:21.360 17 cognex_trigger_ok 8.6
329 18:07:22.373 17 cognex_ftp_start 1010.8
330 18:07:23.123 17 cognex_ftp_done 1761.5 3686454bytes
331 18:07:23.123 17 cognex_patmax_start 1763.0
332 18:07:23.223 17 cognex_patmax_done 1869.6
333 18:07:24.696 17 basler_capture_start 3333.7
334 18:07:24.696 17 basler_capture_done 3337.0 failed
335 18:07:24.696 17 cognex_join_wait 3338.4
336 18:07:24.696 17 cognex_join_done 3339.6
337 18:07:24.696 17 cycle_done 3340.9 result=FAIL cognex=FAIL basler=PASS
338 18:07:24.696 18 cycle_start 0.0 group=A belt_delay=3.33s
339 18:07:24.696 18 cognex_trigger_send 1.6
340 18:07:24.711 18 cognex_trigger_ok 7.3
341 18:07:25.714 18 cognex_ftp_start 1008.8
342 18:07:26.440 18 cognex_ftp_done 1735.4 3686454bytes
343 18:07:26.440 18 cognex_patmax_start 1736.9
344 18:07:26.540 18 cognex_patmax_done 1842.2
345 18:07:28.040 18 basler_capture_start 3334.1
346 18:07:28.042 18 basler_capture_done 3337.6 failed
347 18:07:28.042 18 cognex_join_wait 3340.2
348 18:07:28.042 18 cognex_join_done 3342.2
349 18:07:28.042 18 cycle_done 3344.1 result=FAIL cognex=FAIL basler=PASS
350 18:07:28.042 19 cycle_start 0.0 group=A belt_delay=3.33s
351 18:07:28.042 19 cognex_trigger_send 2.2
352 18:07:28.057 19 cognex_trigger_ok 8.7
353 18:07:29.064 19 cognex_ftp_start 1011.0
354 18:07:29.823 19 cognex_ftp_done 1771.2 3686454bytes
355 18:07:29.823 19 cognex_patmax_start 1772.7
356 18:07:29.923 19 cognex_patmax_done 1879.1
357 18:07:31.387 19 basler_capture_start 3334.3
358 18:07:31.387 19 basler_capture_done 3337.4 failed
359 18:07:31.387 19 cognex_join_wait 3339.9
360 18:07:31.395 19 cognex_join_done 3341.9
361 18:07:31.395 19 cycle_done 3343.9 result=FAIL cognex=FAIL basler=PASS
362 18:07:31.395 20 cycle_start 0.0 group=A belt_delay=3.33s
363 18:07:31.395 20 cognex_trigger_send 2.3
364 18:07:31.395 20 cognex_trigger_ok 8.6
365 18:07:32.412 20 cognex_ftp_start 1010.9
366 18:07:33.123 20 cognex_ftp_done 1737.1 3686454bytes
367 18:07:33.139 20 cognex_patmax_start 1738.7
368 18:07:33.240 20 cognex_patmax_done 1840.1
369 18:07:34.734 20 basler_capture_start 3333.5
370 18:07:34.734 20 basler_capture_done 3336.8 failed
371 18:07:34.734 20 cognex_join_wait 3339.1
372 18:07:34.742 20 cognex_join_done 3341.1
373 18:07:34.742 20 cycle_done 3343.1 result=FAIL cognex=FAIL basler=PASS
374 18:07:34.742 21 cycle_start 0.0 group=A belt_delay=3.33s
375 18:07:34.742 21 cognex_trigger_send 2.2
376 18:07:34.742 21 cognex_trigger_ok 8.8
377 18:07:35.758 21 cognex_ftp_start 1011.1
378 18:07:36.490 21 cognex_ftp_done 1756.5 3686454bytes
379 18:07:36.490 21 cognex_patmax_start 1758.0
380 18:07:36.610 21 cognex_patmax_done 1863.5
381 18:07:38.081 21 basler_capture_start 3333.8
382 18:07:38.081 21 basler_capture_done 3336.6 failed
383 18:07:38.081 21 cognex_join_wait 3338.6
384 18:07:38.081 21 cognex_join_done 3340.5
385 18:07:38.081 21 cycle_done 3342.4 result=FAIL cognex=FAIL basler=PASS
386 18:09:47.668 22 cycle_start 0.0 group=A belt_delay=3.33s
387 18:09:47.670 22 cognex_trigger_send 3.4
388 18:09:47.670 22 cognex_trigger_ok 10.0
389 18:09:48.680 22 cognex_ftp_start 1012.4
390 18:09:49.416 22 cognex_ftp_done 1754.5 3686454bytes
391 18:09:49.416 22 cognex_patmax_start 1755.9
392 18:09:49.551 22 cognex_patmax_done 1896.3
393 18:09:51.002 22 basler_capture_start 3334.0
394 18:09:51.002 22 basler_capture_done 3337.1 failed
395 18:09:51.006 22 cognex_join_wait 3339.1
396 18:09:51.006 22 cognex_join_done 3341.1
397 18:09:51.006 22 cycle_done 3343.0 result=PASS cognex=PASS basler=PASS
398 18:09:51.006 23 cycle_start 0.0 group=A belt_delay=3.33s
399 18:09:51.006 23 cognex_trigger_send 2.4
400 18:09:51.022 23 cognex_trigger_ok 9.0
401 18:09:52.025 23 cognex_ftp_start 1011.6
402 18:09:52.767 23 cognex_ftp_done 1758.5 3686454bytes
403 18:09:52.767 23 cognex_patmax_start 1760.0
404 18:09:52.902 23 cognex_patmax_done 1897.6
405 18:09:54.347 23 basler_capture_start 3333.4
406 18:09:54.347 23 basler_capture_done 3336.3 failed
407 18:09:54.347 23 cognex_join_wait 3338.8
408 18:09:54.347 23 cognex_join_done 3340.7
409 18:09:54.347 23 cycle_done 3342.6 result=PASS cognex=PASS basler=PASS
410 18:11:41.743 24 cycle_start 0.0 group=A belt_delay=3.33s
411 18:11:41.745 24 cognex_trigger_send 3.5
412 18:11:41.745 24 cognex_trigger_ok 10.3
413 18:11:42.757 24 cognex_ftp_start 1013.2
414 18:11:43.483 24 cognex_ftp_done 1749.5 3686454bytes
415 18:11:43.483 24 cognex_patmax_start 1751.0
416 18:11:43.621 24 cognex_patmax_done 1885.8
417 18:11:45.078 24 basler_capture_start 3334.2
418 18:11:45.078 24 basler_capture_done 3337.3 failed
419 18:11:45.078 24 cognex_join_wait 3338.6
420 18:11:45.078 24 cognex_join_done 3339.8
421 18:11:45.078 24 cycle_done 3341.4 result=PASS cognex=PASS basler=PASS
422 18:11:45.078 25 cycle_start 0.0 group=A belt_delay=3.33s
423 18:11:45.078 25 cognex_trigger_send 1.7
424 18:11:45.093 25 cognex_trigger_ok 7.6
425 18:11:46.096 25 cognex_ftp_start 1009.6
426 18:11:46.832 25 cognex_ftp_done 1755.0 3686454bytes
427 18:11:46.832 25 cognex_patmax_start 1756.5
428 18:11:46.970 25 cognex_patmax_done 1884.8
429 18:11:48.421 25 basler_capture_start 3334.0
430 18:11:48.421 25 basler_capture_done 3337.0 failed
431 18:11:48.424 25 cognex_join_wait 3339.0
432 18:11:48.424 25 cognex_join_done 3340.9
433 18:11:48.424 25 cycle_done 3342.7 result=PASS cognex=PASS basler=PASS
434 18:11:48.424 26 cycle_start 0.0 group=A belt_delay=3.33s
435 18:11:48.424 26 cognex_trigger_send 2.3
436 18:11:48.440 26 cognex_trigger_ok 8.8
437 18:11:49.443 26 cognex_ftp_start 1011.2
438 18:11:50.182 26 cognex_ftp_done 1758.9 3686454bytes
439 18:11:50.182 26 cognex_patmax_start 1760.4
440 18:11:50.320 26 cognex_patmax_done 1888.4
441 18:11:51.765 26 basler_capture_start 3333.7
442 18:11:51.765 26 basler_capture_done 3337.0 failed
443 18:11:51.769 26 cognex_join_wait 3338.9
444 18:11:51.769 26 cognex_join_done 3340.8
445 18:11:51.774 26 cycle_done 3342.6 result=PASS cognex=PASS basler=PASS
446 18:11:51.774 27 cycle_start 0.0 group=A belt_delay=3.33s
447 18:11:51.774 27 cognex_trigger_send 2.3
448 18:11:51.785 27 cognex_trigger_ok 8.8
449 18:11:52.788 27 cognex_ftp_start 1011.7
450 18:11:53.520 27 cognex_ftp_done 1749.0 3686454bytes
451 18:11:53.520 27 cognex_patmax_start 1750.6
452 18:11:53.640 27 cognex_patmax_done 1878.4
453 18:11:55.111 27 basler_capture_start 3334.0
454 18:11:55.111 27 basler_capture_done 3337.1 failed
455 18:11:55.111 27 cognex_join_wait 3339.0
456 18:11:55.111 27 cognex_join_done 3340.9
457 18:11:55.111 27 cycle_done 3342.8 result=PASS cognex=PASS basler=PASS
458 18:11:55.111 28 cycle_start 0.0 group=A belt_delay=3.33s
459 18:11:55.111 28 cognex_trigger_send 2.3
460 18:11:55.130 28 cognex_trigger_ok 8.8
461 18:11:56.133 28 cognex_ftp_start 1011.6
462 18:11:56.850 28 cognex_ftp_done 1741.1 3686454bytes
463 18:11:56.850 28 cognex_patmax_start 1742.5
464 18:11:56.992 28 cognex_patmax_done 1872.0
465 18:11:58.456 28 basler_capture_start 3334.1
466 18:11:58.456 28 basler_capture_done 3337.1 failed
467 18:11:58.456 28 cognex_join_wait 3339.0
468 18:11:58.461 28 cognex_join_done 3340.8
469 18:11:58.461 28 cycle_done 3342.8 result=PASS cognex=PASS basler=PASS
470 18:11:58.461 29 cycle_start 0.0 group=A belt_delay=3.33s
471 18:11:58.461 29 cognex_trigger_send 2.3
472 18:11:58.475 29 cognex_trigger_ok 8.9
473 18:11:59.478 29 cognex_ftp_start 1011.7
474 18:12:00.219 29 cognex_ftp_done 1755.7 3686454bytes
475 18:12:00.219 29 cognex_patmax_start 1757.2
476 18:12:00.354 29 cognex_patmax_done 1886.8
477 18:12:01.801 29 basler_capture_start 3333.8
478 18:12:01.801 29 basler_capture_done 3337.1 failed
479 18:12:01.801 29 cognex_join_wait 3339.0
480 18:12:01.801 29 cognex_join_done 3340.9
481 18:12:01.809 29 cycle_done 3342.8 result=PASS cognex=PASS basler=PASS
482 18:12:01.809 30 cycle_start 0.0 group=A belt_delay=3.33s
483 18:12:01.809 30 cognex_trigger_send 2.3
484 18:12:01.820 30 cognex_trigger_ok 8.8
485 18:12:02.823 30 cognex_ftp_start 1011.4
486 18:12:03.551 30 cognex_ftp_done 1749.0 3686454bytes
487 18:12:03.551 30 cognex_patmax_start 1750.5
488 18:12:03.686 30 cognex_patmax_done 1876.1
489 18:12:05.146 30 basler_capture_start 3334.1
490 18:12:05.146 30 basler_capture_done 3337.2 failed
491 18:12:05.146 30 cognex_join_wait 3339.3
492 18:12:05.146 30 cognex_join_done 3341.1
493 18:12:05.146 30 cycle_done 3342.9 result=PASS cognex=PASS basler=PASS
494 18:12:05.157 31 cycle_start 0.0 group=A belt_delay=3.33s
495 18:12:05.157 31 cognex_trigger_send 2.3
496 18:12:05.165 31 cognex_trigger_ok 8.7
497 18:12:06.169 31 cognex_ftp_start 1011.4
498 18:12:06.886 31 cognex_ftp_done 1739.1 3686454bytes
499 18:12:06.886 31 cognex_patmax_start 1740.6
500 18:12:07.020 31 cognex_patmax_done 1868.9
501 18:12:08.491 31 basler_capture_start 3333.6
502 18:12:08.491 31 basler_capture_done 3336.5 failed
503 18:12:08.491 31 cognex_join_wait 3338.3
504 18:12:08.491 31 cognex_join_done 3339.5
505 18:12:08.491 31 cycle_done 3340.8 result=PASS cognex=PASS basler=PASS
506 18:12:08.491 32 cycle_start 0.0 group=A belt_delay=3.33s
507 18:12:08.491 32 cognex_trigger_send 1.7
508 18:12:08.507 32 cognex_trigger_ok 7.6
509 18:12:09.509 32 cognex_ftp_start 1009.6
510 18:12:10.234 32 cognex_ftp_done 1743.2 3686454bytes
511 18:12:10.234 32 cognex_patmax_start 1744.6
512 18:12:10.370 32 cognex_patmax_done 1872.5
513 18:12:11.833 32 basler_capture_start 3333.5
514 18:12:11.833 32 basler_capture_done 3336.8 failed
515 18:12:11.833 32 cognex_join_wait 3338.8
516 18:12:11.833 32 cognex_join_done 3340.6
517 18:12:11.842 32 cycle_done 3342.5 result=PASS cognex=PASS basler=PASS
518 18:12:11.842 33 cycle_start 0.0 group=A belt_delay=3.33s
519 18:12:11.842 33 cognex_trigger_send 2.4
520 18:12:11.854 33 cognex_trigger_ok 9.2
521 18:12:12.856 33 cognex_ftp_start 1011.9
522 18:12:13.586 33 cognex_ftp_done 1749.6 3686454bytes
523 18:12:13.586 33 cognex_patmax_start 1751.1
524 18:12:13.704 33 cognex_patmax_done 1869.1
525 18:12:15.179 33 basler_capture_start 3334.2
526 18:12:15.179 33 basler_capture_done 3337.3 failed
527 18:12:15.179 33 cognex_join_wait 3339.3
528 18:12:15.179 33 cognex_join_done 3341.2
529 18:12:15.179 33 cycle_done 3343.1 result=PASS cognex=PASS basler=PASS
530 18:12:15.179 34 cycle_start 0.0 group=A belt_delay=3.33s
531 18:12:15.190 34 cognex_trigger_send 2.5
532 18:12:15.198 34 cognex_trigger_ok 9.1
533 18:12:16.201 34 cognex_ftp_start 1011.3
534 18:12:16.920 34 cognex_ftp_done 1738.0 3686454bytes
535 18:12:16.920 34 cognex_patmax_start 1739.4
536 18:12:17.053 34 cognex_patmax_done 1868.0
537 18:12:18.524 34 basler_capture_start 3333.6
538 18:12:18.524 34 basler_capture_done 3336.8 failed
539 18:12:18.524 34 cognex_join_wait 3338.7
540 18:12:18.524 34 cognex_join_done 3340.6
541 18:12:18.524 34 cycle_done 3342.5 result=PASS cognex=PASS basler=PASS

View File

@@ -0,0 +1,97 @@
timestamp,seq,event,elapsed_ms,detail
17:36:47.998,1,cycle_start,0.0,group=A belt_delay=3.33s
17:36:47.998,1,cognex_trigger_send,8.0,
17:36:48.045,1,cognex_trigger_ok,44.9,
17:36:49.051,1,cognex_ftp_start,1047.1,
17:36:49.787,1,cognex_ftp_done,1791.9,3686454bytes
17:36:49.787,1,cognex_patmax_start,1793.3,
17:36:49.808,1,cognex_patmax_done,1806.9,
17:36:51.338,1,basler_capture_start,3333.8,
17:36:51.340,1,basler_capture_done,3336.6,failed
17:36:51.342,1,cognex_join_wait,3338.6,
17:36:51.344,1,cognex_join_done,3340.6,
17:36:51.346,1,cycle_done,3342.5,result=FAIL cognex=FAIL basler=PASS
17:36:51.349,2,cycle_start,0.0,group=A belt_delay=3.33s
17:36:51.352,2,cognex_trigger_send,2.4,
17:36:51.384,2,cognex_trigger_ok,35.1,
17:36:52.387,2,cognex_ftp_start,1037.2,
17:36:53.137,2,cognex_ftp_done,1790.2,3686454bytes
17:36:53.137,2,cognex_patmax_start,1791.5,
17:36:53.153,2,cognex_patmax_done,1802.8,
17:36:54.684,2,basler_capture_start,3333.8,
17:36:54.684,2,basler_capture_done,3336.8,failed
17:36:54.684,2,cognex_join_wait,3338.7,
17:36:54.684,2,cognex_join_done,3340.8,
17:36:54.684,2,cycle_done,3342.7,result=FAIL cognex=FAIL basler=PASS
17:36:54.696,3,cycle_start,0.0,group=A belt_delay=3.33s
17:36:54.696,3,cognex_trigger_send,2.3,
17:36:54.727,3,cognex_trigger_ok,35.1,
17:36:55.734,3,cognex_ftp_start,1037.6,
17:36:56.454,3,cognex_ftp_done,1773.0,3686454bytes
17:36:56.471,3,cognex_patmax_start,1774.7,
17:36:56.471,3,cognex_patmax_done,1786.2,
17:36:58.030,3,basler_capture_start,3334.1,
17:36:58.030,3,basler_capture_done,3337.3,failed
17:36:58.030,3,cognex_join_wait,3339.2,
17:36:58.030,3,cognex_join_done,3341.2,
17:36:58.030,3,cycle_done,3343.5,result=FAIL cognex=FAIL basler=PASS
17:36:58.043,4,cycle_start,0.0,group=A belt_delay=3.33s
17:36:58.043,4,cognex_trigger_send,2.4,
17:36:58.075,4,cognex_trigger_ok,35.7,
17:36:59.082,4,cognex_ftp_start,1038.2,
17:36:59.820,4,cognex_ftp_done,1776.9,3686454bytes
17:36:59.821,4,cognex_patmax_start,1778.4,
17:36:59.821,4,cognex_patmax_done,1789.5,
17:37:01.377,4,basler_capture_start,3333.8,
17:37:01.377,4,basler_capture_done,3336.9,failed
17:37:01.377,4,cognex_join_wait,3339.0,
17:37:01.377,4,cognex_join_done,3340.9,
17:37:01.377,4,cycle_done,3342.9,result=FAIL cognex=FAIL basler=PASS
17:37:01.377,5,cycle_start,0.0,group=A belt_delay=3.33s
17:37:01.377,5,cognex_trigger_send,2.3,
17:37:01.422,5,cognex_trigger_ok,35.2,
17:37:02.428,5,cognex_ftp_start,1037.7,
17:37:03.187,5,cognex_ftp_done,1799.2,3686454bytes
17:37:03.187,5,cognex_patmax_start,1800.6,
17:37:03.187,5,cognex_patmax_done,1811.8,
17:37:04.724,5,basler_capture_start,3334.0,
17:37:04.724,5,basler_capture_done,3337.2,failed
17:37:04.729,5,cognex_join_wait,3339.3,
17:37:04.729,5,cognex_join_done,3341.3,
17:37:04.729,5,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
17:37:04.729,6,cycle_start,0.0,group=A belt_delay=3.33s
17:37:04.729,6,cognex_trigger_send,2.3,
17:37:04.760,6,cognex_trigger_ok,35.0,
17:37:05.774,6,cognex_ftp_start,1037.5,
17:37:06.521,6,cognex_ftp_done,1789.0,3686454bytes
17:37:06.521,6,cognex_patmax_start,1790.4,
17:37:06.537,6,cognex_patmax_done,1801.4,
17:37:08.070,6,basler_capture_start,3333.9,
17:37:08.070,6,basler_capture_done,3336.9,failed
17:37:08.070,6,cognex_join_wait,3339.0,
17:37:08.070,6,cognex_join_done,3340.9,
17:37:08.078,6,cycle_done,3342.8,result=FAIL cognex=FAIL basler=PASS
17:37:08.078,7,cycle_start,0.0,group=A belt_delay=3.33s
17:37:08.078,7,cognex_trigger_send,2.3,
17:37:08.109,7,cognex_trigger_ok,35.6,
17:37:09.121,7,cognex_ftp_start,1037.9,
17:37:09.871,7,cognex_ftp_done,1791.7,3686454bytes
17:37:09.871,7,cognex_patmax_start,1793.2,
17:37:09.871,7,cognex_patmax_done,1802.6,
17:37:11.417,7,basler_capture_start,3333.7,
17:37:11.417,7,basler_capture_done,3336.5,failed
17:37:11.417,7,cognex_join_wait,3337.9,
17:37:11.417,7,cognex_join_done,3339.2,
17:37:11.417,7,cycle_done,3340.6,result=FAIL cognex=FAIL basler=PASS
17:37:11.417,8,cycle_start,0.0,group=A belt_delay=3.33s
17:37:11.427,8,cognex_trigger_send,1.7,
17:37:11.458,8,cognex_trigger_ok,33.6,
17:37:12.462,8,cognex_ftp_start,1036.0,
17:37:13.187,8,cognex_ftp_done,1764.3,3686454bytes
17:37:13.187,8,cognex_patmax_start,1765.7,
17:37:13.203,8,cognex_patmax_done,1777.1,
17:37:14.760,8,basler_capture_start,3333.5,
17:37:14.760,8,basler_capture_done,3336.4,failed
17:37:14.760,8,cognex_join_wait,3338.3,
17:37:14.766,8,cognex_join_done,3340.2,
17:37:14.766,8,cycle_done,3342.1,result=FAIL cognex=FAIL basler=PASS
1 timestamp seq event elapsed_ms detail
2 17:36:47.998 1 cycle_start 0.0 group=A belt_delay=3.33s
3 17:36:47.998 1 cognex_trigger_send 8.0
4 17:36:48.045 1 cognex_trigger_ok 44.9
5 17:36:49.051 1 cognex_ftp_start 1047.1
6 17:36:49.787 1 cognex_ftp_done 1791.9 3686454bytes
7 17:36:49.787 1 cognex_patmax_start 1793.3
8 17:36:49.808 1 cognex_patmax_done 1806.9
9 17:36:51.338 1 basler_capture_start 3333.8
10 17:36:51.340 1 basler_capture_done 3336.6 failed
11 17:36:51.342 1 cognex_join_wait 3338.6
12 17:36:51.344 1 cognex_join_done 3340.6
13 17:36:51.346 1 cycle_done 3342.5 result=FAIL cognex=FAIL basler=PASS
14 17:36:51.349 2 cycle_start 0.0 group=A belt_delay=3.33s
15 17:36:51.352 2 cognex_trigger_send 2.4
16 17:36:51.384 2 cognex_trigger_ok 35.1
17 17:36:52.387 2 cognex_ftp_start 1037.2
18 17:36:53.137 2 cognex_ftp_done 1790.2 3686454bytes
19 17:36:53.137 2 cognex_patmax_start 1791.5
20 17:36:53.153 2 cognex_patmax_done 1802.8
21 17:36:54.684 2 basler_capture_start 3333.8
22 17:36:54.684 2 basler_capture_done 3336.8 failed
23 17:36:54.684 2 cognex_join_wait 3338.7
24 17:36:54.684 2 cognex_join_done 3340.8
25 17:36:54.684 2 cycle_done 3342.7 result=FAIL cognex=FAIL basler=PASS
26 17:36:54.696 3 cycle_start 0.0 group=A belt_delay=3.33s
27 17:36:54.696 3 cognex_trigger_send 2.3
28 17:36:54.727 3 cognex_trigger_ok 35.1
29 17:36:55.734 3 cognex_ftp_start 1037.6
30 17:36:56.454 3 cognex_ftp_done 1773.0 3686454bytes
31 17:36:56.471 3 cognex_patmax_start 1774.7
32 17:36:56.471 3 cognex_patmax_done 1786.2
33 17:36:58.030 3 basler_capture_start 3334.1
34 17:36:58.030 3 basler_capture_done 3337.3 failed
35 17:36:58.030 3 cognex_join_wait 3339.2
36 17:36:58.030 3 cognex_join_done 3341.2
37 17:36:58.030 3 cycle_done 3343.5 result=FAIL cognex=FAIL basler=PASS
38 17:36:58.043 4 cycle_start 0.0 group=A belt_delay=3.33s
39 17:36:58.043 4 cognex_trigger_send 2.4
40 17:36:58.075 4 cognex_trigger_ok 35.7
41 17:36:59.082 4 cognex_ftp_start 1038.2
42 17:36:59.820 4 cognex_ftp_done 1776.9 3686454bytes
43 17:36:59.821 4 cognex_patmax_start 1778.4
44 17:36:59.821 4 cognex_patmax_done 1789.5
45 17:37:01.377 4 basler_capture_start 3333.8
46 17:37:01.377 4 basler_capture_done 3336.9 failed
47 17:37:01.377 4 cognex_join_wait 3339.0
48 17:37:01.377 4 cognex_join_done 3340.9
49 17:37:01.377 4 cycle_done 3342.9 result=FAIL cognex=FAIL basler=PASS
50 17:37:01.377 5 cycle_start 0.0 group=A belt_delay=3.33s
51 17:37:01.377 5 cognex_trigger_send 2.3
52 17:37:01.422 5 cognex_trigger_ok 35.2
53 17:37:02.428 5 cognex_ftp_start 1037.7
54 17:37:03.187 5 cognex_ftp_done 1799.2 3686454bytes
55 17:37:03.187 5 cognex_patmax_start 1800.6
56 17:37:03.187 5 cognex_patmax_done 1811.8
57 17:37:04.724 5 basler_capture_start 3334.0
58 17:37:04.724 5 basler_capture_done 3337.2 failed
59 17:37:04.729 5 cognex_join_wait 3339.3
60 17:37:04.729 5 cognex_join_done 3341.3
61 17:37:04.729 5 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS
62 17:37:04.729 6 cycle_start 0.0 group=A belt_delay=3.33s
63 17:37:04.729 6 cognex_trigger_send 2.3
64 17:37:04.760 6 cognex_trigger_ok 35.0
65 17:37:05.774 6 cognex_ftp_start 1037.5
66 17:37:06.521 6 cognex_ftp_done 1789.0 3686454bytes
67 17:37:06.521 6 cognex_patmax_start 1790.4
68 17:37:06.537 6 cognex_patmax_done 1801.4
69 17:37:08.070 6 basler_capture_start 3333.9
70 17:37:08.070 6 basler_capture_done 3336.9 failed
71 17:37:08.070 6 cognex_join_wait 3339.0
72 17:37:08.070 6 cognex_join_done 3340.9
73 17:37:08.078 6 cycle_done 3342.8 result=FAIL cognex=FAIL basler=PASS
74 17:37:08.078 7 cycle_start 0.0 group=A belt_delay=3.33s
75 17:37:08.078 7 cognex_trigger_send 2.3
76 17:37:08.109 7 cognex_trigger_ok 35.6
77 17:37:09.121 7 cognex_ftp_start 1037.9
78 17:37:09.871 7 cognex_ftp_done 1791.7 3686454bytes
79 17:37:09.871 7 cognex_patmax_start 1793.2
80 17:37:09.871 7 cognex_patmax_done 1802.6
81 17:37:11.417 7 basler_capture_start 3333.7
82 17:37:11.417 7 basler_capture_done 3336.5 failed
83 17:37:11.417 7 cognex_join_wait 3337.9
84 17:37:11.417 7 cognex_join_done 3339.2
85 17:37:11.417 7 cycle_done 3340.6 result=FAIL cognex=FAIL basler=PASS
86 17:37:11.417 8 cycle_start 0.0 group=A belt_delay=3.33s
87 17:37:11.427 8 cognex_trigger_send 1.7
88 17:37:11.458 8 cognex_trigger_ok 33.6
89 17:37:12.462 8 cognex_ftp_start 1036.0
90 17:37:13.187 8 cognex_ftp_done 1764.3 3686454bytes
91 17:37:13.187 8 cognex_patmax_start 1765.7
92 17:37:13.203 8 cognex_patmax_done 1777.1
93 17:37:14.760 8 basler_capture_start 3333.5
94 17:37:14.760 8 basler_capture_done 3336.4 failed
95 17:37:14.760 8 cognex_join_wait 3338.3
96 17:37:14.766 8 cognex_join_done 3340.2
97 17:37:14.766 8 cycle_done 3342.1 result=FAIL cognex=FAIL basler=PASS

109
logs/timing/2026-05-08.csv Normal file
View File

@@ -0,0 +1,109 @@
timestamp,seq,event,elapsed_ms,detail
14:32:59.310,1,cycle_start,0.0,group=A belt_delay=3.33s
14:32:59.310,1,cognex_trigger_send,8.2,
14:32:59.310,1,cognex_trigger_ok,14.9,
14:33:00.324,1,cognex_ftp_start,1017.5,
14:33:01.068,1,cognex_ftp_done,1770.0,3686454bytes
14:33:01.068,1,cognex_patmax_start,1771.6,
14:33:01.083,1,cognex_patmax_done,1784.4,
14:33:02.640,1,basler_capture_start,3333.9,
14:33:03.071,1,basler_capture_done,3778.9,"(4504, 4504)"
14:33:04.120,1,cognex_join_wait,4815.0,
14:33:04.120,1,cognex_join_done,4816.8,
14:33:04.136,1,cycle_done,4843.3,result=FAIL cognex=FAIL basler=PASS
14:33:04.152,2,cycle_start,0.0,group=A belt_delay=3.33s
14:33:04.152,2,cognex_trigger_send,2.3,
14:33:04.161,2,cognex_trigger_ok,8.7,
14:33:05.165,2,cognex_ftp_start,1011.3,
14:33:05.903,2,cognex_ftp_done,1754.8,3686454bytes
14:33:05.903,2,cognex_patmax_start,1756.2,
14:33:05.918,2,cognex_patmax_done,1767.5,
14:33:07.488,2,basler_capture_start,3334.0,
14:33:07.635,2,basler_capture_done,3495.6,"(4504, 4504)"
14:33:07.651,2,cognex_join_wait,3500.8,
14:33:07.651,2,cognex_join_done,3503.1,
14:33:07.679,2,cycle_done,3524.7,result=FAIL cognex=FAIL basler=PASS
14:33:07.683,3,cycle_start,0.0,group=A belt_delay=3.33s
14:33:07.686,3,cognex_trigger_send,2.4,
14:33:07.692,3,cognex_trigger_ok,9.2,
14:33:08.695,3,cognex_ftp_start,1011.9,
14:33:09.420,3,cognex_ftp_done,1751.4,3686454bytes
14:33:09.435,3,cognex_patmax_start,1753.0,
14:33:09.435,3,cognex_patmax_done,1764.2,
14:33:11.017,3,basler_capture_start,3334.2,
14:33:11.170,3,basler_capture_done,3499.9,"(4504, 4504)"
14:33:11.185,3,cognex_join_wait,3506.1,
14:33:11.211,3,cognex_join_done,3519.0,
14:33:11.213,3,cycle_done,3530.5,result=FAIL cognex=FAIL basler=PASS
14:33:11.217,4,cycle_start,0.0,group=A belt_delay=3.33s
14:33:11.219,4,cognex_trigger_send,2.4,
14:33:11.226,4,cognex_trigger_ok,9.0,
14:33:12.228,4,cognex_ftp_start,1011.2,
14:33:12.969,4,cognex_ftp_done,1758.8,3686454bytes
14:33:12.969,4,cognex_patmax_start,1760.3,
14:33:12.985,4,cognex_patmax_done,1771.7,
14:33:14.551,4,basler_capture_start,3334.3,
14:33:14.703,4,basler_capture_done,3498.9,"(4504, 4504)"
14:33:14.718,4,cognex_join_wait,3505.0,
14:33:14.734,4,cognex_join_done,3522.9,
14:33:14.743,4,cycle_done,3526.9,result=FAIL cognex=FAIL basler=PASS
14:33:14.747,5,cycle_start,0.0,group=A belt_delay=3.33s
14:33:14.750,5,cognex_trigger_send,2.4,
14:33:14.757,5,cognex_trigger_ok,8.9,
14:33:15.759,5,cognex_ftp_start,1011.3,
14:33:16.504,5,cognex_ftp_done,1761.8,3686454bytes
14:33:16.504,5,cognex_patmax_start,1763.3,
14:33:16.519,5,cognex_patmax_done,1774.5,
14:33:18.082,5,basler_capture_start,3334.0,
14:33:18.282,5,basler_capture_done,3543.4,"(4504, 4504)"
14:33:18.298,5,cognex_join_wait,3550.7,
14:33:18.314,5,cognex_join_done,3567.9,
14:33:18.314,5,cycle_done,3571.6,result=FAIL cognex=FAIL basler=PASS
14:33:18.314,6,cycle_start,0.0,group=A belt_delay=3.33s
14:33:18.314,6,cognex_trigger_send,2.4,
14:33:18.332,6,cognex_trigger_ok,9.0,
14:33:19.335,6,cognex_ftp_start,1011.5,
14:33:20.069,6,cognex_ftp_done,1754.5,3686454bytes
14:33:20.069,6,cognex_patmax_start,1756.0,
14:33:20.085,6,cognex_patmax_done,1767.5,
14:33:21.657,6,basler_capture_start,3334.1,
14:33:21.865,6,basler_capture_done,3544.9,"(4504, 4504)"
14:33:21.865,6,cognex_join_wait,3551.8,
14:33:21.881,6,cognex_join_done,3562.9,
14:33:21.896,6,cycle_done,3573.4,result=FAIL cognex=FAIL basler=PASS
14:33:21.900,7,cycle_start,0.0,group=A belt_delay=3.33s
14:33:21.902,7,cognex_trigger_send,2.4,
14:33:21.908,7,cognex_trigger_ok,8.9,
14:33:22.911,7,cognex_ftp_start,1011.2,
14:33:23.652,7,cognex_ftp_done,1757.8,3686454bytes
14:33:23.652,7,cognex_patmax_start,1759.3,
14:33:23.668,7,cognex_patmax_done,1768.4,
14:33:25.234,7,basler_capture_start,3334.2,
14:33:25.447,7,basler_capture_done,3557.9,"(4504, 4504)"
14:33:25.463,7,cognex_join_wait,3564.9,
14:33:25.463,7,cognex_join_done,3566.6,
14:33:25.478,7,cycle_done,3583.8,result=FAIL cognex=FAIL basler=PASS
14:33:25.478,8,cycle_start,0.0,group=A belt_delay=3.33s
14:33:25.478,8,cognex_trigger_send,1.7,
14:33:25.497,8,cognex_trigger_ok,10.0,
14:33:26.500,8,cognex_ftp_start,1012.4,
14:33:27.221,8,cognex_ftp_done,1747.7,3686454bytes
14:33:27.236,8,cognex_patmax_start,1749.2,
14:33:27.236,8,cognex_patmax_done,1760.8,
14:33:28.822,8,basler_capture_start,3334.2,
14:33:29.032,8,basler_capture_done,3556.5,"(4504, 4504)"
14:33:29.048,8,cognex_join_wait,3563.6,
14:33:29.064,8,cognex_join_done,3576.3,
14:33:29.064,8,cycle_done,3587.3,result=FAIL cognex=FAIL basler=PASS
14:33:29.077,9,cycle_start,0.0,group=A belt_delay=3.33s
14:33:29.080,9,cognex_trigger_send,2.3,
14:33:29.087,9,cognex_trigger_ok,9.0,
14:33:30.089,9,cognex_ftp_start,1011.1,
14:33:30.804,9,cognex_ftp_done,1737.9,3686454bytes
14:33:30.804,9,cognex_patmax_start,1739.4,
14:33:30.829,9,cognex_patmax_done,1750.8,
14:33:32.412,9,basler_capture_start,3333.8,
14:33:32.662,9,basler_capture_done,3590.5,"(4504, 4504)"
14:33:32.662,9,cognex_join_wait,3597.5,
14:33:32.693,9,cognex_join_done,3619.7,
14:33:32.693,9,cycle_done,3623.2,result=FAIL cognex=FAIL basler=PASS
1 timestamp seq event elapsed_ms detail
2 14:32:59.310 1 cycle_start 0.0 group=A belt_delay=3.33s
3 14:32:59.310 1 cognex_trigger_send 8.2
4 14:32:59.310 1 cognex_trigger_ok 14.9
5 14:33:00.324 1 cognex_ftp_start 1017.5
6 14:33:01.068 1 cognex_ftp_done 1770.0 3686454bytes
7 14:33:01.068 1 cognex_patmax_start 1771.6
8 14:33:01.083 1 cognex_patmax_done 1784.4
9 14:33:02.640 1 basler_capture_start 3333.9
10 14:33:03.071 1 basler_capture_done 3778.9 (4504, 4504)
11 14:33:04.120 1 cognex_join_wait 4815.0
12 14:33:04.120 1 cognex_join_done 4816.8
13 14:33:04.136 1 cycle_done 4843.3 result=FAIL cognex=FAIL basler=PASS
14 14:33:04.152 2 cycle_start 0.0 group=A belt_delay=3.33s
15 14:33:04.152 2 cognex_trigger_send 2.3
16 14:33:04.161 2 cognex_trigger_ok 8.7
17 14:33:05.165 2 cognex_ftp_start 1011.3
18 14:33:05.903 2 cognex_ftp_done 1754.8 3686454bytes
19 14:33:05.903 2 cognex_patmax_start 1756.2
20 14:33:05.918 2 cognex_patmax_done 1767.5
21 14:33:07.488 2 basler_capture_start 3334.0
22 14:33:07.635 2 basler_capture_done 3495.6 (4504, 4504)
23 14:33:07.651 2 cognex_join_wait 3500.8
24 14:33:07.651 2 cognex_join_done 3503.1
25 14:33:07.679 2 cycle_done 3524.7 result=FAIL cognex=FAIL basler=PASS
26 14:33:07.683 3 cycle_start 0.0 group=A belt_delay=3.33s
27 14:33:07.686 3 cognex_trigger_send 2.4
28 14:33:07.692 3 cognex_trigger_ok 9.2
29 14:33:08.695 3 cognex_ftp_start 1011.9
30 14:33:09.420 3 cognex_ftp_done 1751.4 3686454bytes
31 14:33:09.435 3 cognex_patmax_start 1753.0
32 14:33:09.435 3 cognex_patmax_done 1764.2
33 14:33:11.017 3 basler_capture_start 3334.2
34 14:33:11.170 3 basler_capture_done 3499.9 (4504, 4504)
35 14:33:11.185 3 cognex_join_wait 3506.1
36 14:33:11.211 3 cognex_join_done 3519.0
37 14:33:11.213 3 cycle_done 3530.5 result=FAIL cognex=FAIL basler=PASS
38 14:33:11.217 4 cycle_start 0.0 group=A belt_delay=3.33s
39 14:33:11.219 4 cognex_trigger_send 2.4
40 14:33:11.226 4 cognex_trigger_ok 9.0
41 14:33:12.228 4 cognex_ftp_start 1011.2
42 14:33:12.969 4 cognex_ftp_done 1758.8 3686454bytes
43 14:33:12.969 4 cognex_patmax_start 1760.3
44 14:33:12.985 4 cognex_patmax_done 1771.7
45 14:33:14.551 4 basler_capture_start 3334.3
46 14:33:14.703 4 basler_capture_done 3498.9 (4504, 4504)
47 14:33:14.718 4 cognex_join_wait 3505.0
48 14:33:14.734 4 cognex_join_done 3522.9
49 14:33:14.743 4 cycle_done 3526.9 result=FAIL cognex=FAIL basler=PASS
50 14:33:14.747 5 cycle_start 0.0 group=A belt_delay=3.33s
51 14:33:14.750 5 cognex_trigger_send 2.4
52 14:33:14.757 5 cognex_trigger_ok 8.9
53 14:33:15.759 5 cognex_ftp_start 1011.3
54 14:33:16.504 5 cognex_ftp_done 1761.8 3686454bytes
55 14:33:16.504 5 cognex_patmax_start 1763.3
56 14:33:16.519 5 cognex_patmax_done 1774.5
57 14:33:18.082 5 basler_capture_start 3334.0
58 14:33:18.282 5 basler_capture_done 3543.4 (4504, 4504)
59 14:33:18.298 5 cognex_join_wait 3550.7
60 14:33:18.314 5 cognex_join_done 3567.9
61 14:33:18.314 5 cycle_done 3571.6 result=FAIL cognex=FAIL basler=PASS
62 14:33:18.314 6 cycle_start 0.0 group=A belt_delay=3.33s
63 14:33:18.314 6 cognex_trigger_send 2.4
64 14:33:18.332 6 cognex_trigger_ok 9.0
65 14:33:19.335 6 cognex_ftp_start 1011.5
66 14:33:20.069 6 cognex_ftp_done 1754.5 3686454bytes
67 14:33:20.069 6 cognex_patmax_start 1756.0
68 14:33:20.085 6 cognex_patmax_done 1767.5
69 14:33:21.657 6 basler_capture_start 3334.1
70 14:33:21.865 6 basler_capture_done 3544.9 (4504, 4504)
71 14:33:21.865 6 cognex_join_wait 3551.8
72 14:33:21.881 6 cognex_join_done 3562.9
73 14:33:21.896 6 cycle_done 3573.4 result=FAIL cognex=FAIL basler=PASS
74 14:33:21.900 7 cycle_start 0.0 group=A belt_delay=3.33s
75 14:33:21.902 7 cognex_trigger_send 2.4
76 14:33:21.908 7 cognex_trigger_ok 8.9
77 14:33:22.911 7 cognex_ftp_start 1011.2
78 14:33:23.652 7 cognex_ftp_done 1757.8 3686454bytes
79 14:33:23.652 7 cognex_patmax_start 1759.3
80 14:33:23.668 7 cognex_patmax_done 1768.4
81 14:33:25.234 7 basler_capture_start 3334.2
82 14:33:25.447 7 basler_capture_done 3557.9 (4504, 4504)
83 14:33:25.463 7 cognex_join_wait 3564.9
84 14:33:25.463 7 cognex_join_done 3566.6
85 14:33:25.478 7 cycle_done 3583.8 result=FAIL cognex=FAIL basler=PASS
86 14:33:25.478 8 cycle_start 0.0 group=A belt_delay=3.33s
87 14:33:25.478 8 cognex_trigger_send 1.7
88 14:33:25.497 8 cognex_trigger_ok 10.0
89 14:33:26.500 8 cognex_ftp_start 1012.4
90 14:33:27.221 8 cognex_ftp_done 1747.7 3686454bytes
91 14:33:27.236 8 cognex_patmax_start 1749.2
92 14:33:27.236 8 cognex_patmax_done 1760.8
93 14:33:28.822 8 basler_capture_start 3334.2
94 14:33:29.032 8 basler_capture_done 3556.5 (4504, 4504)
95 14:33:29.048 8 cognex_join_wait 3563.6
96 14:33:29.064 8 cognex_join_done 3576.3
97 14:33:29.064 8 cycle_done 3587.3 result=FAIL cognex=FAIL basler=PASS
98 14:33:29.077 9 cycle_start 0.0 group=A belt_delay=3.33s
99 14:33:29.080 9 cognex_trigger_send 2.3
100 14:33:29.087 9 cognex_trigger_ok 9.0
101 14:33:30.089 9 cognex_ftp_start 1011.1
102 14:33:30.804 9 cognex_ftp_done 1737.9 3686454bytes
103 14:33:30.804 9 cognex_patmax_start 1739.4
104 14:33:30.829 9 cognex_patmax_done 1750.8
105 14:33:32.412 9 basler_capture_start 3333.8
106 14:33:32.662 9 basler_capture_done 3590.5 (4504, 4504)
107 14:33:32.662 9 cognex_join_wait 3597.5
108 14:33:32.693 9 cognex_join_done 3619.7
109 14:33:32.693 9 cycle_done 3623.2 result=FAIL cognex=FAIL basler=PASS

157
logs/timing/2026-05-13.csv Normal file
View File

@@ -0,0 +1,157 @@
timestamp,seq,event,elapsed_ms,detail
09:17:45.802,1,cycle_start,0.0,group=A belt_delay=3.33s
09:17:45.802,1,cognex_trigger_send,2.0,
09:17:45.844,1,cognex_trigger_ok,34.8,
09:17:46.848,1,cognex_ftp_start,1037.4,
09:17:47.596,1,cognex_ftp_done,1788.8,3686454bytes
09:17:47.596,1,cognex_patmax_start,1790.2,
09:17:47.612,1,cognex_patmax_done,1801.3,
09:17:49.144,1,basler_capture_start,3333.8,
09:17:49.144,1,basler_capture_done,3337.2,failed
09:17:49.144,1,cognex_join_wait,3339.7,
09:17:49.144,1,cognex_join_done,3341.7,
09:17:49.153,1,cycle_done,3343.7,result=FAIL cognex=FAIL basler=PASS
09:17:49.153,2,cycle_start,0.0,group=A belt_delay=3.33s
09:17:49.153,2,cognex_trigger_send,2.4,
09:17:49.184,2,cognex_trigger_ok,35.3,
09:17:50.196,2,cognex_ftp_start,1038.2,
09:17:50.929,2,cognex_ftp_done,1773.1,3686454bytes
09:17:50.929,2,cognex_patmax_start,1774.5,
09:17:50.929,2,cognex_patmax_done,1785.3,
09:17:52.492,2,basler_capture_start,3334.1,
09:17:52.492,2,basler_capture_done,3337.1,failed
09:17:52.492,2,cognex_join_wait,3339.1,
09:17:52.492,2,cognex_join_done,3341.1,
09:17:52.492,2,cycle_done,3343.0,result=FAIL cognex=FAIL basler=PASS
09:17:52.502,3,cycle_start,0.0,group=A belt_delay=3.33s
09:17:52.502,3,cognex_trigger_send,2.3,
09:17:52.533,3,cognex_trigger_ok,35.5,
09:17:53.542,3,cognex_ftp_start,1038.1,
09:17:54.263,3,cognex_ftp_done,1773.4,3686454bytes
09:17:54.279,3,cognex_patmax_start,1774.9,
09:17:54.279,3,cognex_patmax_done,1786.1,
09:17:55.838,3,basler_capture_start,3333.8,
09:17:55.838,3,basler_capture_done,3337.9,failed
09:17:55.838,3,cognex_join_wait,3340.0,
09:17:55.838,3,cognex_join_done,3341.9,
09:17:55.838,3,cycle_done,3343.8,result=FAIL cognex=FAIL basler=PASS
09:17:55.851,4,cycle_start,0.0,group=A belt_delay=3.33s
09:17:55.851,4,cognex_trigger_send,2.3,
09:17:55.882,4,cognex_trigger_ok,35.0,
09:17:56.889,4,cognex_ftp_start,1037.4,
09:17:57.628,4,cognex_ftp_done,1777.1,3686454bytes
09:17:57.629,4,cognex_patmax_start,1778.8,
09:17:57.629,4,cognex_patmax_done,1789.8,
09:17:59.185,4,basler_capture_start,3333.8,
09:17:59.185,4,basler_capture_done,3337.5,failed
09:17:59.185,4,cognex_join_wait,3339.4,
09:17:59.185,4,cognex_join_done,3341.3,
09:17:59.185,4,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
09:17:59.185,5,cycle_start,0.0,group=A belt_delay=3.33s
09:17:59.185,5,cognex_trigger_send,2.3,
09:17:59.232,5,cognex_trigger_ok,34.7,
09:18:00.235,5,cognex_ftp_start,1037.0,
09:18:00.963,5,cognex_ftp_done,1765.1,3686454bytes
09:18:00.963,5,cognex_patmax_start,1766.7,
09:18:00.963,5,cognex_patmax_done,1777.6,
09:18:02.532,5,basler_capture_start,3334.0,
09:18:02.535,5,basler_capture_done,3337.4,failed
09:18:02.535,5,cognex_join_wait,3339.8,
09:18:02.535,5,cognex_join_done,3341.7,
09:18:02.535,5,cycle_done,3343.7,result=FAIL cognex=FAIL basler=PASS
09:18:02.535,6,cycle_start,0.0,group=A belt_delay=3.33s
09:18:02.535,6,cognex_trigger_send,2.4,
09:18:02.567,6,cognex_trigger_ok,35.8,
09:18:03.584,6,cognex_ftp_start,1038.2,
09:18:04.329,6,cognex_ftp_done,1784.6,3686454bytes
09:18:04.329,6,cognex_patmax_start,1786.0,
09:18:04.329,6,cognex_patmax_done,1796.8,
09:18:05.880,6,basler_capture_start,3334.1,
09:18:05.880,6,basler_capture_done,3337.4,failed
09:18:05.880,6,cognex_join_wait,3339.4,
09:18:05.886,6,cognex_join_done,3341.4,
09:18:05.886,6,cycle_done,3343.3,result=FAIL cognex=FAIL basler=PASS
09:18:05.886,7,cycle_start,0.0,group=A belt_delay=3.33s
09:18:05.886,7,cognex_trigger_send,2.3,
09:18:05.918,7,cognex_trigger_ok,35.7,
09:18:06.931,7,cognex_ftp_start,1038.3,
09:18:07.679,7,cognex_ftp_done,1787.3,3686454bytes
09:18:07.679,7,cognex_patmax_start,1788.9,
09:18:07.679,7,cognex_patmax_done,1797.9,
09:18:09.227,7,basler_capture_start,3334.1,
09:18:09.227,7,basler_capture_done,3338.3,failed
09:18:09.227,7,cognex_join_wait,3340.1,
09:18:09.227,7,cognex_join_done,3341.4,
09:18:09.227,7,cycle_done,3342.7,result=FAIL cognex=FAIL basler=PASS
09:18:09.236,8,cycle_start,0.0,group=A belt_delay=3.33s
09:18:09.236,8,cognex_trigger_send,1.6,
09:18:09.267,8,cognex_trigger_ok,33.6,
09:18:10.274,8,cognex_ftp_start,1035.8,
09:18:11.029,8,cognex_ftp_done,1792.5,3686454bytes
09:18:11.029,8,cognex_patmax_start,1794.0,
09:18:11.029,8,cognex_patmax_done,1805.1,
09:18:12.572,8,basler_capture_start,3333.9,
09:18:12.572,8,basler_capture_done,3337.1,failed
09:18:12.572,8,cognex_join_wait,3339.0,
09:18:12.572,8,cognex_join_done,3340.9,
09:18:12.572,8,cycle_done,3342.8,result=FAIL cognex=FAIL basler=PASS
09:18:12.572,9,cycle_start,0.0,group=A belt_delay=3.33s
09:18:12.585,9,cognex_trigger_send,2.2,
09:18:12.617,9,cognex_trigger_ok,35.1,
09:18:13.622,9,cognex_ftp_start,1037.4,
09:18:14.379,9,cognex_ftp_done,1795.2,3686454bytes
09:18:14.379,9,cognex_patmax_start,1796.8,
09:18:14.379,9,cognex_patmax_done,1807.6,
09:18:15.918,9,basler_capture_start,3334.2,
09:18:15.922,9,basler_capture_done,3337.8,failed
09:18:15.922,9,cognex_join_wait,3340.4,
09:18:15.922,9,cognex_join_done,3342.3,
09:18:15.922,9,cycle_done,3344.1,result=FAIL cognex=FAIL basler=PASS
09:18:15.922,10,cycle_start,0.0,group=A belt_delay=3.33s
09:18:15.922,10,cognex_trigger_send,2.2,
09:18:15.953,10,cognex_trigger_ok,35.3,
09:18:16.970,10,cognex_ftp_start,1038.3,
09:18:17.713,10,cognex_ftp_done,1782.4,3686454bytes
09:18:17.713,10,cognex_patmax_start,1783.9,
09:18:17.713,10,cognex_patmax_done,1795.0,
09:18:19.265,10,basler_capture_start,3333.9,
09:18:19.265,10,basler_capture_done,3336.9,failed
09:18:19.269,10,cognex_join_wait,3338.8,
09:18:19.269,10,cognex_join_done,3340.7,
09:18:19.269,10,cycle_done,3342.6,result=FAIL cognex=FAIL basler=PASS
09:18:19.269,11,cycle_start,0.0,group=A belt_delay=3.33s
09:18:19.269,11,cognex_trigger_send,2.2,
09:18:19.300,11,cognex_trigger_ok,35.9,
09:18:20.316,11,cognex_ftp_start,1038.1,
09:18:21.063,11,cognex_ftp_done,1786.5,3686454bytes
09:18:21.063,11,cognex_patmax_start,1788.0,
09:18:21.063,11,cognex_patmax_done,1798.9,
09:18:22.611,11,basler_capture_start,3333.9,
09:18:22.611,11,basler_capture_done,3336.9,failed
09:18:22.611,11,cognex_join_wait,3340.2,
09:18:22.619,11,cognex_join_done,3342.2,
09:18:22.619,11,cycle_done,3344.0,result=FAIL cognex=FAIL basler=PASS
09:18:22.619,12,cycle_start,0.0,group=A belt_delay=3.33s
09:18:22.619,12,cognex_trigger_send,2.3,
09:18:22.650,12,cognex_trigger_ok,35.8,
09:18:23.663,12,cognex_ftp_start,1037.8,
09:18:24.413,12,cognex_ftp_done,1789.3,3686454bytes
09:18:24.413,12,cognex_patmax_start,1790.7,
09:18:24.413,12,cognex_patmax_done,1801.7,
09:18:25.959,12,basler_capture_start,3333.7,
09:18:25.959,12,basler_capture_done,3336.6,failed
09:18:25.959,12,cognex_join_wait,3338.6,
09:18:25.959,12,cognex_join_done,3340.5,
09:18:25.959,12,cycle_done,3342.3,result=FAIL cognex=FAIL basler=PASS
09:18:25.969,13,cycle_start,0.0,group=A belt_delay=3.33s
09:18:25.969,13,cognex_trigger_send,2.3,
09:18:26.004,13,cognex_trigger_ok,34.8,
09:18:27.009,13,cognex_ftp_start,1038.3,
09:18:27.746,13,cognex_ftp_done,1775.8,3686454bytes
09:18:27.746,13,cognex_patmax_start,1777.3,
09:18:27.746,13,cognex_patmax_done,1788.4,
09:18:29.305,13,basler_capture_start,3333.8,
09:18:29.305,13,basler_capture_done,3337.0,failed
09:18:29.305,13,cognex_join_wait,3338.9,
09:18:29.305,13,cognex_join_done,3340.8,
09:18:29.305,13,cycle_done,3342.6,result=FAIL cognex=FAIL basler=PASS
1 timestamp seq event elapsed_ms detail
2 09:17:45.802 1 cycle_start 0.0 group=A belt_delay=3.33s
3 09:17:45.802 1 cognex_trigger_send 2.0
4 09:17:45.844 1 cognex_trigger_ok 34.8
5 09:17:46.848 1 cognex_ftp_start 1037.4
6 09:17:47.596 1 cognex_ftp_done 1788.8 3686454bytes
7 09:17:47.596 1 cognex_patmax_start 1790.2
8 09:17:47.612 1 cognex_patmax_done 1801.3
9 09:17:49.144 1 basler_capture_start 3333.8
10 09:17:49.144 1 basler_capture_done 3337.2 failed
11 09:17:49.144 1 cognex_join_wait 3339.7
12 09:17:49.144 1 cognex_join_done 3341.7
13 09:17:49.153 1 cycle_done 3343.7 result=FAIL cognex=FAIL basler=PASS
14 09:17:49.153 2 cycle_start 0.0 group=A belt_delay=3.33s
15 09:17:49.153 2 cognex_trigger_send 2.4
16 09:17:49.184 2 cognex_trigger_ok 35.3
17 09:17:50.196 2 cognex_ftp_start 1038.2
18 09:17:50.929 2 cognex_ftp_done 1773.1 3686454bytes
19 09:17:50.929 2 cognex_patmax_start 1774.5
20 09:17:50.929 2 cognex_patmax_done 1785.3
21 09:17:52.492 2 basler_capture_start 3334.1
22 09:17:52.492 2 basler_capture_done 3337.1 failed
23 09:17:52.492 2 cognex_join_wait 3339.1
24 09:17:52.492 2 cognex_join_done 3341.1
25 09:17:52.492 2 cycle_done 3343.0 result=FAIL cognex=FAIL basler=PASS
26 09:17:52.502 3 cycle_start 0.0 group=A belt_delay=3.33s
27 09:17:52.502 3 cognex_trigger_send 2.3
28 09:17:52.533 3 cognex_trigger_ok 35.5
29 09:17:53.542 3 cognex_ftp_start 1038.1
30 09:17:54.263 3 cognex_ftp_done 1773.4 3686454bytes
31 09:17:54.279 3 cognex_patmax_start 1774.9
32 09:17:54.279 3 cognex_patmax_done 1786.1
33 09:17:55.838 3 basler_capture_start 3333.8
34 09:17:55.838 3 basler_capture_done 3337.9 failed
35 09:17:55.838 3 cognex_join_wait 3340.0
36 09:17:55.838 3 cognex_join_done 3341.9
37 09:17:55.838 3 cycle_done 3343.8 result=FAIL cognex=FAIL basler=PASS
38 09:17:55.851 4 cycle_start 0.0 group=A belt_delay=3.33s
39 09:17:55.851 4 cognex_trigger_send 2.3
40 09:17:55.882 4 cognex_trigger_ok 35.0
41 09:17:56.889 4 cognex_ftp_start 1037.4
42 09:17:57.628 4 cognex_ftp_done 1777.1 3686454bytes
43 09:17:57.629 4 cognex_patmax_start 1778.8
44 09:17:57.629 4 cognex_patmax_done 1789.8
45 09:17:59.185 4 basler_capture_start 3333.8
46 09:17:59.185 4 basler_capture_done 3337.5 failed
47 09:17:59.185 4 cognex_join_wait 3339.4
48 09:17:59.185 4 cognex_join_done 3341.3
49 09:17:59.185 4 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS
50 09:17:59.185 5 cycle_start 0.0 group=A belt_delay=3.33s
51 09:17:59.185 5 cognex_trigger_send 2.3
52 09:17:59.232 5 cognex_trigger_ok 34.7
53 09:18:00.235 5 cognex_ftp_start 1037.0
54 09:18:00.963 5 cognex_ftp_done 1765.1 3686454bytes
55 09:18:00.963 5 cognex_patmax_start 1766.7
56 09:18:00.963 5 cognex_patmax_done 1777.6
57 09:18:02.532 5 basler_capture_start 3334.0
58 09:18:02.535 5 basler_capture_done 3337.4 failed
59 09:18:02.535 5 cognex_join_wait 3339.8
60 09:18:02.535 5 cognex_join_done 3341.7
61 09:18:02.535 5 cycle_done 3343.7 result=FAIL cognex=FAIL basler=PASS
62 09:18:02.535 6 cycle_start 0.0 group=A belt_delay=3.33s
63 09:18:02.535 6 cognex_trigger_send 2.4
64 09:18:02.567 6 cognex_trigger_ok 35.8
65 09:18:03.584 6 cognex_ftp_start 1038.2
66 09:18:04.329 6 cognex_ftp_done 1784.6 3686454bytes
67 09:18:04.329 6 cognex_patmax_start 1786.0
68 09:18:04.329 6 cognex_patmax_done 1796.8
69 09:18:05.880 6 basler_capture_start 3334.1
70 09:18:05.880 6 basler_capture_done 3337.4 failed
71 09:18:05.880 6 cognex_join_wait 3339.4
72 09:18:05.886 6 cognex_join_done 3341.4
73 09:18:05.886 6 cycle_done 3343.3 result=FAIL cognex=FAIL basler=PASS
74 09:18:05.886 7 cycle_start 0.0 group=A belt_delay=3.33s
75 09:18:05.886 7 cognex_trigger_send 2.3
76 09:18:05.918 7 cognex_trigger_ok 35.7
77 09:18:06.931 7 cognex_ftp_start 1038.3
78 09:18:07.679 7 cognex_ftp_done 1787.3 3686454bytes
79 09:18:07.679 7 cognex_patmax_start 1788.9
80 09:18:07.679 7 cognex_patmax_done 1797.9
81 09:18:09.227 7 basler_capture_start 3334.1
82 09:18:09.227 7 basler_capture_done 3338.3 failed
83 09:18:09.227 7 cognex_join_wait 3340.1
84 09:18:09.227 7 cognex_join_done 3341.4
85 09:18:09.227 7 cycle_done 3342.7 result=FAIL cognex=FAIL basler=PASS
86 09:18:09.236 8 cycle_start 0.0 group=A belt_delay=3.33s
87 09:18:09.236 8 cognex_trigger_send 1.6
88 09:18:09.267 8 cognex_trigger_ok 33.6
89 09:18:10.274 8 cognex_ftp_start 1035.8
90 09:18:11.029 8 cognex_ftp_done 1792.5 3686454bytes
91 09:18:11.029 8 cognex_patmax_start 1794.0
92 09:18:11.029 8 cognex_patmax_done 1805.1
93 09:18:12.572 8 basler_capture_start 3333.9
94 09:18:12.572 8 basler_capture_done 3337.1 failed
95 09:18:12.572 8 cognex_join_wait 3339.0
96 09:18:12.572 8 cognex_join_done 3340.9
97 09:18:12.572 8 cycle_done 3342.8 result=FAIL cognex=FAIL basler=PASS
98 09:18:12.572 9 cycle_start 0.0 group=A belt_delay=3.33s
99 09:18:12.585 9 cognex_trigger_send 2.2
100 09:18:12.617 9 cognex_trigger_ok 35.1
101 09:18:13.622 9 cognex_ftp_start 1037.4
102 09:18:14.379 9 cognex_ftp_done 1795.2 3686454bytes
103 09:18:14.379 9 cognex_patmax_start 1796.8
104 09:18:14.379 9 cognex_patmax_done 1807.6
105 09:18:15.918 9 basler_capture_start 3334.2
106 09:18:15.922 9 basler_capture_done 3337.8 failed
107 09:18:15.922 9 cognex_join_wait 3340.4
108 09:18:15.922 9 cognex_join_done 3342.3
109 09:18:15.922 9 cycle_done 3344.1 result=FAIL cognex=FAIL basler=PASS
110 09:18:15.922 10 cycle_start 0.0 group=A belt_delay=3.33s
111 09:18:15.922 10 cognex_trigger_send 2.2
112 09:18:15.953 10 cognex_trigger_ok 35.3
113 09:18:16.970 10 cognex_ftp_start 1038.3
114 09:18:17.713 10 cognex_ftp_done 1782.4 3686454bytes
115 09:18:17.713 10 cognex_patmax_start 1783.9
116 09:18:17.713 10 cognex_patmax_done 1795.0
117 09:18:19.265 10 basler_capture_start 3333.9
118 09:18:19.265 10 basler_capture_done 3336.9 failed
119 09:18:19.269 10 cognex_join_wait 3338.8
120 09:18:19.269 10 cognex_join_done 3340.7
121 09:18:19.269 10 cycle_done 3342.6 result=FAIL cognex=FAIL basler=PASS
122 09:18:19.269 11 cycle_start 0.0 group=A belt_delay=3.33s
123 09:18:19.269 11 cognex_trigger_send 2.2
124 09:18:19.300 11 cognex_trigger_ok 35.9
125 09:18:20.316 11 cognex_ftp_start 1038.1
126 09:18:21.063 11 cognex_ftp_done 1786.5 3686454bytes
127 09:18:21.063 11 cognex_patmax_start 1788.0
128 09:18:21.063 11 cognex_patmax_done 1798.9
129 09:18:22.611 11 basler_capture_start 3333.9
130 09:18:22.611 11 basler_capture_done 3336.9 failed
131 09:18:22.611 11 cognex_join_wait 3340.2
132 09:18:22.619 11 cognex_join_done 3342.2
133 09:18:22.619 11 cycle_done 3344.0 result=FAIL cognex=FAIL basler=PASS
134 09:18:22.619 12 cycle_start 0.0 group=A belt_delay=3.33s
135 09:18:22.619 12 cognex_trigger_send 2.3
136 09:18:22.650 12 cognex_trigger_ok 35.8
137 09:18:23.663 12 cognex_ftp_start 1037.8
138 09:18:24.413 12 cognex_ftp_done 1789.3 3686454bytes
139 09:18:24.413 12 cognex_patmax_start 1790.7
140 09:18:24.413 12 cognex_patmax_done 1801.7
141 09:18:25.959 12 basler_capture_start 3333.7
142 09:18:25.959 12 basler_capture_done 3336.6 failed
143 09:18:25.959 12 cognex_join_wait 3338.6
144 09:18:25.959 12 cognex_join_done 3340.5
145 09:18:25.959 12 cycle_done 3342.3 result=FAIL cognex=FAIL basler=PASS
146 09:18:25.969 13 cycle_start 0.0 group=A belt_delay=3.33s
147 09:18:25.969 13 cognex_trigger_send 2.3
148 09:18:26.004 13 cognex_trigger_ok 34.8
149 09:18:27.009 13 cognex_ftp_start 1038.3
150 09:18:27.746 13 cognex_ftp_done 1775.8 3686454bytes
151 09:18:27.746 13 cognex_patmax_start 1777.3
152 09:18:27.746 13 cognex_patmax_done 1788.4
153 09:18:29.305 13 basler_capture_start 3333.8
154 09:18:29.305 13 basler_capture_done 3337.0 failed
155 09:18:29.305 13 cognex_join_wait 3338.9
156 09:18:29.305 13 cognex_join_done 3340.8
157 09:18:29.305 13 cycle_done 3342.6 result=FAIL cognex=FAIL basler=PASS

View File

@@ -0,0 +1,25 @@
timestamp,seq,event,elapsed_ms,detail
10:10:37.775,1,cycle_start,0.0,group=A belt_delay=3.33s
10:10:37.775,1,cognex_trigger_send,1.9,
10:10:37.775,1,cognex_error,6.0,'NoneType' object has no attribute 'sendall'
10:10:41.118,1,basler_capture_start,3333.8,
10:10:41.118,1,basler_capture_done,3336.5,failed
10:10:41.118,1,cognex_join_wait,3338.5,
10:10:41.118,1,cognex_join_done,3340.4,
10:10:41.118,1,cycle_done,3342.4,result=FAIL cognex=FAIL basler=PASS
10:10:46.463,2,cycle_start,0.0,group=A belt_delay=3.33s
10:10:46.465,2,cognex_trigger_send,2.5,
10:10:46.469,2,cognex_error,6.0,'NoneType' object has no attribute 'sendall'
10:10:49.797,2,basler_capture_start,3333.9,
10:10:49.797,2,basler_capture_done,3336.8,failed
10:10:49.797,2,cognex_join_wait,3338.8,
10:10:49.797,2,cognex_join_done,3340.8,
10:10:49.797,2,cycle_done,3342.7,result=FAIL cognex=FAIL basler=PASS
10:10:49.797,3,cycle_start,0.0,group=A belt_delay=3.33s
10:10:49.810,3,cognex_trigger_send,2.3,
10:10:49.810,3,cognex_error,5.6,'NoneType' object has no attribute 'sendall'
10:10:53.143,3,basler_capture_start,3334.0,
10:10:53.143,3,basler_capture_done,3337.1,failed
10:10:53.143,3,cognex_join_wait,3339.3,
10:10:53.143,3,cognex_join_done,3341.3,
10:10:53.151,3,cycle_done,3343.2,result=FAIL cognex=FAIL basler=PASS
1 timestamp seq event elapsed_ms detail
2 10:10:37.775 1 cycle_start 0.0 group=A belt_delay=3.33s
3 10:10:37.775 1 cognex_trigger_send 1.9
4 10:10:37.775 1 cognex_error 6.0 'NoneType' object has no attribute 'sendall'
5 10:10:41.118 1 basler_capture_start 3333.8
6 10:10:41.118 1 basler_capture_done 3336.5 failed
7 10:10:41.118 1 cognex_join_wait 3338.5
8 10:10:41.118 1 cognex_join_done 3340.4
9 10:10:41.118 1 cycle_done 3342.4 result=FAIL cognex=FAIL basler=PASS
10 10:10:46.463 2 cycle_start 0.0 group=A belt_delay=3.33s
11 10:10:46.465 2 cognex_trigger_send 2.5
12 10:10:46.469 2 cognex_error 6.0 'NoneType' object has no attribute 'sendall'
13 10:10:49.797 2 basler_capture_start 3333.9
14 10:10:49.797 2 basler_capture_done 3336.8 failed
15 10:10:49.797 2 cognex_join_wait 3338.8
16 10:10:49.797 2 cognex_join_done 3340.8
17 10:10:49.797 2 cycle_done 3342.7 result=FAIL cognex=FAIL basler=PASS
18 10:10:49.797 3 cycle_start 0.0 group=A belt_delay=3.33s
19 10:10:49.810 3 cognex_trigger_send 2.3
20 10:10:49.810 3 cognex_error 5.6 'NoneType' object has no attribute 'sendall'
21 10:10:53.143 3 basler_capture_start 3334.0
22 10:10:53.143 3 basler_capture_done 3337.1 failed
23 10:10:53.143 3 cognex_join_wait 3339.3
24 10:10:53.143 3 cognex_join_done 3341.3
25 10:10:53.151 3 cycle_done 3343.2 result=FAIL cognex=FAIL basler=PASS

156
main.py Normal file
View File

@@ -0,0 +1,156 @@
# 엔트리포인트 — 스플래시 화면 표시 후 백그라운드 초기화, GUI 실행
import multiprocessing
import sys
from logger import setup_logging, teardown_logging
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from utils.path_helper import get_path
from gui.main_window import MainWindow
from gui.splash_screen import SplashScreen, InitWorker
ICON_PATH = get_path("assets", "images", "ant_logo.png")
def _apply_windows_taskbar_icon(app_id: str = "ant.vision.inspection.1.0"):
if sys.platform != "win32":
return
try:
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
except Exception as e:
print(f"[main] AppUserModelID 설정 실패: {e}")
def main():
# setup_logging() must be called inside main(), not at module level.
# On Windows, multiprocessing spawns subprocesses that re-import __main__,
# and module-level setup_logging() would corrupt the log file and replace
# builtins.print in every worker process.
setup_logging()
_apply_windows_taskbar_icon()
app = QApplication(sys.argv)
icon = QIcon(ICON_PATH)
app.setWindowIcon(icon)
app.setStyleSheet(_DARK_STYLE)
splash = SplashScreen()
splash.show()
QApplication.processEvents()
worker = InitWorker()
worker.progress.connect(splash.update_progress)
def on_finished(results):
window = MainWindow(
results["insight"],
results["basler"],
results["config"],
plc_client=results.get("plc"),
)
window.setWindowIcon(icon)
window.show()
splash.close()
def cleanup():
results["insight"].disconnect()
results["basler"].disconnect()
if results["db"].is_connected():
results["db"].disconnect()
plc = results.get("plc")
if plc and plc.is_connected():
plc.disconnect()
app.aboutToQuit.connect(cleanup)
worker.finished.connect(on_finished)
worker.start()
try:
ret = app.exec_()
finally:
teardown_logging()
sys.exit(ret)
_DARK_STYLE = """
QWidget {
background-color: #1a1a1a;
color: #ffffff;
font-size: 14px;
}
QGroupBox {
background-color: #222222;
border: 1px solid #444444;
border-radius: 6px;
margin-top: 14px;
padding: 10px 8px 8px 8px;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 10px;
padding: 0 4px;
color: #aaaaaa;
}
QPushButton {
background-color: #2e2e2e;
color: #ffffff;
border: 1px solid #555555;
border-radius: 4px;
min-height: 56px;
padding: 0 20px;
font-size: 14px;
}
QPushButton:hover { background-color: #3a3a3a; }
QPushButton:pressed { background-color: #1e1e1e; }
QPushButton:checked { background-color: #0055cc; border-color: #0077ff; }
QPushButton:disabled { color: #666666; }
QLineEdit, QSpinBox, QDoubleSpinBox {
background-color: #2a2a2a;
color: #ffffff;
border: 1px solid #555555;
border-radius: 4px;
padding: 6px 8px;
min-height: 38px;
}
QLabel { color: #ffffff; }
QListWidget {
background-color: #222222;
color: #ffffff;
border: 1px solid #444444;
outline: none;
}
QListWidget::item:selected { background-color: #0055cc; }
QProgressBar {
background-color: #2a2a2a;
border: 1px solid #555555;
border-radius: 4px;
text-align: center;
min-height: 22px;
}
QProgressBar::chunk { background-color: #0055cc; border-radius: 4px; }
QCheckBox { min-height: 38px; }
QCheckBox::indicator { width: 22px; height: 22px; }
QScrollBar:vertical {
background: #2a2a2a;
width: 10px;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background: #555555;
border-radius: 5px;
min-height: 30px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
"""
if __name__ == "__main__":
multiprocessing.freeze_support()
main()

195
main_py_필요모듈.txt Normal file
View File

@@ -0,0 +1,195 @@
================================================================================
main.py 실행에 필요한 모듈 및 환경 정리
프로젝트: ANT 리플렉터 비전 검사 시스템
분석 기준: E:\ANT 폴더 소스 코드 및 .venv 설치 목록
작성일: 2026-06-10
================================================================================
■ 1. 실행 방법
--------------------------------------------------------------------------------
1) 프로젝트 루트(E:\ANT)에서 실행
python main.py
2) 가상환경 사용 권장 (.venv)
.venv\Scripts\activate
python main.py
※ Python 3.12 기준으로 .venv가 구성되어 있음 (pyvenv.cfg: 3.12.10)
■ 2. pip 설치 패키지 (서드파티 — 필수)
--------------------------------------------------------------------------------
패키지명 pip 명령 용도 사용 위치
─────────────────────────────────────────────────────────────────────────────
PyQt5 pip install PyQt5 GUI 프레임워크 main.py, gui/*
opencv-python pip install opencv-python 이미지 처리, ORB/NCC inspect_page, register_page,
retrain_page, pattern_matcher, inspector
numpy pip install numpy 배열/이미지 데이터 detector, basler, inspect_page 등
ultralytics pip install ultralytics YOLOv8 추론/재학습 ai/detector.py, ai/trainer.py
pypylon pip install pypylon Basler 카메라 SDK camera/basler.py
pyodbc pip install pyodbc MS SQL Server 연결 db/sql_client.py
pymelsec pip install pymelsec Mitsubishi PLC (MC 3E) plc/plc_client.py
pyyaml pip install pyyaml 학습 데이터셋 YAML 생성 ai/trainer.py
※ ultralytics 설치 시 torch, torchvision 등 AI 관련 의존성이 함께 설치됨
※ 현재 .venv 기준: torch 2.5.1+cu121, torchvision 0.20.1+cu121, ultralytics 8.4.41
■ 3. pip 설치 패키지 (간접/자동 의존성)
--------------------------------------------------------------------------------
ultralytics → torch, torchvision, scipy, matplotlib, pillow, polars, psutil 등
opencv-python → numpy
PyQt5 → PyQt5-Qt5, PyQt5-sip
현재 .venv에 함께 설치된 주요 간접 패키지:
- scipy 1.17.1
- matplotlib 3.10.9
- pillow 12.1.1
- requests 2.33.1
- lap 0.5.13 (ultralytics 추적용)
■ 4. pip 설치 패키지 (선택 / 레거시)
--------------------------------------------------------------------------------
pymysql pip install pymysql MySQL 클라이언트 (레거시) db/mysql_client.py
※ main.py 실행 경로에서는 미사용
※ db/sql_client.py (pyodbc)가 실제 사용됨
pyinstaller pip install pyinstaller EXE 빌드용 reflector_inspector.spec
※ 앱 실행 자체에는 불필요
■ 5. 시스템 / 외부 소프트웨어 의존성
--------------------------------------------------------------------------------
[필수 — Windows]
- Windows 10/11 (main.py에서 작업표시줄 아이콘 설정 시 ctypes 사용)
[DB 사용 시]
- Microsoft ODBC Driver 18 for SQL Server
※ pyodbc 연결 문자열: DRIVER={ODBC Driver 18 for SQL Server}
※ config.json → db.server / database / username / password
[Basler 카메라 사용 시]
- Basler pylon Camera Software Suite (별도 설치 필수)
다운로드: https://www.baslerweb.com/en/downloads/software-downloads/
※ pypylon은 pylon SDK가 먼저 설치되어 있어야 동작함
[Cognex In-Sight 카메라 사용 시]
- 네트워크 연결 (Telnet 포트 23, FTP)
※ config.json → cognex.ip / cognex.port
[PLC 사용 시]
- Mitsubishi PLC (MC Protocol 3E, 기본 포트 5010)
※ config.json → plc.ip / plc.port
[AI 추론/재학습 GPU 가속 (선택)]
- NVIDIA GPU + CUDA 12.1 드라이버
※ CPU만으로도 실행 가능 (속도는 느림)
■ 6. 프로젝트 내부 모듈 (로컬 Python 패키지)
--------------------------------------------------------------------------------
main.py가 직접/간접으로 import하는 프로젝트 모듈:
[루트]
logger.py — 로그 설정/해제, 검사·타이밍·학습 로그
paths.py — PROJECT_ROOT / BUNDLE_ROOT 경로 해석
[utils/]
utils/path_helper.py — get_path(), BASE_PATH (PyInstaller 호환)
[gui/]
gui/main_window.py — 메인 윈도우 (4탭 네비게이션)
gui/splash_screen.py — 스플래시 화면 + InitWorker (백그라운드 초기화)
gui/pages/settings_page.py — 환경설정 탭
gui/pages/register_page.py — 제품 등록 탭
gui/pages/inspect_page.py — 검사 탭
gui/pages/retrain_page.py — 재학습 탭
gui/dialogs/image_settings_dialog.py — 이미지 설정 다이얼로그
[camera/]
camera/insight.py — Cognex In-Sight (Telnet + FTP)
camera/basler.py — Basler USB 카메라 (pypylon)
[ai/]
ai/detector.py — YOLOv8 불량 검출 (ultralytics 지연 로딩)
ai/trainer.py — YOLOv8 재학습 + TrainWorker (subprocess 격리)
[db/]
db/sql_client.py — MS SQL Server (pyodbc)
[plc/]
plc/plc_client.py — Mitsubishi PLC (pymelsec Type3E)
[logic/]
logic/inspector.py — PatMax 결과 판독 + Pass/Fail
logic/group_manager.py — A/B 그룹 전환
logic/pattern_matcher.py — ORB + NCC 패턴 매칭
■ 7. Python 표준 라이브러리 (별도 pip 설치 불필요)
--------------------------------------------------------------------------------
multiprocessing, sys, json, os, socket, ftplib, io, time,
threading, itertools, pickle, shutil, random, csv, builtins,
pathlib, datetime, typing, ctypes (Windows 작업표시줄 아이콘)
■ 8. 실행 시 필요한 파일 / 리소스
--------------------------------------------------------------------------------
[필수]
config.json — 카메라/DB/AI/PLC 설정 (프로젝트 루트)
[권장 / 기능별]
ai/models/best.pt — YOLO AI 모델 (config.json → ai.model_path)
assets/images/ant_logo.png — 앱 아이콘 (main.py ICON_PATH)
assets/patterns.pkl — PatternMatcher 등록 패턴 (없으면 신규 생성 가능)
[자동 생성]
logs/app/ — 앱 로그
logs/inspect/ — 검사 결과 CSV
logs/timing/ — 카메라 타이밍 CSV
logs/train/ — 재학습 로그
■ 9. main.py 실행 흐름 (모듈 로딩 순서)
--------------------------------------------------------------------------------
main.py
→ logger.setup_logging()
→ PyQt5 QApplication 생성
→ gui/splash_screen.SplashScreen 표시
→ gui/splash_screen.InitWorker (QThread) 백그라운드 초기화:
1) config.json 로드
2) camera/insight.InSightCamera 연결
3) camera/basler.BaslerCamera 연결
4) ai/detector.Detector 모델 로드 (ultralytics)
5) db/sql_client.SQLClient DB 연결 (pyodbc)
6) plc/plc_client.PLCClient 연결 (pymelsec)
→ gui/main_window.MainWindow 표시
→ PyQt5 이벤트 루프 (app.exec_())
→ logger.teardown_logging()
■ 10. 한 번에 설치하는 pip 명령 (권장)
--------------------------------------------------------------------------------
pip install PyQt5 opencv-python numpy ultralytics pypylon pyodbc pymelsec pyyaml
※ GPU(CUDA 12.1) torch가 필요한 경우 ultralytics 설치 전/후에
PyTorch 공식 사이트 기준 cu121 wheel을 별도 설치할 수 있음.
■ 11. 현재 .venv 설치 버전 참고 (2026-06-10 기준)
--------------------------------------------------------------------------------
PyQt5==5.15.11
opencv-python==4.13.0.92
numpy==2.4.3
ultralytics==8.4.41
torch==2.5.1+cu121
torchvision==0.20.1+cu121
pypylon==26.3.1
pyodbc==5.3.0
pymelsec==0.2.5
pyyaml==6.0.3
pymysql==1.1.3 (레거시, main 실행 경로 미사용)
pyinstaller==6.20.0 (빌드 전용)
================================================================================

60
paths.py Normal file
View File

@@ -0,0 +1,60 @@
# 프로젝트 경로 유틸리티 — 개발/EXE 환경 모두에서 상대↔절대 경로 일관 처리
#
# 두 종류의 루트가 있음:
# PROJECT_ROOT : 사용자가 보는 실제 폴더. 로그/설정/모델 등 쓰기 가능한 위치.
# - dev 모드: 이 파일이 있는 폴더 (E:\ANT)
# - EXE(--onefile) 모드: EXE가 놓인 폴더 (예: E:\ANT\dist)
# BUNDLE_ROOT : 번들에 포함된 읽기 전용 자원 위치.
# - dev 모드: PROJECT_ROOT 와 동일
# - EXE 모드: PyInstaller 임시 추출 폴더 (sys._MEIPASS)
import os
import sys
_FROZEN = getattr(sys, "frozen", False)
# 번들된 자원(읽기 전용): config.json, ai/models, assets 등
BUNDLE_ROOT = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
# 사용자 데이터(읽기/쓰기): logs, 변경된 config, 새로 저장된 모델 등
if _FROZEN:
PROJECT_ROOT = os.path.dirname(os.path.abspath(sys.executable))
else:
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
def resolve_path(path: str) -> str:
"""상대경로 → 절대경로 변환.
1) 절대경로/빈 값은 그대로 반환
2) PROJECT_ROOT 기준 경로가 존재하면 사용 (사용자가 변경한 사본 우선)
3) BUNDLE_ROOT 기준 경로가 존재하면 폴백 (번들 기본 사본)
4) 둘 다 없으면 PROJECT_ROOT 기준 경로 반환 (신규 생성 시 사용자 영역에 만들도록)
"""
if not path:
return path
if os.path.isabs(path):
return os.path.normpath(path)
primary = os.path.normpath(os.path.join(PROJECT_ROOT, path))
if os.path.exists(primary):
return primary
if BUNDLE_ROOT and BUNDLE_ROOT != PROJECT_ROOT:
fallback = os.path.normpath(os.path.join(BUNDLE_ROOT, path))
if os.path.exists(fallback):
return fallback
return primary
def to_project_relative(path: str) -> str:
"""PROJECT_ROOT 또는 BUNDLE_ROOT 하위면 슬래시 구분 상대경로로, 외부면 슬래시 구분 절대경로로."""
if not path:
return path
abs_path = os.path.abspath(path)
for root in (PROJECT_ROOT, BUNDLE_ROOT):
if not root:
continue
try:
rel = os.path.relpath(abs_path, root)
except ValueError:
continue
if not rel.startswith(".."):
return rel.replace("\\", "/")
return abs_path.replace("\\", "/")

2
plc/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
# plc 패키지 — PLCClient 노출
from .plc_client import PLCClient

46
plc/plc_client.py Normal file
View File

@@ -0,0 +1,46 @@
from pymelsec import Type3E
class PLCClient:
def __init__(self):
self.plc = None
self._ip = ""
self._port = 5010
self._connected = False
def connect(self, ip: str, port: int = 5010) -> bool:
try:
self._ip = ip
self._port = port
self.plc = Type3E(host=ip, port=port, plc_type="Q")
self.plc.connect(ip, port)
self._connected = True
print(f"[PLC] 연결 성공: {ip}:{port}")
return True
except Exception as e:
print(f"[PLC] 연결 실패: {e}")
self._connected = False
self.plc = None
return False
def disconnect(self):
try:
if self.plc:
self.plc.close()
print("[PLC] 연결 해제")
except Exception as e:
print(f"[PLC] 연결 해제 오류: {e}")
finally:
self.plc = None
self._connected = False
def is_connected(self) -> bool:
return self._connected
def send_signal(self, signal: str, value=None):
"""신호 전송 — 미구현"""
pass
def read_signal(self, address: str):
"""신호 읽기 — 미구현"""
pass

372
plc_test_gui.py Normal file
View File

@@ -0,0 +1,372 @@
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_())

235
progress_report.txt Normal file
View File

@@ -0,0 +1,235 @@
=== 리플렉터 검사 시스템 실제 구현 현황 ===
분석 일시: 2026-05-07
전체 진행률: 27/35 (77%)
판정 기준
✅ 완료 : 실제 동작 코드, placeholder 없음
🔶 부분구현 : 일부는 실제 동작, 일부 print()/pass placeholder 존재
❌ 미구현 : 코드 없음 또는 전부 pass/print placeholder
🔲 블로킹 : 코드 외부 의존성(장비 설정/데이터)으로 구현 불가
────────────────────────────────────────────────────────────
[환경설정] 6/7
✅ 1. 코그넥스 IP/포트 연결
근거: settings_page.py L.520~567 _on_cognex_connect()에서
InSightCamera().connect(ip, port) 실제 호출.
insight.py L.22~51 소켓 연결, FTP 세션 수립 실제 구현.
🔶 2. 이미지 설정 팝업 (GV/SV 셀 명령)
근거: gui/dialogs/image_settings_dialog.py 자체는 완성.
_gv() L.126~137 GV{cell} 실제 전송,
_sv() L.172~182 SV{cell} 실제 전송.
BUT settings_page.py에 이 다이얼로그를 여는 버튼 없음.
코드는 있으나 UI에서 접근 불가.
✅ 3. Basler 카메라 설정 (ExposureTime/Gain)
근거: settings_page.py L.278~301 _on_basler_apply()에서
camera.ExposureTime.SetValue(float(exposure)),
camera.Gain.SetValue(float(gain)) 실제 호출.
🔶 4. MySQL DB 연결 버튼
근거: settings_page.py L.414
btn.clicked.connect(lambda: print("[설정] DB 연결")) → print() placeholder.
MySQLClient.connect() 자체(db/mysql_client.py L.9~15)는
pymysql.connect() 실제 구현됨.
연결 테스트(_test_db() L.805~822)도 실제 pymysql.connect() 호출.
단, 설정 탭 "연결" 버튼이 placeholder.
✅ 5. AI 모델 로드 (YOLO)
근거: settings_page.py L.694~720 _do_load_model()에서
Detector().load_model() 실제 호출.
detector.py L.19 YOLO(model_path) 지연 로딩 실제 구현.
L.769~774 앱 시작 시 config.json 경로로 자동 로드.
✅ 6. 설정 저장/불러오기 (config.json)
근거: settings_page.py L.588~608 _load_config()/_save_config()
json.load/json.dump 실제 구현.
L.646~688 _on_save_all()/_on_load_all() 실제 구현.
✅ 7. 전체 연결 테스트 버튼
근거: settings_page.py L.780~844.
코그넥스: socket.connect() 실제 호출 (L.786~792).
Basler: pylon.TlFactory.EnumerateDevices() 실제 호출 (L.794~803).
DB: pymysql.connect() 실제 호출 (L.805~822).
────────────────────────────────────────────────────────────
[제품 등록] 5/6
✅ 8. 제품 목록 표시 (QListWidget)
근거: register_page.py L.232~241 _populate_list()에서
REFLECTOR_LIST 10개를 QListWidget에 표시. 실제 구현.
❌ 9. MES DB 불러오기
근거: register_page.py L.72~79 "MES 불러오기" 버튼 setEnabled(False).
db/mysql_client.py L.26~28 get_reflector_list() → pass placeholder.
연결도 없고 쿼리도 없음.
✅ 10. Type L/R 시각적 표시 (화살표)
근거: register_page.py L.259~264 _on_select()에서
RH이면 "→" (파란색), LH이면 "←" (주황색) 실제 표시.
✅ 11. 캡처 기능 (trigger_and_get_image)
근거: register_page.py L.289 self._insight.trigger_and_get_image() 실제 호출.
insight.py L.99~103 trigger_and_get_image(): SE8 트리거 → sleep(1.0)
→ get_image() 실제 구현.
✅ 12. PatMax 패턴 등록
근거: register_page.py L.322~326 self._matcher.train(img, rid, info, roi=self._roi)
실제 호출.
logic/pattern_matcher.py: ORB 특징점 자동 검출 + Canny 엣지 NCC fallback
실제 구현. (Cognex Explorer PatMax 대신 Python ORB로 대체)
✅ 13. 저장 기능 (products.json)
근거: register_page.py L.335~344 _write_saved()에서 json.dump 실제 저장.
이미지 BMP도 cv2.imwrite() 실제 저장 (L.332~333).
────────────────────────────────────────────────────────────
[검사] 7/9
✅ 14. 그룹 A/B 최대 4종 제한
근거: inspect_page.py L.493~505 _on_group_changed()에서
len(checked) > GroupManager.MAX_PER_GROUP(=4) 이면 체크 해제.
group_manager.py L.11 MAX_PER_GROUP = 4.
✅ 15. 그룹 A/B 수동 전환
근거: inspect_page.py L.507~511 _on_switch()에서
self._groups.switch_group() 실제 호출.
group_manager.py L.22~25 switch_group() A↔B 전환 실제 구현.
✅ 16. 코그넥스 트리거 + 이미지 표시
근거: inspect_page.py L.117~144 InspectWorker._cognex_work()에서
software_trigger() 실제 호출, get_image() FTP 수신 실제 호출.
L.133 cognex_image_ready.emit(raw) → _display_cognex_image() 실제 표시.
(QTimer 방식 아닌 QThread 파이프라인으로 구현)
✅ 17. PatMax 결과 읽기 + 모델 판별
근거: inspect_page.py L.136 read_patmax_results() + L.138 match_image() 병합.
inspector.py L.49~84 GV A27/A77/A127/A177 읽기, #ERR 판별 실제 구현.
inspector.py L.88~117 identify_model() 최고점수 선택 실제 구현.
✅ 18. Basler 이미지 표시
근거: inspect_page.py L.158~172 self._basler.capture() 실제 호출.
basler.py L.36~46 GrabOne(5000) 실제 구현.
inspect_page.py L.644~681 _display_basler_image() 실제 표시.
✅ 19. YOLOv8 추론 + 불량 박스
근거: inspect_page.py L.165~169 self.detector.detect(frame) 실제 호출.
detector.py L.32~58 self._model(image, verbose=False) 실제 추론.
inspect_page.py L.651~663 바운딩 박스 오버레이 실제 구현.
(AI 모델 파일 로드 시 동작)
✅ 20. Pass/Fail AND 판별
근거: inspect_page.py L.199 self._inspector.judge(cognex_pass, basler_pass).
inspector.py L.119~120 "PASS" if cognex_pass and basler_pass else "FAIL".
❌ 21. PLC 불량 신호 전송
근거: inspect_page.py 전체 어디에도 plc_client.send_signal() 호출 없음.
plc_client.py L.20~22 send_signal() → pass placeholder.
settings_page.py L.462 PLC 연결 버튼도 print() placeholder.
✅ 22. 양품/불량 카운터 집계
근거: inspect_page.py L.588~596 _on_result()에서
total/pass/fail/unknown 실시간 카운터 업데이트 실제 구현.
────────────────────────────────────────────────────────────
[재학습] 6/6
✅ 23. 이미지 폴더 선택 + 목록 표시
근거: retrain_page.py L.885~901 _on_select_folder()에서
QFileDialog.getExistingDirectory() 실제 호출, 목록 QListWidget 표시.
✅ 24. 불량 클래스 선택 토글 (4개)
근거: retrain_page.py L.948~958 _on_class_select()에서
버튼 스타일 전환, canvas.set_class() 실제 구현.
선택된 박스 클래스도 즉시 변경됨.
✅ 25. 바운딩 박스 라벨링 + YOLO .txt 저장
근거: retrain_page.py LabelingCanvas L.274~401 마우스 드래그 박스 실제 구현
(new_box/move/resize/pan 4가지 모드, 8핸들 리사이즈, Ctrl+Z 실행취소).
L.982~997 _on_label_save() YOLO format .txt 실제 저장.
L.256~269 get_yolo_labels() cx/cy/nw/nh 정규화 실제 구현.
✅ 26. YOLOv8 학습 실행 (QThread)
근거: retrain_page.py L.1006~1046 _on_train_start()에서
TrainWorker(...).start() 실제 호출.
trainer.py L.101~162 YOLO("yolov8n.pt").train() 실제 구현.
trainer.py L.55~97 prepare_dataset() train/val split 실제 구현.
✅ 27. 학습 로그 실시간 표시
근거: retrain_page.py L.1059~1063 _on_log() → QTextEdit.append() 실제 구현.
trainer.py L.14~41 _StdoutCapture: sys.stdout 가로채기로
YOLOv8 Epoch 로그 실시간 캡처 실제 구현.
✅ 28. 모델 저장 (best.pt 복사)
근거: retrain_page.py L.1084~1109 _on_model_save()에서
shutil.copy(src, dest) 실제 구현.
trainer.py L.151~156 학습 후 자동 best.pt 복사 실제 구현.
config.json ai.model_path도 자동 갱신.
────────────────────────────────────────────────────────────
[공통/기타] 5/7
✅ 29. 하단 상태바 업데이트
근거: main_window.py L.280~304 update_connection_status()에서
코그넥스/Basler/DB 도트 색상 및 텍스트 실시간 갱신.
L.174~178 시그널 연결로 연결 상태 변경 시 자동 업데이트.
❌ 30. PLC 통신 모듈
근거: plc_client.py L.9~25
connect(), disconnect(), send_signal(), read_signal() 전부 pass placeholder.
주석에 "통신 방식 미확정: Modbus TCP / MC프로토콜 / OPC-UA 중 선택 후 구현"
❌ 31. MySQL DB 쿼리 메서드
근거: db/mysql_client.py L.26~35
get_reflector_list() → pass,
save_reflector() → pass,
save_inspection_result() → pass.
connect() 자체는 pymysql.connect() 실제 구현이나
모든 쿼리 메서드가 placeholder.
✅ 32. PyInstaller 패키징 (build.py)
근거: build.py 존재. PyInstaller.__main__.run() 실제 호출.
--onefile --windowed, hidden-import 목록,
pypylon/ultralytics collect-all 포함 실제 구현.
✅ 33. AI 모델 + 검사 연동
근거: inspect_page.py L.165 self.detector.detect(frame) 실제 호출.
L.546~549 update_detector() → self._worker.detector 업데이트 실제 구현.
main_window.py L.106 Detector() 생성, L.273~275 update_detector() 연동.
🔲 34. 트리거 모드 수동 변경
근거: 코드 외부 작업 — Cognex In-Sight Explorer에서
job 파일의 트리거 모드를 "소프트웨어 트리거"로 변경 필요.
소프트웨어 트리거 명령(SE8)은 insight.py에 구현되어 있으나
카메라 job 설정이 외부 의존성.
🔲 35. AI 학습 데이터 수집
근거: trainer.py L.55~97 prepare_dataset()은 이미지+라벨 쌍 처리 가능.
retrain_page.py 라벨링 도구도 완성.
실제 불량 이미지 데이터 수집은 운영 중 진행 필요 (외부 의존성).
────────────────────────────────────────────────────────────
요약
✅ 완료 27개 (77%)
🔶 부분구현 2개 (6%) → #2 이미지설정 팝업 버튼 없음, #4 DB 연결버튼 placeholder
❌ 미구현 4개 (11%) → #9 MES 불러오기, #21 PLC 신호전송, #30 PLC 모듈, #31 DB 쿼리
🔲 블로킹 2개 (6%) → #34 트리거모드 설정, #35 AI 학습 데이터
잔여 작업 우선순위 제안
1순위 (빠른 완성 가능)
- #2 : settings_page.py에 ImageSettingsDialog 열기 버튼 추가 (1~2줄)
- #4 : settings_page.py DB 연결 버튼에 MySQLClient.connect() 실제 호출 연결
2순위 (통신 방식 결정 후)
- #21 : PLC 신호 전송 — 방식 결정(Modbus TCP 권장) 후 plc_client.py 구현
- #30 : PLC 모듈 전체 구현
3순위 (DB 서버 준비 후)
- #31 : DB 쿼리 메서드 구현 (get_reflector_list, save_inspection_result)
- #9 : MES DB 불러오기 연결

14
test.py Normal file
View File

@@ -0,0 +1,14 @@
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()

0
utils/__init__.py Normal file
View File

View File

@@ -0,0 +1,9 @@
names:
- 스크래치
- 이물
- 흑점
- 변형
nc: 4
path: e:\ANT\utils\ai\dataset
train: images/train
val: images/val

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

View File

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

View File

@@ -0,0 +1 @@
0 0.318598 0.542781 0.472561 0.754011

Binary file not shown.

View File

@@ -0,0 +1 @@
0 0.233974 0.409524 0.339744 0.590476

View File

@@ -0,0 +1,110 @@
task: detect
mode: train
model: yolov8n.pt
data: e:\ANT\utils\ai\dataset\data.yaml
epochs: 5
time: null
patience: 100
batch: 16
imgsz: 640
save: true
save_period: -1
cache: false
device: null
workers: 0
project: e:\ANT\utils\ai\runs
name: train
exist_ok: true
pretrained: true
optimizer: auto
verbose: true
seed: 0
deterministic: true
single_cls: false
rect: false
cos_lr: false
close_mosaic: 10
resume: false
amp: false
fraction: 1.0
profile: false
freeze: null
multi_scale: 0.0
compile: false
overlap_mask: true
mask_ratio: 4
dropout: 0.0
val: true
split: val
save_json: false
conf: null
iou: 0.7
max_det: 300
half: false
dnn: false
plots: false
end2end: null
source: null
vid_stride: 1
stream_buffer: false
visualize: false
augment: false
agnostic_nms: false
classes: null
retina_masks: false
embed: null
show: false
save_frames: false
save_txt: false
save_conf: false
save_crop: false
show_labels: true
show_conf: true
show_boxes: true
line_width: null
format: torchscript
keras: false
optimize: false
int8: false
dynamic: false
simplify: true
opset: null
workspace: null
nms: false
lr0: 0.01
lrf: 0.01
momentum: 0.937
weight_decay: 0.0005
warmup_epochs: 3.0
warmup_momentum: 0.8
warmup_bias_lr: 0.1
box: 7.5
cls: 0.5
cls_pw: 0.0
dfl: 1.5
pose: 12.0
kobj: 1.0
rle: 1.0
angle: 1.0
nbs: 64
hsv_h: 0.015
hsv_s: 0.7
hsv_v: 0.4
degrees: 0.0
translate: 0.1
scale: 0.5
shear: 0.0
perspective: 0.0
flipud: 0.0
fliplr: 0.5
bgr: 0.0
mosaic: 1.0
mixup: 0.0
cutmix: 0.0
copy_paste: 0.0
copy_paste_mode: flip
auto_augment: randaugment
erasing: 0.4
cfg: null
tracker: botsort.yaml
save_dir: E:\ANT\utils\ai\runs\train

View File

@@ -0,0 +1,6 @@
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.455317,2.31474,4.08107,2.28289,0.02041,1,0.4975,0.04975,3.2799,4.23304,3.16176,0,0,0
2,0.862726,2.30536,4.11057,2.05367,0.02041,1,0.4975,0.04975,3.26087,4.28545,3.03613,1.0025e-05,1.0025e-05,1.0025e-05
3,1.1759,2.77566,3.95724,3.15328,0.02083,1,0.4975,0.04975,3.25368,4.2721,3.07091,1.51e-05,1.51e-05,1.51e-05
4,1.48949,1.89878,3.81945,2.43286,0.02041,1,0.4975,0.04975,3.22409,4.21402,3.13953,1.5225e-05,1.5225e-05,1.5225e-05
5,1.80265,2.28623,4.60095,2.64542,0.02,1,0.4975,0.04975,3.17688,4.12774,3.17531,1.04e-05,1.04e-05,1.04e-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.455317 2.31474 4.08107 2.28289 0.02041 1 0.4975 0.04975 3.2799 4.23304 3.16176 0 0 0
3 2 0.862726 2.30536 4.11057 2.05367 0.02041 1 0.4975 0.04975 3.26087 4.28545 3.03613 1.0025e-05 1.0025e-05 1.0025e-05
4 3 1.1759 2.77566 3.95724 3.15328 0.02083 1 0.4975 0.04975 3.25368 4.2721 3.07091 1.51e-05 1.51e-05 1.51e-05
5 4 1.48949 1.89878 3.81945 2.43286 0.02041 1 0.4975 0.04975 3.22409 4.21402 3.13953 1.5225e-05 1.5225e-05 1.5225e-05
6 5 1.80265 2.28623 4.60095 2.64542 0.02 1 0.4975 0.04975 3.17688 4.12774 3.17531 1.04e-05 1.04e-05 1.04e-05

Binary file not shown.

Binary file not shown.

47
utils/path_helper.py Normal file
View File

@@ -0,0 +1,47 @@
# PyInstaller frozen 환경과 일반 실행 환경 모두에서 올바른 기준 경로 반환
#
# 두 종류의 루트:
# BASE_PATH : 사용자가 보는 실제 폴더(쓰기 가능). 로그/변경된 설정/저장된 모델이 여기로.
# - dev 모드: __main__ 파일 위치 (= 프로젝트 루트)
# - EXE(--onefile) 모드: EXE가 놓인 폴더 (예: E:\ANT\dist)
# BUNDLE_PATH : 번들 안의 읽기 전용 자원 위치.
# - dev 모드: BASE_PATH 와 동일
# - EXE 모드: PyInstaller 임시 추출 폴더 (sys._MEIPASS)
import os
import sys
def get_base_path() -> str:
"""사용자 데이터 위치(읽기/쓰기)."""
if getattr(sys, "frozen", False):
return os.path.dirname(os.path.abspath(sys.executable))
main_mod = sys.modules.get("__main__")
main_file = getattr(main_mod, "__file__", None)
if main_file:
return os.path.dirname(os.path.abspath(main_file))
# path_helper.py lives in utils/ — go up one level to reach the project root
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_bundle_path() -> str:
"""번들된 자원 위치(읽기 전용). EXE에선 _MEIPASS, dev에선 BASE_PATH."""
return getattr(sys, "_MEIPASS", BASE_PATH)
BASE_PATH = get_base_path()
BUNDLE_PATH = get_bundle_path()
def get_path(*paths) -> str:
"""경로 결합. BASE_PATH 기준 우선, 거기 없으면 BUNDLE_PATH 폴백.
파일이 양쪽에 다 없으면 BASE_PATH 기준 경로 반환(신규 생성 시 사용자 영역에 만들도록)."""
if not paths:
return BASE_PATH
primary = os.path.join(BASE_PATH, *paths)
if os.path.exists(primary):
return primary
if BUNDLE_PATH and BUNDLE_PATH != BASE_PATH:
fallback = os.path.join(BUNDLE_PATH, *paths)
if os.path.exists(fallback):
return fallback
return primary

BIN
yolo26n.pt Normal file

Binary file not shown.

BIN
yolov8n.pt Normal file

Binary file not shown.