์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

PyQt6 ๊ธฐ๋ฐ˜ ๊ณ„์ธก ์ž๋™ํ™” UI ๋ฉˆ์ถค ํ•ด๊ฒฐ: PyVISA ํ†ต์‹  ๋ธ”๋กœํ‚น ๊ทน๋ณต๊ณผ QThread ๋น„๋™๊ธฐ ๋ช…๋ น์–ด ํ ๊ตฌํ˜„

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 9. 2. 01:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article addresses the critical issue of UI freezing caused by blocking I/O operations in PyVISA during the development of PyQt6-based test and measurement automation systems. It provides a comprehensive troubleshooting guide for implementing an asynchronous command queue using PyQt6's QThread and Python's thread-safe Queue module. Readers will learn how to architecture non-blocking VISA communication, handle edge-case timeouts gracefully, and ensure complete thread safety in high-throughput hardware test environments.

๋ฐ˜๋„์ฒด ๊ฒ€์‚ฌ ์žฅ๋น„, ์ „์ž ๋ถ€ํ’ˆ ํŠน์„ฑ ํ‰๊ฐ€, ๋˜๋Š” ์ƒ์‚ฐ ๋ผ์ธ์˜ ์ž๋™ํ™” ์‹œํ—˜ ์„ค๋น„๋ฅผ ๊ฐœ๋ฐœํ•  ๋•Œ Python์˜ PyQt6์™€ PyVISA ์กฐํ•ฉ์€ ์‚ฌ์‹ค์ƒ์˜ ํ‘œ์ค€(De-facto Standard)์œผ๋กœ ์ž๋ฆฌ ์žก์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๊ฐœ๋ฐœ ์ดˆ๊ธฐ ๋‹จ์ˆœํ•œ ์Šคํฌ๋ฆฝํŠธ ์ž‘์„ฑ ๋ฐฉ์‹์„ ๊ทธ๋Œ€๋กœ GUI(Graphical User Interface) ๊ตฌ์กฐ์— ์ด์‹ํ•˜๋‹ค ๋ณด๋ฉด, ์žฅ๋น„์™€ ๋ช…๋ น์–ด ์ˆ˜์‹ /์†ก์‹  ๊ณผ์ •์—์„œ **UI๊ฐ€ ๋ฌด์‘๋‹ต ์ƒํƒœ(Not Responding)**๋กœ ๋น ์ง€๊ฑฐ๋‚˜ ์‹ฌํ•˜๋ฉด ์•ฑ์ด ๊ฐ•์ œ ์ข…๋ฃŒ๋˜๋Š” ํ˜„์ƒ์„ ๋นˆ๋ฒˆํžˆ ๊ฒช๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋ณธ ํฌ์ŠคํŒ…์—์„œ๋Š” ๊ณ„์ธก ์ž๋™ํ™” ์‹œ์Šคํ…œ ๊ฐœ๋ฐœ ์‹œ ๋ฐœ์ƒํ•˜๋Š” PyVISA์˜ ๋ธ”๋กœํ‚น I/O(Blocking Input/Output) ์›์ธ์„ ๊นŠ์ด ์žˆ๊ฒŒ ๋ถ„์„ํ•˜๊ณ , QThread์™€ ๋น„๋™๊ธฐ ๋ช…๋ น์–ด ํ(Asynchronous Command Queue)๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ์™„๋ฒฝํ•˜๊ฒŒ ๋ฐ˜์‘ํ˜• GUI๋ฅผ ์œ ์ง€ํ•˜๋Š” ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ฐ€์ด๋“œ๋ฅผ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.



1. UI ์‘๋‹ต ์ง€์—ฐ์˜ ๊ทผ๋ณธ ์›์ธ: PyVISA ๋™๊ธฐ ํ†ต์‹ ๊ณผ ๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฃจํ”„

