본문 바로가기

개발/PLC

Python JSON 파일 관리: PLC 레지스터 설정 CRUD 구현과 Modbus TCP 동적 조회

반응형

반응형

PLC 수집 프로그램에서 설정 파일을 분리해야 하는 이유?

Python으로 PLC 데이터를 수집하는 프로그램을 처음 만들 때는 레지스터 주소를 코드에 직접 작성하는 방식이 간단합니다. 예를 들어 생산량을 HR1에서 읽고 온도를 HR4에서 읽는다면 각 주소를 Python 코드에 지정하면 됩니다.

하지만 설비가 늘어나거나 PLC 프로그램이 변경되면 이러한 방식은 유지보수에 불리해질 수 있습니다. 설비마다 레지스터 주소가 다르고, 새로 수집해야 하는 데이터가 추가될 수도 있기 때문입니다.

이러한 상황에서 사용할 수 있는 방법이 JSON 설정 파일입니다. JSON은 설비 접속 정보와 레지스터 주소를 프로그램 외부에서 관리할 수 있도록 해줍니다.

이번 글에서는 Python으로 JSON 파일을 읽고 저장하는 방법부터 레지스터 설정의 추가·조회·수정·삭제 기능을 구현하겠습니다. 마지막에는 pymodbus를 이용해 JSON에 등록된 레지스터를 실제 가상 PLC에서 읽는 과정까지 진행합니다. 실습은 앞선 PLC 시리즈에서 구성한 가상 PLC 서버를 기준으로 합니다.


개발환경과 프로젝트 구성

이번 실습 환경은 다음과 같습니다.

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

Python 버전과 라이브러리 설치 상태를 확인합니다.

python --version
python -m pip show pymodbus

pymodbus가 설치되지 않았다면 다음 명령어로 3.8 계열을 설치합니다.

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

이번에는 다음 파일을 사용합니다.

virtual-plc/
├── plc_server.py
├── registers.json
├── register_store.py
├── manage_registers.py
└── plc_reader.py
  1. plc_server.py는 앞선 4편에서 만든 가상 PLC 서버입니다.
  2. registers.json은 설정 파일입니다.
  3. register_store.py는 JSON 파일에 접근하는 기능을 담당합니다.
  4. manage_registers.py는 설정을 추가·수정·삭제하는 명령어 프로그램입니다.
  5. plc_reader.py는 저장된 설정을 이용해 Modbus TCP 통신을 수행합니다.

설정 관리와 통신 기능을 파일로 분리하면 각 기능을 독립적으로 점검하기 쉽습니다.


레지스터 설정 JSON 구조 정의하기

먼저 registers.json 파일을 생성합니다.

{
  "machine": "MACHINE-001",
  "connection": {
    "host": "127.0.0.1",
    "port": 5020,
    "unit_id": 1
  },
  "registers": [
    {
      "name": "total",
      "address": 1,
      "scale": 1.0
    },
    {
      "name": "temperature",
      "address": 4,
      "scale": 0.1
    },
    {
      "name": "target",
      "address": 5,
      "scale": 1.0
    }
  ]
}

 

설비 접속 정보는 connection에 저장합니다. registers에는 수집할 데이터 항목을 배열로 관리합니다. 각 항목은 이름, 주소, 배율을 포함합니다.

이번 실습에서는 모두 Holding Register를 대상으로 하므로 별도의 레지스터 유형을 추가하지 않았습니다. 실제 MES에서는 Coil이나 Input Register 등을 함께 수집할 수 있으므로 유형 정보가 필요할 수 있습니다. 또한 자료형이 32비트 정수나 부동소수점인 경우에는 레지스터 개수와 워드 순서 등을 별도로 정의해야 합니다.


JSON 파일 읽기와 저장 함수 구현

register_store.py 파일을 생성합니다.

import json
from pathlib import Path

CONFIG_PATH = Path(__file__).with_name(
    "registers.json"
)


def load_config():
    with CONFIG_PATH.open(
        "r",
        encoding="utf-8"
    ) as file:
        return json.load(file)


def save_config(config):
    with CONFIG_PATH.open(
        "w",
        encoding="utf-8"
    ) as file:
        json.dump(
            config,
            file,
            ensure_ascii=False,
            indent=2
        )

 

load_config 함수는 JSON 파일을 읽어 Python 딕셔너리로 변환합니다. save_config 함수는 Python 딕셔너리를 JSON 파일로 저장합니다.

