373 lines
14 KiB
Python
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_())
|