본문 바로가기

(개인Project)_개발/PLC-PC 연결

[Program][보족] pymodbus 버전별 API 변경 정리 : 3.6 이하에서 3.13으로 마이그레이션

반응형

이런 경험 있으신가요?

pymodbus를 최신 버전으로 올렸는데 이런 오류가 납니다.

TypeError: read_holding_registers() got an unexpected keyword argument 'unit'

또는 이런 것도 있어요.

TypeError: read_holding_registers() got an unexpected keyword argument 'slave'

분명히 예전에 잘 됐던 코드인데, 버전을 올렸더니 터집니다.

더 이상한 케이스도 있습니다.

Modbus 시뮬레이터를 직접 만들어서 테스트하는데, Holding Register(FC03)를 읽으면 Input Register(FC04) 값이 나오고, Input Register를 읽으면 Holding Register 값이 나오는 현상이에요.

분명히 코드는 맞게 짰는데 값이 뒤집혀 나옵니다.

PLCLink Phase 4에서 Modbus 프로토콜을 추가하면서 두 문제를 모두 만났습니다.

하나씩 풀어볼게요.


용어 먼저 짚고 넘어갈게요

Modbus 함수 코드(FC)란

Modbus 프로토콜에서 "무엇을 읽을 것인가"를 구분하는 번호입니다.

택배 분류 코드처럼, FC01은 Coil(출력 비트), FC02는 Discrete Input(입력 비트), FC03는 Holding Register(읽기/쓰기 워드), FC04는 Input Register(읽기 전용 워드)를 읽습니다.

 

device_id란

Modbus에서 같은 버스에 여러 슬레이브 장치가 연결될 수 있는데, 어느 장치에 요청하는지를 구분하는 번호입니다.

TCP/IP 환경에서는 보통 1을 씁니다.

예전 pymodbus 버전에서 이걸 unit 또는 slave라고 불렀는데, 3.13에서 device_id로 이름이 바뀌었어요.

 

ModbusDeviceContext란

pymodbus로 Modbus 서버(시뮬레이터)를 만들 때 쓰는 클래스입니다.

서버가 가지고 있는 메모리를 정의합니다.

HR(Holding Register), IR(Input Register), CO(Coil), DI(Discrete Input) 네 종류를 따로 설정합니다.


첫 번째 문제 : API 이름이 바뀌었다

pymodbus 3.13에서 두 가지가 바뀌었습니다.

 

변경 1 : unit / slavedevice_id

# 3.6 이하 (이제 오류)
client.read_holding_registers(address=0, count=5, unit=1)
client.read_holding_registers(address=0, count=5, slave=1)

# 3.13 이상 (올바른 방법)
client.read_holding_registers(0, count=5, device_id=1)

unitslave로 바뀌었다가, 3.13에서 다시 device_id로 바뀌었어요.

버전별로 이름이 달라서 혼란스럽습니다.

 

변경 2 : count가 keyword-only 인자로 바뀌었다

# 이전 방식 (위치 인자로도 가능했음)
client.read_holding_registers(0, 5, device_id=1)  # 3.13에서 오류 가능

# 3.13 이후 (count는 반드시 키워드로)
client.read_holding_registers(0, count=5, device_id=1)

count를 위치 인자로 쓰면 3.13에서 예상대로 동작하지 않을 수 있습니다.

마이그레이션 체크리스트로 정리하면 이렇습니다.

# 바꿔야 할 것들
unit=N    → device_id=N
slave=N   → device_id=N
count=N 를 위치 인자로 쓴 경우 → count=N 키워드 인자로 명시

# 확인할 것
result.registers  (읽기 결과 접근 방식은 동일)
result.isError()  (오류 체크 방식 동일)

두 번째 문제 : hr/ir 값이 뒤집혀 나온다

이게 더 당황스러운 버그였습니다.

pymodbus로 Modbus 서버 시뮬레이터를 만들어서 PLCLink와 연결해 테스트했어요.

그런데 Holding Register(FC03)를 읽으면 Input Register 데이터가 나오고, Input Register(FC04)를 읽으면 Holding Register 데이터가 나왔습니다.

코드를 몇 번을 봐도 틀린 게 없어요.

HR에 [100, 200, 300]을 넣고 IR에 [10, 20, 30]을 넣었는데, FC03으로 읽으면 [10, 20, 30]이 나옵니다.

원인을 찾는 데 시간이 걸렸어요.

pymodbus 3.13에서 ModbusDeviceContext의 deprecated API를 쓰면 hr과 ir 키워드 인자가 내부적으로 역전되어 있었습니다.