PyQt6๋ฅผ ํฌํ•จํ•œ ๋Œ€๋ถ€๋ถ„์˜ GUI ํ”„๋ ˆ์ž„์›Œํฌ๋Š” ๋‹จ์ผ ์Šค๋ ˆ๋“œ ๊ธฐ๋ฐ˜ ์ด๋ฒคํŠธ ๋ฃจํ”„(Single-threaded Event Loop)๋กœ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰, ํ™”๋ฉด์„ ์ƒˆ๋กœ ๊ทธ๋ฆฌ๊ณ (Redraw), ๋งˆ์šฐ์Šค ํด๋ฆญ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ํ‚ค๋ณด๋“œ ์ž…๋ ฅ์„ ๋ฐ›๋Š” ๋ชจ๋“  ์ž‘์—…์ด '๋ฉ”์ธ ์Šค๋ ˆ๋“œ(Main Thread / UI Thread)' ํ•˜๋‚˜์—์„œ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด, ์˜ค์‹ค๋กœ์Šค์ฝ”ํ”„, ์ „์› ๊ณต๊ธ‰๊ธฐ(Power Supply), ๋ฉ€ํ‹ฐ๋ฏธํ„ฐ(DMM) ๋“ฑ์˜ ๊ณ„์ธก๊ธฐ์™€ ํ†ต์‹ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” PyVISA ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๊ธฐ๋ณธ ํ•จ์ˆ˜๋“ค(inst.query(), inst.read(), inst.write())์€ **๋™๊ธฐ์‹ ๋ธ”๋กœํ‚น(Synchronous Blocking) ๋ฐฉ์‹**์œผ๋กœ ๋™์ž‘ํ•ฉ๋‹ˆ๋‹ค.

  • I/O ๋Œ€๊ธฐ ์‹œ๊ฐ„(Latency): ๊ณ„์ธก๊ธฐ๊ฐ€ SCPI(Standard Commands for Programmable Instruments) ๋ช…๋ น์–ด๋ฅผ ํ•ด์„ํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐ ์ˆ˜์‹ญ ๋ฐ€๋ฆฌ์ดˆ(ms)์—์„œ ์ˆ˜ ์ดˆ(s)๊ฐ€ ์†Œ์š”๋ฉ๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ ๋ฃจํ”„ ์ฐจ๋‹จ: ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ query('*IDN?')๋‚˜ ๋ฐ์ดํ„ฐ ๋กœ๊น… ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๋ฉด, ๊ณ„์ธก๊ธฐ๋กœ๋ถ€ํ„ฐ ์‘๋‹ต์ด ๋Œ์•„์˜ฌ ๋•Œ๊นŒ์ง€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ๊ฐ€ ์™„์ „ํžˆ ๋ฉˆ์ถฅ๋‹ˆ๋‹ค(Block).
  • ๊ฒฐ๊ณผ: ์‚ฌ์šฉ์ž๋Š” ์ฐฝ ์ด๋™, ๋ฒ„ํŠผ ํด๋ฆญ, ๊ทธ๋ž˜ํ”ฝ ์ฐจํŠธ ์—…๋ฐ์ดํŠธ ๋“ฑ ๋ชจ๋“  GUI ๋ฐ˜์‘์ด ์ผ์‹œ ์ •์ง€๋˜๋Š” ์ด๋ฅธ๋ฐ” 'UI ๋ฉˆ์ถค ํ˜„์ƒ'์„ ๊ฒฝํ—˜ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ Python์˜ ๊ธฐ๋ณธ threading ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, PyQt6์˜ GUI ์š”์†Œ๋ฅผ ์„œ๋ธŒ ์Šค๋ ˆ๋“œ์—์„œ ์ง์ ‘ ์ˆ˜์ •ํ•˜๋ ค๋‹ค **์Šค๋ ˆ๋“œ ์„ธ์ดํ”„ํ‹ฐ(Thread Safety)** ์œ„๋ฐ˜์œผ๋กœ ์ธํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ฉ”๋ชจ๋ฆฌ ํฌ๋ž˜์‹œ(Segmentation Fault)๋ฅผ ์ผ์œผํ‚ค๋Š” 2์ฐจ ๋ฌธ์ œ๋กœ ์ด์–ด์ง‘๋‹ˆ๋‹ค.

2. ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„: ์ƒ์‚ฐ์ž-์†Œ๋น„์ž ํŒจํ„ด ๊ธฐ๋ฐ˜์˜ ๋น„๋™๊ธฐ Queue ์‹œ์Šคํ…œ

