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