본문 바로가기

개발/PLC

pymodbus 설치와 실행 방법: Python Modbus TCP 서버·클라이언트 실습 및 오류 해결

반응형

반응형

Python으로 Modbus TCP 서버를 직접 실행하는 이유?

PLC와 MES를 연동하는 프로그램을 개발할 때 가장 먼저 필요한 것은 PLC에서 데이터를 정상적으로 읽을 수 있는지 확인하는 작업입니다.

실제 제조현장에서는 PLC가 생산량, 설비 가동 상태, 온도 등의 데이터를 보유하고 있으며, 외부 프로그램이 통신을 통해 해당 데이터를 가져옵니다.

하지만 개인 개발자가 PLC 통신을 학습하기 위해 실제 산업용 PLC를 구매하고 주변 장치까지 구성하는 것은 부담스러울 수 있습니다.

PLC는 제조사와 기종에 따라 가격 차이가 있고, 전원 장치나 통신 모듈, 센서 등 별도의 구성 요소가 필요할 수 있기 때문입니다. 이러한 환경에서는 Python으로 Modbus TCP 서버를 구현하는 방법이 유용합니다.

Python 프로그램이 PLC 역할을 수행하고, 다른 Python 프로그램이 해당 서버에 접속하여 데이터를 읽는 방식입니다. 이번 실습에서는 pymodbus 라이브러리를 설치하고, Holding Register 10개를 제공하는 서버를 만든 다음 클라이언트에서 생산량을 조회하겠습니다.

이번에 만드는 프로그램은 실제 기계를 제어하지 않는 가상 PLC입니다. 산업용 PLC의 통신 기능 일부를 모의하는 학습용 환경이라는 점을 먼저 이해해야 합니다.


Python 3.13.2와 pymodbus 개발환경 준비

이번 실습은 다음 환경을 기준으로 진행합니다.

Python       3.13.2
pymodbus     3.8.x
Protocol     Modbus TCP
Host         127.0.0.1
Port         5020
Unit ID      1

PHP          8.2
Laravel      12

 

 

PHP와 라라벨은 이번 편에서 직접 사용하지 않습니다. 이후 MES 데이터 저장과 API 연동을 구현할 때 사용할 예정입니다. 먼저 Python 버전을 확인합니다.

 

python --version

다음으로 프로젝트 폴더를 생성합니다.

mkdir python-plc
cd python-plc

다른 프로젝트에 설치된 Python 패키지와 충돌하지 않도록 가상환경을 생성합니다.

python -m venv .venv

Windows 명령 프롬프트에서는 다음과 같이 활성화할 수 있습니다.

.venv\Scripts\activate.bat

PowerShell을 사용하는 경우에는 다음 명령어를 입력합니다.

.\.venv\Scripts\Activate.ps1

가상환경 활성화가 실행 정책에 의해 차단된다면 명령 프롬프트를 사용하거나 가상환경 내부의 Python 실행 파일을 직접 실행할 수 있습니다.

가상환경을 사용하는 이유는 프로젝트마다 필요한 라이브러리 버전을 독립적으로 관리하기 위해서입니다. 특히 PLC 통신 라이브러리는 버전에 따라 함수와 클래스 구성이 달라질 수 있으므로 프로젝트 환경을 구분하는 것이 중요합니다.


pymodbus 3.8 계열 설치와 버전 확인

pymodbus는 Python에서 Modbus 통신을 구현할 수 있는 라이브러리입니다.

Modbus TCP 클라이언트를 만들거나 테스트용 Modbus 서버를 구성할 때 사용할 수 있습니다.

이번 실습에서는 3.8 계열을 사용합니다.

python -m pip install "pymodbus>=3.8,<3.9"

설치가 완료되면 버전을 확인합니다.

python -m pip show pymodbus

출력된 Version 항목이 3.8 계열인지 확인합니다. 버전을 제한하는 이유는 pymodbus가 업데이트되면서 일부 API가 변경되기 때문입니다.

예를 들어 서버 데이터 저장소를 구성하는 클래스 이름이나 레지스터를 조회할 때 사용하는 인자가 버전에 따라 달라질 수 있습니다.

