feat: 초기 프로젝트 구조 추가
This commit is contained in:
1
gui/dialogs/__init__.py
Normal file
1
gui/dialogs/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# gui/dialogs 패키지
|
||||
245
gui/dialogs/image_settings_dialog.py
Normal file
245
gui/dialogs/image_settings_dialog.py
Normal 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)
|
||||
Reference in New Issue
Block a user