[Python] pySerial로 RS-232 통신하기: COM Port 검색·Hex 송수신·패킷 로그
Python pySerial로 RS-232 통신하기: COM Port 검색·Hex 송수신·패킷 로그#
1장 Python으로 RS-232 장비와 통신하려면#
RS-232 Cable로 장비와 PC를 연결했다고 가정해보겠습니다.
물리적인 연결은 다음처럼 단순해 보입니다.
Python Application
│
▼
Serial Port
│
▼
COM Port
│
▼
RS-232 Cable
│
▼
Device하지만 프로그램이 장비와 정상적으로 통신하려면 여러 조건이 동시에 맞아야 합니다.
올바른 COM Port
Baud Rate
Data Bits
Parity
Stop Bits
Flow Control
장비 Protocol
Packet 구조하나라도 다르면 화면에서는 단순히:
응답 없음또는:
알 수 없는 데이터 수신으로 보일 수 있습니다.
따라서 Python Serial 프로그램에서는 다음 흐름을 차례대로 확인하는 것이 중요합니다.
COM Port 탐색
↓
Port 설정
↓
Port Open
↓
Raw byte 송신
↓
Raw byte 수신
↓
Receive Buffer
↓
Packet Parser
↓
로그·업무 처리2장 pySerial은 무엇인가#
Python에서 Serial Port를 제어할 때 널리 사용하는 Library가 pySerial입니다.
기본적으로 다음과 같은 기능을 제공합니다.
Serial Port Open / Close
Baud Rate 설정
Data Bits 설정
Parity 설정
Stop Bits 설정
Read / Write Timeout
Software Flow Control
Hardware Flow Control
byte Read / Write
COM Port 검색설치:
pip install pyserialImport:
import serialPort 검색에는 다음 Module을 사용할 수 있습니다.
import serial.tools.list_ports장비 통신 프로그램에서는 문자열보다 byte 중심으로 설계하는 것이 중요합니다.
RS-232는 문자 통신만을 의미하지 않으며, 실제 산업 장비에서는 Binary Protocol도 흔하게 사용하기 때문입니다.
3장 COM Port부터 정확하게 찾는다#
Windows에서는 Serial Port가 일반적으로 다음과 같이 나타납니다.
COM1
COM3
COM7
COM12Linux에서는 다음과 같은 Device 이름을 볼 수 있습니다.
/dev/ttyS0
/dev/ttyUSB0
/dev/ttyACM0pySerial을 사용하면 현재 인식된 Port를 검색할 수 있습니다.
import serial.tools.list_ports
ports = serial.tools.list_ports.comports()
for port in ports:
print(
port.device,
port.description,
port.hwid
)출력 예:
COM3 USB Serial Port USB VID:PID=0403:6001
COM5 Communications Port
COM7 USB-RS232 Adapter USB VID:PID=067B:2303여기에서 중요한 것은:
COM7이 존재한다.와:
COM7이 내가 찾는 장비다.는 다른 문제라는 것입니다.
USB-RS232 Adapter가 여러 개라면#
COM 번호만 믿으면 장비를 잘못 선택할 수 있습니다.
가능하면 다음 정보도 함께 기록합니다.
Port 이름
Description
VID / PID
Serial Number
USB 위치
연결 장비명현장용 프로그램이라면 프로그램 시작 시 Port 목록을 보여주고 사용자가 선택하도록 하는 것도 좋은 방법입니다.
4장 Serial 통신 설정을 장비와 맞춘다#
장비 Manual에 다음과 같이 적혀 있다고 하겠습니다.
9600-8-N-1의미는:
Baud Rate
9600
Data Bits
8
Parity
None
Stop Bits
1입니다.
pySerial에서는 다음과 같이 설정할 수 있습니다.
import serial
ser = serial.Serial(
port='COM3',
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1,
write_timeout=1
)Flow Control이 필요한 경우 추가할 수 있습니다.
ser = serial.Serial(
port='COM3',
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1,
xonxoff=False,
rtscts=True
)여기서 중요한 원칙은:
통신이 안 된다고 임의로 Baud Rate나 Parity를 바꾸는 것이 아니라 장비 Protocol 문서를 먼저 확인하는 것입니다.
Baud Rate가 틀리면#
장비:
9600프로그램:
19200이면 다음과 같은 현상이 나타날 수 있습니다.
깨진 byte
이상한 문자
Checksum 오류
완전히 응답 없음Parity와 Stop Bit도 마찬가지입니다.
5장 Hex 데이터를 송신하고 수신한다#
장비 Protocol에서는 다음과 같은 데이터를 만날 수 있습니다.
02 10 01 A0 00 03이것은 문자열 "02 10 01 A0 00 03" 자체를 보내라는 뜻이 아닙니다.
실제 byte 값:
0x02
0x10
0x01
0xA0
0x00
0x03을 보내야 하는 경우가 많습니다.
Python에서는 다음처럼 만들 수 있습니다.
request = bytes.fromhex(
'02 10 01 A0 00 03'
)
ser.write(request)수신:
data = ser.read(64)
print(data)Raw byte를 Hex로 보면 훨씬 이해하기 쉽습니다.
print(data.hex(' '))예:
02 06 01 00 07 03대문자로 보고 싶다면:
print(data.hex(' ').upper())출력:
02 06 01 00 07 03이 방식은 Binary Protocol 분석에서 매우 유용합니다.
6장 ASCII와 Binary 데이터를 구분한다#
장비가 다음 데이터를 보냈다고 하겠습니다.
48 45 4C 4C 4FASCII로 해석하면:
HELLO입니다.
Python에서는:
data = bytes.fromhex(
'48 45 4C 4C 4F'
)
print(data.decode('ascii'))처럼 볼 수 있습니다.
하지만 모든 Serial 데이터가 문자열인 것은 아닙니다.
예:
02 81 FF 00 A7 03같은 Binary Frame을 무작정:
data.decode('ascii')로 변환하면 오류가 발생하거나 의미 없는 결과가 나올 수 있습니다.
따라서 장비 통신 로그는 기본적으로:
HEX를 기준으로 기록하고, 사람이 읽을 수 있는 데이터만 선택적으로 ASCII 등으로 표현하는 방식이 좋습니다.
예:
print("HEX :", data.hex(' ').upper())
print(
"ASCII:",
''.join(
chr(b)
if 32 <= b <= 126
else '.'
for b in data
)
)출력 예:
HEX : 02 48 45 4C 4C 4F 03
ASCII: .HELLO.이렇게 하면 Binary와 Text를 동시에 확인할 수 있습니다.
7장 read 한 번이 Packet 하나라는 보장은 없다#
Serial 통신 구현에서 가장 중요한 부분 중 하나입니다.
장비가 다음 Packet을 보냈다고 하겠습니다.
02 10 04 41 42 43 44 8A 03Python에서:
ser.read(64)를 호출했다고 해서 항상 전체 Packet이 한 번에 반환되는 것은 아닙니다.
첫 번째 Read:
02 10 04두 번째:
41 42 43세 번째:
44 8A 03처럼 나뉠 수도 있습니다.
반대로:
PACKET A
PACKET B
PACKET C가 한 번에 들어올 수도 있습니다.
따라서:
read()
=
Packet 하나라고 가정해서는 안 됩니다.
Receive Buffer가 필요한 이유#
기본 구조는 다음과 같습니다.
Serial Port
↓
read()
↓
Receive Buffer
↓
Packet 경계 검색
↓
완전한 Packet 추출
↓
Packet Parser예:
rx_buffer = bytearray()
while True:
data = ser.read(64)
if data:
rx_buffer.extend(data)
print(
"RX:",
data.hex(' ').upper()
)
# 여기에서 Packet 추출Buffer에 데이터를 누적한 뒤 Protocol 규칙에 맞춰 완전한 Packet만 꺼내야 합니다.
8장 Packet 경계를 어떻게 찾을까#
Packet 구조는 장비 Protocol에 따라 다릅니다.
대표적인 방식은 네 가지입니다.
STX·ETX 방식#
STX
DATA
ETX예:
02
10 20 30 40
03이 경우:
0x02부터:
0x03까지를 하나의 Frame으로 볼 수 있습니다.
Length 기반 방식#
Header
Length
Data
Checksum예:
AA 55 04 10 20 30 40 8FLength를 읽어 필요한 byte 수가 들어올 때까지 기다립니다.
Fixed Length 방식#
모든 Packet이:
16 bytes처럼 동일한 길이를 가질 수도 있습니다.
Timing 방식#
일정 시간 동안 추가 byte가 들어오지 않으면 하나의 Frame이 끝났다고 판단하는 Protocol도 있습니다.
따라서 Packet Parser는 Serial Port 코드와 분리하는 것이 좋습니다.
SerialTransport
→ Raw byte 송수신
ReceiveBuffer
→ byte 누적
PacketParser
→ Frame 추출
DeviceProtocol
→ Command·Response 해석9장 Timeout을 이해해야 프로그램이 멈추지 않는다#
pySerial의 timeout 설정은 read() 동작에 직접 영향을 줍니다.
예:
ser = serial.Serial(
'COM3',
9600,
timeout=1
)이 경우 필요한 byte가 들어오지 않으면 무한정 기다리지 않고 설정된 시간 이후 현재까지 들어온 데이터를 반환할 수 있습니다.
Timeout을 너무 짧게 하면#
정상 응답인데도 Timeout이 발생할 수 있습니다.
Timeout을 너무 길게 하면#
장애 감지가 늦어진다.따라서 장비의 정상 Response Time을 알아야 합니다.
예:
정상 Response
평균 40ms
최대 예상
200ms라면 이를 기준으로 Timeout과 Retry 정책을 설계해야 합니다.
단순히 모든 장비에:
timeout=1을 적용하는 것이 정답은 아닙니다.
Blocking 방식도 구분한다#
개념적으로:
timeout=None
→ 필요한 데이터가 올 때까지 대기
timeout=0
→ Non-blocking
timeout=지정값
→ 일정 시간까지 대기처럼 동작 방식이 달라집니다.
프로그램 구조에 맞는 방식을 선택해야 합니다.
10장 Packet Logger를 만들면 장애 분석이 쉬워진다#
장비 통신 프로그램에서는 Raw Packet Log가 매우 중요합니다.
다음 Log만 있다면:
Device error원인을 찾기 어렵습니다.
반면:
2026-09-24 10:14:31.120
PORT=COM3
CONFIG=9600-8-N-1
TX
02 10 01 A0 00 03
2026-09-24 10:14:31.184
RX
02 06 01 00 07 03처럼 기록하면 통신 흐름을 재구성할 수 있습니다.
기본 Logger 예제#
from datetime import datetime
def log_packet(direction, data):
timestamp = datetime.now().isoformat(
timespec='milliseconds'
)
print(
f"{timestamp} "
f"{direction} "
f"{data.hex(' ').upper()}"
)사용:
request = bytes.fromhex(
'02 10 01 A0 00 03'
)
ser.write(request)
log_packet(
'TX',
request
)
response = ser.read(64)
if response:
log_packet(
'RX',
response
)Log에는 가능하면 다음 정보를 포함합니다.
Timestamp
Port
Serial 설정
TX / RX
Packet Length
Hex Dump
Device ID
Timeout
Retry Count
Exception이런 정보가 있어야 현장에서:
송신 자체가 안 됐는가?
장비가 응답하지 않았는가?
응답은 왔는데 Parser가 실패했는가?
Checksum 오류인가?를 구분할 수 있습니다.
11장 예외 처리와 재연결을 준비한다#
Serial 통신은 다양한 이유로 중단될 수 있습니다.
USB Adapter 제거
Cable 단선
Device 전원 OFF
Port 점유
Driver 문제
권한 문제따라서 Port Open부터 예외 처리가 필요합니다.
import serial
try:
ser = serial.Serial(
port='COM3',
baudrate=9600,
timeout=1
)
except serial.SerialException as e:
print(
f"Serial open failed: {e}"
)재연결은 상태를 구분한다#
다음 두 상태는 다릅니다.
COM Port 열림과:
실제 Device 정상Port가 열려도 장비가 응답하지 않을 수 있습니다.
따라서 재연결은 다음처럼 설계하는 것이 좋습니다.
통신 실패
↓
Port Close
↓
대기
↓
Port 다시 검색
↓
Port Open
↓
Serial 설정 적용
↓
Test Request
↓
Response 확인
↓
Device Online무한히 빠르게 재접속을 반복하는 것보다는 Retry 간격을 둘 수 있습니다.
예:
1초
2초
4초
8초처럼 점차 간격을 증가시키는 Backoff 전략을 적용할 수도 있습니다.
12장 범용 RS-232 진단 도구로 확장해보자#
Python의 장점은 작은 Test Tool을 빠르게 만들 수 있다는 점입니다.
최소한 다음 기능을 가진 Serial Test Tool을 만들면 현장에서 상당히 유용합니다.
COM Port 검색
Port 선택
Baud Rate 설정
Parity 설정
Stop Bits 설정
Flow Control 설정
Hex 입력
Hex 송신
HEX 수신 표시
ASCII 수신 표시
Timestamp
TX / RX 구분
Log File 저장구조는 다음과 같이 나눌 수 있습니다.
SerialPortManager
│
├─ Port 검색
├─ Open
├─ Close
└─ Reconnect
SerialTransport
│
├─ Send
└─ Receive
PacketLogger
│
├─ Hex
├─ ASCII
└─ Timestamp
ReceiveBuffer
│
└─ byte 누적
PacketParser
│
├─ STX / ETX
├─ Length
├─ Checksum
└─ CRC최소 통합 예제#
import serial
import serial.tools.list_ports
from datetime import datetime
def list_ports():
for port in serial.tools.list_ports.comports():
print(
port.device,
port.description
)
def to_hex(data):
return data.hex(' ').upper()
def log_packet(direction, data):
timestamp = datetime.now().isoformat(
timespec='milliseconds'
)
print(
f"{timestamp} "
f"{direction} "
f"{to_hex(data)}"
)
def main():
list_ports()
ser = serial.Serial(
port='COM3',
baudrate=9600,
bytesize=serial.EIGHTBITS,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
timeout=1,
write_timeout=1
)
try:
request = bytes.fromhex(
'02 10 01 A0 00 03'
)
ser.write(request)
log_packet(
'TX',
request
)
response = ser.read(64)
if response:
log_packet(
'RX',
response
)
else:
print(
'Response timeout'
)
finally:
ser.close()
if __name__ == '__main__':
main()이 프로그램에서 중요한 것은 특정 Device Command가 아닙니다.
전체 흐름입니다.
Port 검색
↓
Port 설정
↓
Open
↓
Hex → byte 변환
↓
TX
↓
RX
↓
Hex Log
↓
Close이 기본 구조에 Receive Buffer와 Packet Parser를 추가하면 실제 장비 분석 도구로 확장할 수 있습니다.
13장 장애는 물리 계층부터 Protocol까지 순서대로 본다#
Python 프로그램에서 아무 데이터도 받지 못한다고 하겠습니다.
바로 Python Code를 수정하면 안 됩니다.
물리 연결#
먼저 확인합니다.
Device Power
TX / RX
GND
DTE / DCE
Cable
USB-RS232 Adapter운영체제#
그다음 확인합니다.
COM Port 존재
Driver 정상
Port 권한
다른 프로그램의 Port 점유Serial 설정#
Baud Rate
Data Bits
Parity
Stop Bits
Flow ControlRaw 데이터#
TX byte가 실제로 맞는가?
RX byte가 들어오는가?
Hex Dump가 정상인가?Protocol#
마지막으로 봅니다.
STX / ETX
Length
Address
Command
Checksum
CRC즉:
Python에서 응답이 없다.는 사실만으로 Python 코드가 원인이라고 판단하면 안 됩니다.
전체 경로는:
Python
↓
Serial Driver
↓
COM Port
↓
USB-RS232 Adapter
↓
Cable
↓
RS-232 Interface
↓
Device
↓
Device Protocol입니다.
각 경계를 하나씩 확인해야 합니다.
14장 자기 점검과 마무리#
COM Port가 여러 개라면 어떻게 장비를 찾는가#
Port 이름뿐 아니라 Description·VID/PID·USB 위치와 실제 Cable Label을 함께 확인하는 것이 좋습니다.
Hex 문자열을 실제 byte로 보내려면 어떻게 하는가#
bytes.fromhex()를 사용할 수 있습니다.
예:
data = bytes.fromhex(
'02 10 01 A0 00 03'
)read 한 번으로 Packet 하나를 처리해도 되는가#
아닙니다. 하나의 Packet이 여러 Read로 나뉘거나 여러 Packet이 한 Read에 들어올 수 있으므로 Receive Buffer와 Packet Parser가 필요합니다.
Binary Protocol을 바로 문자열로 변환해도 되는가#
권장되지 않습니다. 먼저 Raw byte와 Hex 기준으로 확인하고 Protocol이 Text 기반일 때만 적절한 Encoding으로 Decode해야 합니다.
Port가 열렸는데 장비가 응답하지 않는다면#
TX·RX·GND, Baud Rate·Parity·Stop Bits·Flow Control, 전송 Packet과 Device Protocol을 순서대로 확인해야 합니다.
이 글에서 다룬 전체 흐름을 다시 정리하면 다음과 같습니다.
COM Port 검색
↓
장비 Port 식별
↓
Baud·Data·Parity·Stop 설정
↓
Port Open
↓
Hex Data → byte
↓
TX
↓
RX
↓
Receive Buffer
↓
Packet Parser
↓
Hex·ASCII Log
↓
Application특히 다음 다섯 가지를 기억하면 됩니다.
pySerial을 이용하면 Python에서 COM Port 검색부터 RS-232 byte 송수신까지 비교적 간단하게 구현할 수 있지만, 프로그램이 짧다고 해서 장비 통신 자체가 단순한 것은 아닙니다.
Serial Protocol을 분석할 때는 문자열보다 Raw byte와 Hex Dump를 먼저 확인해야 ASCII Protocol과 Binary Protocol을 혼동하지 않을 수 있습니다.
한 번의 read()와 하나의 Packet은 같은 개념이 아니므로 Receive Buffer에서 데이터를 누적한 뒤 STX·ETX·Length 등의 규칙으로 Frame을 분리해야 합니다.
Port Open 성공과 Device 정상 통신은 별개의 상태이므로 재연결 후에는 실제 Request·Response까지 확인해야 합니다.
장비 통신 장애는 Python Code만 볼 것이 아니라 COM Port → Serial 설정 → Cable → Raw byte → Packet Protocol 순으로 경계를 나누어 추적해야 합니다.
결국 Python으로 RS-232 통신을 구현한다는 것은 단순히 ser.write()와 ser.read()를 사용하는 것이 아닙니다.
현장에서 들어오는 Raw byte를 그대로 관찰하고 기록할 수 있는 도구를 만든 뒤, 그 데이터를 Packet 단위로 복원하고 장비 Protocol의 의미로 해석할 수 있도록 전체 통신 흐름을 구성하는 것이 핵심입니다.