
pymodbus로 여러 Holding Register를 조회하는 방법
Python으로 Modbus TCP 통신을 구현할 때 가장 자주 사용하는 기능 중 하나가 Holding Register 조회입니다. PLC에는 생산량, 설비 상태, 온도, 목표 수량 등 다양한 정보가 저장될 수 있습니다. 이러한 데이터를 수집하려면 먼저 각 레지스터의 주소를 파악하고, 해당 주소에 읽기 요청을 보내야 합니다.
앞선 4편에서는 Python으로 가상 PLC 서버를 만들고 클라이언트를 연결하는 방법을 살펴보았습니다. 이번에는 pymodbus의 read_holding_registers 함수를 중심으로 여러 레지스터를 한 번에 읽는 방법을 실습하겠습니다.
특히 시작 주소와 조회 개수의 관계, 반환 배열의 순서, 주소 오프셋, 통신 오류와 Modbus 예외 응답의 차이를 살펴보겠습니다. 이번 실습은 Python 3.13.2와 pymodbus 3.8 계열을 기준으로 작성했습니다. 실제 실행환경에서 검증한 로그가 아니라 코드에 따른 예상 결과를 사용합니다.
Python 개발환경과 가상 PLC 서버 준비
이번 실습에서는 4편에서 작성한 가상 PLC 서버를 그대로 사용합니다.
개발환경은 다음과 같습니다.
Python 3.13.2
pymodbus 3.8.x
Protocol Modbus TCP
Host 127.0.0.1
Port 5020
Unit ID 1
먼저 설치된 Python과 pymodbus 버전을 확인합니다.
python --version
python -m pip show pymodbus
pymodbus가 설치되지 않았다면 다음 명령어를 사용합니다.
python -m pip install "pymodbus>=3.8,<3.9"
버전을 지정하는 이유는 pymodbus의 주요 클래스와 함수 인자가 버전에 따라 달라질 수 있기 때문입니다. 이번 예제에서는 3.8 계열에서 사용하는 slave 인자를 기준으로 설명합니다. 다음으로 가상 PLC 서버를 실행합니다.
python plc_server.py
서버가 정상적으로 시작되었다면 해당 터미널을 유지합니다. 클라이언트는 별도의 터미널에서 실행합니다. 서버에 저장한 레지스터 초기값은 다음과 같습니다.
HR0 = 1
HR1 = 120
HR2 = 115
HR3 = 5
HR4 = 253
HR5 = 500
HR6 = 1200
HR7 = 0
HR8 = 101
HR9 = 360
이 값들은 실제 생산설비의 데이터가 아니라 학습용으로 정의한 값입니다.
read_holding_registers 함수의 주요 인자
pymodbus에서 Holding Register를 읽을 때는 read_holding_registers 함수를 사용합니다.
기본적인 호출 형태는 다음과 같습니다.
response = client.read_holding_registers(
address=0,
count=10,
slave=1
)
여기에는 세 가지 중요한 인자가 있습니다. address는 읽기를 시작할 레지스터 주소입니다. count는 읽을 레지스터 개수입니다. slave는 통신 대상의 Unit ID를 지정합니다.
이번 예제에서는 주소 0부터 10개를 요청합니다. 따라서 HR0부터 HR9까지 조회하게 됩니다. Modbus 프로토콜에서 Holding Register 읽기는 기능 코드 03을 사용합니다.
Modbus TCP에서는 클라이언트가 요청 메시지를 전송하고, 서버가 해당 주소의 데이터를 응답합니다. 응답받은 값은 순서대로 배열에 저장됩니다. 예를 들어 주소 1부터 3개를 요청하면 HR1, HR2, HR3에 해당하는 값을 읽습니다.
response = client.read_holding_registers(
address=1,
count=3,
slave=1
)
이번 가상 PLC에서 예상되는 응답 데이터는 다음과 같습니다.
[120, 115, 5]
생산량 120개, 양품 115개, 불량 5개를 한 번의 요청으로 가져온 것입니다.
전체 레지스터 조회 프로그램 작성하기
이번에는 여러 레지스터를 읽고 주소별 값을 출력하는 프로그램을 작성합니다.
파일 이름은 read_all_registers.py로 지정합니다.
from pymodbus.client import ModbusTcpClient
HOST = "127.0.0.1"
PORT = 5020
UNIT_ID = 1
client = ModbusTcpClient(
HOST,
port=PORT,
timeout=3
)
try:
if not client.connect():
raise ConnectionError(
"Modbus TCP 연결 실패"
)
response = client.read_holding_registers(
address=0,
count=10,
slave=UNIT_ID
)
if response.isError():
raise RuntimeError(
f"Modbus 응답 오류: {response}"
)
values = response.registers
for address, value in enumerate(values):
print(f"HR{address} = {value}")
finally:
client.close()
프로그램은 먼저 ModbusTcpClient 객체를 생성합니다. 이후 connect를 호출해 서버에 연결합니다. 연결에 성공하면 read_holding_registers로 10개의 레지스터를 조회합니다.
isError는 서버가 Modbus 예외 응답을 반환했는지 확인하는 데 사용합니다. 정상적인 응답이라면 registers 배열을 가져와 반복문으로 출력합니다. 마지막으로 finally 구문에서 연결을 종료합니다. 이렇게 구성하면 조회 도중 오류가 발생하더라도 연결을 정리할 수 있습니다.
조회 결과와 배열 인덱스 확인하기
서버가 실행 중인 상태에서 다음 명령어를 입력합니다.
python read_all_registers.py
예상 출력 결과는 다음과 같습니다.
HR0 = 1
HR1 = 120
HR2 = 115
HR3 = 5
HR4 = 253
HR5 = 500
HR6 = 1200
HR7 = 0
HR8 = 101
HR9 = 360
이 결과에서 주의할 점은 배열 인덱스와 레지스터 주소의 관계입니다. 이번에는 주소 0부터 조회했기 때문에 배열 인덱스와 레지스터 주소가 일치합니다. 하지만 주소 5부터 3개를 조회한다면 상황이 달라집니다.
response = client.read_holding_registers(
address=5,
count=3,
slave=1
)
예상 응답은 다음과 같습니다.
[500, 1200, 0]
이 배열의 첫 번째 값은 HR0이 아니라 HR5입니다. 따라서 조회 시작 주소를 함께 고려해야 합니다.
start_address = 5
for index, value in enumerate(response.registers):
address = start_address + index
print(f"HR{address} = {value}")
이처럼 실제 레지스터 주소를 계산하면 시작 주소가 변경되어도 올바르게 출력할 수 있습니다.
레지스터 주소 오프셋 오류 이해하기
Modbus 통신에서는 주소를 다룰 때 혼동이 발생하기 쉽습니다. 일부 PLC 매뉴얼에서는 Holding Register를 40001과 같은 참조번호로 표시합니다.
반면 Python 라이브러리에서는 주소 0을 사용해야 하는 경우가 있습니다. 예를 들어 매뉴얼의 40001이 통신 요청 주소 0에 대응하는 방식입니다.
하지만 모든 장비가 같은 규칙을 사용하는 것은 아닙니다. 제조사별 주소 표기 방식과 실제 통신 주소를 확인해야 합니다. 이번 가상 PLC 서버에서는 다음과 같이 데이터 블록을 구성했습니다.
store = ModbusSlaveContext(
hr=ModbusSequentialDataBlock(
1,
[
1, 120, 115, 5, 253,
500, 1200, 0, 101, 360
]
)
)
데이터 블록의 시작 주소가 1인 이유는 pymodbus 3.8 계열의 ModbusSlaveContext가 요청 주소를 내부 데이터 블록 주소로 변환하는 방식 때문입니다.
따라서 클라이언트는 주소 0부터 요청합니다. 이 구조를 이해하지 못하고 서버의 시작 주소와 클라이언트의 요청 주소를 모두 1로 변경하면 값이 한 칸씩 어긋날 수 있습니다. 주소 오류가 발생한다면 먼저 서버의 데이터 블록 구성과 클라이언트의 요청 주소를 함께 확인해야 합니다.
조회 범위를 벗어나면 어떻게 될까?
이번 서버에서는 HR0부터 HR9까지 총 10개의 Holding Register를 초기화했습니다. 그런데 클라이언트에서 주소 9부터 5개를 요청한다면 어떻게 될까요?
response = client.read_holding_registers(
address=9,
count=5,
slave=1
)
요청 범위는 HR9부터 HR13까지입니다. 하지만 서버에는 해당 범위 전체에 대한 데이터가 구성되어 있지 않습니다. 따라서 서버 구현에 따라 유효하지 않은 주소 범위에 대한 Modbus 예외 응답이 반환될 수 있습니다.
이 경우 연결 자체는 성공했더라도 레지스터 조회는 실패할 수 있습니다. 즉, TCP 연결 성공과 Modbus 데이터 조회 성공은 별개의 단계입니다.
실제 PLC에서도 존재하지 않는 주소나 읽기가 허용되지 않은 영역을 요청하면 오류가 발생할 수 있습니다. 따라서 주소와 조회 개수는 실제 장비의 레지스터 맵에 맞춰 설정해야 합니다.
통신 실패와 Modbus 예외 응답 구분하기
PLC 데이터 수집 프로그램을 만들 때는 오류 유형을 구분하는 것이 중요합니다.
첫번째 TCP 연결 실패가 있습니다.
서버가 실행되지 않았거나 IP 주소와 포트가 잘못되었다면 연결 자체가 이루어지지 않을 수 있습니다.
두 번째는 통신 중 타임아웃입니다.
연결 이후 응답이 정해진 시간 안에 도착하지 않으면 통신 예외가 발생할 수 있습니다.
세 번째는 Modbus 예외 응답입니다.
서버가 요청을 받았지만 주소 범위나 기능이 유효하지 않다고 판단할 때 발생할 수 있습니다.
이러한 오류를 모두 생산량 0으로 처리하면 데이터의 의미가 왜곡됩니다. 실제 MES에서는 정상적으로 수집한 값과 통신 실패 상태를 구분해야 합니다.
예외 처리까지 포함한 조회 함수
이번에는 조회 기능을 함수로 분리하겠습니다. 다음 코드는 연결 실패, Modbus 예외 응답, 통신 중 발생한 예외를 구분하여 호출자에게 전달합니다.
from pymodbus.client import ModbusTcpClient
def read_registers(
host,
port,
address,
count,
unit_id=1
):
client = ModbusTcpClient(
host,
port=port,
timeout=3
)
try:
if not client.connect():
raise ConnectionError(
"PLC 서버에 연결하지 못했습니다."
)
response = client.read_holding_registers(
address=address,
count=count,
slave=unit_id
)
if response.isError():
raise RuntimeError(
f"Modbus 예외 응답: {response}"
)
return response.registers
finally:
client.close()
try:
values = read_registers(
host="127.0.0.1",
port=5020,
address=1,
count=3
)
print("생산량:", values[0])
print("양품:", values[1])
print("불량:", values[2])
except ConnectionError as exc:
print("연결 오류:", exc)
except RuntimeError as exc:
print("조회 오류:", exc)
except Exception as exc:
print("통신 또는 처리 오류:", exc)
정상적으로 실행된다면 다음 결과가 예상됩니다.
생산량: 120
양품: 115
불량: 5
이 코드에서 주의할 점은 values[0]이 HR0이 아니라는 것입니다. 주소 1부터 조회했으므로 values[0]은 HR1입니다. 또한 마지막 Exception 처리는 실습용으로 모든 예외를 표시하도록 구성했습니다. 실제 운영환경에서는 통신 예외와 프로그램 오류를 세분화하고, 오류 로그를 별도로 남기는 것이 좋습니다.
연속된 주소를 묶어서 조회하는 이유?
연속된 레지스터를 한 번에 읽으면 통신 요청 횟수를 줄일 수 있습니다. 예를 들어 HR1, HR2, HR3을 각각 읽으면 세 번의 요청이 필요합니다.
하지만 시작 주소 1과 조회 개수 3을 지정하면 한 번의 요청으로 처리할 수 있습니다. 이러한 방식은 여러 설비의 데이터를 주기적으로 수집할 때 도움이 될 수 있습니다.
다만 요청 횟수가 적다고 항상 전체 성능이 좋아지는 것은 아닙니다. 읽어야 할 주소가 서로 멀리 떨어져 있거나 중간에 유효하지 않은 주소가 있다면 요청을 나누는 것이 필요할 수 있습니다.
Modbus 기능 코드 03의 표준적인 최대 조회 개수는 한 요청당 125개 레지스터입니다. 실제 장비에서는 더 작은 범위를 요구할 수도 있으므로 제조사 문서를 확인해야 합니다.
수집 데이터를 MES에서 활용하려면?
이번에는 Python 클라이언트가 레지스터를 읽어 출력했습니다. 실제 MES에서는 데이터를 일정한 주기로 수집하고 저장해야 합니다.
PLC 데이터 수집기 설계: Python으로 다중 레지스터 해석과 MES 생산정보 변환하기
PLC 원시 데이터를 MES 생산정보로 변환하는 과정MES에서 PLC 데이터를 활용하려면 통신에 성공하는 것만으로는 충분하지 않습니다. PLC가 제공하는 레지스터 값은 기본적...
www.th-study.com
▲ 파이썬으로 데이터 수집기 만드는 방법 핵심 정리 ▲
예를 들어 5초마다 생산량을 읽는 수집기를 만들 수 있습니다. 하지만 수집 주기가 짧을수록 항상 좋은 것은 아닙니다. PLC와 네트워크의 처리 능력, 데이터 변경 빈도, MES에서 필요한 실시간성을 고려해야 합니다.
또한 통신이 실패했을 때는 무조건 재시도하기보다 재시도 간격과 횟수를 관리해야 합니다. 이후 시리즈에서는 Python 수집 프로그램에 SQLite 저장 기능을 추가하고 라라벨 API와 MySQL을 연동할 예정입니다.
마무리
이번 실습에서는 pymodbus의 read_holding_registers 함수를 사용하여 연속된 Holding Register 여러 개를 조회하는 방법을 알아보았습니다.
시작 주소와 조회 개수에 따라 반환 배열의 의미가 달라지는 점을 확인했습니다. 또한 주소 오프셋, 범위 초과, 연결 실패, Modbus 예외 응답 등 실제 개발 과정에서 확인해야 할 문제를 살펴보았습니다.
여러 레지스터를 안정적으로 읽으려면 단순한 함수 호출뿐 아니라 주소 정의와 오류 처리까지 함께 설계해야 합니다. 다음 편에서는 레지스터 주소를 Python 코드에 고정하지 않고 JSON 설정으로 관리하는 방법을 구현하겠습니다.
'개발 > PLC' 카테고리의 다른 글
| pymodbus write_register 사용법: Modbus TCP 쓰기와 읽기 검증 실습 (0) | 2026.10.24 |
|---|---|
| Python JSON 파일 관리: PLC 레지스터 설정 CRUD 구현과 Modbus TCP 동적 조회 (0) | 2026.10.23 |
| pymodbus 설치와 실행 방법: Python Modbus TCP 서버·클라이언트 실습 및 오류 해결 (0) | 2026.10.21 |
| Modbus TCP 통신 원리: PLC 서버와 Python 클라이언트의 데이터 요청·응답 구조 (6) | 2026.10.20 |
| PLC 레지스터 종류와 주소 체계: Holding Register·Coil·Modbus 데이터 구조 이해하기 (0) | 2026.10.19 |