따라서 인터넷에서 찾은 최신 버전의 예제와 3.8 계열의 예제를 혼용하면 오류가 발생할 수 있습니다. 이번 코드는 pymodbus 3.8 계열 API를 기준으로 작성했습니다. 다만 Python 3.13.2 환경에서 실제 실행한 결과를 제시하는 것은 아니므로, 설치 및 실행 후 결과를 직접 확인해야 합니다.


프로젝트 폴더와 파일 구성

이번 프로젝트에서는 서버와 클라이언트를 각각 별도의 파일로 작성합니다.

python-plc/
│
├── .venv/
├── plc_server.py
└── plc_client.py

 

 

plc_server.py는 가상 PLC 서버입니다. plc_client.py는 서버에 접속해 레지스터를 읽는 프로그램입니다. 두 프로그램은 서로 독립적으로 실행됩니다.

 

따라서 서버를 실행한 터미널을 유지한 상태에서 새로운 터미널을 열어 클라이언트를 실행해야 합니다. 이러한 구성은 실제 PLC와 외부 수집 프로그램이 서로 다른 프로세스로 동작하는 구조를 이해하는 데 도움이 됩니다.


Holding Register 10개와 초기값 정의

이번 가상 PLC에는 10개의 Holding Register를 구성합니다.

각 레지스터의 주소와 의미는 다음과 같습니다.

HR0 = 1       설비 가동 상태
HR1 = 120     누적 생산량
HR2 = 115     양품 수량
HR3 = 5       불량 수량
HR4 = 253     현재 온도
HR5 = 500     목표 생산량
HR6 = 1200    모터 회전 속도
HR7 = 0       알람 코드
HR8 = 101     작업지시 번호
HR9 = 360     누적 가동 시간

HR0은 설비 가동 상태를 나타냅니다. 이번 실습에서는 0을 정지, 1을 가동으로 정의합니다. HR1은 누적 생산량이며, HR2와 HR3은 각각 양품과 불량 수량입니다. HR4는 현재 온도입니다. 여기에는 253을 저장하지만 실제 온도는 25.3도로 해석합니다.

원시값: 253
배율: 0.1
변환값: 25.3°C

이처럼 PLC의 레지스터 값은 실제 단위로 변환하는 과정이 필요할 수 있습니다. HR5에는 목표 생산량 500개를 저장합니다. 이후 실습에서는 해당 값을 클라이언트에서 변경하고 다시 읽어 확인할 예정입니다.

여기서 사용하는 주소와 데이터 의미는 학습을 위해 정의한 것입니다. 실제 PLC에서는 제조사 매뉴얼과 설비 프로그램에 따라 주소 체계가 달라집니다.


Modbus TCP 서버 코드 작성하기

이제 plc_server.py 파일을 생성합니다.

다음 코드를 작성합니다.

from pymodbus.datastore import (
    ModbusSequentialDataBlock,
    ModbusSlaveContext,
    ModbusServerContext,
)
from pymodbus.server import StartTcpServer

HOST = "127.0.0.1"
PORT = 5020
UNIT_ID = 1

REGISTER_VALUES = [
    1,      # HR0: 설비 가동 상태
    120,    # HR1: 누적 생산량
    115,    # HR2: 양품 수량
    5,      # HR3: 불량 수량
    253,    # HR4: 온도 x 10
    500,    # HR5: 목표 생산량
    1200,   # HR6: 모터 RPM
    0,      # HR7: 알람 코드
    101,    # HR8: 작업지시 번호
    360,    # HR9: 누적 가동 시간
]

store = ModbusSlaveContext(
    hr=ModbusSequentialDataBlock(
        1,
        REGISTER_VALUES
    )
)

context = ModbusServerContext(
    slaves={UNIT_ID: store},
    single=False
)

if __name__ == "__main__":
    print("Modbus TCP 서버 시작")
    print(f"주소: {HOST}")
    print(f"포트: {PORT}")

    StartTcpServer(
        context=context,
        address=(HOST, PORT)
    )

 

 

코드에서 REGISTER_VALUES는 가상 PLC가 제공할 레지스터 초기값입니다. ModbusSequentialDataBlock은 연속된 레지스터 데이터를 저장하는 역할을 합니다.

 

