본문 바로가기
PYTHON(파이썬)/파이썬 활용

파이썬으로 작성한 SERIAL 통신 프로그램

by eplus 2024. 10. 31.

RS-232 시리얼 통신을 위한 GUI 애플리케이션을 PySide6와 Python을 사용하여 구현한 것입니다. 주요 기능과 구조는 다음과 같습니다.

주요 구성 요소

  1. GUI 초기화 (initUI):
    • QGroupBox, QComboBox, QPushButton, QLineEdit 등을 사용하여 UI를 구성합니다.
    • 시리얼 포트 설정 및 연결, 수신 데이터 확인, 데이터 전송 등의 인터페이스를 제공합니다.
    • 스타일 시트(setStyleSheet)를 사용하여 전체적인 UI의 색상, 폰트, 버튼 등의 디자인을 설정합니다.
  2. 시리얼 포트 설정:
    • 사용 가능한 시리얼 포트를 자동으로 검색하고, 사용자가 포트, 보드레이트, 데이터 비트, 패리티, 정지 비트 등의 설정을 선택할 수 있도록 합니다.
    • 연결 및 해제 버튼을 통해 시리얼 포트를 제어합니다.
  3. 시리얼 포트 연결 및 데이터 송수신:
    • connect_serial(): 사용자가 선택한 시리얼 포트와 설정 값으로 포트를 연결합니다.
    • disconnect_serial(): 연결된 포트를 해제합니다.
    • read_serial_data(): 주기적으로 데이터를 비동기적으로 읽어 들여 화면에 표시합니다. QTimer를 사용하여 일정 시간 간격으로 데이터를 확인합니다.
    • send_data_with_control(): 전송할 데이터를 입력받고, 선택된 제어 문자(STX, ETX 등)를 추가하여 전송합니다.
  4. 수신 데이터 표시 및 관리:
    • 수신된 데이터는 ASCII와 HEX 형식으로 표시됩니다.
    • QTextEdit를 사용해 수신 데이터를 표시하며, 사용자는 Clear, Save, Load 버튼을 통해 데이터를 관리할 수 있습니다.
    • 데이터를 텍스트 파일로 저장하거나 불러올 수 있는 기능도 제공됩니다.
  5. 제어 문자 선택 및 데이터 전송:
    • 사용자가 전송할 데이터를 입력하고, 그 앞과 뒤에 추가할 제어 문자를 선택할 수 있도록 인터페이스를 구성하였습니다.
    • 제어 문자로 STX, ETX, ACK, NAK 등 여러 가지가 있으며, 사용자 선택에 따라 전송 메시지에 포함됩니다.
import sys
import serial
import serial.tools.list_ports
from PySide6.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QLabel, QLineEdit, QPushButton, QTextEdit, QComboBox, QMessageBox, QFormLayout, QGroupBox, QHBoxLayout, QFileDialog
)
from PySide6.QtCore import QTimer


