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>
This commit is contained in:
281
docs/plan.md
Normal file
281
docs/plan.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user