ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 파이썬으로 작성한 SERIAL 통신 프로그램
    PYTHON(파이썬)/파이썬 활용 2024. 10. 31. 07:27
    728x90
    반응형

    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
Designed by Tistory.