PLC 데이터를 엑셀에 손으로 옮기고 있다면, 이 글이 그 작업을 끝내줄 수 있습니다.
저도 예전에 현장에서 매일 아침 PLC 화면 보면서 수기로 데이터 옮겼습니다. "이걸 자동으로 못 하나?" 싶어서 Python으로 직접 연결해봤는데, 생각보다 간단했습니다. 한번 만들어 놓으니까 매일 30분씩 아끼더라고요.
개념이 궁금하시면 이전 글을 먼저 보세요.
→ LS PLC 통신 방법 비교 – Modbus TCP vs XGT 전용 프로토콜
여기서는 실제 코드를 다룹니다.
둘 다 실제 PLC에 연결해서 테스트 완료한 코드입니다.
이 글에서 다루는 내용

준비물
| 항목 | XGT FEnet | Modbus TCP |
|---|---|---|
| PLC | LS XGK/XGB/XGI + FEnet 모듈 (또는 내장 이더넷) | LS XGK/XGB + Modbus 서버 설정 |
| PC | Python 3.8 이상 | Python 3.8 이상 + pymodbus |
| 네트워크 | 같은 서브넷 (이더넷 케이블) | 같은 서브넷 (이더넷 케이블) |
| PLC 포트 | 2004 | 502 |
공통 확인사항:
Part 1: XGT FEnet – Python으로 PLC 데이터 읽기
XGT FEnet이 좋은 이유 (다시 한번)
별도 라이브러리 설치가 필요 없습니다. 개인적으로 이게 XGT FEnet의 가장 큰 장점이라고 생각합니다. 현장 PC에 뭔가 설치하는 게 쉬운 일이 아니거든요.
Python 기본 socket과 struct만 있으면 됩니다.
PLC 쪽에서도 아무 설정 안 해도 됩니다. FEnet 모듈만 꽂혀 있으면 바로 통신 가능합니다.

