계기와 수집
Modbus, MQTT 실무
수집기 개발은 프로토콜 문서와 현장 장비 사이의 차이를 메우는 일입니다. 스펙대로 짰는데 값이 이상하게 나오는 경우가 대부분 바이트 순서, 스케일, 주소 오프셋 문제입니다.
1. Modbus 기본
마스터(수집기)가 슬레이브(계기)에 요청하고 응답을 받는 폴링 방식입니다.
| 구분 | Modbus RTU | Modbus TCP |
|---|---|---|
| 물리 계층 | RS-485 시리얼 | 이더넷 |
| 주소 | Slave ID (1~247) | IP + Port(502) + Unit ID |
| 속도 | 9600~115200 bps | 네트워크 속도 |
| 동시 요청 | 한 라인에 한 번에 하나 | 연결별 가능 |
| 현장 | 분전반 계기 체인 | 게이트웨이, 최신 계기 |
RS-485 한 라인에 계기 여러 대를 데이지 체인으로 묶고, 컨버터(RS-485 → TCP)를 통해 서버가 읽는 구조가 흔합니다.
2. 레지스터와 펑션 코드
| 펑션 코드 | 대상 | 용도 |
|---|---|---|
| 01 | Coil | 디지털 출력 읽기 |
| 02 | Discrete Input | 디지털 입력 읽기 |
| 03 | Holding Register | 설정값, 계측값 읽기 (가장 많이 씀) |
| 04 | Input Register | 계측값 읽기 |
| 06 / 16 | Holding Register | 쓰기 |
- 레지스터 하나는 16bit
- 32bit float, int32는 레지스터 2개, 64bit 적산값은 4개를 씁니다
- 문서의 주소가 40001부터인지 0부터인지 확인하세요. 오프셋 1 차이로 엉뚱한 값을 읽는 일이 매우 흔합니다
3. 메모리맵
계기 제조사마다 메모리맵이 다릅니다. 코드에 하드코딩하지 말고 데이터로 관리합니다.
{
"model": "PM-3000",
"registers": [
{ "key": "kwh", "address": 3204, "type": "uint64", "scale": 0.001, "fc": 3 },
{ "key": "kw", "address": 3060, "type": "float32", "scale": 1, "fc": 3 },
{ "key": "pf", "address": 3084, "type": "float32", "scale": 1, "fc": 3 },
{ "key": "v_a", "address": 3028, "type": "float32", "scale": 1, "fc": 3 }
],
"byte_order": "big",
"word_order": "big"
}
collector_converter.memorymap,collector_gems.memorymap처럼 수집기에 맵을 연결해 두면 계기 모델이 바뀌어도 코드 수정이 없습니다- 연속된 주소는 한 번에 묶어 읽으세요(블록 읽기). 레지스터마다 요청하면 폴링 주기를 못 맞춥니다
4. 바이트 순서
32bit 값이 이상하게 크거나 작게 나오면 대부분 엔디안 문제입니다.
| 표기 | 순서 | 비고 |
|---|---|---|
| ABCD | Big endian, big word | 표준 |
| CDAB | Word swap | 제조사에서 자주 씀 |
| BADC | Byte swap | 드묾 |
| DCBA | Little endian | 드묾 |
from pymodbus.client import ModbusTcpClient
from pymodbus.payload import BinaryPayloadDecoder
from pymodbus.constants import Endian
client = ModbusTcpClient("192.168.0.10", port=502)
rr = client.read_holding_registers(3060, count=2, slave=1)
decoder = BinaryPayloadDecoder.fromRegisters(
rr.registers, byteorder=Endian.BIG, wordorder=Endian.LITTLE
)
kw = decoder.decode_32bit_float()
시운전 때 계기 LCD 표시값과 읽은 값을 나란히 놓고 하나씩 맞추는 것이 가장 빠릅니다.
5. MQTT 기본
현장 게이트웨이가 값을 브로커에 publish하고 서버가 subscribe하는 구조입니다. 공장망에서 서버로 들어오는 방화벽을 열 필요가 없어 보안팀이 선호합니다.
[계기] --Modbus--> [게이트웨이] --MQTT publish--> [브로커] --subscribe--> [수집 서버]
토픽 설계 예시:
enerone/{company_code}/{plant_code}/{collector_code}/data
enerone/{company_code}/{plant_code}/{collector_code}/status
페이로드 예시:
{
"ltid": "RTU-001",
"ts": "2026-10-22T09:15:00+09:00",
"ports": [
{ "port": 1, "kwh": 123456.78, "kw": 52.3 },
{ "port": 2, "kwh": 98765.43, "kw": 18.1 }
]
}
6. QoS와 유실
| QoS | 의미 | 에너지 데이터 |
|---|---|---|
| 0 | 최대 한 번 | 실시간 kW 표시용 |
| 1 | 최소 한 번 (중복 가능) | 사용량 데이터 권장 |
| 2 | 정확히 한 번 | 오버헤드 큼 |
- QoS 1이면 중복이 오므로 DB에
(meter_id, measured_at)unique로 멱등 처리합니다 - 게이트웨이에 로컬 버퍼(store and forward)가 있는지 확인하세요. 네트워크가 끊겼다 붙을 때 몰아서 들어옵니다
ts는 게이트웨이 시각입니다. 게이트웨이 NTP가 틀어지면 데이터 시각이 틀어집니다
7. 폴링 설계
- 폴링 주기(
polling_interval_sec)는 수집기 단위로 관리 - 타임아웃은 주기보다 짧게. 한 계기가 안 붙어도 다른 계기는 계속 읽어야 함
- 연속 실패 N회 시 통신 장애 상태로 전환하고 알람
- 측정 시각은 요청 시각을 주기에 맞춰 정렬(예: 09:15:00)해서 저장하면 집계가 쉬워짐
8. 현장 체크리스트
- 계기 모델과 메모리맵 문서 확보
- Slave ID, 통신 속도, 패리티 확인 (RTU)
- IP, 포트, Unit ID 확인 (TCP)
- 주소 오프셋, 바이트/워드 순서 확인
- 적산값 단위(Wh/kWh)와 스케일 확인
- 배율이 계기에 설정되어 있는지 확인
- 게이트웨이 NTP 동기화 확인
- 방화벽 방향(인바운드/아웃바운드) 확인