# 로그 시스템 — 앱 로그(텍스트) + 검사 결과(CSV) + 학습 이력(텍스트) # 폴더 구조: logs//. 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//. 경로 (폴더 자동 생성).""" 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