1. 목표

더보기

(1) 학습 목표

  • PySide6 기반 응용 프로그램 구현
  • 시리얼 통신을 이해한다.
  • GUI 이벤트 → 시리얼 송신 → 아두이노 동작 → 수신 처리 구조를 이해하고 구현한다.
    • 앱에서 버튼 클릭으로 Arduino LED를 ON / OFF 제어할 수 있다.
    • TX / RX 로그를 통해 통신 흐름을 이해할 수 있다.
    • QThread를 사용해 시리얼 수신을 비동기로 처리할 수 있다.

 

 

 

 

(2) 학습 단계

 

2. 아두이노 스케치 구현 

: 교재 P.110 ~ 143

더보기

(1) 아두이노 동작 구조

 

 

(2) 아두이노 측 LED 회로 선택

 

  • 내장 LED 사용
    • 13번 핀에 이미 저항 포함 LED 연결
    • 추가 배선 없이 바로 사용 가능
  • 외부 LED 사용
    • LED + 220옴 저항을 핀(예: 13)과 GND에 연결
    • 회로 학습 및 확장에 적합
  • 주의사항
    • 외부 LED를 13번 핀에 연결하면
      내장 LED와 외부 LED가 동시에 동작
          ┌─[저항]─▶ 내장 LED
핀 13 ────┤
          └─[저항]─▶ 외부 LED ── GND

 

 

(3) 아두이노 스케치 코드

// Arduino: LED control + Echo
// - '1' 수신: LED ON
// - '0' 수신: LED OFF
// - 그 외 문자열: 그대로 echo 출력

const int LED_PIN = 13;

void setup() {
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW);

  Serial.begin(9600);
  while (!Serial) { ; }
  Serial.println("READY");
}

void loop() {
  if (Serial.available() > 0) {
    String msg = Serial.readStringUntil('\n');
    msg.trim();

    if (msg == "1") {
      digitalWrite(LED_PIN, HIGH);
      Serial.println("LED:ON");
    } else if (msg == "0") {
      digitalWrite(LED_PIN, LOW);
      Serial.println("LED:OFF");
    } else {
      Serial.print("ECHO:");
      Serial.println(msg);
    }
  }
}

 

 

(4) 시리얼 모니터 테스트

 

3. PySide6 앱 구현

더보기

(1) 구현 목표

Arduino IDE 의 시리얼 통신이 아닌, 내가 만든 프로그램에서 아두이노 장치를 제어합니다.

 

 

(2) 소스 코드    

 

Step 1. GUI + LED 제어

Step 2. 포트

   └─ 포트(Port) 개념

Step 3. 문자열 송신/수신

   └─ TX & RX 개념

Step 4. QTimer 수신 처리

Step 5. QThread 비동기 수신

   └─QThread 수신

Step 6. 최종단계 

import sys
import time
from dataclasses import dataclass

import serial
from serial.tools import list_ports

from PySide6.QtCore import QThread, Signal, Qt
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QFormLayout,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QMainWindow,
    QMessageBox,
    QPushButton,
    QTextEdit,
    QVBoxLayout,
    QWidget,
)


@dataclass
class SerialConfig:
    port: str
    baudrate: int


