라즈베리파이를 시리얼 통신 중계 서버로 사용한 현장 문제 해결기
language
kor
date
Jan 7, 2026
slug
raspberry-pi-serial-bridge-workaround
author
status
Public
tags
Hardware
Error
summary
PC–아두이노 간 시리얼 통신이 3~4m 거리에서 불안정해지는 현장 문제를 해결하기 위해,
라즈베리파이를 중계 서버로 구성하고 가상 COM 포트를 활용한 우회적인 해결 방법을 정리했다.
비효율적이지만 기존 Host 애플리케이션 수정 없이 빠르게 문제를 해결할 수 있었던 사례이다.
type
Post
category
🐞TroubleShooting
updatedAt
Jan 6, 2026 05:59 PM
원래는 시스템이 다음과 같은 구조로 구성되어 있다.
PC ===== 아두이노 ===== 카메라 IO
===== 조명
PC에서 시리얼 통신으로 아두이노에서 만들어내는 신호의 주파수 및 duration 등을 컨트롤 하는 방식이다. 이게 잘 동작하다가 현장에서 설치를 하다보니 PC와 아두이노 사이의 거리가 3~4m가 되면서 문제가 생겼다. 송수신에서 이상 문자가 들어오는 것으로 보아 신호 감쇠 때문이거나 노이즈 때문인것 같은데, 케이블을 아무리 바꿔봐도 다 문제가 생겨서 곤란하게 됐다. 이를 해결하기 위해서 불가피하게 비효율적이더라도 라즈베리파이를 중간에 중계기 역할로 수행하도록 만들었다. Host PC와 라즈베리파이의 연결은 gigabit으로 거리에 관대하게 사용이 가능하다. 다만 문제는 Host PC에서 돌아가는 어플리케이션은 수정이 불가능하기 때문에 실제 통신은 gigabit 이더라도 사용 자체는 시리얼통신으로(혹은 시리얼통신처럼)해야한다는 것이다.
PC ===== 라즈베리파이 ====== 아두이노 ===== 카메라 IO
===== 조명
이를 위해 com0com이라는 가상화 프로그램을 사용했다.
PC 설정
com0com을 이용하여 두 개의 가상 포트를 묶음
hub4com --baud=9600 COM4 --use-driver=tcp 192.168.0.10:5000
COM4와 묶여있는 COM5를 이용하여 시리얼 통신 가능.
COM to TCP 바인딩 *.bat 파일 스크립트
@echo off REM --------------------------------------------- REM 1) 작업 디렉터리로 이동 REM com0com hub4com 실행 파일이 있는 경로 REM --------------------------------------------- cd /d "C:\Users\Downloads\hub4com-2.1.0.0-386\hub4com-2.1.0.0-386" REM --------------------------------------------- REM 2) hub4com 실행 REM COM4 포트(9600 bps) ↔ TCP 클라이언트 모드(192.168.0.10:5000) REM --------------------------------------------- hub4com --baud=9600 COM4 --use-driver=tcp 192.168.0.10:5000 REM --------------------------------------------- REM (선택) 창이 바로 닫히지 않도록 일시 정지 REM 필요 없으면 다음 줄을 지우세요. REM --------------------------------------------- pause
라즈베리파이 코드
#!/usr/bin/env python3 import socket import threading import serial import time # ============================ # 시리얼 포트 설정 (아두이노 측) # ============================ SERIAL_PORT = '/dev/ttyACM0' # 실제 연결된 아두이노 시리얼 포트 BAUD_RATE = 9600 # 아두이노 통신 속도 SERIAL_TIMEOUT = 1 # 시리얼 읽기 타임아웃 (초) # ============================ # TCP 서버 설정 (라즈베리파이 측) # ============================ TCP_IP = '0.0.0.0' # 모든 인터페이스에서 수신 TCP_PORT = 5000 # 클라이언트가 접속할 포트 def handle_client(conn, ser): """ 클라이언트 소켓(conn)을 통해 들어오는 바이트를 1바이트씩 읽어들여 '$' 구분자가 나올 때까지 조각을 모아 full_cmd로 간주한 뒤 시리얼로 전송합니다. 아두이노 응답은 다시 TCP 클라이언트로 송신합니다. """ peer = conn.getpeername() print(f"[+] 클라이언트 연결: {peer}") # 1) 소켓을 바이너리 모드 파일 객체로 감싸기 conn_file = conn.makefile('rb') partial_chunks = [] # '$' 나오기 전까지 받은 조각을 저장할 리스트 try: while True: # 2) 한 번에 1바이트씩 읽어서 '$'를 기다린다 byte = conn_file.read(1) if not byte: # 연결이 종료된 경우 break partial_chunks.append(byte) if byte == b'$': # 3) '$'까지 모인 조각 전체를 하나의 명령으로 합친다 full_cmd_bytes = b''.join(partial_chunks) partial_chunks.clear() # 4) UTF-8로 디코딩 (옵션: 로그 출력) try: full_cmd_str = full_cmd_bytes.decode('utf-8', errors='replace') except Exception: full_cmd_str = '' print(f"[완전수신] {full_cmd_str.strip()}") # 5) 시리얼 포트로 한 번에 전송 ser.write(full_cmd_bytes) # 6) 아두이노 응답을 잠시 대기 후 읽어서 TCP로 송신 time.sleep(0.1) bytes_to_read = ser.in_waiting if bytes_to_read: response_bytes = ser.read(bytes_to_read) conn.sendall(response_bytes) else: # 응답이 없으면 넘어감 pass except Exception as e: print(f"[!] 클라이언트 처리 중 예외 발생 ({peer}): {e}") finally: conn_file.close() conn.close() print(f"[-] 클라이언트 연결 종료: {peer}") def main(): # 1) 아두이노 시리얼 포트 열기 ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=SERIAL_TIMEOUT) # 일부 아두이노 보드는 시리얼 포트 오픈 후 안정화 대기가 필요합니다. time.sleep(2) print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] 시리얼 포트 열림: {SERIAL_PORT} @ {BAUD_RATE}bps") # 2) TCP 소켓 서버 생성 srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((TCP_IP, TCP_PORT)) srv.listen(1) print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] TCP 서버 시작: {TCP_IP}:{TCP_PORT} - 클라이언트 접속 대기 중...") try: while True: conn, addr = srv.accept() # 클라이언트가 연결되면 별도 스레드에서 처리 client_thread = threading.Thread(target=handle_client, args=(conn, ser), daemon=True) client_thread.start() except KeyboardInterrupt: print("\n[!] 서버를 종료합니다...") finally: srv.close() ser.close() print("[*] 시리얼 연결 및 소켓 서버 종료") if __name__ == '__main__': main()
비효율적이지만, 급하게 문제를 해결하는 용도로는 사용할만하다.
Related posts
FNIRSI 1014D 오실로스코프 디스플레이 수리
Nov 15, 2025
깨져버린 FNIRSI 1014D 오실로스코프 디스플레이를 고쳐보자
MobaXterm ssh 사용 시 troubleshooting
Nov 6, 2025
mobaxterm에서 ssh 사용 시 발생하는 /etc/ssh_config line 1: Missing argument 에러에 대한 내용
AI와 만든 첫 PCB 샘플, 설계보다 어려웠던 것은
Aug 31, 2026
회사 업무용 보조 보드를 처음 설계·발주해 PCBA로 받아본 기록이다. AI가 회로와 생산 준비를 빠르게 밀어줬지만, 커넥터 수급과 실물 크기·배치 검증은 결국 직접 해결해야 했다.