ensure_ascii=False는 한글 데이터를 저장할 때 유용합니다. indent=2는 JSON 구조를 읽기 쉽게 정리합니다. 파일 경로를 현재 실행 위치가 아니라 Python 파일 위치를 기준으로 지정한 것도 중요합니다. 이렇게 하면 다른 디렉터리에서 프로그램을 실행하더라도 같은 설정 파일을 참조할 수 있습니다.


설정값 검증 함수 만들기

설정을 추가하거나 수정할 때는 잘못된 입력을 막아야 합니다. 예를 들어 레지스터 주소가 음수이거나 데이터 이름이 비어 있으면 정상적인 설정으로 사용하기 어렵습니다. register_store.py에 다음 함수를 추가합니다.

def validate_register(name, address, scale):
    if not isinstance(name, str) or not name.strip():
        raise ValueError(
            "레지스터 이름이 필요합니다."
        )

    if type(address) is not int:
        raise ValueError(
            "주소는 정수여야 합니다."
        )

    if not 0 <= address <= 65535:
        raise ValueError(
            "주소 범위가 올바르지 않습니다."
        )

    if type(scale) not in (int, float):
        raise ValueError(
            "배율은 숫자여야 합니다."
        )

    if not -1000000 <= scale <= 1000000:
        raise ValueError(
            "배율 범위가 올바르지 않습니다."
        )

 

이 검증은 이번 학습 프로그램의 기본적인 입력 검증입니다. 주소를 0부터 65535까지 제한했지만, 실제 PLC에서 해당 범위의 모든 주소를 읽을 수 있다는 의미는 아닙니다.

실제 장비가 지원하는 주소 범위와 레지스터 맵은 별도로 확인해야 합니다. 배율 역시 학습용 범위만 검사합니다. 실제 MES에서는 업무에 필요한 허용 범위를 더 엄격하게 정의하는 것이 좋습니다.


레지스터 설정 추가 기능

이제 register_store.py에 추가 기능을 작성합니다.

def add_register(name, address, scale=1.0):
    validate_register(name, address, scale)

    config = load_config()

    if any(
        item["name"] == name
        for item in config["registers"]
    ):
        raise ValueError(
            "동일한 이름이 이미 등록되어 있습니다."
        )

    config["registers"].append({
        "name": name,
        "address": address,
        "scale": scale
    })

    save_config(config)

 

동일한 이름이 이미 등록되어 있다면 추가를 거부합니다. 하지만 같은 주소를 여러 이름으로 참조하는 경우는 허용합니다. 하나의 레지스터를 여러 데이터 항목이 참조해야 하는 경우도 있기 때문입니다.

물론 이러한 설정이 실제로 필요한지는 업무 기준에 따라 확인해야 합니다. 예를 들어 양품 수량을 추가하려면 다음과 같이 호출합니다.

add_register("good", 2, 1.0)

이후 JSON 파일에 양품 수량 설정이 추가됩니다.


레지스터 설정 조회 기능

설정을 변경하기 전에 현재 등록된 항목을 확인할 수 있어야 합니다.

다음 함수를 추가합니다.

def list_registers():
    config = load_config()

    return config["registers"]

이 함수는 JSON 파일에 등록된 레지스터 목록을 반환합니다.

화면에 표시할 때는 다음과 같이 사용할 수 있습니다.

for item in list_registers():
    print(
        item["name"],
        item["address"],
        item["scale"]
    )

설정 목록을 확인하는 기능은 이후 웹 기반 관리 화면을 구현할 때도 필요합니다.

Laravel에서 설비별 레지스터 목록을 조회하는 화면을 만든다면 이와 유사한 데이터 구조를 활용할 수 있습니다.

레지스터 설정 수정 기능

다음은 기존 설정을 변경하는 기능입니다.

def update_register(name, address, scale):
    validate_register(name, address, scale)

    config = load_config()

    for item in config["registers"]:
        if item["name"] == name:
            item["address"] = address
            item["scale"] = scale

            save_config(config)
            return

    raise ValueError(
        "수정할 레지스터가 없습니다."
    )

 

수정 대상은 name을 기준으로 찾습니다.

예를 들어 온도 레지스터의 주소와 배율을 지정하려면 다음과 같이 호출합니다.

update_register(    "temperature",    4,    0.1)