위 사진이 FEnet 통신 모듈(XBL-EMTA)입니다. PLC 베이스에 장착하면 이더넷 통신이 가능해집니다.
XGT FEnet 프로토콜 구조
XGT FEnet은 TCP 소켓 위에 자체 프로토콜을 사용합니다.
모든 패킷은 20바이트 헤더 + 응용 데이터로 구성됩니다.
┌──────────────────────────────────────────┐
│ 헤더 (20 bytes) │
├──────────────────────────────────────────┤
│ Company ID : "LSIS-XGT" (8 bytes) │
│ Reserved : 0x0000 (2 bytes) │
│ PLC Info : 0x0033 (2 bytes) │
│ CPU Info : 0xA0 = XGK (1 byte) │
│ Source : 0x33 = PC (1 byte) │
│ Invoke ID : 순번 (2 bytes) │
│ Data Length : 데이터 길이 (2 bytes) │
│ Slot / Base : 0x00 (2 bytes) │
├──────────────────────────────────────────┤
│ 응용 데이터 (N bytes) │
├──────────────────────────────────────────┤
│ Command : 0x0054=읽기 (2 bytes) │
│ Data Type : 0x0000 (2 bytes) │
│ Reserved : 0x0000 (2 bytes) │
│ Block Count : 블록 수 (2 bytes) │
│ Var Name Len : 이름 길이 (2 bytes) │
│ Var Name : "%MW100" 등 (N bytes) │
│ Data Count : 읽을 개수 (2 bytes) │
└──────────────────────────────────────────┘핵심은 Var Name 부분입니다. "%MW100"처럼 PLC 메모리 주소를 문자열 그대로 보냅니다.
Modbus처럼 레지스터 번호를 쓰는 게 아니라, PLC 메모리 이름을 직접 보내는 겁니다.
XGT FEnet Python 읽기 코드
아래 함수 하나면 PLC에서 데이터를 읽을 수 있습니다.
코드가 길어 보이지만, 핵심은 헤더 조립 → 전송 → 응답 파싱 세 단계입니다.
import socket
import struct
def read_xgt(host: str, port: int, address: str, count: int = 1) -> list:
"""XGT FEnet으로 PLC 메모리 읽기Args:
host: PLC IP 주소 (예: '192.168.0.10')
port: 포트 번호 (기본 2004)
address: PLC 메모리 주소 (예: '%MW100')
count: 읽을 워드 수Returns:
읽은 값 리스트
"""
# 1) TCP 소켓 연결
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3.0)
sock.connect((host, port))# 2) 변수 이름을 ASCII 바이트로 변환
var_name = address.encode('ascii')# 3) 응용 데이터 조립
app_data = struct.pack('<H', 0x0054) # 명령: 읽기
app_data += struct.pack('<H', 0x0000) # 데이터 타입: 개별
app_data += struct.pack('<H', 0x0000) # Reserved
app_data += struct.pack('<H', 1) # 블록 수: 1
app_data += struct.pack('<H', len(var_name)) # 변수 이름 길이
app_data += var_name # 변수 이름 ("%MW100")
app_data += struct.pack('<H', count) # 읽을 개수# 4) 20바이트 헤더 조립
header = b'LSIS-XGT' # Company ID (8 bytes)
header += struct.pack('<H', 0x0000) # Reserved
header += struct.pack('<H', 0x0033) # PLC Info
header += struct.pack('<B', 0xA0) # CPU Info (XGK)
header += struct.pack('<B', 0x33) # Source (PC)
header += struct.pack('<H', 1) # Invoke ID
header += struct.pack('<H', len(app_data)) # Data Length
header += struct.pack('<B', 0x00) # Slot
header += struct.pack('<B', 0x00) # Base# 5) 전송
sock.sendall(header + app_data)# 6) 응답 수신 — 헤더 20바이트 먼저
resp_header = b''
while len(resp_header) < 20:
resp_header += sock.recv(20 - len(resp_header))# 응답 데이터 길이 추출
data_len = struct.unpack('<H', resp_header[16:18])[0]# 응답 데이터 수신
resp_data = b''
while len(resp_data) < data_len:
resp_data += sock.recv(data_len - len(resp_data))# 7) 에러 체크
error = struct.unpack('<H', resp_data[6:8])[0]
if error != 0:
sock.close()
raise RuntimeError(f"PLC 에러: 0x{error:04X}")# 8) 데이터 파싱
values = []
offset = 10 # 응답 헤더(8) + block_count(2)
data_count = struct.unpack('<H', resp_data[offset:offset + 2])[0]
offset += 2for i in range(count):
val = struct.unpack('<H', resp_data[offset:offset + 2])[0]
values.append(val)
offset += 2
sock.close()
return values
위 코드에서 가장 중요한 부분은 3번(응용 데이터 조립)입니다. "%MW100" 같은 PLC 주소를 ASCII 문자열로 직접 보냅니다.
주소만 바꾸면 어떤 메모리든 읽을 수 있습니다.
XGT FEnet 읽기 사용 예시
# %MW100부터 5개 워드 읽기
values = read_xgt('192.168.0.10', 2004, '%MW100', count=5)
print(f"MW100~104: {values}")
출력 예: MW100~104: [1234, 0, 5678, 100, 0]
비트나 다른 메모리 영역도 주소만 바꾸면 됩니다.
# 비트 읽기 (ON/OFF)
bits = read_xgt('192.168.0.10', 2004, '%MX100', count=1)
print(f"MX100: {'ON' if bits[0] else 'OFF'}")D 영역 읽기
d_values = read_xgt('192.168.0.10', 2004, '%DW0', count=3)
print(f"DW0~2: {d_values}")XGT FEnet Python 쓰기 코드
읽기와 거의 같습니다. 명령 코드만 다릅니다.
0x00540x0058나머지 구조는 동일하고, 쓰기에는 데이터 값이 추가됩니다.
def write_xgt(host: str, port: int, address: str, values: list) -> bool:
"""XGT FEnet으로 PLC 메모리 쓰기"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3.0)
sock.connect((host, port))var_name = address.encode('ascii')
# 응용 데이터 — 명령이 0x0058 (쓰기)
app_data = struct.pack('<H', 0x0058) # 명령: 쓰기
app_data += struct.pack('<H', 0x0000)
app_data += struct.pack('<H', 0x0000)
app_data += struct.pack('<H', 1)
app_data += struct.pack('<H', len(var_name))
app_data += var_name
app_data += struct.pack('<H', len(values))# 데이터 추가
for val in values:
app_data += struct.pack('<H', val & 0xFFFF)# 헤더
header = b'LSIS-XGT'
header += struct.pack('<H', 0x0000)
header += struct.pack('<H', 0x0033)
header += struct.pack('<B', 0xA0)
header += struct.pack('<B', 0x33)
header += struct.pack('<H', 1)
header += struct.pack('<H', len(app_data))
header += struct.pack('<B', 0x00)
header += struct.pack('<B', 0x00)sock.sendall(header + app_data)
# 응답 확인
resp_header = b''
while len(resp_header) < 20:
resp_header += sock.recv(20 - len(resp_header))
data_len = struct.unpack('<H', resp_header[16:18])[0]
resp_data = b''
while len(resp_data) < data_len:
resp_data += sock.recv(data_len - len(resp_data))error = struct.unpack('<H', resp_data[6:8])[0]
sock.close()if error != 0:
raise RuntimeError(f"PLC 쓰기 에러: 0x{error:04X}")
return True
쓰기 사용법도 간단합니다.
# %MW200에 값 1234 쓰기
write_xgt('192.168.0.10', 2004, '%MW200', [1234])%MW300부터 3개 워드에 값 쓰기
write_xgt('192.168.0.10', 2004, '%MW300', [100, 200, 300])XGT CPU 타입별 설정
PLC 시리즈에 따라 헤더의 CPU Info 바이트가 다릅니다.
| PLC 시리즈 | CPU Info 값 | 비고 |
|---|---|---|
| XGK | 0xA0 | 기본값 (변경 불필요) |
| XGB | 0xA4 | 코드에서 0xA0 → 0xA4 변경 |
| XGI | 0xA8 | 코드에서 0xA0 → 0xA8 변경 |
XGB를 쓰신다면, 코드에서 0xA0을 0xA4로 바꾸면 됩니다.
헤더 조립 부분에서 CPU Info 한 줄만 수정하면 됩니다.
Part 2: Modbus TCP – Python으로 PLC 데이터 읽기
Modbus TCP는 1979년에 만들어진 산업 표준 프로토콜입니다. LS뿐만 아니라 지멘스, 미쓰비시, AB 등 거의 모든 PLC가 지원합니다.
위 그림처럼 Modbus TCP는 Master(PC) → Slave(PLC) 구조입니다. PC에서 요청을 보내면 PLC가 데이터를 응답합니다. XGT FEnet과 방향은 같지만, 주소 방식이 다릅니다.
"%MW100" (이름으로 접근)100 (번호로 접근, 매핑 필요)Modbus TCP 사전 준비 (PLC 쪽)
XGT와 가장 큰 차이입니다.
Modbus TCP를 쓰려면 PLC 쪽에서 먼저 설정해야 합니다.
이 과정은 XG5000(PLC 프로그래밍 소프트웨어)에서 해야 하고, PLC 프로그래머가 해야 합니다.
XGT FEnet은 이런 과정이 전혀 필요 없었다는 점과 비교해보세요.
pymodbus 설치
pip install pymodbusModbus TCP Python 읽기 코드
Modbus는 pymodbus 라이브러리를 쓰면 코드가 간단합니다.
라이브러리가 프로토콜 처리를 전부 해주기 때문에, XGT처럼 헤더를 직접 조립할 필요가 없습니다.
from pymodbus.client import ModbusTcpClient
def read_modbus(host: str, port: int, address: int,
count: int = 1, slave_id: int = 1) -> list:
"""Modbus TCP로 홀딩 레지스터 읽기 (FC03)Args:
host: PLC IP 주소
port: 포트 번호 (기본 502)
address: 시작 레지스터 번호
count: 읽을 레지스터 수
slave_id: Slave ID (기본 1)Returns:
읽은 값 리스트
"""
client = ModbusTcpClient(host=host, port=port, timeout=3)
client.connect()result = client.read_holding_registers(
address=address,
count=count,
slave=slave_id
)client.close()
if result.isError():
raise RuntimeError(f"Modbus 에러: {result}")
return result.registers
코드가 짧은 이유는 pymodbus가 패킷 조립·파싱을 전부 처리하기 때문입니다.
Modbus TCP 읽기 사용 예시
# 레지스터 100번부터 5개 읽기
values = read_modbus('192.168.0.10', 502, address=100, count=5)
print(f"레지스터 100~104: {values}")여기서 "레지스터 100번"은 PLC 쪽 매핑 테이블에서 정한 번호입니다. PLC 프로그래머가 %MW100을 레지스터 100번에 넣었다면, 위 코드로 읽을 수 있습니다.
# Slave ID가 2인 장비에서 읽기
values = read_modbus('192.168.0.10', 502, address=0, count=10, slave_id=2)Modbus TCP Python 쓰기 코드
def write_modbus(host: str, port: int, address: int,
values: list, slave_id: int = 1) -> bool:
"""Modbus TCP로 레지스터 쓰기 (FC16)"""
client = ModbusTcpClient(host=host, port=port, timeout=3)
client.connect()result = client.write_registers(
address=address,
values=values,
slave=slave_id
)client.close()
if result.isError():
raise RuntimeError(f"Modbus 쓰기 에러: {result}")
return True
쓰기도 마찬가지로 매핑된 레지스터 번호를 사용합니다.
# 레지스터 200번에 1234 쓰기
write_modbus('192.168.0.10', 502, address=200, values=[1234])레지스터 300번부터 3개에 값 쓰기
write_modbus('192.168.0.10', 502, address=300, values=[100, 200, 300])Modbus TCP Function Code 정리
Modbus에는 용도별로 여러 명령(Function Code)이 있습니다. 가장 많이 쓰는 건 FC03(레지스터 읽기)입니다.
# FC01: 코일 읽기 (비트 단위, ON/OFF)
result = client.read_coils(address=0, count=8, slave=1)FC02: 디스크리트 입력 읽기 (비트, 읽기 전용)
result = client.read_discrete_inputs(address=0, count=8, slave=1)FC03: 홀딩 레지스터 읽기 (워드 단위) ← 가장 많이 사용
result = client.read_holding_registers(address=100, count=5, slave=1)FC04: 입력 레지스터 읽기 (워드, 읽기 전용)
result = client.read_input_registers(address=100, count=5, slave=1)FC05: 코일 1개 쓰기
client.write_coil(address=0, value=True, slave=1)FC06: 레지스터 1개 쓰기
client.write_register(address=200, value=1234, slave=1)FC16: 레지스터 여러 개 쓰기
client.write_registers(address=300, values=[100, 200, 300], slave=1)실무에서는 FC03(읽기)과 FC16(여러 개 쓰기)을 가장 많이 씁니다.
XGT vs Modbus – 같은 데이터를 읽을 때 코드 비교
%MW100에서 5개 워드를 읽는 코드를 비교합니다.
XGT FEnet 코드
values = read_xgt('192.168.0.10', 2004, '%MW100', count=5)Modbus TCP 코드
values = read_modbus('192.168.0.10', 502, address=100, count=5)코드 길이만 보면 Modbus가 간단해 보이지만, PLC 쪽 설정(매핑)까지 포함하면 XGT가 전체적으로 더 간단합니다.
Python에서 32비트 값 읽기 (온도, 실수 등)
PLC 레지스터는 16비트(워드) 단위입니다.
32비트 정수(DWORD)나 실수(FLOAT)를 읽으려면 워드 2개를 합쳐야 합니다.
아래 변환 함수를 쓰면 됩니다.
import struct
def words_to_dword(w1: int, w2: int, signed: bool = False) -> int:
"""워드 2개 → 32비트 정수"""
raw = struct.pack('<HH', w1, w2)
fmt = '<i' if signed else '<I'
return struct.unpack(fmt, raw)[0]
def words_to_float(w1: int, w2: int) -> float:
"""워드 2개 → 32비트 실수 (소수점)"""
raw = struct.pack('<HH', w1, w2)
return struct.unpack('<f', raw)[0]
실제 사용 예시입니다. XGT와 Modbus 모두 동일하게 적용됩니다.
# XGT로 32비트 정수 읽기 (%MW100~101 두 워드 사용)
words = read_xgt('192.168.0.10', 2004, '%MW100', count=2)
dword_value = words_to_dword(words[0], words[1])
print(f"32비트 정수: {dword_value}")온도 같은 실수값 읽기 (%MW200~201)
words = read_xgt('192.168.0.10', 2004, '%MW200', count=2)
temp = words_to_float(words[0], words[1])
print(f"온도: {temp:.1f}°C")PLC 통신 연결 안 될 때 체크리스트

PLC 통신이 안 되는 원인의 80%는 물리 연결이나 네트워크 설정 문제입니다. 코드를 의심하기 전에, 아래 순서대로 확인하세요.
1단계: 물리 연결 확인
ping이 가는가?ping 192.168.0.10ping이 안 가면 케이블이나 IP 설정 문제입니다. 코드를 볼 필요 없습니다.
2단계: 네트워크 서브넷 확인
192.168.0.100 / PLC 192.168.0.10 → OK192.168.1.100 / PLC 192.168.0.10 → NG (서브넷 다름)서브넷이 다르면 공유기나 라우터 설정을 확인해야 합니다.
3단계: 포트 및 방화벽 확인
# Windows에서 포트 연결 테스트
Test-NetConnection -ComputerName 192.168.0.10 -Port 2004포트 테스트에서 실패하면 방화벽 문제일 가능성이 높습니다.
4단계: PLC 설정 확인 (Modbus만 해당)
XGT FEnet은 이 단계가 필요 없습니다. FEnet 모듈만 있으면 됩니다.
자주 묻는 질문 – LS PLC Python 통신
pymodbus 말고 다른 Modbus 라이브러리는?
pyModbusTCP도 있습니다. 더 가볍지만 기능이 적습니다.
pip install pyModbusTCPfrom pyModbusTCP.client import ModbusClient
client = ModbusClient(host='192.168.0.10', port=502, auto_open=True)
values = client.read_holding_registers(100, 5)
어떤 라이브러리를 쓰든 PLC 쪽 매핑 설정은 동일하게 필요합니다.
XGT FEnet Python 라이브러리가 있나?
공식 라이브러리는 없습니다.
하지만 위 코드처럼 socket + struct만으로 충분합니다. 프로토콜 자체가 단순하기 때문입니다.
프로덕션에서 쓸 때는 연결 유지, 재연결, 멀티스레딩 처리를 추가하면 됩니다.
PLC 주소를 여러 개 한 번에 읽을 수 있나?
XGT FEnet — Block Count를 늘려서 여러 주소를 한 프레임에 읽을 수 있습니다.
Modbus TCP — 연속된 레지스터는 count를 늘려서 한 번에 읽고, 띄엄띄엄이면 여러 번 요청해야 합니다.
여러 PLC에 동시에 접속할 수 있나?
가능합니다. PLC마다 별도 소켓 연결을 만들면 됩니다.
# PLC 2대에서 동시에 읽기
values_1 = read_xgt('192.168.0.10', 2004, '%MW100', count=5)
values_2 = read_xgt('192.168.0.11', 2004, '%MW100', count=5)대수가 많으면 threading이나 asyncio로 병렬 처리하는 게 효율적입니다.
Python 말고 다른 언어(C#, Java 등)로도 되나?
네. XGT FEnet은 TCP 소켓만 있으면 되니까 어떤 언어든 가능합니다.
Modbus TCP도 대부분 언어에 라이브러리가 있습니다.
모니터링 polling 주기는 어느 정도가 적당한가?
일반적으로 100ms ~ 1초 사이입니다.
| 용도 | 권장 주기 |
|---|---|
| 화면 표시용 (HMI) | 500ms ~ 1초 |
| 데이터 로깅 | 1초 ~ 5초 |
| 알람 감시 | 100ms ~ 500ms |
너무 빠르면 PLC에 부하가 걸리고, 너무 느리면 값 변화를 놓칩니다. 현장 상황에 맞게 조절하세요.
PLC 데이터를 DB에 저장하려면?
읽은 값을 SQLite(로컬)나 PostgreSQL(서버)에 넣으면 됩니다.
import sqlite3, timeconn = sqlite3.connect('plc_log.db')
conn.execute('CREATE TABLE IF NOT EXISTS log (ts TEXT, mw100 INT, mw101 INT)')
while True:
values = read_xgt('192.168.0.10', 2004, '%MW100', count=2)
conn.execute('INSERT INTO log VALUES (?, ?, ?)',
(time.strftime('%Y-%m-%d %H:%M:%S'), values[0], values[1]))
conn.commit()
time.sleep(1)
이렇게 하면 시간별 데이터를 엑셀이나 대시보드로 분석할 수 있습니다.
다음 단계 – PLC 모니터링 만들기
위 코드를 기반으로 1초마다 PLC 값을 읽는 간단한 모니터링을 만들 수 있습니다.
import time
while True:
try:
values = read_xgt('192.168.0.10', 2004, '%MW100', count=5)
print(f"[{time.strftime('%H:%M:%S')}] MW100~104: {values}")
except Exception as e:
print(f"통신 에러: {e}")
time.sleep(1)
이것만으로도 실시간 값 확인용 도구가 됩니다. 여기서 더 확장하면:
csv.writer로 값을 파일에 기록 → 나중에 엑셀로 분석LS PLC 현장이라면 XGT FEnet으로 시작하는 걸 추천합니다.
PLC 쪽 설정이 필요 없어서, 코드만 짜면 바로 통신됩니다.
이 코드를 현장 산업용 PC에서 돌리려면, 어떤 PC를 쓸지도 중요합니다. 사이즈별 가격과 스펙을 비교해 봤습니다.
>
→ 10인치 산업용 터치PC 가격비교 2026



