1. 목표

더보기

(1) 전체 시스템 구조와 학습 목표

디지털 입력 센서(PIR)를 Arduino에 연결하고,
PySide6 GUI에서 시리얼 통신을 통해 센서 상태를 실시간으로 수신하는 구조를 구현합니다.

  • PySide6 GUI에서 Arduino와 시리얼 통신을 연결할 수 있다.
  • QThread를 사용해 센서 데이터 수신을 비동기로 처리할 수 있다.
  • TX / RX 로그를 통해 통신 흐름을 이해할 수 있다.
  • PIR 센서의 디지털 입력(HIGH / LOW)을 GUI에서 확인할 수 있다.
  • 센서 입력 → Arduino 처리 → 시리얼 송신 → PySide6 수신 구조를 구현한다.

 

 

 

 

(2) 최종 결과물

  • PySide6 GUI 프로그램
  • PIR 동작 감지 센서 상태 표시
  • Arduino에서 읽은 디지털 입력값(0 / 1) 실시간 수신
  • TX / RX 로그가 출력되는 시리얼 모니터 형태의 프로그램

 

2. 아두이노 스케치 구현 (디지털 입력 – PIR 센서)

: 교재 P.172~ 176

더보기

(1) 아두이노 동작 구조

PIR

PIR 센서는 움직임이 감지되면 HIGH,
움직임이 없으면 LOW 값을 출력하는 디지털 입력 센서입니다.

 

  1. PIR 센서 상태를 디지털 핀에서 읽음
  2. 읽은 값을 시리얼로 전송
  3. 센서 상태에 따라 LED 출력 제어

 

 

(2) 아두이노 측 회로

 

PIR 센서 기본 연결 (Arduino Uno 기준)


PIR 센서 핀 Arduino 연결
VCC 5V
GND GND
OUT 디지털 7번 핀

 

LED 연결


항목 연결
LED 디지털 8번 핀
저항 220Ω
GND 공통 GND

PIR 센서의 OUT 핀은 디지털 입력으로 사용합니다.

 

 

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

void setup()
{
  Serial.begin(9600);             // 시리얼 통신 초기화

  pinMode(8, OUTPUT);             // LED 출력 핀
  pinMode(7, INPUT);              // PIR 센서 입력 핀
}

void loop()
{
  int readValue = digitalRead(7); // PIR 센서 값 읽기
  Serial.println(readValue);      // 시리얼로 값 전송

  if(readValue == HIGH) {         // 움직임 감지
    digitalWrite(8, HIGH);        // LED ON
  }
  else {
    digitalWrite(8, LOW);         // LED OFF
  }
}

 

 

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

 

Arduino IDE의 시리얼 모니터에서 다음을 확인합니다.

  • 사람이 움직일 때 → 1 출력
  • 움직임이 없을 때 → 0 출력
  • LED가 센서 상태에 따라 자동 ON / OFF

이 단계는 PySide6 연동 전 반드시 확인해야 합니다.

 

 

3. PySide6 GUI 구현

더보기

(1) GUI 구현 목표

Arduino IDE의 시리얼 모니터가 아닌,
내가 만든 PySide6 프로그램에서 센서 상태를 확인하는 것이 목표입니다.

  • 센서 값(0 / 1)을 GUI에서 실시간 확인
  • 수신 데이터가 있어도 GUI가 멈추지 않도록 QThread 사용

 

 

(2) 준비사항

pip install pyside6 pyserial

 

 

(3) PySide6 GUI 전체 코드 (pir_serial_gui.py)


클래스 역할
SerialConfig 포트/보드레이트 설정
SerialReaderThread 시리얼 수신 전용 스레드
MainWindow GUI 및 전체 제어
import sys
import time
from dataclasses import dataclass

import serial
from serial.tools import list_ports