# 문제가 있는 코드 (deprecated API)
store = ModbusDeviceContext(
    hr=ModbusSequentialDataBlock(1, hr_data),  # FC03 읽기 → ir_data 반환!
    ir=ModbusSequentialDataBlock(1, ir_data),  # FC04 읽기 → hr_data 반환!
    co=ModbusSequentialDataBlock(1, co_data),
    di=ModbusSequentialDataBlock(1, di_data),
)

hr= 에 넣은 데이터가 FC04(IR) 읽기 시 반환되고, ir=에 넣은 데이터가 FC03(HR) 읽기 시 반환됩니다.

정반대로 동작해요.

택배 물류센터 비유로 설명하면 이렇습니다.

창고 관리자가 "HR 선반에 A 상자, IR 선반에 B 상자를 넣어"라고 지시했는데, 내부 직원이 실수로 반대로 넣었어요.

그래서 HR 선반에서 꺼내달라고 하면 B 상자가 나오는 상황입니다.


hr/ir 역전 문제 해결 방법

두 가지 방법이 있습니다.

 

방법 1 : 시뮬레이터에서 hr과 ir을 교차로 전달 (임시방편)

# deprecated API를 계속 쓸 경우, 교차로 전달하면 정상 동작
store = ModbusDeviceContext(
    hr=ModbusSequentialDataBlock(1, ir_data),  # 의도적으로 교차
    ir=ModbusSequentialDataBlock(1, hr_data),  # 의도적으로 교차
    co=ModbusSequentialDataBlock(1, co_data),
    di=ModbusSequentialDataBlock(1, di_data),
)

이건 버그를 버그로 맞받아치는 방법입니다.

코드 가독성이 떨어지고, 나중에 pymodbus가 버그를 고치면 다시 틀려집니다.

 

방법 2 : 신규 API 사용 (권장)

from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext
from pymodbus.datastore import ModbusSequentialDataBlock

# 신규 API (역전 없음)
store = ModbusSlaveContext(
    hr=ModbusSequentialDataBlock(1, hr_data),  # FC03 정상 반환
    ir=ModbusSequentialDataBlock(1, ir_data),  # FC04 정상 반환
    co=ModbusSequentialDataBlock(1, co_data),
    di=ModbusSequentialDataBlock(1, di_data),
)
context = ModbusServerContext(slaves=store, single=True)

ModbusDeviceContext 대신 ModbusSlaveContext를 씁니다. 신규 API에서는 역전이 없어요.


PLCLink에서의 적용

PLCLink의 Modbus 클라이언트 코드에서 API 이름 변경을 적용했습니다.

# 변경 전
result = await asyncio.get_event_loop().run_in_executor(
    None,
    lambda: self._client.read_holding_registers(
        address=start, count=count, unit=1
    )
)

# 변경 후
result = await asyncio.get_event_loop().run_in_executor(
    None,
    lambda: self._client.read_holding_registers(
        start, count=count, device_id=1
    )
)

hr/ir 역전 문제는 실제 PLC와 연결할 때는 발생하지 않습니다.

실제 PLC는 내부에서 이미 FC03과 FC04를 구분해서 응답하거든요.

시뮬레이터를 직접 만들어서 테스트할 때만 나타나는 문제예요.

그래서 실기 테스트 전에 시뮬레이터로 먼저 검증하는 경우에 만날 수 있습니다.


정리 : pymodbus 3.13 마이그레이션 체크리스트

항목 3.6 이하 3.13 이상
슬레이브 ID 파라미터 unit=N 또는 slave=N device_id=N
count 인자 위치 인자 가능 반드시 count=N 키워드
서버 컨텍스트 ModbusDeviceContext ModbusSlaveContext 권장
hr/ir 동작 deprecated API에서 역전 신규 API에서 정상
결과 접근 result.registers 동일
오류 체크 result.isError() 동일

마치며

pymodbus는 활발하게 업데이트되는 라이브러리입니다.

덕분에 기능이 좋아지는데, 그만큼 버전 간 API 변경도 잦아요.

unitslave가 됐다가 device_id가 된 것처럼, 변경 이력이 꽤 복잡합니다.

오류 메시지가 나오면 파라미터 이름 변경인지 먼저 확인하는 게 빠릅니다.

hr/ir 역전 버그는 시뮬레이터를 직접 만들 때만 나타나는 희귀한 케이스입니다.

"코드는 맞는 것 같은데 값이 뒤집혀 나온다"면 ModbusDeviceContext의 deprecated API를 쓰고 있는지 확인해보세요.

반응형