Files
HC900-Crawler/docs/베이직아키텍처-태그-디자인-전면재설계.md
windpacer d88784635e docs: 작업지시·진단·아키텍처 설계 문서 추가
온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:12:01 +09:00

34 KiB

베이직 아키텍처 · 태그 디자인 전면 재설계

본 문서는 HC900-AX 프로젝트의 전면 재설계 결과를 정의한다. 2026-06-08, 전면 개정.


목차

  1. 핵심 원칙
  2. HC900 Modbus 통신 규칙
  3. Sinam_Tag_all.xlsx 데이터 구조
  4. 주소 변환 규칙 (공식 검증 완료)
  5. 태그명 체계 및 등록 규칙
  6. register-map-cN.json 포맷
  7. 아카이브/모니터링 제어
  8. 컨트롤러별 독립 맵 전략
  9. 데이터 흐름 (전면 개정)
  10. build_register_map_from_sinam.py 처리 규칙
  11. C# 서비스 수정 사항
  12. tag_metadata 적재
  13. 알려진 문제
  14. 폐기 항목
  15. 마이그레이션 순서

1. 핵심 원칙

1.1 단일 진실 공급원 (Single Source of Truth)

  • docs/Sinam_Tag_all.xlsx 가 모든 태그 정보의 유일한 출처
  • HC Designer CSV (SignalTags.csv, Variables.csv, SummaryFunctionBlockReport.csv)는 주소 검증용 보조 자료
  • Experion OPC UA legacy (소문자 태그명, node_id 등)는 완전히 폐기

1.2 태그명 표기법

  • 모든 태그명은 대문자 (OPC UA 강제 소문자 규칙 폐기)
  • 구분자: - (하이픈) + . (파라미터)
    • 예: FICQ-6101.PV, P-9114.STATE, LI-9100.PV
  • Experion ItemName 기준 (FICQ-6101), HC900 네이티브명(FIQ6101) 사용 안 함

1.3 컨트롤러별 독립 맵

  • 각 HC900 컨트롤러(C1~C4)는 독립적인 register-map-cN.json 사용
  • config/gateway-config.json에서 각 컨트롤러의 enabled/disabled 관리
  • 웹 UI Setup 페이지에서 컨트롤러 추가/삭제/시작/중지 가능

1.4 레지스터 맵 엔트리 설계 원칙

  • Loop 블록 통째로(192 regs) 등록하지 않음 — 게이트웨이의 배치 그룹핑이 개별 엔트리를 쪼갤 수 없으므로 count > 120 인 단일 엔트리는 단일 FC03 호출로 192 regs 읽기를 시도하여 HC900에서 실패
  • 각 파라미터를 개별 엔트리로 등록 (PV=2regs, SP=2regs, OP=2regs, MODE=1reg, ...)
  • 게이트웨이가 주소 순서로 정렬 후 인접 엔트리들을 동적으로 ≤120 배치로 묶어서 FC03 호출

2. HC900 Modbus 통신 규칙

2.1 기본 프로토콜

항목 근거
전송 Modbus TCP modbus_tcp.cpp
HC900 IP 컨트롤러별 설정 gateway-config.json
포트 502 기본값
Unit ID 1 modbus_tcp.cpp:13
소켓 timeout 2초 modbus_tcp.cpp:14
Watchdog 5초 무응답 → Reconnecting modbus_tcp.cpp:15
Max retry 5회 → Fault (30초 후 자동 재시도) modbus_tcp.cpp:16
Float format FP B (IEEE 754 BigEndian, HighFirst, Normal) HC900 매뉴얼, vendor_formats.hpp

2.2 Function Codes

기능 FC 방향 구현
Read Holding Registers FC03 HC900 → Gateway modbus_tcp.cpp:171
Write Multiple Registers FC16 (0x10) Gateway → HC900 modbus_tcp.cpp:251

2.3 배치 읽기 규칙 (C++ 게이트웨이 자동 수행)

MAX_BATCH = 120 연속 레지스터  (gateway.cpp:126)

게이트웨이의 ReadAllRegisters()는 런타임에 register-map 엔트리들을 주소 순서로 정렬한 후, 동일한 120-register 윈도우 안에 들어가는 엔트리들을 한 번의 FC03로 묶어서 읽음:

// gateway.cpp:124-174 — 동적 배치 그룹핑
while (i < sorted_indices_.size()) {
    uint32_t batch_start = registers_[sorted_indices_[i]].addr;
    size_t j = i;
    while (j < sorted_indices_.size()) {
        const auto& e = registers_[sorted_indices_[j]];
        if (e.addr + e.count - batch_start > MAX_BATCH) break;
        ++j;
    }
    // FC03 read_raw(batch_start, read_count)
    // 각 엔트리를 버퍼에서 개별 decode
    i = j;
}

중요: 개별 엔트리의 addr + count가 120을 넘으면 단일 FC03로 처리되므로, 192-regs 통째 등록은 불가능. 각 파라미터는 count=1(uint16) 또는 2(float32)로 등록해야 함.