class SerialApp(QWidget):
    def __init__(self):
        super().__init__()
        self.initUI()
        self.serial_port = None

    def initUI(self):
        # 스타일 시트 적용
        self.setStyleSheet("""
            QWidget {
                background-color: #f0f0f0;
                font-family: Arial, sans-serif;
                font-size: 14px;
            }
            QGroupBox {
                background-color: #ffffff;
                border: 2px solid #c0c0c0;
                border-radius: 5px;
                margin-top: 10px;
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                subcontrol-position: top left;
                padding: 0 3px;
                background-color: #e0e0e0;
                color: #333333;
                font-weight: bold;
            }
            QLabel {
                color: #333333;
            }
            QComboBox, QLineEdit {
                border: 1px solid #c0c0c0;
                border-radius: 3px;
                padding: 2px 4px;
            }
            QPushButton {
                background-color: #4CAF50;
                color: white;
                border: none;
                border-radius: 5px;
                padding: 5px 10px;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
            QTextEdit {
                border: 1px solid #c0c0c0;
                border-radius: 5px;
                padding: 5px;
                background-color: #ffffff;
                color: #333333;
            }
            QPushButton#clearButton {
                background-color: #f44336;
            }
            QPushButton#clearButton:hover {
                background-color: #d32f2f;
            }
            QPushButton#saveButton, QPushButton#loadButton {
                background-color: #2196F3;
            }
            QPushButton#saveButton:hover, QPushButton#loadButton:hover {
                background-color: #1976D2;
            }
        """)

        main_layout = QVBoxLayout()

        # 시리얼 설정 그룹박스
        serial_groupbox = QGroupBox("Serial Port Settings")
        form_layout = QFormLayout()

        # 포트 선택
        self.port_combobox = QComboBox()
        self.update_ports()
        form_layout.addRow("Select Port:", self.port_combobox)

        # Baudrate 설정
        self.baudrate_combobox = QComboBox()
        self.baudrate_combobox.addItems(["9600", "19200", "38400", "57600", "115200"])
        form_layout.addRow("Select Baudrate:", self.baudrate_combobox)

        # Data Bits 설정
        self.databits_combobox = QComboBox()
        self.databits_combobox.addItems(["5", "6", "7", "8"])
        self.databits_combobox.setCurrentText("8")
        form_layout.addRow("Select Data Bits:", self.databits_combobox)

        # Parity 설정
        self.parity_combobox = QComboBox()
        self.parity_combobox.addItems(["None", "Even", "Odd", "Mark", "Space"])
        self.parity_combobox.setCurrentText("None")
        form_layout.addRow("Select Parity:", self.parity_combobox)

        # Stop Bits 설정
        self.stopbits_combobox = QComboBox()
        self.stopbits_combobox.addItems(["1", "1.5", "2"])
        self.stopbits_combobox.setCurrentText("1")
        form_layout.addRow("Select Stop Bits:", self.stopbits_combobox)

        # 연결 및 연결 해제 버튼 레이아웃
        button_layout = QHBoxLayout()
        self.connect_button = QPushButton("Connect")
        self.connect_button.clicked.connect(self.connect_serial)
        button_layout.addWidget(self.connect_button)

        self.disconnect_button = QPushButton("Disconnect")
        self.disconnect_button.clicked.connect(self.disconnect_serial)
        button_layout.addWidget(self.disconnect_button)
        self.disconnect_button.setEnabled(False)  # 초기에는 비활성화

        form_layout.addRow(button_layout)

        # 포트 상태 표시 라벨
        self.status_label = QLabel("Port Status: Disconnected")
        form_layout.addRow("Status:", self.status_label)

        serial_groupbox.setLayout(form_layout)
        main_layout.addWidget(serial_groupbox)

        # 수신 데이터 표시
        self.data_label = QLabel("Received Data:")
        main_layout.addWidget(self.data_label)

        self.received_data_text = QTextEdit()
        self.received_data_text.setReadOnly(True)
        self.received_data_text.setMinimumHeight(200)  # 수신 데이터 창을 더 크게 설정
        main_layout.addWidget(self.received_data_text)

        # 데이터 창 지우기, 저장, 불러오기 버튼 레이아웃
        data_control_layout = QHBoxLayout()
        clear_button = QPushButton("Clear")
        clear_button.setObjectName("clearButton")
        clear_button.clicked.connect(self.clear_received_data)
        data_control_layout.addWidget(clear_button)

        save_button = QPushButton("Save")
        save_button.setObjectName("saveButton")
        save_button.clicked.connect(self.save_received_data)
        data_control_layout.addWidget(save_button)

        load_button = QPushButton("Load")
        load_button.setObjectName("loadButton")
        load_button.clicked.connect(self.load_data_from_file)
        data_control_layout.addWidget(load_button)

        main_layout.addLayout(data_control_layout)

        # 전송 데이터 입력
        self.input_label = QLabel("Send Data:")
        main_layout.addWidget(self.input_label)

        self.input_line_edit = QLineEdit()
        main_layout.addWidget(self.input_line_edit)

        # 제어문자 선택 레이아웃 (앞과 뒤)
        control_layout = QHBoxLayout()
        self.control_combobox_start = QComboBox()
        self.control_combobox_start.addItems(["None", "STX", "ETX", "ACK", "NAK", "EOT"])
        control_layout.addWidget(QLabel("Start Control:"))
        control_layout.addWidget(self.control_combobox_start)

        self.control_combobox_end = QComboBox()
        self.control_combobox_end.addItems(["None", "STX", "ETX", "ACK", "NAK", "EOT"])
        control_layout.addWidget(QLabel("End Control:"))
        control_layout.addWidget(self.control_combobox_end)

        main_layout.addLayout(control_layout)

        # 전송 버튼
        self.send_button = QPushButton("Send")
        self.send_button.clicked.connect(self.send_data_with_control)
        self.send_button.setEnabled(False)  # 초기에는 비활성화
        main_layout.addWidget(self.send_button)

        self.setLayout(main_layout)
        self.setWindowTitle('RS-232 Serial Communication')
        self.setGeometry(300, 300, 500, 600)

    def update_ports(self):
        ports = serial.tools.list_ports.comports()
        self.port_combobox.clear()
        for port in ports:
            self.port_combobox.addItem(port.device)

    def connect_serial(self):
        port = self.port_combobox.currentText()
        baudrate = int(self.baudrate_combobox.currentText())
        databits = int(self.databits_combobox.currentText())

        parity_dict = {
            "None": serial.PARITY_NONE,
            "Even": serial.PARITY_EVEN,
            "Odd": serial.PARITY_ODD,
            "Mark": serial.PARITY_MARK,
            "Space": serial.PARITY_SPACE,
        }
        parity = parity_dict[self.parity_combobox.currentText()]

        stopbits_dict = {
            "1": serial.STOPBITS_ONE,
            "1.5": serial.STOPBITS_ONE_POINT_FIVE,
            "2": serial.STOPBITS_TWO,
        }
        stopbits = stopbits_dict[self.stopbits_combobox.currentText()]

        try:
            self.serial_port = serial.Serial(
                port=port,
                baudrate=baudrate,
                bytesize=databits,
                parity=parity,
                stopbits=stopbits,
                timeout=1
            )
            self.serial_port.flush()
            self.status_label.setText(f"Port Status: Connected to {port} at {baudrate} baud.")
            self.connect_button.setEnabled(False)
            self.disconnect_button.setEnabled(True)
            self.send_button.setEnabled(True)
            QMessageBox.information(self, "Success", f"Connected to {port} at {baudrate} baud.")
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to connect: {str(e)}")

        self.serial_port.timeout = 0.1  # Short timeout for non-blocking read
        self.read_serial_data()

    def disconnect_serial(self):
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()
            self.status_label.setText("Port Status: Disconnected")
            self.connect_button.setEnabled(True)
            self.disconnect_button.setEnabled(False)
            self.send_button.setEnabled(False)
            QMessageBox.information(self, "Disconnected", "Serial port disconnected successfully.")

    def read_serial_data(self):
        if self.serial_port and self.serial_port.is_open:
            try:
                data = self.serial_port.read(1024)
                if data:
                    ascii_data = data.decode(errors='ignore')
                    hex_data = ' '.join(f'{x:02x}' for x in data)
                    self.received_data_text.append(f"ASCII: {ascii_data}\nHEX: {hex_data}\n")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Error reading from serial port: {str(e)}")

        # Keep checking for data
        if self.serial_port and self.serial_port.is_open:
            QTimer.singleShot(100, self.read_serial_data)

    def send_data_with_control(self):
        if self.serial_port and self.serial_port.is_open:
            data = self.input_line_edit.text()

            control_char_dict = {
                "None": b'',
                "STX": b'\x02',
                "ETX": b'\x03',
                "ACK": b'\x06',
                "NAK": b'\x15',
                "EOT": b'\x04',
            }
            start_control = control_char_dict[self.control_combobox_start.currentText()]
            end_control = control_char_dict[self.control_combobox_end.currentText()]

            full_message = start_control + data.encode() + end_control

            try:
                self.serial_port.write(full_message)
                self.received_data_text.append(f"Sent: {self.control_combobox_start.currentText()} {data} {self.control_combobox_end.currentText()}\n")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to send data: {str(e)}")
        else:
            QMessageBox.warning(self, "Warning", "Not connected to any serial port.")

    def clear_received_data(self):
        self.received_data_text.clear()

    def save_received_data(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getSaveFileName(self, "Save Received Data", "", "Text Files (*.txt);;All Files (*)", options=options)
        if file_path:
            if not file_path.endswith('.txt'):
                file_path += '.txt'  # .txt 확장자를 자동으로 추가
            try:
                with open(file_path, 'w') as file:
                    file.write(self.received_data_text.toPlainText())
                QMessageBox.information(self, "Saved", f"Data successfully saved to {file_path}.")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to save data: {str(e)}")

    def load_data_from_file(self):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_path, _ = QFileDialog.getOpenFileName(self, "Load Data", "", "Text Files (*.txt);;All Files (*)", options=options)
        if file_path:
            try:
                with open(file_path, 'r') as file:
                    data = file.read()
                    self.received_data_text.setPlainText(data)
                QMessageBox.information(self, "Loaded", f"Data successfully loaded from {file_path}.")
            except Exception as e:
                QMessageBox.critical(self, "Error", f"Failed to load data: {str(e)}")

    def closeEvent(self, event):
        if self.serial_port and self.serial_port.is_open:
            self.serial_port.close()

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = SerialApp()
    ex.show()
    sys.exit(app.exec())

이 프로그램은 직관적인 GUI와 PySide6를 사용하여 시리얼 통신을 쉽게 제어할 수 있도록 설계되었습니다. 여러 포트를 관리하고 데이터를 송수신하며, 이를 텍스트 파일로 저장하거나 불러오는 기능도 있어 사용성 측면에서 좋습니다. 몇 가지 잠재적인 성능 문제와 리소스 관리 측면에서의 개선이 가능하며, 사용자 입력 검증과 더 세밀한 오류 처리가 필요할 수 있습니다.

실행 화면으로 PORT 및 PROTOCOL 선택하고 Connect 버튼을 누르고 Send Data에 입력하고 Send를 

누르면 PORT로 데이터가 전송되고 데이터가 수신되면 Received Data에 표시된다.

728x90
반응형