이 함수는 기존 항목을 찾아 address와 scale을 변경합니다. 설정 파일에 저장한 주소를 바꿔도 실제 PLC 내부의 주소가 변경되는 것은 아닙니다. 수집 프로그램이 참조하는 주소만 바뀝니다.


레지스터 설정 삭제 기능

마지막으로 삭제 기능을 구현합니다.

def delete_register(name):
    config = load_config()

    old_count = len(config["registers"])

    config["registers"] = [
        item
        for item in config["registers"]
        if item["name"] != name
    ]

    if len(config["registers"]) == old_count:
        raise ValueError(
            "삭제할 레지스터가 없습니다."
        )

    save_config(config)

 

삭제할 이름이 존재하면 해당 설정을 목록에서 제외합니다. 존재하지 않으면 오류를 발생시킵니다. 설정이 삭제되더라도 가상 PLC 서버의 레지스터 값은 유지됩니다. 이 기능은 외부 수집 프로그램의 설정 삭제일 뿐 실제 PLC 메모리 삭제가 아닙니다.


명령어로 CRUD 기능 테스트하기

이번에는 manage_registers.py 파일을 생성합니다.

import argparse

from register_store import (
    add_register,
    update_register,
    delete_register,
    list_registers,
)

parser = argparse.ArgumentParser()

subparsers = parser.add_subparsers(
    dest="command",
    required=True
)

subparsers.add_parser("list")

add_parser = subparsers.add_parser("add")
add_parser.add_argument("name")
add_parser.add_argument("address", type=int)
add_parser.add_argument("scale", type=float)

update_parser = subparsers.add_parser("update")
update_parser.add_argument("name")
update_parser.add_argument("address", type=int)
update_parser.add_argument("scale", type=float)

delete_parser = subparsers.add_parser("delete")
delete_parser.add_argument("name")

args = parser.parse_args()

try:
    if args.command == "list":
        for item in list_registers():
            print(item)

    elif args.command == "add":
        add_register(
            args.name,
            args.address,
            args.scale
        )
        print("등록 완료")

    elif args.command == "update":
        update_register(
            args.name,
            args.address,
            args.scale
        )
        print("수정 완료")

    elif args.command == "delete":
        delete_register(args.name)
        print("삭제 완료")

except (ValueError, OSError) as exc:
    print("설정 처리 실패:", exc)

 

이 프로그램은 argparse를 이용해 명령어를 구분합니다.

  1. list는 목록 조회입니다.
  2. add는 설정 추가입니다.
  3. update는 설정 수정입니다.
  4. delete는 설정 삭제입니다.

각 명령어를 실행할 때 필요한 이름과 주소, 배율을 전달합니다.


레지스터 추가·수정·삭제 실행 예시

먼저 현재 등록된 설정을 조회합니다.

python manage_registers.py list

이후 모터 RPM 설정을 추가합니다.

python manage_registers.py add rpm 6 1

설정이 정상적으로 저장되었다면 다음 메시지가 출력됩니다.

등록 완료

이번에는 RPM 배율을 수정합니다.

python manage_registers.py update rpm 6 1.0

수정 후에는 목록을 다시 확인합니다.

python manage_registers.py list

마지막으로 RPM 설정을 삭제합니다.

python manage_registers.py delete rpm

예상 결과는 다음과 같습니다.

삭제 완료

이렇게 하면 Python 소스코드를 수정하지 않고 JSON 설정 파일의 내용을 변경할 수 있습니다.


JSON 설정으로 PLC 데이터를 동적으로 조회하기

이제 plc_reader.py 파일을 작성합니다.

이번에는 JSON에 등록된 주소를 하나씩 읽습니다.

from pymodbus.client import ModbusTcpClient
from register_store import load_config


def main():
    config = load_config()
    connection = config["connection"]

    client = ModbusTcpClient(
        connection["host"],
        port=connection["port"],
        timeout=3
    )

    try:
        if not client.connect():
            raise ConnectionError(
                "가상 PLC 연결 실패"
            )

        for item in config["registers"]:
            response = client.read_holding_registers(
                address=item["address"],
                count=1,
                slave=connection["unit_id"]
            )

            if response.isError():
                print(
                    item["name"],
                    "조회 오류",
                    response
                )
                continue

            raw_value = response.registers[0]
            value = raw_value * item["scale"]

            print(f'{item["name"]}: {value}')

    finally:
        client.close()