여기서 데이터 블록의 시작 주소를 1로 지정했습니다. pymodbus 3.8 계열의 ModbusSlaveContext에서는 요청 주소를 내부 데이터 블록 주소로 변환하는 과정이 있기 때문입니다. 클라이언트가 요청하는 주소는 0부터 시작합니다. ModbusServerContext에서는 Unit ID 1에 데이터 저장소를 연결합니다.

single=False 설정은 Unit ID를 기준으로 해당 저장소를 선택하도록 구성한 것입니다. 마지막으로 StartTcpServer를 호출하면 서버가 실행됩니다.

이번 실습에서는 표준 Modbus TCP 포트인 502 대신 5020을 사용합니다. 로컬 학습 환경에서 사용하기 위해 선택한 포트이며, 서버와 클라이언트에서 동일하게 설정하면 됩니다.


Python 서버 실행과 연결 대기 확인

첫 번째 터미널에서 다음 명령어를 실행합니다.

python plc_server.py

예상 출력은 다음과 같습니다.

Modbus TCP 서버 시작
주소: 127.0.0.1
포트: 5020

서버가 실행되면 프로그램이 종료되지 않고 클라이언트의 연결을 기다립니다. 이는 정상적인 동작입니다. 서버를 실행한 터미널을 닫으면 통신도 중단되므로 그대로 유지합니다. 서버를 중지하려면 Ctrl+C를 사용합니다.

Python 클라이언트에서 레지스터 조회하기

이제 plc_client.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"레지스터 조회 실패: {response}"
        )

    values = response.registers

    print("전체 레지스터:", values)
    print("누적 생산량:", values[1])
    print("양품 수량:", values[2])
    print("불량 수량:", values[3])
    print("현재 온도:", values[4] / 10)
    print("목표 생산량:", values[5])

finally:
    client.close()

 

 

ModbusTcpClient는 TCP 서버에 접속하는 역할을 합니다. connect를 호출하면 지정한 IP 주소와 포트로 연결을 시도합니다.

 

read_holding_registers는 Holding Register를 조회합니다.Modbus에서 Holding Register 읽기는 기능 코드 03에 해당합니다.

address는 조회 시작 주소이며, count는 읽을 레지스터 개수입니다.

이번에는 주소 0부터 10개를 읽도록 구성했습니다.slave에는 Unit ID 1을 전달합니다. 응답이 정상적이면 registers 배열에 조회 결과가 저장됩니다. 마지막으로 close를 호출하여 연결을 종료합니다.


클라이언트 실행과 예상 출력

서버가 실행 중인 상태에서 두 번째 터미널을 엽니다.

다음 명령어를 입력합니다.

python plc_client.py

정상적인 통신이 이루어졌을 때 예상되는 출력은 다음과 같습니다.

전체 레지스터: [1, 120, 115, 5, 253, 500, 1200, 0, 101, 360]
누적 생산량: 120
양품 수량: 115
불량 수량: 5
현재 온도: 25.3
목표 생산량: 500

서버에 지정한 초기값과 클라이언트가 읽은 값이 일치하는지 확인합니다. 특히 첫 번째 값이 1이고 두 번째 값이 120인지 확인하면 주소 매핑이 올바른지 점검할 수 있습니다. 만약 값이 한 칸씩 어긋난다면 데이터 블록의 시작 주소와 클라이언트 요청 주소를 확인해야 합니다.


Modbus TCP 연결 실패 시 확인할 사항

실습 과정에서 가장 흔하게 발생할 수 있는 문제는 연결 실패입니다.

첫 번째로 서버가 실행 중인지 확인합니다.

서버가 실행되지 않았다면 클라이언트는 연결할 대상이 없으므로 접속에 실패합니다.

두 번째로 IP 주소와 포트 번호를 확인합니다.

이번 실습에서는 서버와 클라이언트 모두 127.0.0.1과 5020을 사용해야 합니다.

세 번째는 포트 충돌입니다.

다른 프로그램이 이미 5020 포트를 사용하고 있다면 서버가 정상적으로 실행되지 않을 수 있습니다.

Windows에서는 다음 명령어로 포트 사용 여부를 확인할 수 있습니다.

netstat -ano | findstr :5020