2.4 Loop 블록 구조 (매뉴얼 Table 6-3)

각 PID 루프는 256개 레지스터 블록 (루프 블록 크기 = 192 regs, 0x40~0xFF).

Offset 파라미터 타입 본문 접근
0x00 PV float32 R
0x02 Remote SP (SP2) float32 R
0x04 Working Set Point (SP) float32 RW
0x06 Output (OP) float32 RW
0x0C Gain #1 float32 RW
0x0E Direction float32 R
0x10 Reset #1 float32 RW
0x12 Rate #1 float32 RW
0x14 Cycle Time #1 float32 R
0x16 PV Low Range float32 R
0x18 PV High Range float32 R
0x1A Alarm #1 SP #1 (AL1SP1) float32 RW
0x1C Alarm #1 SP #2 (AL1SP2) float32 RW
0x20 Gain #2 float32 RW
0x2A Local SP #1 (LSP1) float32 RW
0x2C Local SP #2 (LSP2) float32 RW
0x2E Alarm #2 SP #1 (AL2SP1) float32 RW
0x30 Alarm #2 SP #2 (AL2SP2) float32 RW
0x34 SP Low Limit float32 RW
0x36 SP High Limit float32 RW
0x3A Output Low Limit float32 RW
0x3C Output High Limit float32 RW
0x3E Working Output (OPWORK) float32 RW
0x46 Ratio float32 RW
0x48 Bias float32 RW
0x4A Deviation float32 R
0x4E Manual Reset float32 RW
0x50 Feedforward Gain float32 RW
0x52 Local %CO float32 RW
0xB7 Fuzzy Enable uint16 RW
0xB8 Autotune Request uint16 RW
0xB9 Anti-soot Enable uint16 RW
0xBA Auto/Manual State (MODE write target) uint16 RW
0xBB SP Select State uint16 RW
0xBC Remote/Local SP State uint16 RW
0xBE Loop Status (MODE read source) uint16 R

2.5 루프 주소 계산

Loop 1~24:   base = 0x0040 + (N-1) * 0x0100
Loop 25~32:  base = 0x7840 + (N-25) * 0x0100

검증 완료 (SummaryFunctionBlockReport.csv):

Loop CSV 주소 계산 일치
#1 0x0040 0x0040 + 0*0x0100
#11 0x0A40 0x0040 + 10*0x0100
#18 0x1140 0x0040 + 17*0x0100
#24 0x1740 0x0040 + 23*0x0100
#25 0x7840 0x7840 + 0*0x0100
#32 0x7F40 0x7840 + 7*0x0100

2.6 주소 영역

영역 시작 설명
Misc 0x0000 0x003F 기타 파라미터
Loop 1~24 0x0040 0x15FF 루프 블록 (각 256 regs)
Loop 25~32 0x7840 0x78FF 루프 블록 확장
Analog Input 0x1800 0x187F AI 값 (Function Code 03)
Variable 0x18C0 0x1D6F R/W 변수 (MATH_VAR)
Signal Tag 0x2000 0x27CF R 태그 (TAG)

3. Sinam_Tag_all.xlsx 데이터 구조

3.1 파일 개요

  • 1개 시트 (Sheet1)
  • 3407 데이터 행, 150 컬럼
  • 다중 섹션 구조

3.2 Section 구성

Section 행 범위 헤더 첫 컬럼 설명
1 (ItemName) Row 2~ ItemName 메인 태그 정의 (AnalogPoint, StatusPoint, HistoryParameters)
2+ (ParentItemName) Row 1163~4424 ParentItemName FlexibleParameters 확장 속성

3.3 Section 1: ItemName — 주요 컬럼

컬럼명 0-indexed 예시 설명
ItemName 0 FICQ-6101 Experion 태그명 (대문자)
Class 1 AnalogPoint / StatusPoint / HistoryParameters 태그 클래스
ItemDescription 5 FICQ-6101 PV 설명
DownloadedName 6 FICQ-6101 다운로드명
AreaCode 11 P6 영역 코드
TrendParameter 18 True / PV / False Experion 트렌드 대상 (참고용, 우리 시스템과 무관)
SourceAddressPV 29 C3 LOOP 11 PV / C4 TAG 32 VALUE PV의 HC900 주소
SourceAddressOP 30 C3 LOOP 11 OPWORK / C4 MATH_VAR 5 VALUE OP의 HC900 주소
SourceAddressMD 31 C3 LOOP 11 LOOPSTAT MODE의 HC900 주소
DescriptorState0~7 61~68 RUN, STOP, FAULT 상태 레이블 (StatusPoint 한정)
Units (AnalogPoint) kg/h 엔지니어링 단위
RangeLow / RangeHigh (AnalogPoint) 0 / 100 측정 범위

3.4 Section 2+: ParentItemName (FlexibleParameters)