if __name__ == "__main__":
    main()

 

이 프로그램은 JSON에 등록된 항목을 반복하면서 각 레지스터를 읽습니다. 레지스터 주소는 Python 코드에 직접 작성하지 않았습니다.

따라서 JSON 파일에서 주소를 변경하면 다음 실행부터 변경된 설정을 사용합니다. 이번에는 설정과 조회 결과의 관계를 쉽게 확인하기 위해 개별 요청 방식을 사용했습니다. 실제 운영에서는 연속된 주소를 묶어 읽는 방식도 고려해야 합니다.


동적 조회 결과 검증하기

먼저 가상 PLC 서버를 실행합니다.

python plc_server.py

다른 터미널에서 조회 프로그램을 실행합니다.

python plc_reader.py

초기 JSON 설정을 기준으로 예상되는 출력은 다음과 같습니다.

total: 120.0
temperature: 25.3
target: 500.0

앞서 good 설정을 추가했다면 양품 수량도 출력됩니다. 반대로 해당 설정을 삭제했다면 출력 대상에서 제외됩니다. 이 과정에서 중요한 점은 실제 PLC 서버의 값은 변경되지 않았다는 사실입니다. 변경한 것은 외부 수집 프로그램이 읽어야 할 주소 목록입니다.


JSON 파일 관리에서 발생할 수 있는 오류

실제 운영에서는 JSON 설정 파일이 항상 정상적이라고 가정할 수 없습니다. 파일이 존재하지 않으면 FileNotFoundError가 발생할 수 있습니다.

JSON 형식이 잘못되면 JSONDecodeError가 발생할 수 있습니다. 설정에 필요한 필드가 누락되면 KeyError가 발생할 수 있습니다.

또한 같은 파일을 여러 프로그램이 동시에 수정하면 데이터가 덮어써질 위험이 있습니다. 이번 실습의 save_config 함수는 학습을 위한 단순 저장 방식입니다.

실제 운영환경에서는 임시 파일에 먼저 저장한 뒤 교체하는 방식이나 파일 잠금, 데이터베이스 트랜잭션 등을 고려할 수 있습니다. 여러 사용자가 동시에 설정을 수정해야 한다면 JSON 파일보다 데이터베이스 기반 관리가 더 적합할 수 있습니다.


Modbus TCP 통신과 설정 관리의 관계

JSON 설정은 통신 프로토콜 자체를 변경하지 않습니다. Modbus TCP는 여전히 지정된 주소의 레지스터 값을 읽습니다. JSON 파일은 Python 프로그램에 어떤 주소를 요청할지 알려주는 역할을 합니다.

예를 들어 생산량 주소가 1이라면 클라이언트는 주소 1을 요청합니다. 설정을 100으로 바꾸면 클라이언트는 주소 100을 요청합니다.

하지만 실제 PLC에 주소 100이 존재하지 않는다면 조회가 실패할 수 있습니다. 따라서 설정 변경 기능과 실제 장비의 레지스터 맵 검증은 함께 이루어져야 합니다.


마무리

이번 실습에서는 Python으로 JSON 기반 PLC 레지스터 설정 관리 기능을 구현했습니다. 설정 파일을 읽고 저장하는 함수부터 추가·조회·수정·삭제 기능까지 작성했습니다.

 

MES PLC 레지스터 관리 구조 설계: JSON 설정 기반 동적 수집과 데이터 검증

MES에서 PLC 레지스터 설정을 별도로 관리해야 하는 이유?제조실행시스템(MES)에서 PLC 데이터를 수집하는 기능을 개발할 때는 통신 프로그램뿐 아니라 설정 관리 구조도...

www.th-study.com

▲ MES와 연동할 때 PLC 레지스터 관리 구조 설계 정리 ▲

또한 argparse를 이용해 명령어로 설정을 관리하고, pymodbus 클라이언트가 해당 설정을 이용해 데이터를 조회하도록 구성했습니다.

이 방식은 레지스터 주소를 소스코드에 고정하는 문제를 줄여줍니다. 다만 실제 MES 운영환경에서는 설정 검증과 동시 수정, 파일 저장 안정성, 통신 오류 처리 등을 추가로 고려해야 합니다.

다음 편에서는 Modbus TCP의 쓰기 기능을 사용하여 목표 생산량을 레지스터에 기록하고 다시 읽어 확인하는 실습을 진행하겠습니다.

반응형