1220 lines
48 KiB
Python
1220 lines
48 KiB
Python
import json
|
||
import os
|
||
|
||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
|
||
from PyQt5.QtWidgets import (
|
||
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout,
|
||
QPushButton, QLineEdit, QSpinBox, QDoubleSpinBox,
|
||
QLabel, QMessageBox, QApplication, QFileDialog,
|
||
QDialog, QTabWidget, QFrame,
|
||
)
|
||
|
||
from camera.insight import InSightCamera
|
||
from camera.basler import BaslerCamera
|
||
from ai.detector import Detector
|
||
from db.sql_client import SQLClient
|
||
from plc.plc_client import PLCClient
|
||
from paths import resolve_path, to_project_relative
|
||
from utils.path_helper import get_path
|
||
from logger import log_action
|
||
|
||
ADMIN_PASSWORD = "1234"
|
||
|
||
# ── 카드 스타일 ──────────────────────────────────────────────────────── #
|
||
_CARD = (
|
||
"QFrame {"
|
||
" background:#1a1a1a;"
|
||
" border:1px solid #2a2a2a;"
|
||
" border-radius:8px;"
|
||
"}"
|
||
)
|
||
_STATUS_OK = "color:#1D9E75; font-size:12px; background:transparent; border:none;"
|
||
_STATUS_FAIL = "color:#E24B4A; font-size:12px; background:transparent; border:none;"
|
||
|
||
_BTN_CARD_CONNECT = (
|
||
"QPushButton {"
|
||
" background:#1D9E75; color:#E1F5EE;"
|
||
" border:none; border-radius:4px;"
|
||
" min-height:43px; max-height:43px;"
|
||
" padding:0 19px; font-size:14px;"
|
||
"}"
|
||
"QPushButton:hover { background:#20b585; }"
|
||
"QPushButton:disabled { background:#145f48; color:#5a9e7e; }"
|
||
)
|
||
_BTN_CARD_DISCONNECT = (
|
||
"QPushButton {"
|
||
" background:#3D1515; color:#F09595;"
|
||
" border:none; border-radius:4px;"
|
||
" min-height:43px; max-height:43px;"
|
||
" padding:0 19px; font-size:14px;"
|
||
"}"
|
||
"QPushButton:hover { background:#4a1818; }"
|
||
"QPushButton:disabled { background:#222222; color:#666666; }"
|
||
)
|
||
_BTN_ADMIN = (
|
||
"QPushButton {"
|
||
" background:#333333; color:#aaaaaa;"
|
||
" border:1px solid #555555; border-radius:4px;"
|
||
" font-size:13px; padding:0 16px;"
|
||
"}"
|
||
"QPushButton:hover { background:#444444; color:#ffffff; }"
|
||
)
|
||
|
||
# ── 관리자 다이얼로그 내부 스타일 ────────────────────────────────────── #
|
||
_GRP = (
|
||
"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; }"
|
||
)
|
||
_BTN_DLG = (
|
||
"QPushButton {"
|
||
" background:#333333; color:#ffffff; border:1px solid #555555;"
|
||
" border-radius:4px; min-height:56px; font-size:14px;"
|
||
"}"
|
||
"QPushButton:hover { background:#444444; }"
|
||
)
|
||
_BTN_DLG_PRIMARY = (
|
||
"QPushButton {"
|
||
" background:#1D9E75; color:#ffffff; border:none;"
|
||
" border-radius:4px; min-height:56px; font-size:14px; font-weight:bold;"
|
||
"}"
|
||
"QPushButton:hover { background:#20b585; }"
|
||
)
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
# PasswordDialog
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
|
||
class PasswordDialog(QDialog):
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("관리자 인증")
|
||
self.setModal(True)
|
||
self.setFixedSize(360, 570)
|
||
self.setStyleSheet("background:#1a1a1a;")
|
||
self._pw = ""
|
||
self._locked = False # 오입력 후 잠시 입력 차단
|
||
self._build_ui()
|
||
|
||
# ── UI ───────────────────────────────────────────────────────────── #
|
||
|
||
def _build_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
layout.setContentsMargins(24, 28, 24, 24)
|
||
layout.setSpacing(12)
|
||
|
||
title = QLabel("관리자 인증")
|
||
title.setAlignment(Qt.AlignCenter)
|
||
title.setStyleSheet("color:#ffffff; font-size:17px; font-weight:bold;")
|
||
layout.addWidget(title)
|
||
|
||
desc = QLabel("비밀번호를 입력하세요.")
|
||
desc.setAlignment(Qt.AlignCenter)
|
||
desc.setStyleSheet("color:#888888; font-size:13px;")
|
||
layout.addWidget(desc)
|
||
|
||
# 4자리 점 표시
|
||
self._dot_lbl = QLabel("○ ○ ○ ○")
|
||
self._dot_lbl.setAlignment(Qt.AlignCenter)
|
||
self._dot_lbl.setStyleSheet(
|
||
"color:#444444; font-size:30px; background:transparent;"
|
||
)
|
||
layout.addWidget(self._dot_lbl)
|
||
layout.addSpacing(6)
|
||
|
||
# 키패드
|
||
pad = QGridLayout()
|
||
pad.setSpacing(8)
|
||
pad.setContentsMargins(0, 0, 0, 0)
|
||
|
||
for idx, digit in enumerate("123456789"):
|
||
btn = self._pad_btn(digit, font_size=22)
|
||
btn.clicked.connect(lambda _, d=digit: self._on_digit(d))
|
||
pad.addWidget(btn, idx // 3, idx % 3)
|
||
|
||
btn_del = self._pad_btn("⌫", bg="#252525", fg="#888888", font_size=20)
|
||
btn_del.clicked.connect(self._on_backspace)
|
||
|
||
btn_0 = self._pad_btn("0", font_size=22)
|
||
btn_0.clicked.connect(lambda: self._on_digit("0"))
|
||
|
||
btn_ok = self._pad_btn("확인", bg="#1D9E75", fg="#E1F5EE", font_size=16)
|
||
btn_ok.clicked.connect(self._on_confirm)
|
||
|
||
pad.addWidget(btn_del, 3, 0)
|
||
pad.addWidget(btn_0, 3, 1)
|
||
pad.addWidget(btn_ok, 3, 2)
|
||
|
||
layout.addLayout(pad)
|
||
layout.addSpacing(4)
|
||
|
||
btn_cancel = QPushButton("취소")
|
||
btn_cancel.setFixedHeight(52)
|
||
btn_cancel.setStyleSheet(
|
||
"QPushButton { background:#222222; color:#777777; border:none;"
|
||
" border-radius:6px; font-size:15px; }"
|
||
"QPushButton:pressed { background:#1a1a1a; }"
|
||
)
|
||
btn_cancel.clicked.connect(self.reject)
|
||
layout.addWidget(btn_cancel)
|
||
|
||
@staticmethod
|
||
def _pad_btn(text: str, bg="#2a2a2a", fg="#ffffff", font_size=22) -> QPushButton:
|
||
btn = QPushButton(text)
|
||
btn.setFixedHeight(68)
|
||
btn.setStyleSheet(
|
||
f"QPushButton {{ background:{bg}; color:{fg}; border:none;"
|
||
f" border-radius:6px; font-size:{font_size}px; }}"
|
||
f"QPushButton:pressed {{ background:#111111; }}"
|
||
)
|
||
return btn
|
||
|
||
# ── 입력 로직 ────────────────────────────────────────────────────── #
|
||
|
||
def _on_digit(self, digit: str):
|
||
if self._locked or len(self._pw) >= 4:
|
||
return
|
||
self._pw += digit
|
||
self._refresh_dots()
|
||
if len(self._pw) == 4:
|
||
# 4자리 완성 → 120ms 후 자동 확인 (시각 피드백 확보)
|
||
QTimer.singleShot(120, self._on_confirm)
|
||
|
||
def _on_backspace(self):
|
||
if self._locked:
|
||
return
|
||
self._pw = self._pw[:-1]
|
||
self._refresh_dots()
|
||
|
||
def _on_confirm(self):
|
||
if self._pw == ADMIN_PASSWORD:
|
||
self.accept()
|
||
return
|
||
# 오입력: 빨간 점 → 600ms 후 초기화
|
||
self._locked = True
|
||
self._pw = ""
|
||
self._dot_lbl.setText("● ● ● ●")
|
||
self._dot_lbl.setStyleSheet(
|
||
"color:#E24B4A; font-size:30px; background:transparent;"
|
||
)
|
||
QTimer.singleShot(600, self._reset_dots)
|
||
|
||
def _reset_dots(self):
|
||
self._locked = False
|
||
self._refresh_dots()
|
||
|
||
def _refresh_dots(self):
|
||
n = len(self._pw)
|
||
dots = " ".join("●" if i < n else "○" for i in range(4))
|
||
color = "#1D9E75" if n > 0 else "#444444"
|
||
self._dot_lbl.setText(dots)
|
||
self._dot_lbl.setStyleSheet(
|
||
f"color:{color}; font-size:30px; background:transparent;"
|
||
)
|
||
|
||
def keyPressEvent(self, event):
|
||
# 터치 전용 — 키보드 입력 차단 (Escape 제외)
|
||
if event.key() == Qt.Key_Escape:
|
||
self.reject()
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
# AdminSettingsDialog
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
|
||
class AdminSettingsDialog(QDialog):
|
||
def __init__(self, settings_page, parent=None):
|
||
super().__init__(parent)
|
||
self._sp = settings_page
|
||
self.setWindowTitle("관리자 설정")
|
||
self.setModal(True)
|
||
self.setFixedSize(900, 700)
|
||
self.setStyleSheet("background:#1a1a1a;")
|
||
self._build_ui()
|
||
self._populate()
|
||
|
||
# ── UI 뼈대 ─────────────────────────────────────────────────────── #
|
||
|
||
def _build_ui(self):
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(0, 0, 0, 0)
|
||
root.setSpacing(0)
|
||
|
||
self._tabs = QTabWidget()
|
||
self._tabs.setStyleSheet(
|
||
"QTabWidget::pane { border:none; background:#1a1a1a; }"
|
||
"QTabBar::tab {"
|
||
" background:#222222; color:#888888;"
|
||
" padding:10px 24px; font-size:14px; border:none;"
|
||
" border-right:1px solid #333333;"
|
||
"}"
|
||
"QTabBar::tab:selected { background:#2e2e2e; color:#ffffff; font-weight:bold; }"
|
||
"QTabBar::tab:hover { background:#2a2a2a; color:#cccccc; }"
|
||
)
|
||
|
||
self._tabs.addTab(self._build_tab_cognex(), "코그넥스")
|
||
self._tabs.addTab(self._build_tab_basler(), "Basler")
|
||
self._tabs.addTab(self._build_tab_db(), "DB")
|
||
self._tabs.addTab(self._build_tab_ai(), "AI 모델")
|
||
self._tabs.addTab(self._build_tab_conveyor(), "컨베이어")
|
||
self._tabs.addTab(self._build_tab_plc(), "PLC")
|
||
|
||
root.addWidget(self._tabs, stretch=1)
|
||
root.addWidget(self._build_bottom_bar())
|
||
|
||
@staticmethod
|
||
def _tab_wrap(inner_widget) -> QWidget:
|
||
w = QWidget()
|
||
w.setStyleSheet("background:#1a1a1a;")
|
||
layout = QVBoxLayout(w)
|
||
layout.setContentsMargins(32, 24, 32, 20)
|
||
layout.setSpacing(16)
|
||
layout.addWidget(inner_widget)
|
||
layout.addStretch()
|
||
return w
|
||
|
||
@staticmethod
|
||
def _btn_pair(*btns) -> QWidget:
|
||
w = QWidget()
|
||
row = QHBoxLayout(w)
|
||
row.setContentsMargins(0, 0, 0, 0)
|
||
row.setSpacing(8)
|
||
for b in btns:
|
||
row.addWidget(b, stretch=1)
|
||
return w
|
||
|
||
# ── 탭 1 — 코그넥스 ─────────────────────────────────────────────── #
|
||
|
||
def _build_tab_cognex(self) -> QWidget:
|
||
g = self._make_group("코그넥스 In-Sight 2000C")
|
||
form = QFormLayout(g)
|
||
form.setHorizontalSpacing(16)
|
||
form.setVerticalSpacing(12)
|
||
|
||
self._cognex_ip = QLineEdit()
|
||
self._cognex_ip.setPlaceholderText("예: 169.254.0.1")
|
||
self._cognex_ip.setFixedHeight(42)
|
||
|
||
self._cognex_port = QSpinBox()
|
||
self._cognex_port.setRange(1, 65535)
|
||
self._cognex_port.setFixedHeight(42)
|
||
|
||
btn_connect = QPushButton("연결")
|
||
btn_connect.setFixedHeight(56)
|
||
btn_connect.setStyleSheet(_BTN_DLG)
|
||
btn_connect.clicked.connect(self._on_cognex_connect)
|
||
|
||
btn_save = QPushButton("저장")
|
||
btn_save.setFixedHeight(56)
|
||
btn_save.setStyleSheet(_BTN_DLG)
|
||
btn_save.clicked.connect(self._on_cognex_save)
|
||
|
||
form.addRow("IP 주소", self._cognex_ip)
|
||
form.addRow("포트", self._cognex_port)
|
||
form.addRow("", self._btn_pair(btn_connect, btn_save))
|
||
return self._tab_wrap(g)
|
||
|
||
# ── 탭 2 — Basler ───────────────────────────────────────────────── #
|
||
|
||
def _build_tab_basler(self) -> QWidget:
|
||
g = self._make_group("Basler USB 카메라")
|
||
form = QFormLayout(g)
|
||
form.setHorizontalSpacing(16)
|
||
form.setVerticalSpacing(12)
|
||
|
||
self._basler_exposure = QSpinBox()
|
||
self._basler_exposure.setRange(100, 1000000)
|
||
self._basler_exposure.setFixedHeight(42)
|
||
|
||
self._basler_gain = QSpinBox()
|
||
self._basler_gain.setRange(0, 100)
|
||
self._basler_gain.setFixedHeight(42)
|
||
|
||
btn_connect = QPushButton("연결")
|
||
btn_connect.setFixedHeight(56)
|
||
btn_connect.setStyleSheet(_BTN_DLG)
|
||
btn_connect.clicked.connect(self._sp._on_basler_connect)
|
||
|
||
btn_apply = QPushButton("설정 적용")
|
||
btn_apply.setFixedHeight(56)
|
||
btn_apply.setStyleSheet(_BTN_DLG)
|
||
btn_apply.clicked.connect(self._on_basler_apply)
|
||
|
||
form.addRow("노출 (µs)", self._basler_exposure)
|
||
form.addRow("게인", self._basler_gain)
|
||
form.addRow("", self._btn_pair(btn_connect, btn_apply))
|
||
return self._tab_wrap(g)
|
||
|
||
# ── 탭 3 — DB ───────────────────────────────────────────────────── #
|
||
|
||
def _build_tab_db(self) -> QWidget:
|
||
g = self._make_group("MS SQL Server DB")
|
||
form = QFormLayout(g)
|
||
form.setHorizontalSpacing(16)
|
||
form.setVerticalSpacing(12)
|
||
|
||
self._db_server = QLineEdit()
|
||
self._db_server.setPlaceholderText("예: Wizis.iptime.org,20220")
|
||
self._db_server.setFixedHeight(42)
|
||
|
||
self._db_database = QLineEdit()
|
||
self._db_database.setFixedHeight(42)
|
||
|
||
self._db_username = QLineEdit()
|
||
self._db_username.setFixedHeight(42)
|
||
|
||
self._db_password = QLineEdit()
|
||
self._db_password.setEchoMode(QLineEdit.Password)
|
||
self._db_password.setFixedHeight(42)
|
||
|
||
btn_connect = QPushButton("연결")
|
||
btn_connect.setFixedHeight(56)
|
||
btn_connect.setStyleSheet(_BTN_DLG)
|
||
btn_connect.clicked.connect(self._on_db_connect)
|
||
|
||
btn_save = QPushButton("저장")
|
||
btn_save.setFixedHeight(56)
|
||
btn_save.setStyleSheet(_BTN_DLG)
|
||
btn_save.clicked.connect(self._on_db_save)
|
||
|
||
form.addRow("서버", self._db_server)
|
||
form.addRow("DB명", self._db_database)
|
||
form.addRow("사용자명", self._db_username)
|
||
form.addRow("비밀번호", self._db_password)
|
||
form.addRow("", self._btn_pair(btn_connect, btn_save))
|
||
return self._tab_wrap(g)
|
||
|
||
# ── 탭 4 — AI 모델 ──────────────────────────────────────────────── #
|
||
|
||
def _build_tab_ai(self) -> QWidget:
|
||
g = self._make_group("AI 모델 (YOLOv8)")
|
||
layout = QVBoxLayout(g)
|
||
layout.setSpacing(10)
|
||
|
||
path_row = QHBoxLayout()
|
||
self._ai_path_edit = QLineEdit()
|
||
self._ai_path_edit.setReadOnly(True)
|
||
self._ai_path_edit.setFixedHeight(42)
|
||
self._ai_path_edit.setPlaceholderText("모델 경로 미설정")
|
||
|
||
btn_browse = QPushButton("파일 선택")
|
||
btn_browse.setFixedHeight(42)
|
||
btn_browse.setFixedWidth(120)
|
||
btn_browse.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
|
||
btn_browse.clicked.connect(self._on_ai_browse)
|
||
|
||
path_row.addWidget(self._ai_path_edit, stretch=1)
|
||
path_row.addWidget(btn_browse)
|
||
|
||
btn_load = QPushButton("로드")
|
||
btn_load.setFixedHeight(56)
|
||
btn_load.setStyleSheet(_BTN_DLG_PRIMARY)
|
||
btn_load.clicked.connect(self._on_ai_load)
|
||
|
||
btn_unload = QPushButton("해제")
|
||
btn_unload.setFixedHeight(56)
|
||
btn_unload.setStyleSheet(_BTN_DLG)
|
||
btn_unload.clicked.connect(self._on_ai_unload)
|
||
|
||
self._ai_info_lbl = QLabel("")
|
||
self._ai_info_lbl.setStyleSheet("color:#888888; font-size:12px; background:transparent;")
|
||
self._ai_info_lbl.setWordWrap(True)
|
||
|
||
layout.addLayout(path_row)
|
||
layout.addWidget(self._btn_pair(btn_load, btn_unload))
|
||
layout.addWidget(self._ai_info_lbl)
|
||
return self._tab_wrap(g)
|
||
|
||
# ── 탭 5 — 컨베이어 ─────────────────────────────────────────────── #
|
||
|
||
def _build_tab_conveyor(self) -> QWidget:
|
||
g = self._make_group("컨베이어 설정")
|
||
form = QFormLayout(g)
|
||
form.setHorizontalSpacing(16)
|
||
form.setVerticalSpacing(12)
|
||
|
||
self._conv_distance = QDoubleSpinBox()
|
||
self._conv_distance.setRange(1.0, 10000.0)
|
||
self._conv_distance.setDecimals(1)
|
||
self._conv_distance.setSuffix(" cm")
|
||
self._conv_distance.setValue(100.0)
|
||
self._conv_distance.setFixedHeight(42)
|
||
|
||
self._conv_speed = QDoubleSpinBox()
|
||
self._conv_speed.setRange(0.1, 10000.0)
|
||
self._conv_speed.setDecimals(1)
|
||
self._conv_speed.setSuffix(" cm/s")
|
||
self._conv_speed.setValue(30.0)
|
||
self._conv_speed.setFixedHeight(42)
|
||
|
||
self._conv_delay_lbl = QLabel("3.33 s")
|
||
self._conv_delay_lbl.setStyleSheet(
|
||
"color:#44cc88; font-size:14px; font-weight:bold; background:transparent;"
|
||
)
|
||
|
||
self._conv_distance.valueChanged.connect(self._on_conveyor_changed)
|
||
self._conv_speed.valueChanged.connect(self._on_conveyor_changed)
|
||
|
||
btn_apply = QPushButton("적용")
|
||
btn_apply.setFixedHeight(56)
|
||
btn_apply.setStyleSheet(_BTN_DLG_PRIMARY)
|
||
btn_apply.clicked.connect(self._on_conveyor_apply)
|
||
|
||
form.addRow("카메라 간 거리", self._conv_distance)
|
||
form.addRow("벨트 속도", self._conv_speed)
|
||
form.addRow("계산된 딜레이", self._conv_delay_lbl)
|
||
form.addRow("", btn_apply)
|
||
return self._tab_wrap(g)
|
||
|
||
# ── 탭 6 — PLC ──────────────────────────────────────────────────── #
|
||
|
||
def _build_tab_plc(self) -> QWidget:
|
||
g = self._make_group("PLC 설정 (MELSEC Q06UDEHCPU · MC 프로토콜 3E)")
|
||
form = QFormLayout(g)
|
||
form.setHorizontalSpacing(16)
|
||
form.setVerticalSpacing(12)
|
||
|
||
self._plc_ip = QLineEdit()
|
||
self._plc_ip.setPlaceholderText("예: 192.168.3.39")
|
||
self._plc_ip.setFixedHeight(42)
|
||
|
||
self._plc_port = QSpinBox()
|
||
self._plc_port.setRange(1, 65535)
|
||
self._plc_port.setFixedHeight(42)
|
||
|
||
btn_connect = QPushButton("연결")
|
||
btn_connect.setFixedHeight(56)
|
||
btn_connect.setStyleSheet(_BTN_DLG)
|
||
btn_connect.clicked.connect(self._on_plc_connect)
|
||
|
||
form.addRow("IP", self._plc_ip)
|
||
form.addRow("포트", self._plc_port)
|
||
form.addRow("", btn_connect)
|
||
return self._tab_wrap(g)
|
||
|
||
def _on_plc_connect(self):
|
||
ip = self._plc_ip.text().strip()
|
||
port = self._plc_port.value()
|
||
log_action(f"[설정] PLC 연결 시도: {ip}:{port}")
|
||
if not ip:
|
||
QMessageBox.warning(self, "입력 오류", "IP 주소를 입력하세요.")
|
||
return
|
||
if self._sp._plc_client and self._sp._plc_client.is_connected():
|
||
self._sp._plc_client.disconnect()
|
||
client = PLCClient()
|
||
ok = client.connect(ip, port)
|
||
if ok:
|
||
self._sp._plc_client = client
|
||
self._sp._set_plc_connected(True)
|
||
self._sp._save_config({"plc": {"ip": ip, "port": port}})
|
||
self._sp._config.setdefault("plc", {}).update({"ip": ip, "port": port})
|
||
if self._sp._update_plc_cb:
|
||
self._sp._update_plc_cb(client)
|
||
self._sp.plc_status_changed.emit(True)
|
||
QMessageBox.information(self, "연결 성공", f"PLC 연결 성공\n{ip}:{port}")
|
||
else:
|
||
QMessageBox.critical(self, "연결 실패", f"PLC에 연결할 수 없습니다.\n{ip}:{port}")
|
||
|
||
# ── 하단 버튼 바 ─────────────────────────────────────────────────── #
|
||
|
||
def _build_bottom_bar(self) -> QWidget:
|
||
bar = QWidget()
|
||
bar.setStyleSheet("background:#111111;")
|
||
bar.setFixedHeight(72)
|
||
row = QHBoxLayout(bar)
|
||
row.setContentsMargins(24, 8, 24, 8)
|
||
row.setSpacing(12)
|
||
|
||
btn_save_all = QPushButton("전체 설정 저장")
|
||
btn_save_all.setFixedHeight(56)
|
||
btn_save_all.setStyleSheet(_BTN_DLG_PRIMARY)
|
||
btn_save_all.clicked.connect(self._on_save_all)
|
||
|
||
btn_close = QPushButton("닫기")
|
||
btn_close.setFixedHeight(56)
|
||
btn_close.setStyleSheet(_BTN_DLG)
|
||
btn_close.clicked.connect(self.accept)
|
||
|
||
row.addWidget(btn_save_all, stretch=2)
|
||
row.addWidget(btn_close, stretch=1)
|
||
return bar
|
||
|
||
@staticmethod
|
||
def _make_group(title: str):
|
||
from PyQt5.QtWidgets import QGroupBox
|
||
g = QGroupBox(title)
|
||
g.setStyleSheet(_GRP)
|
||
return g
|
||
|
||
# ── 필드 채우기 ──────────────────────────────────────────────────── #
|
||
|
||
def _populate(self):
|
||
cfg = self._sp._config
|
||
|
||
cognex = cfg.get("cognex", {})
|
||
self._cognex_ip.setText(cognex.get("ip", ""))
|
||
self._cognex_port.setValue(cognex.get("port", 23))
|
||
|
||
basler = cfg.get("basler", {})
|
||
self._basler_exposure.setValue(basler.get("exposure", 10000))
|
||
self._basler_gain.setValue(basler.get("gain", 20))
|
||
|
||
conv = cfg.get("conveyor", {})
|
||
self._conv_distance.setValue(conv.get("distance_cm", 100.0))
|
||
self._conv_speed.setValue(conv.get("speed_cms", 30.0))
|
||
self._on_conveyor_changed()
|
||
|
||
db = cfg.get("db", {})
|
||
self._db_server.setText(db.get("server", ""))
|
||
self._db_database.setText(db.get("database", ""))
|
||
self._db_username.setText(db.get("username", ""))
|
||
self._db_password.setText(db.get("password", ""))
|
||
|
||
ai = cfg.get("ai", {})
|
||
if "model_path" in ai:
|
||
self._ai_path_edit.setText(to_project_relative(ai["model_path"]))
|
||
|
||
if self._sp._detector and self._sp._detector.is_loaded():
|
||
names = ", ".join(self._sp._detector.class_names)
|
||
self._ai_info_lbl.setText(
|
||
f"클래스: {names} ({len(self._sp._detector.class_names)}개)"
|
||
)
|
||
|
||
plc = cfg.get("plc", {})
|
||
self._plc_ip.setText(plc.get("ip", ""))
|
||
self._plc_port.setValue(plc.get("port", 5010))
|
||
|
||
# ── 코그넥스 탭 슬롯 ─────────────────────────────────────────────── #
|
||
|
||
def _on_cognex_connect(self):
|
||
ip = self._cognex_ip.text().strip()
|
||
port = self._cognex_port.value()
|
||
log_action(f"[설정] Cognex 연결 시도: {ip}:{port}")
|
||
if not ip:
|
||
QMessageBox.warning(self, "입력 오류", "IP 주소를 입력하세요.")
|
||
return
|
||
if self._sp._insight and self._sp._insight._sock is not None:
|
||
self._sp._insight.disconnect()
|
||
new_cam = InSightCamera()
|
||
new_cam.connect(ip, port)
|
||
if new_cam._sock is None:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
f"In-Sight 카메라에 연결할 수 없습니다.\nIP: {ip} 포트: {port}",
|
||
)
|
||
return
|
||
self._sp._insight = new_cam
|
||
if self._sp._update_insight_cb:
|
||
self._sp._update_insight_cb(new_cam)
|
||
self._sp._set_cognex_connected(True)
|
||
self._sp._save_cognex_config(ip, port)
|
||
self._sp.cognex_status_changed.emit(True)
|
||
QMessageBox.information(self, "연결 성공", f"In-Sight 카메라 연결 성공\n{ip}:{port}")
|
||
|
||
def _on_cognex_save(self):
|
||
ip = self._cognex_ip.text().strip()
|
||
port = self._cognex_port.value()
|
||
self._sp._save_cognex_config(ip, port)
|
||
QMessageBox.information(self, "저장", "코그넥스 설정이 저장되었습니다.")
|
||
|
||
# ── Basler 탭 슬롯 ───────────────────────────────────────────────── #
|
||
|
||
def _on_basler_apply(self):
|
||
log_action(f"[설정] Basler 설정 적용: 노출={self._basler_exposure.value()}µs 게인={self._basler_gain.value()}")
|
||
if not self._sp._basler or self._sp._basler.camera is None:
|
||
QMessageBox.warning(self, "경고", "Basler 카메라가 연결되어 있지 않습니다.")
|
||
return
|
||
exposure = self._basler_exposure.value()
|
||
gain = self._basler_gain.value()
|
||
errors = []
|
||
try:
|
||
self._sp._basler.camera.ExposureAuto.SetValue("Off")
|
||
self._sp._basler.camera.ExposureTime.SetValue(float(exposure))
|
||
except Exception as e:
|
||
errors.append(f"노출: {e}")
|
||
try:
|
||
self._sp._basler.camera.Gain.SetValue(float(gain))
|
||
except Exception as e:
|
||
errors.append(f"게인: {e}")
|
||
if errors:
|
||
QMessageBox.warning(self, "설정 적용 오류", "\n".join(errors))
|
||
else:
|
||
self._sp._save_config({"basler": {"exposure": exposure, "gain": gain}})
|
||
self._sp._config.setdefault("basler", {}).update(
|
||
{"exposure": exposure, "gain": gain}
|
||
)
|
||
QMessageBox.information(self, "적용 완료", f"노출: {exposure} µs 게인: {gain}")
|
||
|
||
# ── DB 탭 슬롯 ───────────────────────────────────────────────────── #
|
||
|
||
def _on_db_connect(self):
|
||
server = self._db_server.text().strip()
|
||
database = self._db_database.text().strip()
|
||
username = self._db_username.text().strip()
|
||
password = self._db_password.text()
|
||
log_action(f"[설정] DB 연결 시도: {server}/{database}")
|
||
if not server or not database:
|
||
QMessageBox.warning(self, "경고", "서버와 DB명을 입력해주세요.")
|
||
return
|
||
client = SQLClient()
|
||
ok = client.connect(server, database, username, password)
|
||
if ok:
|
||
self._sp._db_client = client
|
||
self._sp._set_db_connected(True)
|
||
cfg = {"server": server, "database": database,
|
||
"username": username, "password": password}
|
||
self._sp._save_config({"db": cfg})
|
||
self._sp._config.setdefault("db", {}).update(cfg)
|
||
if self._sp._update_db_cb:
|
||
self._sp._update_db_cb(client)
|
||
QMessageBox.information(self, "연결 성공", f"DB 연결 성공\n{server}/{database}")
|
||
else:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
"DB 연결에 실패했습니다.\n서버 주소, DB명, 계정 정보를 확인하세요.",
|
||
)
|
||
|
||
def _on_db_save(self):
|
||
cfg = {
|
||
"server": self._db_server.text().strip(),
|
||
"database": self._db_database.text().strip(),
|
||
"username": self._db_username.text().strip(),
|
||
"password": self._db_password.text(),
|
||
}
|
||
self._sp._save_config({"db": cfg})
|
||
self._sp._config.setdefault("db", {}).update(cfg)
|
||
QMessageBox.information(self, "저장", "DB 설정이 저장되었습니다.")
|
||
|
||
# ── AI 탭 슬롯 ───────────────────────────────────────────────────── #
|
||
|
||
def _on_ai_browse(self):
|
||
path, _ = QFileDialog.getOpenFileName(
|
||
self, "YOLOv8 모델 선택", "", "YOLOv8 모델 (*.pt)"
|
||
)
|
||
if path:
|
||
self._ai_path_edit.setText(to_project_relative(path))
|
||
|
||
def _on_ai_load(self):
|
||
path = self._ai_path_edit.text().strip()
|
||
log_action(f"[설정] AI 모델 로드: {path}")
|
||
if not path:
|
||
QMessageBox.warning(self, "경고", "모델 경로를 먼저 선택하세요.")
|
||
return
|
||
ok = self._sp._do_load_model(path)
|
||
if ok:
|
||
names = ", ".join(self._sp._detector.class_names)
|
||
self._ai_info_lbl.setText(
|
||
f"클래스: {names} ({len(self._sp._detector.class_names)}개)"
|
||
)
|
||
self._sp._set_ai_loaded(True)
|
||
QMessageBox.information(self, "로드 완료", f"모델 로드 완료\n{path}")
|
||
else:
|
||
self._sp._set_ai_loaded(False)
|
||
QMessageBox.critical(self, "로드 실패", f"모델을 로드할 수 없습니다.\n{path}")
|
||
|
||
def _on_ai_unload(self):
|
||
log_action("[설정] AI 모델 해제")
|
||
if self._sp._detector:
|
||
self._sp._detector._model = None
|
||
self._sp._detector.model_path = None
|
||
self._sp._set_ai_loaded(False)
|
||
self._ai_info_lbl.setText("")
|
||
|
||
# ── 컨베이어 탭 슬롯 ─────────────────────────────────────────────── #
|
||
|
||
def _on_conveyor_changed(self):
|
||
dist = self._conv_distance.value()
|
||
speed = self._conv_speed.value()
|
||
self._conv_delay_lbl.setText(f"{dist / speed:.2f} s" if speed > 0 else "—")
|
||
|
||
def _on_conveyor_apply(self):
|
||
dist = self._conv_distance.value()
|
||
speed = self._conv_speed.value()
|
||
delay = dist / speed if speed > 0 else 0.0
|
||
log_action(f"[설정] 컨베이어 적용: {dist}cm / {speed}cm·s → 딜레이 {delay:.2f}s")
|
||
try:
|
||
self._sp._save_config({"conveyor": {"distance_cm": dist, "speed_cms": speed}})
|
||
self._sp._config.setdefault("conveyor", {}).update(
|
||
{"distance_cm": dist, "speed_cms": speed}
|
||
)
|
||
self._sp.belt_settings_changed.emit(delay)
|
||
QMessageBox.information(
|
||
self, "적용 완료",
|
||
f"거리: {dist} cm / 속도: {speed} cm/s\n딜레이: {delay:.2f}s",
|
||
)
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "저장 실패", str(e))
|
||
|
||
# ── 전체 저장 ────────────────────────────────────────────────────── #
|
||
|
||
def _on_save_all(self):
|
||
log_action("[설정] 전체 설정 저장")
|
||
data = {
|
||
"cognex": {
|
||
"ip": self._cognex_ip.text().strip(),
|
||
"port": self._cognex_port.value(),
|
||
},
|
||
"basler": {
|
||
"exposure": self._basler_exposure.value(),
|
||
"gain": self._basler_gain.value(),
|
||
},
|
||
"db": {
|
||
"server": self._db_server.text().strip(),
|
||
"database": self._db_database.text().strip(),
|
||
"username": self._db_username.text().strip(),
|
||
"password": self._db_password.text(),
|
||
},
|
||
"ai": {
|
||
"model_path": to_project_relative(self._ai_path_edit.text().strip()),
|
||
},
|
||
"conveyor": {
|
||
"distance_cm": self._conv_distance.value(),
|
||
"speed_cms": self._conv_speed.value(),
|
||
},
|
||
"plc": {
|
||
"ip": self._plc_ip.text().strip(),
|
||
"port": self._plc_port.value(),
|
||
},
|
||
}
|
||
try:
|
||
self._sp._save_config(data)
|
||
for k, v in data.items():
|
||
if isinstance(self._sp._config.get(k), dict):
|
||
self._sp._config[k].update(v)
|
||
else:
|
||
self._sp._config[k] = v
|
||
QMessageBox.information(self, "저장 완료", "전체 설정이 저장되었습니다.")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "저장 실패", str(e))
|
||
|
||
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
# SettingsPage (작업자 메인 화면)
|
||
# ══════════════════════════════════════════════════════════════════════ #
|
||
|
||
class SettingsPage(QWidget):
|
||
cognex_status_changed = pyqtSignal(bool)
|
||
basler_status_changed = pyqtSignal(bool)
|
||
belt_settings_changed = pyqtSignal(float)
|
||
plc_status_changed = pyqtSignal(bool)
|
||
|
||
def __init__(self, insight_cam, basler_cam, config: dict,
|
||
detector=None, update_insight_cb=None,
|
||
update_basler_cb=None, update_detector_cb=None,
|
||
update_db_cb=None, update_plc_cb=None,
|
||
plc_client=None, parent=None):
|
||
super().__init__(parent)
|
||
self._insight = insight_cam
|
||
self._basler = basler_cam
|
||
self._config = config
|
||
self._detector = detector
|
||
self._update_insight_cb = update_insight_cb
|
||
self._update_basler_cb = update_basler_cb
|
||
self._update_detector_cb = update_detector_cb
|
||
self._update_db_cb = update_db_cb
|
||
self._update_plc_cb = update_plc_cb
|
||
self._db_client = None
|
||
self._plc_client = plc_client
|
||
|
||
self._build_ui()
|
||
self._sync_connection_status()
|
||
self._auto_load_model()
|
||
|
||
# ── UI 구성 ──────────────────────────────────────────────────────── #
|
||
|
||
def _build_ui(self):
|
||
root = QVBoxLayout(self)
|
||
root.setContentsMargins(40, 32, 40, 32)
|
||
root.setSpacing(0)
|
||
|
||
# 상단 행: 페이지 제목 + 관리자 버튼
|
||
top_row = QHBoxLayout()
|
||
top_row.setContentsMargins(0, 0, 0, 16)
|
||
|
||
title_lbl = QLabel("연결 상태")
|
||
title_lbl.setStyleSheet("color:#888888; font-size:14px;")
|
||
top_row.addWidget(title_lbl)
|
||
top_row.addStretch()
|
||
|
||
admin_btn = QPushButton("⚙ 관리자 설정")
|
||
admin_btn.setFixedHeight(40)
|
||
admin_btn.setStyleSheet(_BTN_ADMIN)
|
||
admin_btn.clicked.connect(self._on_admin_settings)
|
||
top_row.addWidget(admin_btn)
|
||
|
||
root.addLayout(top_row)
|
||
|
||
# 2×2 카드 그리드 — 카드 고정 너비 500px, 왼쪽 정렬
|
||
self._cognex_card = self._build_card(
|
||
"코그넥스 In-Sight 2000C",
|
||
icon_color="#042C53", icon_text="IN",
|
||
connect_cb=self._on_cognex_connect_quick,
|
||
disconnect_cb=self._on_cognex_disconnect,
|
||
)
|
||
self._basler_card = self._build_card(
|
||
"Basler USB 카메라",
|
||
icon_color="#26215C", icon_text="BA",
|
||
connect_cb=self._on_basler_connect,
|
||
disconnect_cb=self._on_basler_disconnect,
|
||
)
|
||
self._db_card = self._build_card(
|
||
"MS SQL Server DB",
|
||
icon_color="#412402", icon_text="DB",
|
||
connect_cb=self._on_db_connect_quick,
|
||
disconnect_cb=self._on_db_disconnect,
|
||
)
|
||
self._ai_card = self._build_card(
|
||
"AI 모델 (YOLOv8)",
|
||
icon_color="#085041", icon_text="AI",
|
||
connect_label="로드",
|
||
disconnect_label="해제",
|
||
connect_cb=self._on_ai_load_quick,
|
||
disconnect_cb=self._on_ai_unload,
|
||
)
|
||
self._plc_card = self._build_card(
|
||
"PLC (MELSEC Q06UDEHCPU)",
|
||
icon_color="#3B1A04", icon_text="PLC",
|
||
connect_cb=self._on_plc_connect_quick,
|
||
disconnect_cb=self._on_plc_disconnect,
|
||
)
|
||
|
||
grid = QGridLayout()
|
||
grid.setSpacing(10)
|
||
grid.setContentsMargins(0, 0, 0, 0)
|
||
grid.addWidget(self._cognex_card["frame"], 0, 0)
|
||
grid.addWidget(self._basler_card["frame"], 0, 1)
|
||
grid.addWidget(self._db_card["frame"], 1, 0)
|
||
grid.addWidget(self._ai_card["frame"], 1, 1)
|
||
grid.addWidget(self._plc_card["frame"], 2, 0)
|
||
|
||
# 그리드를 왼쪽 정렬로 감싸는 컨테이너
|
||
grid_container = QWidget()
|
||
grid_container.setLayout(grid)
|
||
|
||
outer = QHBoxLayout()
|
||
outer.setContentsMargins(0, 0, 0, 0)
|
||
outer.addWidget(grid_container)
|
||
outer.addStretch()
|
||
|
||
root.addLayout(outer)
|
||
root.addStretch()
|
||
|
||
def _build_card(self, title: str,
|
||
icon_color: str = "#333333", icon_text: str = "",
|
||
connect_label: str = "연결",
|
||
disconnect_label: str = "연결 해제",
|
||
connect_cb=None, disconnect_cb=None) -> dict:
|
||
frame = QFrame()
|
||
frame.setStyleSheet(_CARD)
|
||
frame.setFixedSize(500, 96)
|
||
|
||
row = QHBoxLayout(frame)
|
||
row.setContentsMargins(16, 0, 16, 0)
|
||
row.setSpacing(14)
|
||
|
||
# 아이콘 박스
|
||
icon = QLabel(icon_text)
|
||
icon.setFixedSize(36, 36)
|
||
icon.setAlignment(Qt.AlignCenter)
|
||
icon.setStyleSheet(
|
||
f"background:{icon_color}; color:#ffffff;"
|
||
"border-radius:6px; font-size:13px; font-weight:500;"
|
||
)
|
||
|
||
# 제목 + 상태 (세로 스택)
|
||
info_w = QWidget()
|
||
info_w.setStyleSheet("background:transparent;")
|
||
info_v = QVBoxLayout(info_w)
|
||
info_v.setContentsMargins(0, 0, 0, 0)
|
||
info_v.setSpacing(2)
|
||
|
||
title_lbl = QLabel(title)
|
||
title_lbl.setStyleSheet(
|
||
"color:#cccccc; font-size:14px; background:transparent; border:none;"
|
||
)
|
||
|
||
status_lbl = QLabel("● 연결 안됨")
|
||
status_lbl.setStyleSheet(_STATUS_FAIL)
|
||
|
||
info_v.addWidget(title_lbl)
|
||
info_v.addWidget(status_lbl)
|
||
|
||
# 버튼 쌍
|
||
btn_connect = QPushButton(connect_label)
|
||
btn_connect.setFixedHeight(43)
|
||
btn_connect.setStyleSheet(_BTN_CARD_CONNECT)
|
||
if connect_cb:
|
||
btn_connect.clicked.connect(connect_cb)
|
||
|
||
btn_disconnect = QPushButton(disconnect_label)
|
||
btn_disconnect.setFixedHeight(43)
|
||
btn_disconnect.setStyleSheet(_BTN_CARD_DISCONNECT)
|
||
btn_disconnect.setEnabled(False)
|
||
if disconnect_cb:
|
||
btn_disconnect.clicked.connect(disconnect_cb)
|
||
|
||
row.addWidget(icon)
|
||
row.addWidget(info_w, stretch=1)
|
||
row.addWidget(btn_connect)
|
||
row.addWidget(btn_disconnect)
|
||
|
||
return {
|
||
"frame": frame,
|
||
"status_lbl": status_lbl,
|
||
"btn_connect": btn_connect,
|
||
"btn_disconnect": btn_disconnect,
|
||
}
|
||
|
||
# ── 관리자 설정 ──────────────────────────────────────────────────── #
|
||
|
||
def _on_admin_settings(self):
|
||
dlg = PasswordDialog(self)
|
||
if dlg.exec_() == QDialog.Accepted:
|
||
AdminSettingsDialog(self, self).exec_()
|
||
|
||
# ── 연결 상태 동기화 (외부에서도 호출 가능) ──────────────────────── #
|
||
|
||
def _sync_connection_status(self):
|
||
self._set_cognex_connected(bool(self._insight and self._insight.is_connected()))
|
||
self._set_basler_connected(bool(self._basler and self._basler.is_connected()))
|
||
self._set_db_connected(bool(self._db_client and self._db_client.is_connected()))
|
||
if self._detector:
|
||
self._set_ai_loaded(self._detector.is_loaded())
|
||
self._set_plc_connected(bool(self._plc_client and self._plc_client.is_connected()))
|
||
|
||
def _set_card_state(self, card: dict, connected: bool,
|
||
ok_text: str, fail_text: str):
|
||
if connected:
|
||
card["status_lbl"].setText(f"● {ok_text}")
|
||
card["status_lbl"].setStyleSheet(_STATUS_OK)
|
||
else:
|
||
card["status_lbl"].setText(f"● {fail_text}")
|
||
card["status_lbl"].setStyleSheet(_STATUS_FAIL)
|
||
card["btn_connect"].setEnabled(not connected)
|
||
card["btn_disconnect"].setEnabled(connected)
|
||
|
||
def _set_cognex_connected(self, connected: bool):
|
||
self._set_card_state(self._cognex_card, connected, "연결됨", "연결 안됨")
|
||
|
||
def _set_basler_connected(self, connected: bool):
|
||
self._set_card_state(self._basler_card, connected, "연결됨", "연결 안됨")
|
||
|
||
def _set_db_connected(self, connected: bool):
|
||
self._set_card_state(self._db_card, connected, "연결됨", "연결 안됨")
|
||
|
||
def _set_ai_loaded(self, loaded: bool):
|
||
self._set_card_state(self._ai_card, loaded, "로드됨", "로드 안됨")
|
||
|
||
def _set_plc_connected(self, connected: bool):
|
||
self._set_card_state(self._plc_card, connected, "연결됨", "연결 안됨")
|
||
|
||
# ── 빠른 연결 (카드 버튼 — config.json 값 그대로 사용) ──────────── #
|
||
|
||
def _on_cognex_connect_quick(self):
|
||
cfg = self._config.get("cognex", {})
|
||
ip = cfg.get("ip", "").strip()
|
||
port = cfg.get("port", 23)
|
||
log_action(f"[설정] Cognex 연결: {ip}:{port}")
|
||
if not ip:
|
||
QMessageBox.warning(
|
||
self, "설정 필요",
|
||
"관리자 설정에서 코그넥스 IP를 먼저 설정해주세요.",
|
||
)
|
||
return
|
||
if self._insight and self._insight._sock is not None:
|
||
self._insight.disconnect()
|
||
new_cam = InSightCamera()
|
||
new_cam.connect(ip, port)
|
||
if new_cam._sock is None:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
f"In-Sight 카메라에 연결할 수 없습니다.\n{ip}:{port}",
|
||
)
|
||
return
|
||
self._insight = new_cam
|
||
if self._update_insight_cb:
|
||
self._update_insight_cb(new_cam)
|
||
self._set_cognex_connected(True)
|
||
self.cognex_status_changed.emit(True)
|
||
|
||
def _on_cognex_disconnect(self):
|
||
log_action("[설정] Cognex 연결 해제")
|
||
if self._insight:
|
||
self._insight.disconnect()
|
||
self._set_cognex_connected(False)
|
||
self.cognex_status_changed.emit(False)
|
||
|
||
def _on_basler_connect(self):
|
||
log_action("[설정] Basler 연결")
|
||
if self._basler and self._basler.camera is not None:
|
||
self._basler.disconnect()
|
||
new_cam = BaslerCamera()
|
||
new_cam.connect()
|
||
if new_cam.camera is None:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
"Basler 카메라에 연결할 수 없습니다.\nUSB 연결 및 전원을 확인하세요.",
|
||
)
|
||
return
|
||
self._basler = new_cam
|
||
if self._update_basler_cb:
|
||
self._update_basler_cb(new_cam)
|
||
self._set_basler_connected(True)
|
||
self.basler_status_changed.emit(True)
|
||
|
||
def _on_basler_disconnect(self):
|
||
log_action("[설정] Basler 연결 해제")
|
||
if self._basler:
|
||
self._basler.disconnect()
|
||
self._set_basler_connected(False)
|
||
self.basler_status_changed.emit(False)
|
||
|
||
def _on_db_connect_quick(self):
|
||
cfg = self._config.get("db", {})
|
||
server = cfg.get("server", "").strip()
|
||
log_action(f"[설정] DB 연결: {server}")
|
||
database = cfg.get("database", "").strip()
|
||
username = cfg.get("username", "").strip()
|
||
password = cfg.get("password", "")
|
||
if not server or not database:
|
||
QMessageBox.warning(
|
||
self, "설정 필요",
|
||
"관리자 설정에서 DB 서버 정보를 먼저 설정해주세요.",
|
||
)
|
||
return
|
||
client = SQLClient()
|
||
ok = client.connect(server, database, username, password)
|
||
if ok:
|
||
self._db_client = client
|
||
self._set_db_connected(True)
|
||
if self._update_db_cb:
|
||
self._update_db_cb(client)
|
||
else:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
"DB 연결에 실패했습니다.\n관리자 설정에서 서버 정보를 확인하세요.",
|
||
)
|
||
|
||
def _on_db_disconnect(self):
|
||
log_action("[설정] DB 연결 해제")
|
||
if self._db_client:
|
||
self._db_client.disconnect()
|
||
self._db_client = None
|
||
if self._update_db_cb:
|
||
self._update_db_cb(None)
|
||
self._set_db_connected(False)
|
||
|
||
def _on_ai_load_quick(self):
|
||
path = self._config.get("ai", {}).get("model_path", "").strip()
|
||
log_action(f"[설정] AI 모델 로드: {path}")
|
||
if not path:
|
||
QMessageBox.warning(
|
||
self, "설정 필요",
|
||
"관리자 설정에서 AI 모델 경로를 먼저 설정해주세요.",
|
||
)
|
||
return
|
||
ok = self._do_load_model(path)
|
||
if ok:
|
||
self._set_ai_loaded(True)
|
||
else:
|
||
QMessageBox.critical(self, "로드 실패", f"모델을 로드할 수 없습니다.\n{path}")
|
||
|
||
def _on_ai_unload(self):
|
||
log_action("[설정] AI 모델 해제")
|
||
if self._detector:
|
||
self._detector._model = None
|
||
self._detector.model_path = None
|
||
self._set_ai_loaded(False)
|
||
|
||
def _on_plc_connect_quick(self):
|
||
cfg = self._config.get("plc", {})
|
||
ip = cfg.get("ip", "").strip()
|
||
port = cfg.get("port", 5010)
|
||
log_action(f"[설정] PLC 연결: {ip}:{port}")
|
||
if not ip:
|
||
QMessageBox.warning(
|
||
self, "설정 필요",
|
||
"관리자 설정에서 PLC IP를 먼저 설정해주세요.",
|
||
)
|
||
return
|
||
client = PLCClient()
|
||
ok = client.connect(ip, port)
|
||
if ok:
|
||
self._plc_client = client
|
||
self._set_plc_connected(True)
|
||
if self._update_plc_cb:
|
||
self._update_plc_cb(client)
|
||
self.plc_status_changed.emit(True)
|
||
else:
|
||
QMessageBox.critical(
|
||
self, "연결 실패",
|
||
f"PLC에 연결할 수 없습니다.\n{ip}:{port}",
|
||
)
|
||
|
||
def _on_plc_disconnect(self):
|
||
log_action("[설정] PLC 연결 해제")
|
||
if self._plc_client:
|
||
self._plc_client.disconnect()
|
||
self._plc_client = None
|
||
if self._update_plc_cb:
|
||
self._update_plc_cb(None)
|
||
self._set_plc_connected(False)
|
||
self.plc_status_changed.emit(False)
|
||
|
||
# ── AI 모델 로드 공통 ────────────────────────────────────────────── #
|
||
|
||
def _do_load_model(self, path: str) -> bool:
|
||
if self._detector is None:
|
||
self._detector = Detector()
|
||
abs_path = resolve_path(path)
|
||
ok = self._detector.load_model(abs_path)
|
||
if ok:
|
||
saved_path = to_project_relative(abs_path)
|
||
self._save_config({"ai": {"model_path": saved_path}})
|
||
self._config.setdefault("ai", {})["model_path"] = saved_path
|
||
if self._update_detector_cb:
|
||
self._update_detector_cb(self._detector)
|
||
return ok
|
||
|
||
def _auto_load_model(self):
|
||
path = self._config.get("ai", {}).get("model_path", "")
|
||
abs_path = resolve_path(path)
|
||
if abs_path and os.path.exists(abs_path):
|
||
ok = self._do_load_model(abs_path)
|
||
if ok:
|
||
self._set_ai_loaded(True)
|
||
|
||
# ── config 읽기 / 쓰기 ───────────────────────────────────────────── #
|
||
|
||
def _load_config(self) -> dict:
|
||
try:
|
||
with open(get_path("config.json"), "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
return {}
|
||
|
||
def _save_config(self, data: dict):
|
||
cfg_path = get_path("config.json")
|
||
try:
|
||
with open(cfg_path, "r", encoding="utf-8") as f:
|
||
existing = json.load(f)
|
||
except (FileNotFoundError, json.JSONDecodeError):
|
||
existing = {}
|
||
for key, val in data.items():
|
||
if isinstance(val, dict) and isinstance(existing.get(key), dict):
|
||
existing[key].update(val)
|
||
else:
|
||
existing[key] = val
|
||
with open(cfg_path, "w", encoding="utf-8") as f:
|
||
json.dump(existing, f, ensure_ascii=False, indent=2)
|
||
|
||
def _save_cognex_config(self, ip: str, port: int):
|
||
try:
|
||
self._save_config({"cognex": {"ip": ip, "port": port}})
|
||
self._config.setdefault("cognex", {}).update({"ip": ip, "port": port})
|
||
except Exception as e:
|
||
print(f"[설정] config.json 저장 실패: {e}")
|