from PySide6.QtCore import QThread, Signal
from PySide6.QtWidgets import (
    QApplication,
    QComboBox,
    QFormLayout,
    QHBoxLayout,
    QLabel,
    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("[SYS] RX thread started")
        try:
            while self._running:
                if self._ser is None or not self._ser.is_open:
                    time.sleep(0.05)
                    continue

                try:
                    line = self._ser.readline()  # Arduino println 기준 한 줄 수신
                    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"[SYS] SerialException: {e}")
                    time.sleep(0.2)
                except Exception as e:
                    self.status.emit(f"[SYS] RX error: {e}")
                    time.sleep(0.2)
        finally:
            self.status.emit("[SYS] RX thread stopped")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("PySide6 Arduino Serial Controller (PIR)")
        self.resize(760, 520)

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

        # 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.lbl_pir = QLabel("PIR 상태: (연결 안됨)")
        self.lbl_pir.setStyleSheet("font-weight: 700; padding: 6px;")

        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.lbl_pir, 0)

        root = QVBoxLayout()
        root.addLayout(top_row)
        root.addLayout(form)
        root.addLayout(btn_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)

        # Init
        self.refresh_ports()
        self.append_log("[INFO] Ready. Select port and baudrate, then Open.")

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

    def set_pir_state(self, detected: bool):
        if detected:
            self.lbl_pir.setText("PIR 상태: 감지됨")
            self.lbl_pir.setStyleSheet(
                "font-weight: 800; padding: 6px; border-radius: 6px;"
                "background: #ffe7e7; color: #8a0000;"
            )
        else:
            self.lbl_pir.setText("PIR 상태: 대기 중")
            self.lbl_pir.setStyleSheet(
                "font-weight: 800; padding: 6px; border-radius: 6px;"
                "background: #eef3ff; color: #0b2a6a;"
            )

    def refresh_ports(self):
        self.combo_port.clear()
        ports = list_ports.comports()
        for p in ports:
            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("[INFO] 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"[INFO] Opened: {cfg.port} @ {cfg.baudrate}")
        self.set_pir_state(False)  # 기본 표시

        # RX thread
        self.rx_thread = SerialReaderThread(self.ser)
        self.rx_thread.received.connect(self.on_rx_line)
        self.rx_thread.status.connect(self.append_log)
        self.rx_thread.start()

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

    def on_rx_line(self, line: str):
        # Arduino가 0/1을 println으로 계속 보낸다는 전제
        self.append_log(f"RX << {line}")

        if line.strip() == "1":
            self.set_pir_state(True)
        elif line.strip() == "0":
            self.set_pir_state(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("[INFO] Closed")
        self.lbl_pir.setText("PIR 상태: (연결 안됨)")
        self.lbl_pir.setStyleSheet("font-weight: 700; padding: 6px;")

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

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



ㄴ 3.1 RX (수신) 구현 - QThread 사용

더보기


왜 QThread가 필요한가?

  • 시리얼 수신은 블로킹 작업
  • GUI 응답성 유지 필요

 

학습 포인트

  • 스레드 → UI 직접 접근 금지
  • Signal / Slot 구조 필수

ㄴ 3.2 TX & RX

더보기

(1) TR & TX 기본 의미

TX / RX는 시리얼 통신, 네트워크, 임베디드 전반에서 사용되는 데이터 송수신 방향 표기입니다.

 

 

(2) 용어 개념

용어 약어 의미 설명
TX Transmit 송신 장치에서 밖으로 내보내는 데이터
RX Receive 수신 장치로 들어오는 데이터
  • TX: 내가 보낸다
  • RX: 내가 받는다
  • TX/RX는 절대적 개념이 아니다
    •  항상 "누구 기준인지" 먼저 생각한다
    • 기준 장치가 바뀌면 의미가 바뀜
  • 핀 이름과 데이터 흐름을 혼동
    • TX 핀은 "여기서 나간다"
    • TX >>   /   RX <<  처럼 방향을 명확히 표기하는 것이 좋음

 

4. 테스트

더보기

테스트 절차

  1. Arduino 연결
  2. PySide6 프로그램 실행
  3. 포트 / 보드레이트 선택
  4. Open 클릭
  5. PIR 센서 앞에서 움직임 발생
  6. GUI 로그에 RX << 1 또는 RX << 0 확인

 

실습 과제 (확장)

더보기

기본 과제

  • PIR 센서 값 수신 확인
  • RX 로그 구분 출력

 

확장 과제

  • PIR 센서 감지 시 QLabel 색상 변경
  • 감지 횟수 카운트 표시
  • 감지 시간 기록 로그 추가
  • CSV 파일로 센서 로그 저장

 

다음 단계 제안

  • 센서 데이터 실시간 시각화
  • 멀티 센서 입력 구조
  • 프로토콜 기반 메시지 설계 (SENSOR:PIR:1)
  • IoT 교육과정용 프로젝트 확장