์ด ๋ฌธ์ œ๋ฅผ ๊ทผ๋ณธ์ ์œผ๋กœ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด ๊ณ„์ธก๊ธฐ ํ†ต์‹  ์ „์šฉ ๋น„๋™๊ธฐ Worker ์Šค๋ ˆ๋“œ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ , ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์™€ ํ†ต์‹  ์Šค๋ ˆ๋“œ ์‚ฌ์ด์— **์ƒ์‚ฐ์ž-์†Œ๋น„์ž ํŒจํ„ด(Producer-Consumer Pattern)**์„ ์ ์šฉํ•œ ์•ˆ์ „ํ•œ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ๊ตฌ์ถ•ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ „์ฒด ์‹œ์Šคํ…œ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค๊ณ„๋ฉ๋‹ˆ๋‹ค.

  1. UI Thread (Producer): ์‚ฌ์šฉ์ž์˜ ๋ฒ„ํŠผ ํด๋ฆญ์ด๋‚˜ ํƒ€์ด๋จธ ๊ธฐ๋ฐ˜ ๋กœ๊น… ์š”์ฒญ์„ **๋ช…๋ น์–ด ๊ฐ์ฒด(Command Data Class)**๋กœ ์ƒ์„ฑํ•˜์—ฌ queue.Queue์— ์‚ฝ์ž…(Put)ํ•ฉ๋‹ˆ๋‹ค.
  2. Thread-Safe Queue: ๋ช…๋ น์–ด์™€ ํŒŒ๋ผ๋ฏธํ„ฐ, ์ฝœ๋ฐฑ ์ •๋ณด๋ฅผ ๋‹ด๋Š” FIFO(First-In-First-Out) ๋Œ€๊ธฐ์—ด์ž…๋‹ˆ๋‹ค. ์Šค๋ ˆ๋“œ ๊ฐ„ ๋™๊ธฐํ™”๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.
  3. Worker QThread (Consumer): ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฌดํ•œ ๋ฃจํ”„๋ฅผ ๋Œ๋ฉฐ ํ์— ๋ช…๋ น์–ด๊ฐ€ ๋“ค์–ด์˜ค๋Š”์ง€ ๊ฐ์‹œํ•˜๋‹ค๊ฐ€, ๋ช…๋ น์–ด๊ฐ€ ๋„์ฐฉํ•˜๋ฉด PyVISA ๋™๊ธฐ ํ†ต์‹ ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.
  4. PyQt Signal/Slot: ํ†ต์‹  ๊ฒฐ๊ณผ(์ธก์ •๊ฐ’, ์ƒํƒœ ๋ฉ”์‹œ์ง€, ์—๋Ÿฌ ๋‚ด์—ญ)๋ฅผ PyQt6์˜ ์‹œ๊ทธ๋„์„ ํ†ตํ•ด ์•ˆ์ „ํ•˜๊ฒŒ UI ์Šค๋ ˆ๋“œ๋กœ ์ „๋‹ฌ(Emit)ํ•˜์—ฌ ํ™”๋ฉด์— ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.


3. ์‹ค์ „ ๊ตฌํ˜„: QThread ๋ฐ PyVISA ๋น„๋™๊ธฐ ๋ช…๋ น์–ด ํ ์ฝ”๋“œ

์•„๋ž˜ ์ฝ”๋“œ๋Š” ๋ฐ”๋กœ ํ˜„์—… ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋œ ํด๋ž˜์Šค ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค. PyVISA ์ฒ˜๋ฆฌ์šฉ Worker ์Šค๋ ˆ๋“œ์™€ ์ปค์Šคํ…€ ์‹œ๊ทธ๋„, ๊ทธ๋ฆฌ๊ณ  ํ ์‹œ์Šคํ…œ์„ ํฌํ•จํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

import sys
import time
import queue
from dataclasses import dataclass
from typing import Any, Optional
import pyvisa

from PyQt6.QtCore import QThread, pyqtSignal, QObject
from PyQt6.QtWidgets import QApplication, QMainWindow, QPushButton, QTextEdit, QVBoxLayout, QWidget

# 1. ๋ช…๋ น์–ด ๊ตฌ์กฐ์ฒด ์ •์˜
@dataclass
class VisaCommand:
    cmd_type: str  # 'WRITE', 'QUERY', 'READ'
    command: str
    kwargs: dict = None

