Files
ant-vision-inspector/plc_test_gui.py
2026-06-10 16:18:41 +09:00

373 lines
14 KiB
Python

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