컬럼명 0-indexed 예시 설명
ParentItemName 0 FICQ-6101 부모 태그명
Class 1 FlexibleParameters 고정
ParamName 2 QV, RST, STATE, AL1SP1, HZ 확장 파라미터명
TypeDatabaseReference 5 16 bit signed integer (INT2) 데이터타입
ScanPeriodPVUDSP 25 2 스캔 주기 (초)
UnitsUDSP 26 kg 단위
RangeHighUDSP / RangeLowUDSP 27/28 범위
StatusNumStatesUDSP 32 2 상태 개수
DescriptorState0~7UDSP 33~40 OFF, ON 상태 레이블 (Status 한정)
DestinationAddressPVUDSP 23 C3 MATH_VAR 37 VALUE 쓰기 주소 (있으면 RW, 없으면 R)

3.5 SourceAddress 형식

형식 예시 변환 규칙
C{N} TAG {N} VALUE C4 TAG 32 VALUE addr = 0x2000 + (N-1)*2, access=R
C{N} MATH_VAR {N} VALUE C4 MATH_VAR 5 VALUE addr = 0x18C0 + (N-1)*2, access=RW
C{N} LOOP {N} {PARAM} C3 LOOP 11 PV 루프 블록 내 파라미터 오프셋
C{N} LOOPX {N} {PARAM} C3 LOOPX 25 WSP LOOPX = Loop 25~32와 동일 주소 체계

4. 주소 변환 규칙 (공식 검증 완료)

Sinam_Tag_all.xlsx의 SourceAddress 문자열을 HC900 Modbus 주소로 변환하는 공식:

4.1 TAG N → Modbus 주소

addr = 0x2000 + (N-1) * 2

검증: CSV TAG 4 = FIQ6101 @ 0x2006

4.2 MATH_VAR N → Modbus 주소

addr = 0x18C0 + (N-1) * 2

검증: CSV VAR 2 = LT3211_LSET @ 0x18C2

4.3 LOOP N PARAM → Modbus 주소

base = loop_base(N)
off  = MNEMONIC_OFFSET[PARAM]
addr = base + off

검증: C2 LOOP 18 WSP → base=0x1140, off=0x04 → addr=0x1144

4.4 Mnemonic → Offset 매핑

Mnemonic Offset Experion Attr
PV 0x00 PV
RSP / SP2 0x02
WSP / SPWORK 0x04 SP
OP / OPWORK 0x06 OP
GAIN1 / PROP1 0x0C GAIN
DIR 0x0E
RESET1 0x10 RESET
RATE1 0x12 RATE
PVLOW 0x16
PVHIGH 0x18
AL1SP1 0x1A
AL1SP2 0x1C
LSP1 0x2A
LSP2 0x2C
AL2SP1 0x2E
SPLOW 0x34 SP_LO
SPHIGH 0x36 SP_HI
OPLOW 0x3A OP_LO
OPHIGH 0x3C OP_HI
OPWORK 0x3E OP
DEV 0x4A
LOOPSTAT 0xBE MD (읽기)
MODEIN 0xBA MD (쓰기)
AMSTAT 0xBA

⚠️ MODE 주의: 읽기는 LOOPSTAT(0xBE), 쓰기는 Auto/Man State(0xBA). build_register_map_from_sinam.py 초기 버전에서 write_addr이 0xBE로 잘못 설정된 버그 있음 → 수정 완료.


5. 태그명 체계 및 등록 규칙

5.1 태그명 포맷

{Experion-ItemName}.{Parameter}
유형 태그명 예시 출처
Loop PV FICQ-6101.PV ItemName + Mnemonic
Loop SP FICQ-6101.SP WSP/SPWORK → SP
Loop OP FICQ-6101.OP OP/OPWORK → OP
Loop MD FICQ-6101.MD LOOPSTAT(읽기)/MODEIN(쓰기) → MD
Loop GAIN FICQ-6101.GAIN GAIN1 → GAIN
Loop RESET FICQ-6101.RESET RESET1 → RESET
Loop RATE FICQ-6101.RATE RATE1 → RATE
Loop 기타 FICQ-6101.Deviation HC900명 유지
AnalogPoint (TAG source) LI-9100 Class=AnalogPoint, PV source=TAG
StatusPoint PV (TAG) P-9114 Class=StatusPoint, PV source=TAG
StatusPoint OP (MATH_VAR) P-9114.OP Class=StatusPoint, OP source=MATH_VAR
FlexibleParameter FICQ-6101.QV / P-9114.STATE ParentItemName.ParamName

5.2 Loop 파라미터 Experion명 매핑

Loop 블록 내 모든 파라미터 중 Experion이 노출하는 것만 Experion 속성명 사용, 나머지는 HC900 네이티브명 유지:

LOOP_LAYOUT 오프셋 → EXPERION_ATTR 매핑:
  0x00(PV)     → .PV
  0x04(WSP)    → .SP
  0x06(OP)     → .OP
  0x0C(GAIN1)  → .GAIN
  0x10(RESET1) → .RESET
  0x12(RATE1)  → .RATE
  0xBE(LOOPSTAT) → .MD
  그 외        → HC900명 유지 (예: .Deviation, .PV_LowRange)

5.3 FlexibleParameter 등록

Section 2+에서 ParentItemName.ParamName 으로 등록:

FlexibleParameter 등록명 비고
FICQ-6101 / QV FICQ-6101.QV Quality Value (적산)
FICQ-6101 / RST FICQ-6101.RST Reset
FICQ-6101 / AL1SP1 FICQ-6101.AL1SP1 Alarm Setpoint
P-9114 / STATE P-9114.STATE 운전 상태
P-9114 / HZ P-9114.HZ 주파수
P-9114 / HZSET P-9114.HZSET 주파수 설정

6. register-map-cN.json 포맷

6.1 엔트리 구조

{
  "tag": "FICQ-6101.PV",
  "addr": 2624,
  "write_addr": 2624,
  "count": 2,
  "type": "float32",
  "access": "R",
  "description": "LOOP #11 PV",
  "archive": true
}
필드 타입 설명
tag string 태그명 (Experion ItemName + 파라미터)
addr uint16 Modbus holding register 주소 (0-based)
write_addr uint16 쓰기 주소 (기본값 = addr, MODE 등 별도 쓰기 레지스터가 있는 경우 다름)
count uint16 레지스터 수 (float32=2, uint16=1)
type string 데이터 타입 (float32 또는 uint16)
access string 접근 권한 (R 또는 RW)
description string 설명
archive bool history_table 기록 대상 여부

6.2 Loop 엔트리 (개별 파라미터)

{
  "tag": "FICQ-6101.PV",    "addr": 2624, "count": 2, "type": "float32", "access": "R",  "archive": true,
  "tag": "FICQ-6101.SP",    "addr": 2628, "count": 2, "type": "float32", "access": "RW", "archive": true,
  "tag": "FICQ-6101.OP",    "addr": 2630, "count": 2, "type": "float32", "access": "RW", "archive": true,
  "tag": "FICQ-6101.MD",    "addr": 2750, "count": 1, "type": "uint16",  "access": "R",  "archive": true,
  "tag": "FICQ-6101.GAIN",  "addr": 2636, "count": 2, "type": "float32", "access": "RW", "archive": false,
  "tag": "FICQ-6101.RESET", "addr": 2640, "count": 2, "type": "float32", "access": "RW", "archive": false,
  ...
}

6.3 일반 TAG 엔트리

{
  "tag": "LI-9100",
  "addr": 8194,
  "write_addr": 8194,
  "count": 2,
  "type": "float32",
  "access": "R",
  "description": "Signal Tag #2",
  "archive": true
}

6.4 Variable 엔트리 (MATH_VAR)

{
  "tag": "P-9114.OP",
  "addr": 6280,
  "write_addr": 6280,
  "count": 2,
  "type": "float32",
  "access": "RW",
  "description": "Variable (MATH_VAR) #5",
  "archive": false
}

6.5 FlexibleParameter 엔트리

{
  "tag": "FICQ-6101.QV",
  "addr": 8198,
  "write_addr": 8198,
  "count": 2,
  "type": "float32",
  "access": "R",
  "description": "FlexibleParameter QV",
  "archive": true
}

7. 아카이브/모니터링 제어

7.1 문제 정의

현재 시스템은 다음과 같이 동작:

hc900_map_master WHERE is_active = TRUE
  → realtime_table upsert (1초)
    → history_table snapshot (60초, digital 제외)

문제점:

  • realtime_table: GAIN1, RESET1, AL1SP1 등 불필요한 태그까지 표시
  • history_table: 아카이브할 가치 없는 태그까지 60초마다 기록 (GAIN1, BIAS, RATIO, DEV 등)
  • is_active 하나로만 제어 → 실시간 표시와 이력 기록을 분리 불가

7.2 대상 선별 규칙

태그 유형 예시 realtime history 근거
Loop PV FICQ-6101.PV 공정값, 추세 필수
Loop SP FICQ-6101.SP 설정값 변경 이력
Loop OP FICQ-6101.OP 출력값 추세
Loop MODE FICQ-6101.MD Auto/Man 전환 이력
Loop QV FICQ-6101.QV Quality Value 추세
AnalogPoint PV LI-9100 일반 공정값
StatusPoint PV P-9114 상태 변화 (event_history_table)
Loop 기타 파라미터 FICQ-6101.GAIN1, .RESET1, .RATE1, .AL1SP1, .DIR, .PV_LO, .PV_HI, .SP_LO, .SP_HI, .DEV 설정값, 실시간 확인만
Variable (MATH_VAR) P-9114.RST, FICQ-6101.RST 중간 계산값, 불필요
StatusPoint OP P-9114.OP MATH_VAR 기반
기타 FlexibleParameter P-9114.STATE, FICQ-6101.AL1SP1 필요시 사용자 활성화

