Files
HC900-Crawler/docs/plan.md
windpacer 16fc7a2598 Initial commit: HC900 Crawler
Honeywell HC900을 Modbus TCP로 직접 폴링 → gRPC → C# 크롤러 → PostgreSQL.
기존 Experion OPC UA 데이터 경로를 HC900 직접 통신으로 대체.

- industrial-comm/cpp: C++ Modbus 게이트웨이 (gRPC 서버)
- src: C# .NET 8 ASP.NET Core 크롤러 + 웹 UI (3-Layer)
- mcp-server: Python FastMCP (RAG/NL2SQL/P&ID)
- 다중 컨트롤러(N-Controller) 지원

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 20:28:14 +09:00

282 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HC900 Gateway — 아키텍처 및 구현 계획
## 1. 개요
ExperionCrawler의 OPC UA → Experion HS R530 경로를
**Modbus TCP → HC900** 경로로 대체.
```
Before:
HC900 ──Modbus TCP──▶ Experion R530 ──OPC UA──▶ ExperionCrawler ──▶ PostgreSQL
After:
HC900 ──Modbus TCP──▶ C++ Gateway ──gRPC──▶ HC900Crawler ──▶ PostgreSQL
```
## 2. C++ 게이트웨이 (Modbus TCP → gRPC)
### 설계 원칙
- **단순, 고효율, 경량** (예상 RSS 5~8 MB)
- **Full poll 1초 주기** — 모든 레지스터 읽어서 캐시
- **Scattered read는 안 함** — 어차피 Modbus는 batch가 빠름
### 아키텍처
```
┌─────────────────────────────────────────────┐
│ C++ Gateway │
│ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Poller │─▶│ Cache │◀─│ gRPC Server│ │
│ │ (1s 주기) │ │ (47KB) │ │ │ │
│ │ 32+3+13 │ │ │ │ ReadTags │ │
│ │ batch │ │ │ │ WriteTag │ │
│ │ reads │ │ │ │ ListTags │ │
│ └────┬──────┘ └──────────┘ └─────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ industrial-comm (libcomm_core) │ │
│ │ ModbusTCP → HC900 @ 192.168.0.240 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
```
### 폴링 전략
- `read_holding_registers(addr, count=120)` batch
- Loop 1~24: 32 reads (120 regs each)
- Loop 25~32: 32 reads (120 regs each)
- Variable area: 3 reads (0x18C0~, 336 regs)
- Signal area: 13 reads (0x2000~, 1,508 regs)
- **Total: 48 batch reads → ~117 ms full poll**
- 1초 간격 poll, 캐시 갱신
### gRPC Service (`proto/modbus_gateway.proto`)
```protobuf
service ModbusGateway {
rpc ReadTags(ReadTagsRequest) returns (ReadTagsResponse); // 캐시 응답
rpc WriteTag(WriteTagRequest) returns (WriteTagResponse); // Modbus write
rpc StreamTags(StreamTagsRequest) returns (stream TagValue); // 실시간 구독
rpc ListTags(ListTagsRequest) returns (ListTagsResponse); // 메타정보
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
}
```
- `ReadTags`: 캐시에서 즉시 응답 (sub-millisecond)
- `WriteTag`: Modbus FC16 직접 write 후 캐시 갱신
- `StreamTags`: 1초 poll 주기에 맞춰 push
### 레지스터 맵 로딩
- 시작 시 `register-map.json` 파일 읽음 (174 KB)
- 또는 `hc900_map_master` DB 테이블에서 읽음
## 3. hc900_map_master (DB 기반 태그 카탈로그)
### 개념
- `node_map_master`와 동일한 패턴
- HC Designer CSV 3종을 DB에 로드
- 태그 prefix 기반 분류 (기존 PidPrefixRule 재활용)
### DDL
```sql
CREATE TABLE hc900_map_master (
id SERIAL PRIMARY KEY,
tag_name TEXT NOT NULL, -- 레지스터 맵 태그명 (e.g. 'FICQ3101.PV', 'FIQ6101')
base_tag TEXT NOT NULL, -- 베이스 태그 (e.g. 'FICQ3101', 'FIQ6101')
attribute TEXT, -- 속성 (e.g. 'PV', 'SP', 'QV') — NULL이면 베이스 태그 자체
addr INTEGER NOT NULL, -- Modbus 주소
count INTEGER NOT NULL, -- 레지스터 수 (1 or 2)
type TEXT NOT NULL, -- 'float32' or 'uint16'
access TEXT NOT NULL, -- 'R' or 'RW'
description TEXT,
eu TEXT,
category TEXT, -- PidPrefixRule 기반 분류 ('instrument', 'power_equipment', ...)
tag_class TEXT, -- 'field' or 'system'
tag_dcs BOOLEAN, -- DCS function block 여부
source TEXT NOT NULL, -- 'loop', 'signal', 'variable'
group_name TEXT, -- PointBuilder 그룹 ('Controller1', 'Custom', ...)
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_hc900_map_base ON hc900_map_master(base_tag);
CREATE INDEX idx_hc900_map_addr ON hc900_map_master(addr);
CREATE INDEX idx_hc900_map_group ON hc900_map_master(group_name);
```
### 데이터 출처
| CSV 파일 | source | 건수 | 범위 |
|---|---|---|---|
| SummaryFunctionBlockReport.csv | `loop` | 32 loops × 25 params = 800 | 0x0040~0x7FFF |
| SignalTags.csv | `signal` | 530 | 0x2000~0x25E4 |
| Variables.csv | `variable` | 155 | 0x18C0~0x1A10 |
### Prefix Rules (PidPrefixRule 활용)
| Prefix | Category | 예시 |
|---|---|---|
| FIC, FICA, FICQ | instrument, tag_dcs=true | FICQ3101 (PID Loop) |
| FIT, FIQ, FT | instrument, tag_dcs=false | FIT6101, FIQ6101 (Signal Tag) |
| TI, TIC, TICA | instrument, tag_dcs=true | TICA6111A, TICA3202A |
| LI, LIC, LICA | instrument | LICA6213, LICA5113 |
| PI, PIC, PICA | instrument | PICA6111 |
| XV | instrument | XV3208B_REM |
| VP, P- | power_equipment | VP8117_HS, P-3101B_RUN |
### PointBuilder-like Selection
웹 UI에서 `hc900_map_master`를 대상으로:
| Group | 필터 조건 | 용도 |
|---|---|---|
| Controller1 | prefix=FIC/TIC/PIC/LIC, attribute=PV/SP/OP | 주요 PID 값 |
| Custom | user-defined | QV, TRIP, ESD, HS 등 |
DB 조회 예시:
```sql
-- FICQ-6101 계열 모든 태그
SELECT * FROM hc900_map_master
WHERE base_tag IN (
SELECT base_tag FROM hc900_map_master
WHERE base_tag ILIKE 'ficq6101'
);
```
## 4. OPC UA 이름 매핑 (핵심 난제)
### 문제
- DB (Experion OPC UA): `ficq-6101.pv`, `ficq-6101.qv` (lowercase, dash, dot attribute)
- HC Designer CSV: `FICQ3101`, `FIQ6101` (uppercase, no dash, no prefix-dot suffix)
- R530이 중간에서 이름 변환 + 서브 속성 구성하며, **이 매핑 정보는 R530 설정에만 존재**
```
OPC UA (DB) HC Designer (CSV) Modbus
──────────────────── ────────────────── ───────
ficq-6101.pv FICA6101 (Loop #11) 0x0A40 ← prefix 다름 (FICQ vs FICA)
ficq-6101.qv.value FIQ6101 (Signal Tag) 0x2006 ← 사용자 정의 규칙 (FIQ=QV)
ficq-6101.sp FICA6101 (Loop #11) 0x0A44
ficq-3101.pv FICQ3101 (Loop #1) 0x0040
ficq-3101.qv.value FIQ3101 (Signal Tag) 0x2164
```
prefix 예: `FICQ`/`FICA`/`FIT`/`FIQ` 모두 숫자부 `6101` 공유 → **prefix는 접근 방식(loop, PV, QV)을 나타내고, 숫자부가 실제 연결고리**
### 매핑 전략: 숫자부 조인 + prefix 검증
`realtime_table`의 OPC UA 이름과 `hc900_map_master`의 HC Designer 이름을 **숫자부(6101) 기준으로 매칭**하고, prefix rules로 검증:
```sql
-- Phase A: 후보 추출 (숫자부 기반 매칭)
WITH rt_base AS (
SELECT DISTINCT
split_part(tagname, '.', 1) AS opcua_tag,
tagname,
substring(tagname FROM '(\d+)') AS tag_number
FROM realtime_table
),
hc_base AS (
SELECT
tag_name,
base_tag,
addr,
substring(tag_name FROM '(\d+)') AS tag_number
FROM hc900_map_master
)
SELECT
r.opcua_tag,
r.tagname AS opcua_name,
h.tag_name AS hc_name,
h.addr
FROM rt_base r
JOIN hc_base h ON r.tag_number = h.tag_number
AND r.opcua_tag ~* h.base_tag -- base_tag regex 매칭
ORDER BY r.opcua_tag, h.addr;
```
### 결과물: opcua_aliases
```sql
ALTER TABLE hc900_map_master ADD COLUMN opcua_aliases TEXT[];
-- 예시 (자동 생성 후 사용자 검증 필요):
-- FIQ6101 → {"ficq-6101.qv", "ficq-6101.qv.value"}
-- FICQ3101.PV → {"ficq-3101.pv"}
-- FICA6101-WSP → {"ficq-6101.wsp"}
```
### 사용자 검증 플로우
```
CSV → hc900_map_master realtime_table (OPC UA)
│ │
└──── SQL 숫자부 조인 ───┘
alias 후보 목록
사용자 검증 (Y/N)
opcua_aliases 확정
C# 앱이 gRPC 호출 시
aliases로 주소 조회
```
C# 앱에서 gRPC 요청 시:
```csharp
// C# 앱은 DB 이름(ficq-6101.pv)으로 요청
// 게이트웨이는 aliases 매칭으로 레지스터 주소 조회
var resp = client.ReadTags(new[] { "ficq-6101.pv", "ficq-6101.qv" });
```
## 5. 구현 단계
### Phase 1: C++ 게이트웨이 (완성)
- [ ] `industrial-comm`에 gRPC 서버 추가
- [ ] `register-map.json` 로더
- [ ] Poller (full poll 1초)
- [ ] gRPC 서비스 구현 (ReadTags, WriteTag, ListTags, HealthCheck)
- [ ] CMakeLists.txt에 gRPC/Protobuf 종속성 추가
- [ ] systemd unit file
### Phase 2: DB 및 백엔드
- [ ] `hc900_map_master` 테이블 생성 SQL
- [ ] `build_hc900_map.py` CSV → DB 로더 (Python)
- [ ] `scripts/build_register_map.py` → DB 직접 적재
- [ ] Prefix Rules 추가 (FIQ, FIT, FICA 등)
- [ ] PointBuilder-like 웹 UI (기존 ExperionCrawler UI 확장)
### Phase 3: C# HC900Crawler
- [ ] ExperionCrawler 포크 → gRPC Client 추가
- [ ] 기존 OPC UA 계층 대체 (IExperionOpcClient → IModbusGatewayClient)
- [ ] Hc900RealtimeService (캐시 폴링 기반)
- [ ] Write 서비스
- [ ] 기존 DB 스키마 재활용 (realtime_table, history_table, etc.)
### Phase 4: OPC UA 이름 매핑
- [ ] node_map_master 조회로 실제 OPC UA 이름-레지스터 관계 분석
- [ ] opcua_aliases DB 구축
- [ ] 매핑 검증 툴
## 6. 검증된 사실
| 항목 | 상태 |
|---|---|
| HC900 Modbus TCP 연결 | ✅ C70, port 502, IP=192.168.0.240 |
| Float 포맷 | ✅ FP_B (IEEE 754 big-endian) |
| Loop 주소 체계 | ✅ Loop #N = 0x40+(N-1)*0x100 (1~24), 0x7840+(N-25)*0x100 (25~32) |
| batch read 성능 | ✅ 120 regs = 2.4ms, full poll = 117ms |
| C70 최대 연결 | ✅ 10개 |
| R530 사용 연결 | ✅ 1개 (Modbus TCP 채널) |
| 우리 사용 가능 | ✅ 9개, 실제론 1~2개면 충분 |
| Signal Tag FIQ6101 = QV | ✅ `ficq-6101.qv.value` = `FIQ6101` @ 0x2006 |
| 사용자 정의 네이밍 | ✅ FIT=PV, FIQ=QV 등 prefix 기반 규칙 존재 |
## 7. 오픈 이슈
- [ ] **OPC UA ↔ Modbus 주소 매칭 (최대 난제)** — 숫자부 조인으로 후보 추출 후 사용자 검증 필요
- [ ] HC900 Custom Map/User Defined 영역 태그 확인 (현재는 Fixed Map만 커버)
- [ ] Write 시 Modbus FC16 테스트 필요
- [ ] 캐시 무효화 전략 (C#에서 Write 후 gateway cache 갱신)
- [ ] StreamTags 구현 방식 (1초 poll마다 변경분 push vs 전부 push)