네 번째는 pymodbus 버전 문제입니다.

예제에서 사용한 클래스나 함수가 설치된 버전과 일치하지 않으면 ImportError 또는 TypeError가 발생할 수 있습니다.

다음 명령어로 버전을 확인합니다.

python -m pip show pymodbus

다섯 번째는 레지스터 주소 문제입니다.

서버 연결에는 성공했지만 조회 결과가 잘못되거나 Modbus 예외 응답이 반환될 수 있습니다. 이 경우 요청 주소, 개수, 데이터 블록의 시작 주소, Unit ID를 확인해야 합니다.


통신 오류와 정상값 0을 구분해야 하는 이유?

MES 데이터 수집 프로그램에서는 정상적인 값 0과 통신 실패를 구분해야 합니다. 예를 들어 HR7은 알람 코드이며 초기값이 0입니다.

이 값은 이번 실습에서 알람이 없다는 의미로 정의했습니다. 하지만 통신에 실패했을 때도 무조건 0을 반환하도록 작성하면 실제 알람이 없는 것인지 데이터를 읽지 못한 것인지 구분할 수 없습니다.

따라서 통신에 실패했다면 정상 데이터로 처리하지 않고 별도의 오류 상태를 기록해야 합니다. 실제 MES에서는 마지막 정상 수집 시각과 통신 상태를 관리하는 것이 중요합니다.


실제 PLC와 가상 PLC의 차이

이번 가상 PLC는 Python 프로그램에서 지정한 레지스터 값을 Modbus TCP로 제공하는 서버입니다. 실제 PLC처럼 센서 입력을 처리하거나 모터를 제어하지 않습니다.

또한 실제 PLC의 스캔 주기, 하드웨어 입출력, 산업용 안전 기능을 재현하지 않습니다. 이번 실습에서 생산량 120개는 초기값으로 지정한 숫자입니다.

따라서 별도로 값을 변경하는 프로그램을 작성하지 않는 한 자동으로 증가하지 않습니다. 실제 PLC에 연결할 때는 제조사 매뉴얼과 장비의 통신 설정을 확인해야 합니다.


라라벨로 MES 연동으로 확장하기

이번 실습에서는 Python 클라이언트가 조회한 데이터를 터미널에 출력했습니다. 이후에는 Python 수집 프로그램에서 레지스터 값을 일정한 주기로 조회하고, 라라벨 프레임워크로 API를 통해 MySQL에 저장하는 구조를 구현할 예정입니다.

[가상 PLC]
     ↓
 Modbus TCP
     ↓
[Python 수집기]
     ↓
 Laravel API
     ↓
   MySQL
     ↓
 MES 생산현황

 

 

Python 가상 PLC 서버 구현: Holding Register 설계와 Modbus TCP 데이터 수집 테스트

MES 개발을 위한 가상 PLC 환경 구축제조실행시스템(MES)에서 설비 데이터를 자동으로 수집하려면 PLC가 제공하는 데이터를 읽을 수 있어야 합니다. 실제 제조현장에서는 P...

www.th-study.com

▲ MES 개발 학습을 위한 가상 PLC 서버 구현 방법 정리 ▲

 

실제 MES에서는 설비별 주소 설정과 수집 주기, 통신 실패 처리 등을 함께 관리해야 합니다. 또한 PLC가 제공하는 누적 생산량이 현재 작업지시의 생산실적과 반드시 일치하는 것은 아닙니다. 작업 시작 시점의 기준값과 카운터 초기화 정책을 고려해야 합니다.

 


마무리

이번 실습에서는 pymodbus 3.8 계열을 기준으로 Python Modbus TCP 서버와 클라이언트를 작성했습니다. 서버에는 Holding Register 10개를 구성하고 생산량과 온도, 목표 생산량 등의 초기값을 저장했습니다.

클라이언트에서는 해당 값을 조회하여 출력하는 과정을 살펴보았습니다. 실습 과정에서는 라이브러리 버전, 레지스터 주소 오프셋, IP 주소, 포트 충돌 등을 주의해야 합니다. 이러한 기초를 이해하면 이후 PLC 데이터 자동 수집과 라라벨로 MES 연동으로 확장할 수 있습니다.

반응형