# 2. PyVISA ์ „์šฉ Worker Thread ๊ตฌํ˜„
class InstrumentWorker(QThread):
    # GUI๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•œ PyQt ์‹œ๊ทธ๋„
    data_received = pyqtSignal(str, str)  # (command, response)
    error_occurred = pyqtSignal(str, str) # (command, error_message)
    status_changed = pyqtSignal(str)      # status updates

    def __init__(self, resource_name: str, parent: Optional[QObject] = None):
        super().__init__(parent)
        self.resource_name = resource_name
        self.cmd_queue = queue.Queue()
        self._is_running = True
        self.rm = None
        self.inst = None

    def run(self):
        """๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ๋ฉ”์ธ ๋ฃจํ”„"""
        try:
            self.rm = pyvisa.ResourceManager()
            self.inst = self.rm.open_resource(self.resource_name)
            self.inst.timeout = 3000  # 3์ดˆ ํƒ€์ž„์•„์›ƒ ์„ค์ •
            self.status_changed.emit(f"์—ฐ๊ฒฐ ์„ฑ๊ณต: {self.resource_name}")
        except Exception as e:
            self.error_occurred.emit("CONNECT", f"์žฅ๋น„ ์—ฐ๊ฒฐ ์‹คํŒจ: {str(e)}")
            return

        while self._is_running:
            try:
                # Queue์—์„œ ๋ช…๋ น์–ด๋ฅผ ๊บผ๋ƒ„ (0.1์ดˆ ํƒ€์ž„์•„์›ƒ์œผ๋กœ ๋ฃจํ”„ ํƒˆ์ถœ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•จ)
                cmd_obj: VisaCommand = self.cmd_queue.get(timeout=0.1)
            except queue.Empty:
                continue

            # ๋ช…๋ น์–ด ์ฒ˜๋ฆฌ (PyVISA ๋™๊ธฐ ํ˜ธ์ถœ ๋ธ”๋กœํ‚น ๊ตฌ๊ฐ„)
            try:
                if cmd_obj.cmd_type == 'QUERY':
                    res = self.inst.query(cmd_obj.command)
                    self.data_received.emit(cmd_obj.command, res.strip())
                elif cmd_obj.cmd_type == 'WRITE':
                    self.inst.write(cmd_obj.command)
                    self.data_received.emit(cmd_obj.command, "OK")
            except pyvisa.VisaIOError as e:
                self.error_occurred.emit(cmd_obj.command, f"VISA I/O ์—๋Ÿฌ: {e.description}")
            except Exception as e:
                self.error_occurred.emit(cmd_obj.command, f"์ผ๋ฐ˜ ์˜ˆ์™ธ: {str(e)}")
            finally:
                self.cmd_queue.task_done()

        # ์ž์› ํ•ด์ œ
        if self.inst:
            self.inst.close()
        if self.rm:
            self.rm.close()
        self.status_changed.emit("ํ†ต์‹  ์Šค๋ ˆ๋“œ ์ข…๋ฃŒ ์™„๋ฃŒ")

    def send_command(self, cmd_type: str, command: str):
        """UI ์Šค๋ ˆ๋“œ์—์„œ ํ˜ธ์ถœํ•˜๋Š” ๋ช…๋ น์–ด ํˆฌ์ž… ๋ฉ”์„œ๋“œ"""
        self.cmd_queue.put(VisaCommand(cmd_type, command))

    def stop(self):
        """์Šค๋ ˆ๋“œ ์•ˆ์ „ ์ข…๋ฃŒ"""
        self._is_running = False
        self.wait()

๋ฉ”์ธ UI ์ธํ„ฐํŽ˜์ด์Šค ๊ฒฐํ•ฉ

