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

911 lines
34 KiB
Markdown

# 베이직 아키텍처 · 태그 디자인 전면 재설계
> 본 문서는 HC900-AX 프로젝트의 전면 재설계 결과를 정의한다.
> 2026-06-08, 전면 개정.
---
## 목차
1. [핵심 원칙](#1-핵심-원칙)
2. [HC900 Modbus 통신 규칙](#2-hc900-modbus-통신-규칙)
3. [Sinam_Tag_all.xlsx 데이터 구조](#3-sinam_tag_allxlsx-데이터-구조)
4. [주소 변환 규칙 (공식 검증 완료)](#4-주소-변환-규칙-공식-검증-완료)
5. [태그명 체계 및 등록 규칙](#5-태그명-체계-및-등록-규칙)
6. [register-map-cN.json 포맷](#6-register-map-cnjson-포맷)
7. [아카이브/모니터링 제어](#7-아카이브모니터링-제어)
8. [컨트롤러별 독립 맵 전략](#8-컨트롤러별-독립-맵-전략)
9. [데이터 흐름 (전면 개정)](#9-데이터-흐름-전면-개정)
10. [build_register_map_from_sinam.py 처리 규칙](#10-build_register_map_from_sinampy-처리-규칙)
11. [C# 서비스 수정 사항](#11-c-서비스-수정-사항)
12. [tag_metadata 적재](#12-tag_metadata-적재)
13. [알려진 문제](#13-알려진-문제)
14. [폐기 항목](#14-폐기-항목)
15. [마이그레이션 순서](#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로 묶어서 읽음:
```cpp
// 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 엔트리 구조
```json
{
"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 엔트리 (개별 파라미터)
```json
{
"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 엔트리
```json
{
"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)
```json
{
"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 엔트리
```json
{
"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 플래그 선별 로직
```python
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 컬럼 추가
```sql
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 마이그레이션
```sql
-- 신규 컬럼 추가
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 설정 파일
```json
// 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 실행 명령어
```bash
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 처리
```python
# 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 플래그 설정
```python
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 엔티티
```csharp
// 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
```csharp
// 변경 전
"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
```csharp
// 변경 전
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 (멱등)
```csharp
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) |
```sql
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 25~32는 Loop 25~32와 동일한 `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.py``archive` 필드 추가 + 선별 로직 구현 | 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_master``realtime_enabled`, `archive_enabled` DDL 추가 | C# |
| B2 | Phase A에서 생성한 register-map으로 hc900_map_master 초기 적재 | Python |
### Phase C: C# 서비스 수정
| 단계 | 작업 | 담당 |
|:----:|------|------|
| C1 | `Hc900MapEntry``RealtimeEnabled`, `ArchiveEnabled` 속성 추가 | C# |
| C2 | `Hc900RealtimeService.LoadMappingAsync``AND realtime_enabled = TRUE` 추가 | C# |
| C3 | `SnapshotToHistoryAsync``archive_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`) |