7.3 archive 플래그 선별 로직

def should_archive(kind: str, param_name: str | None) -> bool:
    if kind == 'loop':
        return param_name in ('PV', 'SP', 'OP', 'MD', 'QV')
    if kind == 'tag':        # AnalogPoint / StatusPoint TAG source
        return True
    if kind in ('var', 'raw'):
        return False
    if kind == 'flexparam':
        return False
    return False

7.4 hc900_map_master 컬럼 추가

ALTER TABLE hc900_map_master
  ADD COLUMN IF NOT EXISTS realtime_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  ADD COLUMN IF NOT EXISTS archive_enabled  BOOLEAN NOT NULL DEFAULT FALSE;
컬럼 설명 기본값
realtime_enabled realtime_table 표시 여부 TRUE (모든 활성 태그)
archive_enabled history_table 기록 여부 register-map의 archive 필드

사용자는 PointBuilder UI에서 두 플래그를 개별적으로 override 가능:

태그명 실시간 이력
FICQ-6101.PV
FICQ-6101.SP
FICQ-6101.OP
FICQ-6101.MD
FICQ-6101.QV
FICQ-6101.GAIN1
LI-9100
P-9114
P-9114.RST

7.5 마이그레이션

-- 신규 컬럼 추가
ALTER TABLE hc900_map_master
  ADD COLUMN IF NOT EXISTS realtime_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  ADD COLUMN IF NOT EXISTS archive_enabled  BOOLEAN NOT NULL DEFAULT FALSE;

-- build_register_map_from_sinam.py 가 archive=true 인 태그 UPDATE
-- (스크립트가 hc900_map_master upsert 시 archive_enabled 도 함께 설정)

8. 컨트롤러별 독립 맵 전략

8.1 컨트롤러 구성

컨트롤러 IP 포트 gRPC Port 현재 상태
C1 192.168.0.250 502 50051 미연결
C2 192.168.0.230 502 50052 미연결
C3 192.168.0.240 502 50053 활성
C4 192.168.0.220 502 50054 미연결

8.2 설정 파일

// config/gateway-config.json
{
  "controllers": [
    { "id": "C1", "host": "192.168.0.250", "port": 502,
      "map_path": "docs/register-map-c1.json", "grpc_port": 50051, "enabled": false },
    { "id": "C2", "host": "192.168.0.230", "port": 502,
      "map_path": "docs/register-map-c2.json", "grpc_port": 50052, "enabled": false },
    { "id": "C3", "host": "192.168.0.240", "port": 502,
      "map_path": "docs/register-map-c3.json", "grpc_port": 50053, "enabled": true },
    { "id": "C4", "host": "192.168.0.220", "port": 502,
      "map_path": "docs/register-map-c4.json", "grpc_port": 50054, "enabled": false }
  ]
}

8.3 컨트롤러 등급별 구분

각 컨트롤러는 독립된 C++ 게이트웨이 프로세스로 동작:

  • 각자의 register-map-cN.json 로드
  • 각자의 IP/Port로 Modbus TCP 연결
  • 각자의 gRPC 포트에서 서비스
  • C# ControllerGrpcClientPool이 enabled 컨트롤러별로 클라이언트 생성

9. 데이터 흐름 (전면 개정)

9.1 전체 흐름

Sinam_Tag_all.xlsx  (단일 진실 공급원)
        │
        ▼
build_register_map_from_sinam.py
  ├─ 주소 변환 (TAG/MATH_VAR/LOOP)
  ├─ 컨트롤러별 분할
  ├─ archive 플래그 설정
  ├─ tag_metadata upsert (상태 레이블, 단위, desc, area)
  └─ hc900_map_master upsert (is_active, realtime_enabled, archive_enabled)
        │
        ▼  각 컨트롤러별
register-map-c1.json  ──── C++ Gateway C1 ── gRPC:50051
register-map-c2.json  ──── C++ Gateway C2 ── gRPC:50052
register-map-c3.json  ──── C++ Gateway C3 ── gRPC:50053
register-map-c4.json  ──── C++ Gateway C4 ── gRPC:50054
        │
        ▼