class SerialReaderThread(QThread):
    received = Signal(str)
    status = Signal(str)

    def __init__(self, ser: serial.Serial):
        super().__init__()
        self._ser = ser
        self._running = True

    def stop(self):
        self._running = False

    def run(self):
        self.status.emit("RX thread started")
        try:
            while self._running:
                if self._ser is None or not self._ser.is_open:
                    time.sleep(0.05)
                    continue

                try:
                    # 한 줄 단위 수신(아두이노 println 기준)
                    line = self._ser.readline()
                    if line:
                        text = line.decode(errors="replace").rstrip("\r\n")
                        self.received.emit(text)
                    else:
                        time.sleep(0.01)
                except serial.SerialException as e:
                    self.status.emit(f"SerialException: {e}")
                    time.sleep(0.2)
                except Exception as e:
                    self.status.emit(f"RX error: {e}")
                    time.sleep(0.2)
        finally:
            self.status.emit("RX thread stopped")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Arduino Serial Controller")
        self.resize(700, 450)

        self.ser: serial.Serial | None = None
        self.rx_thread: SerialReaderThread | None = None
        self.led_on = False

        # UI
        self.combo_port = QComboBox()
        self.btn_refresh = QPushButton("Refresh")

        self.combo_baud = QComboBox()
        self.combo_baud.addItems(["9600", "115200"])

        self.btn_open = QPushButton("Open")
        self.btn_close = QPushButton("Close")
        self.btn_close.setEnabled(False)

        self.edit_send = QLineEdit()
        self.edit_send.setPlaceholderText("Send text (Enter to send)")
        self.btn_send = QPushButton("Send")
        self.btn_send.setEnabled(False)

        self.btn_led = QPushButton("LED: OFF")
        self.btn_led.setEnabled(False)

        self.log = QTextEdit()
        self.log.setReadOnly(True)

        # Layout
        top_row = QHBoxLayout()
        top_row.addWidget(QLabel("Port"))
        top_row.addWidget(self.combo_port, 1)
        top_row.addWidget(self.btn_refresh)

        form = QFormLayout()
        form.addRow("Baudrate", self.combo_baud)

        btn_row = QHBoxLayout()
        btn_row.addWidget(self.btn_open)
        btn_row.addWidget(self.btn_close)
        btn_row.addStretch(1)
        btn_row.addWidget(self.btn_led)

        send_row = QHBoxLayout()
        send_row.addWidget(self.edit_send, 1)
        send_row.addWidget(self.btn_send)

        root = QVBoxLayout()
        root.addLayout(top_row)
        root.addLayout(form)
        root.addLayout(btn_row)
        root.addLayout(send_row)
        root.addWidget(self.log, 1)

        central = QWidget()
        central.setLayout(root)
        self.setCentralWidget(central)

        # Signals
        self.btn_refresh.clicked.connect(self.refresh_ports)
        self.btn_open.clicked.connect(self.open_serial)
        self.btn_close.clicked.connect(self.close_serial)
        self.btn_send.clicked.connect(self.send_text)
        self.edit_send.returnPressed.connect(self.send_text)
        self.btn_led.clicked.connect(self.toggle_led)

        # Init
        self.refresh_ports()

    def append_log(self, text: str):
        self.log.append(text)

    def refresh_ports(self):
        self.combo_port.clear()
        ports = list_ports.comports()
        for p in ports:
            # p.device: "COM3" or "/dev/ttyUSB0"
            # p.description: 장치 설명
            self.combo_port.addItem(f"{p.device} - {p.description}", p.device)

        if self.combo_port.count() == 0:
            self.combo_port.addItem("(no ports)", "")
        self.append_log("Ports refreshed")

    def get_config(self) -> SerialConfig:
        port = self.combo_port.currentData()
        baud = int(self.combo_baud.currentText())
        return SerialConfig(port=port, baudrate=baud)

    def open_serial(self):
        cfg = self.get_config()
        if not cfg.port:
            QMessageBox.warning(self, "Warning", "No serial port selected.")
            return

        try:
            self.ser = serial.Serial(
                port=cfg.port,
                baudrate=cfg.baudrate,
                timeout=0.1,
                write_timeout=0.5,
            )
        except Exception as e:
            QMessageBox.critical(self, "Open failed", str(e))
            self.ser = None
            return

        self.append_log(f"Opened: {cfg.port} @ {cfg.baudrate}")

        # RX thread start
        self.rx_thread = SerialReaderThread(self.ser)
        self.rx_thread.received.connect(lambda s: self.append_log(f"RX << {s}"))
        self.rx_thread.status.connect(lambda s: self.append_log(f"[SYS] {s}"))
        self.rx_thread.start()

        # UI state
        self.btn_open.setEnabled(False)
        self.btn_close.setEnabled(True)
        self.btn_send.setEnabled(True)
        self.btn_led.setEnabled(True)
        self.combo_port.setEnabled(False)
        self.combo_baud.setEnabled(False)
        self.btn_refresh.setEnabled(False)

    def close_serial(self):
        # stop thread
        if self.rx_thread is not None:
            self.rx_thread.stop()
            self.rx_thread.wait(800)
            self.rx_thread = None

        # close serial
        if self.ser is not None:
            try:
                if self.ser.is_open:
                    self.ser.close()
            except Exception:
                pass
            self.ser = None

        self.append_log("Closed")

        # UI state
        self.btn_open.setEnabled(True)
        self.btn_close.setEnabled(False)
        self.btn_send.setEnabled(False)
        self.btn_led.setEnabled(False)
        self.combo_port.setEnabled(True)
        self.combo_baud.setEnabled(True)
        self.btn_refresh.setEnabled(True)

    def write_line(self, text: str):
        if self.ser is None or not self.ser.is_open:
            self.append_log("[ERR] Serial not open")
            return

        try:
            data = (text + "\n").encode("utf-8")
            self.ser.write(data)
            self.ser.flush()
            self.append_log(f"TX >> {text}")
        except Exception as e:
            self.append_log(f"[ERR] TX failed: {e}")

    def send_text(self):
        text = self.edit_send.text().strip()
        if not text:
            return
        self.write_line(text)
        self.edit_send.clear()

    def toggle_led(self):
        self.led_on = not self.led_on
        self.write_line("1" if self.led_on else "0")
        self.btn_led.setText("LED: ON" if self.led_on else "LED: OFF")

    def closeEvent(self, event):
        self.close_serial()
        super().closeEvent(event)


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

 

4. 테스트

 

실습 과제 (확장)

더보기

기본 과제

  • LED ON/OFF 버튼 구현
  • TX/RX 로그 구분 출력

확장 과제

  • 가변저항 값 수신 후 QLabel 표시
  • 슬라이더로 PWM 제어
  • 센서 값에 따른 UI 색상 변경
  • CSV 로그 저장

다음 단계 제안

  • "센서 데이터 실시간 시각화"
  • "멀티 장치 제어 구조"
  • "프로토콜 기반 명령 설계(CMD:VALUE)"
  • "IoT 교육과정용 프로젝트 확장"