์ž‘์„ฑํ•œ InstrumentWorker๋ฅผ PyQt6 ๋ฉ”์ธ ์œˆ๋„์šฐ์™€ ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ”๋“œ์ž…๋‹ˆ๋‹ค. UI ์ด๋ฒคํŠธ๊ฐ€ ์Šค๋ ˆ๋“œ๋ฅผ ๋ฐฉํ•ดํ•˜์ง€ ์•Š๊ณ  ํ์— ์ฐจ๊ณก์ฐจ๊ณก ์Œ“์ด๋Š” ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PyQt6 + PyVISA ๋น„๋™๊ธฐ ๊ณ„์ธก ์ œ์–ด๊ธฐ")
        self.resize(600, 400)

        # UI ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ
        layout = QVBoxLayout()
        self.log_widget = QTextEdit()
        self.log_widget.setReadOnly(True)
        
        self.btn_idn = QPushButton("*IDN? ์‹๋ณ„ ์ฝ”๋“œ ์กฐํšŒ")
        self.btn_measure = QPushButton("VOLT? ์ „์•• ์ธก์ • (์—ฐ์† ์š”์ฒญ)")

        layout.addWidget(self.log_widget)
        layout.addWidget(self.btn_idn)
        layout.addWidget(self.btn_measure)

        container = QWidget()
        container.setLayout(layout)
        self.setCentralWidget(container)

        # Worker ์Šค๋ ˆ๋“œ ์ƒ์„ฑ ๋ฐ ์‹œ๊ทธ๋„ ์—ฐ๊ฒฐ
        # ์‹ค์ œ ํ™˜๊ฒฝ์— ๋งž๋Š” VISA Resource Name ์ž…๋ ฅ (์˜ˆ: 'GPIB0::14::INSTR' ๋˜๋Š” 'TCPIP::192.168.1.100::INSTR')
        self.worker = InstrumentWorker('ASRL1::INSTR') 
        self.worker.data_received.connect(self.handle_data)
        self.worker.error_occurred.connect(self.handle_error)
        self.worker.status_changed.connect(self.handle_status)

        # ์ด๋ฒคํŠธ ๋ฒ„ํŠผ ์—ฐ๊ฒฐ
        self.btn_idn.clicked.connect(lambda: self.worker.send_command('QUERY', '*IDN?'))
        self.btn_measure.clicked.connect(self.trigger_multiple_queries)

        # ์Šค๋ ˆ๋“œ ์‹œ์ž‘
        self.worker.start()

    def trigger_multiple_queries(self):
        # ์—ฐ์†๋œ ๋ช…๋ น์–ด๋ฅผ ํ์— ๋„ฃ์–ด๋„ UI๊ฐ€ ์ „ํ˜€ ๋ฉˆ์ถ”์ง€ ์•Š์Œ
        for _ in range(5):
            self.worker.send_command('QUERY', 'MEAS:VOLT:DC?')

    def handle_data(self, cmd, response):
        self.log_widget.append(f"[์ˆ˜์‹ ] {cmd} -> {response}")

    def handle_error(self, cmd, error):
        self.log_widget.append(f"[์—๋Ÿฌ] {cmd}: {error}")

    def handle_status(self, status):
        self.log_widget.append(f"[์ƒํƒœ] {status}")

    def closeEvent(self, event):
        # ์•ฑ ์ข…๋ฃŒ ์‹œ Worker ์Šค๋ ˆ๋“œ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ cleanup
        self.worker.stop()
        event.accept()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec())

4. ํ˜„์—… ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฐ ์—ฃ์ง€ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ

์œ„์˜ ๊ธฐ๋ณธ์ ์ธ ๋น„๋™๊ธฐ ํ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถ•ํ–ˆ๋”๋ผ๋„ ์‹ค์ œ ํ•˜๋“œ์›จ์–ด ๊ฒ€์‚ฌ ํ˜„์žฅ์—์„œ๋Š” ๋‹ค์–‘ํ•œ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์•ˆ์ •์„ฑ์„ ๋Œ€ํญ ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ํ•„์ˆ˜ ์—ฃ์ง€ ์ผ€์ด์Šค(Edge Cases) ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์ž…๋‹ˆ๋‹ค.

๊ฐ€. VISA Timeout ๋ฐ ๋ฒ„์Šค ๋ก์—…(Lockup) ๋Œ€์‘