Hc900RealtimeService (C# BackgroundService)
  1. hc900_map_master WHERE is_active=TRUE
     AND realtime_enabled=TRUE AND controller_id=$1
  2. client.ReadTagsAsync(tagNames)  ← 1초 간격
  3. BatchUpdateRealtimeTableAsync()
     → INSERT INTO realtime_table
       (controller_id, tagname, node_id, livevalue, timestamp)
       ON CONFLICT (controller_id, tagname) DO UPDATE ...
        │
        ▼
realtime_table  (최신값, tag당 1행, 1초 갱신)
        │
        ▼  60초 간격
Hc900HistoryService (C# BackgroundService)
  1. hc900_map_master WHERE archive_enabled=TRUE
  2. realtime_table JOIN archive_enabled → tagged points
  3. stamp DateTime.UtcNow
  4. INSERT INTO history_table (TimeScaleDB hypertable)
        │
        ▼
event_history_table  (Hc900DigitalEventDetectorService)
  └─ StatusPoint PV 변화 감지 → ALARM/TRIP/RUN/NORMAL

9.2 C++ 게이트웨이 상세

C++ Gateway (hc900_gateway)
  LoadRegisterMap()
    → JSON 파싱 → registers_[] + tag_index_[]
    → sorted_indices_[] (addr 정렬)
  
  PollLoop()
    → ReadAllRegisters():
        registers_를 addr 순서로 스캔
        ≤120 regs 윈도우에 들어가는 엔트리들 배치
        → controller_->read_raw(batch_start, read_count)
        → 각 엔트리 decode_float(FP_B) → cache_[tag_name]
  
  gRPC ReadTags()
    → cache_ 에서 즉시 반환 (Modbus I/O 없음)
  
  gRPC WriteTag(tag, value)
    → transport_mutex_ 획득
    → controller_->write_raw(entry.write_addr, ...)  ← FC16
    → cache_ 업데이트
  
  gRPC HealthCheck()
    → 연결 상태, poll_count, last_poll_duration

10. build_register_map_from_sinam.py 처리 규칙

10.1 실행 명령어

python3 scripts/build_register_map_from_sinam.py \
    --sinam docs/Sinam_Tag_all.xlsx \
    --controller C3 \
    -o docs/register-map-c3.json \
    [--db-conn "Host=...;Database=hc900;..."]

10.2 처리 순서

  1. Sinam_Tag_all.xlsx 로드 (openpyxl, read_only=True, data_only=True)
  2. Section 1 처리 (ItemName 행):
    • 각 행의 모든 셀에서 정규식으로 C{N} LOOP/TAG/MATH_VAR/LOOPX/RAW 검색
    • scan_point() 분류: loop / tag / var / raw
    • Loop: build_loop_entries() → 모든 LOOP_LAYOUT 파라미터를 개별 엔트리로 전개
    • Tag: build_point_entry() → 단일 엔트리
    • Var: build_point_entry() → 단일 엔트리
  3. Section 2+ 처리 (FlexibleParameters 행):
    • resolve_one() 으로 개별 주소 해석
    • ParentItemName.ParamName 태그명 생성
  4. archive 플래그 설정 (should_archive())
  5. 컨트롤러별 필터링 (--controller 인자)
  6. 주소 순서 정렬 후 JSON 출력
  7. 선택적 DB 적재:
    • tag_metadata upsert (상태 레이블, 단위, desc, area)
    • hc900_map_master upsert (is_active, realtime_enabled, archive_enabled)

10.3 Loop 엔트리 전개 상세

build_loop_entries(item_name, loop_no, mnemonics):

입력: item_name="FICQ-6101", loop_no=11, mnemonics={PV, WSP, OPWORK, LOOPSTAT}
base = loop_base(11) = 0x0A40 = 2624

LOOP_LAYOUT 각 offset을 순회:
  off=0x00 → suffix="PV", attr=EXPERION_ATTR.get("PV")="PV"
    → tag="FICQ-6101.PV", addr=2624, archive=true
  off=0x04 → suffix="WSP", attr=EXPERION_ATTR.get("WSP")="SP"
    → tag="FICQ-6101.SP", addr=2628, archive=true
  off=0x06 → suffix="Output", attr=EXPERION_ATTR.get("OPWORK")="OP"
    → tag="FICQ-6101.OP", addr=2630, archive=true
  off=0xBE → has_mode=True → 별도 .MD 엔트리
    → tag="FICQ-6101.MD", addr=2750 (=2624+0xBE),
       write_addr=2746 (=2624+0xBA), archive=true

  나머지 LOOP_LAYOUT 파라미터는 HC900명 유지:
  off=0x0C → tag="FICQ-6101.Gain1_PropBand1", addr=2636, archive=false
  off=0x10 → tag="FICQ-6101.Reset1", addr=2640, archive=false
  ...

10.4 MODE write_addr 처리

# Loop Status 0xBE → read source
# Auto/Man State 0xBA → write target
if has_mode:
    entries.append({
        'tag': f'{item_name}.MD',
        'addr': base + 0xBE,            # 읽기 = Loop Status
        'write_addr': base + 0xBA,       # 쓰기 = Auto/Man State ⚠️
        'count': 1,
        'type': 'uint16',
        'access': 'R',                   # 읽기전용 (게이트웨이가 MD 쓰기를 MODEIN으로 라우팅)
        'description': f'LOOP #{n} Mode status',
        'archive': True,
    })

⚠️ 초기 버전에서 write_addr = base + 0xBE로 잘못 설정된 버그 수정 완료. 올바른 값: 읽기=0xBE(LoopStatus), 쓰기=0xBA(Auto/Man State).

10.5 archive 플래그 설정

def should_archive(kind: str, is_loop: bool, param_name: str | None,
                   access: str, is_signal_tag: bool) -> bool:
    """history_table 기록 대상 선별"""
    if is_loop and param_name in ('PV', 'SP', 'OP', 'MD', 'QV'):
        return True
    if not is_loop and access == 'R' and is_signal_tag:
        return True      # AnalogPoint / StatusPoint TAG source (PV)
    return False

11. C# 서비스 수정 사항

11.1 Hc900MapEntry 엔티티

// src/Core/Domain/Entities/Hc900Entities.cs
[Table("hc900_map_master")]
public class Hc900MapEntry
{
    [Key] public int Id { get; set; }
    
    [Column("tagname")]          public string TagName        { get; set; } = "";
    [Column("hc900_tag")]        public string Hc900Tag       { get; set; } = "";
    [Column("modbus_addr")]      public int    ModbusAddr     { get; set; }
    [Column("data_type")]        public string DataType       { get; set; } = "float32";
    [Column("access")]           public string Access         { get; set; } = "R";
    [Column("is_active")]        public bool   IsActive       { get; set; } = true;
    [Column("controller_id")]    public string ControllerId   { get; set; } = "HC1";
    [Column("realtime_enabled")] public bool   RealtimeEnabled { get; set; } = true;  // 신규
    [Column("archive_enabled")]  public bool   ArchiveEnabled  { get; set; } = false; // 신규
}

11.2 Hc900RealtimeService.LoadMappingAsync

// 변경 전
"SELECT tagname, hc900_tag FROM hc900_map_master WHERE is_active = TRUE AND controller_id = $1"

// 변경 후
"SELECT tagname, hc900_tag FROM hc900_map_master WHERE is_active = TRUE AND realtime_enabled = TRUE AND controller_id = $1"

11.3 Hc900HistoryService.SnapshotToHistoryAsync

// 변경 전
var digitalTagNames = await GetDigitalTagNamesCachedAsync();
var points = await _ctx.RealtimePoints
    .Where(p => !digitalTagNames.Contains(p.TagName))
    .ToListAsync();

// 변경 후
var archiveTags = await _ctx.Hc900MapEntries
    .Where(m => m.ArchiveEnabled)
    .Select(m => m.TagName)
    .ToListAsync();

var points = await _ctx.RealtimePoints
    .Where(p => archiveTags.Contains(p.TagName))
    .ToListAsync();

11.4 DDL (멱등)

await _ctx.Database.ExecuteSqlRawAsync("""
    ALTER TABLE hc900_map_master
    ADD COLUMN IF NOT EXISTS realtime_enabled BOOLEAN NOT NULL DEFAULT TRUE,
    ADD COLUMN IF NOT EXISTS archive_enabled  BOOLEAN NOT NULL DEFAULT FALSE
    """);

12. tag_metadata 적재

build_register_map_from_sinam.py 실행 시 --db-conn 옵션으로 tag_metadata도 함께 upsert:

attribute 출처 (Sinam_Tag_all.xlsx)
desc ItemDescription 또는 DownloadedName
area AreaCode (예: P6, P9)
state0~7 DescriptorState0~7 (StatusPoint) / DescriptorState0~7UDSP (FlexibleParameters)
units Units (AnalogPoint) / UnitsUDSP (FlexibleParameters)
eulo / euhi RangeLow / RangeHigh (AnalogPoint) / RangeLowUDSP / RangeHighUDSP (FlexibleParameters)
INSERT INTO tag_metadata (base_tag, attribute, value, controller_id)
VALUES (%s, %s, %s, %s)
ON CONFLICT (base_tag, attribute, controller_id)
DO UPDATE SET value = EXCLUDED.value, loaded_at = NOW()

13. 알려진 문제

13.1 MODE write_addr 버그 (수정 완료)

  • build_register_map_from_sinam.py 초기 버전: MODE 엔트리의 write_addr = base + 0xBE (LoopStatus, 읽기전용)
  • 올바름: write_addr = base + 0xBA (Auto/Man State, R/W)
  • 영향: 게이트웨이를 통한 MODE 쓰기(MAN→AUTO 전환)가 HC900에 반영되지 않음
  • 해결: 개정 코드에서 MD_WRITE_OFFSET = 0xBA 사용

13.2 LOOPX (Custom Map) 주소 검증

  • C4 LOOPX 25 AL1SP1 등의 Custom Map 영역
  • HC Designer Custom Map 보고서 필요 (현재 SummaryFunctionBlockReport.csv는 Fixed Map 기준)
  • 주소 체계 검증: LOOPX 2532는 Loop 2532와 동일한 0x7840 + (N-25)*0x0100 사용한다고 가정

13.3 C4 PID Loop 데이터 부재

  • FICQ-9101, FICQ-9214 등 C4 루프 태그는 Sinam_Tag_all.xlsx에 SourceAddress가 있음
  • HC Designer CSV는 C4(Custom Map) PID Loop 데이터 없음 (빈 맵)
  • PointBuilder로 수동 등록 또는 HC Designer Custom Map Export 필요

13.4 Config 게이트웨이 경로

  • gateway-config.json이 구버전 register-map-c3.json(2189개 엔트리, HC900명) 참조 중
  • 신규 register-map-c3.json(Experion명) 생성 후 경로 업데이트 필요

13.5 hc900_map_master.tagname = hc900_tag 중복

  • 현재 tagname(소문자 OPC UA 명)과 hc900_tag(대문자 Experion 명)가 동일해짐
  • 향후 tagname 컬럼 제거하고 hc900_tag로 통일 가능
  • 마이그레이션 동안은 두 컬럼 모두 동일한 값(Experion 대문자명) 유지

14. 폐기 항목

항목 사유 대체
load_state_labels.py Sinam_Tag_all.xlsx 통합 build_register_map_from_sinam.py에 통합
HC Designer CSVs (SignalTags.csv 등) 주소 검증용 보조 자료 Sinam_Tag_all.xlsx가 주 소스
OPC UA node_id HC900 직결 방식에서 불필요 빈 문자열로 저장
소문자 태그명 (ficq-6101.pv) OPC UA 강제 규칙 폐기 대문자 (FICQ-6101.PV)
build_register_map.py (기존) CSV 의존, Experion명 미지원 build_register_map_from_sinam.py
build_register_map_from_csv.py CSV Full Map 단순 변환 Sinam 기반 스크립트로 대체
개별 루프 파라미터 → 루프 블록 통째 게이트웨이 MAX_BATCH=120 제약 개별 파라미터 유지 (게이트웨이가 동적 배치)

15. 마이그레이션 순서

Phase A: 스크립트 완성

단계 작업 담당
A1 build_register_map_from_sinam.py MODE write_addr 버그 수정 (0xBE→0xBA) Python
A2 build_register_map_from_sinam.pyarchive 필드 추가 + 선별 로직 구현 Python
A3 build_register_map_from_sinam.py에 tag_metadata upsert 로직 추가 Python
A4 build_register_map_from_sinam.py에 hc900_map_master upsert 로직 추가 Python
A5 C1~C4 전 컨트롤러 register-map 생성 테스트 Python

Phase B: DB 마이그레이션

단계 작업 담당
B1 hc900_map_masterrealtime_enabled, archive_enabled DDL 추가 C#
B2 Phase A에서 생성한 register-map으로 hc900_map_master 초기 적재 Python

Phase C: C# 서비스 수정

단계 작업 담당
C1 Hc900MapEntryRealtimeEnabled, ArchiveEnabled 속성 추가 C#
C2 Hc900RealtimeService.LoadMappingAsyncAND realtime_enabled = TRUE 추가 C#
C3 SnapshotToHistoryAsyncarchive_enabled 필터 적용 C#
C4 PointBuilder UI에 실시간/이력 토글 컬럼 추가 C#

Phase D: 배포

단계 작업 담당
D1 C++ 게이트웨이 신규 register-map-c3.json으로 교체 Config
D2 gateway-config.json 경로 업데이트 Config
D3 C# 서비스 재시작 Ops
D4 기존 archive_enabled = FALSE → register-map 기준 UPDATE SQL

관련 파일

파일 설명
docs/Sinam_Tag_all.xlsx 단일 진실 공급원 — 모든 태그 정의 + 메타데이터
docs/51-52-25-111-HC900-Process-Controller-Communications-manual.pdf HC900 통신 매뉴얼 (Table 6-1, 6-3)
docs/register-map-cN.json 컨트롤러별 레지스터 맵 (build_register_map_from_sinam.py 생성)
config/gateway-config.json 게이트웨이 프로세스 설정 (IP, port, 맵 경로)
scripts/build_register_map_from_sinam.py 단일 빌드 스크립트 — 맵 생성 + 메타데이터 + archive 플래그
src/Infrastructure/Hc900/Hc900RealtimeService.cs 실시간 폴링 서비스 (gRPC → realtime_table)
src/Infrastructure/Hc900/Hc900HistoryService.cs 이력 스냅샷 서비스 (realtime_table → history_table)
src/Infrastructure/Database/Hc900DbContext.cs DB 컨텍스트 (테이블 정의, 마이그레이션 + SnapshotToHistoryAsync)
src/Core/Domain/Entities/Hc900Entities.cs Hc900MapEntry 엔티티 (hc900_map_master 매핑)
industrial-comm/cpp/src/gateway.cpp C++ 게이트웨이 (Modbus 폴링 + gRPC + 동적 배치 그룹핑)
industrial-comm/cpp/src/modbus_tcp.cpp Modbus TCP 전송 계층 (FC03, FC16)
industrial-comm/cpp/src/vendor_formats.hpp FP_B float format (bigEndian, highFirst, normal)