01. 디지털 출력 제어

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
핀 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 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 교육과정용 프로젝트 확장"