๊ณ„์ธก๊ธฐ ์ „์›์ด ๊บผ์ง€๊ฑฐ๋‚˜ ์ผ€์ด๋ธ”์ด ์ดํƒˆ๋œ ๊ฒฝ์šฐ inst.query()๋Š” ๋ฌดํ•œ์ • ๋Œ€๊ธฐํ•˜๊ฑฐ๋‚˜ ํƒ€์ž„์•„์›ƒ ์˜ˆ์™ธ(VI_ERROR_TMO)๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ํƒ€์ž„์•„์›ƒ์ด ๋ฐœ์ƒํ•˜๋ฉด VISA ๋ฆฌ์†Œ์Šค ๋ฒ„์Šค์— ์ž”์—ฌ ๋ฐ์ดํ„ฐ(Stale Data)๊ฐ€ ๋‚จ์•„ ๋‹ค์Œ ๋ช…๋ น์–ด๊ฐ€ ์—‰ํ‚ค๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ์ฑ…: ์˜ˆ์™ธ ์ฒ˜๋ฆฌ(except pyvisa.VisaIOError) ๋ธ”๋ก ๋‚ด๋ถ€์—์„œ inst.clear()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ํ•˜๋“œ์›จ์–ด ๋ฒ„์Šค ๋ฒ„ํผ๋ฅผ ์ฆ‰์‹œ ๋น„์›Œ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ช…๋ น์–ด ์ˆ˜ํ–‰ ์ „ํ›„์— ํƒ€์ž„์•„์›ƒ ์ˆ˜์น˜๋ฅผ ์œ ๋™์ ์œผ๋กœ ์กฐ์ ˆ(์˜ˆ: ์ผ๋ฐ˜ ์กฐํšŒ์˜ ๊ฒฝ์šฐ 1์ดˆ, Sweep ์ธก์ •์˜ ๊ฒฝ์šฐ 10์ดˆ)ํ•˜๋Š” ๊ฐ€๋ณ€ ํƒ€์ž„์•„์›ƒ(Dynamic Timeout)์„ VisaCommand ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์— ํฌํ•จ์‹œํ‚ค๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

๋‚˜. ํ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ(Queue Overflow) ๋ฐ Backpressure ์ œ์–ด

GUI ํƒ€์ด๋จธ๊ฐ€ 10ms ๋‹จ์œ„๋กœ ๋น ๋ฅด๊ฒŒ ์ธก์ • ๋ช…๋ น์„ ํ์— ์‚ฝ์ž…ํ•˜๋Š”๋ฐ, ๊ณ„์ธก๊ธฐ์˜ ์‘๋‹ต ์†๋„๊ฐ€ 50ms๋ผ๋ฉด ํ์— ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฌดํ•œํžˆ ์Œ“์—ฌ **๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜(Memory Leak)**์™€ **๋ฐ์ดํ„ฐ ์‹œ์ฐจ ์ง€์—ฐ(Latency Shift)**์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ์ฑ…: queue.Queue(maxsize=100)๊ณผ ๊ฐ™์ด ์ตœ๋Œ€ ํ ํฌ๊ธฐ๋ฅผ ์ œํ•œํ•˜๊ฑฐ๋‚˜, ํ์— ๋™์ผํ•œ ์œ ํ˜•์˜ ์กฐํšŒ ๋ช…๋ น์ด ์ด๋ฏธ ๋‚จ์•„ ์žˆ๋‹ค๋ฉด ๊ธฐ์กด ๋ช…๋ น์„ ๋ฎ์–ด์“ฐ๊ฑฐ๋‚˜ ๋ฌด์‹œํ•˜๋Š” **์ตœ์‹  ๋ฐ์ดํ„ฐ ์šฐ์„  ์ˆœ์œ„ ์ •์ฑ…(Latest-Only Policy)**์„ ์ ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค. ์•ˆ์ „ํ•œ ์Šค๋ ˆ๋“œ ์ข…๋ฃŒ (Graceful Shutdown)

์‚ฌ์šฉ์ž๊ฐ€ ํ”„๋กœ๊ทธ๋žจ X ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ข…๋ฃŒํ•  ๋•Œ ๊ณ„์ธก๊ธฐ์™€ ํ†ต์‹  ์ค‘์ธ ๊ฒฝ์šฐ ํ”„๋กœ์„ธ์Šค๊ฐ€ ๊ณ ์ŠคํŠธ ์Šค๋ ˆ๋“œ๋กœ ๋‚จ์•„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์ฃฝ์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ์ž์ฃผ ์ผ์–ด๋‚ฉ๋‹ˆ๋‹ค.

  • ํ•ด๊ฒฐ์ฑ…: ๋ฉ”์ธ ์œˆ๋„์šฐ์˜ closeEvent์—์„œ ๋‹จ์ˆœ terminate()๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ๊ฐ•์ œ ์ข…๋ฃŒ๋กœ ์ธํ•ด VISA ๋“œ๋ผ์ด๋ฒ„ ์„ธ์…˜์ด ์—ด๋ฆฐ ์ฑ„ ์ž ๊ฒจ๋ฒ„๋ฆฝ๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ _is_running ํ”Œ๋ž˜๊ทธ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  wait(msecs=2000) ๋ฐฉ์‹์œผ๋กœ ์Šค๋ ˆ๋“œ๊ฐ€ ์ •์ƒ ์ •๋ฆฌ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ ํ›„ ์ข…๋ฃŒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


5. ๊ฒฐ๋ก  ๋ฐ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์š”์•ฝ

PyQt6 ๊ธฐ๋ฐ˜์˜ ๊ณ„์ธก ์ž๋™ํ™” ํ”„๋กœ๊ทธ๋žจ ๊ฐœ๋ฐœ ์‹œ PyVISA์˜ ๋™๊ธฐ์‹ ํ†ต์‹  ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ  ์ด๋ฅผ ๋ฉ”์ธ UI ์Šค๋ ˆ๋“œ๋กœ๋ถ€ํ„ฐ ๋ถ„๋ฆฌํ•˜๋Š” ๊ฒƒ์€ ์•ˆ์ •์ ์ธ ์„ค๋น„ ์†Œํ”„ํŠธ์›จ์–ด ๊ตฌ์ถ•์˜ ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.

๊ตฌ๋ถ„ ๊ธฐ์กด ๋™๊ธฐ ํ†ต์‹  ๋ฐฉ์‹ QThread + Queue ๋น„๋™๊ธฐ ๋ฐฉ์‹
UI ๋ฐ˜์‘์„ฑ ๋ช…๋ น์–ด ์ „์†ก ์ค‘ ์ „์ฒด UI ๋ฉˆ์ถค (๋ฌด์‘๋‹ต) ํ•ญ์ƒ 60FPS ์ˆ˜์ค€์˜ ๋งค๋„๋Ÿฌ์šด ๋ฐ˜์‘์„ฑ ์œ ์ง€
์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ†ต์‹  ์˜ค๋ฅ˜ ์‹œ GUI ์ „์ฒด ํฌ๋ž˜์‹œ ๋ฐœ์ƒ ๊ฐ€๋Šฅ ์Šค๋ ˆ๋“œ ๋‚ด๋ถ€ ์บ์น˜ ํ›„ UI ์‹œ๊ทธ๋„๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์—๋Ÿฌ ๋ฆฌํฌํŒ…
ํ™•์žฅ์„ฑ ๋‹ค์ค‘ ์žฅ๋น„ ์ œ์–ด ์‹œ ๊ทน์‹ฌํ•œ ๋ณ‘๋ชฉ ํ˜„์ƒ ์žฅ๋น„๋ณ„ ๋…๋ฆฝ Worker Thread ํ™•์žฅ ์šฉ์ด

๋ณธ ๊ฐ€์ด๋“œ์—์„œ ๋‹ค๋ฃฌ QThread ๋น„๋™๊ธฐ ๋ช…๋ น์–ด ํ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ๋„์ž…ํ•˜๋ฉด, ์ˆ˜์‹ญ ๋Œ€์˜ ๊ณ„์ธก๊ธฐ๋ฅผ ๋™์‹œ์— ์ œ์–ดํ•˜๊ฑฐ๋‚˜ ๊ณ ์† ๋ฐ์ดํ„ฐ ๋กœ๊น…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ๋„ ์‚ฌ์šฉ์ž๋Š” ์พŒ์ ํ•˜๊ณ  ์•ˆ์ •์ ์œผ๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ํƒ€์ž„์•„์›ƒ ์„ธ์…˜ ํด๋ฆฌ์–ด ์กฐ์น˜์™€ ์Šค๋ ˆ๋“œ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ๊นŒ์ง€ ๊ผผ๊ผผํžˆ ์ ์šฉํ•ด ๋ณด์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•