21 KiB
실시간 태그 확장 설계안
1. 배경
현재 ExperionCrawler는 아날로그 태그의 .pv, .sp, .op, .qv.value만 realtime_table에 저장하고 있음.
디지털 장비(Pump, XV)의 상태 정보와 모든 태그의 .desc, .area 메타데이터를 추가하고자 함.
2. 현재 시스템 아키텍처
Experion HS R530 OPC Server
↓ (1. 크롤링 - 전체 노드 스캔)
CSV 파일 저장
↓ (2. CSV 로드)
node_map_master (530,080 행)
↓ (3. UI에서 태그+속성 선택 - 최대 8개)
realtime_table (등록된 태그만)
↓ (4. OPC UA 구독)
livevalue 실시간 업데이트
↓ (5. 히스토리 저장)
history_table
node_map_master 구조 특징
name: 속성명만 (pv,sp,desc,area,instate0등)node_id: 전체 경로 (ns=1;s=sinamserver:xv-6124.pv)- 상위 Object (
xv-6124)와 하위 속성 (pv)이 별도 행으로 존재
3. 추가할 태그
3.1 아날로그 태그 (기존 확장)
| 태그 | 데이터 타입 | 설명 |
|---|---|---|
ficq-6101.area |
Enum(i=7594) | 소속 플랜트/Asset |
참고: ficq-6101은
.desc속성이 없음 (DB 확인 결과)
3.2 디지털 태그 (신규 추가) (0~7까지 모두 추가)
| 태그 | 데이터 타입 | 설명 |
|---|---|---|
xv-6124.pv |
Int32 (BCD 조합값) | 프로세스 값 |
xv-6124.op |
Int32 | 출력값 |
xv-6124.desc |
String | 장비 설명 |
xv-6124.area |
Enum(i=7594) | 소속 플랜트/Asset |
xv-6124.instate0 |
Boolean | 상태 비트 0 (현재 값) |
xv-6124.instate1 |
Boolean | 상태 비트 1 (현재 값) |
xv-6124.instate2 |
Boolean | 상태 비트 2 (현재 값) |
xv-6124.state0descriptor |
String | 비트 0 의미 (예: "Run/Stop") |
xv-6124.state1descriptor |
String | 비트 1 의미 (예: "Remote/Local") |
xv-6124.state2descriptor |
String | 비트 2 의미 (예: "Trip/Normal") |
핵심 발견: Experion HS에
state0descriptor~state7descriptor가 이미 존재합니다. 이는 사용자가 정의한 BCD 상태 의미를 String으로 저장한 태그로, OPC UA에서 구독하면realtime_table에 저장 가능하므로 별도 테이블 불필요.
3.3 BCD 상태값 해석
pv는 HC900 컨트롤러에서 Digital Input을 BCD로 조합하여 32bit Float로 전송.
직접 pv 값을 파싱하지 않고 instate0~7 Boolean 태그를 사용.
| 사용 비트 | 현재 프로젝트 예시 (Pump) | 현재 프로젝트 예시 (Valve) |
|---|---|---|
| instate0 | Run(1)/Stop(0) | Open(1)/Close(0) |
| instate0+1 | Run/Stop + R/L | Open/Close + R/L |
| instate0+1+2 | Run/Stop + R/L + Trip/Normal | Open/Close + R/L + Fault/Normal |
중요: 위 값은 현재 프로젝트에서만 알려진 사용자 정의 값입니다. 다른 프로젝트에서는 BCD 상태 의미가 다를 수 있으므로 정형화 불가능합니다. 각 장비의 실제 비트 사용 수와 의미는Experion HS에서 사용자 정의됩니다.
4. 설계 결정
4.1 DB 스키마 변경: tag_metadata 테이블 추가
메타데이터(desc, area, state0descriptor~7)는 실시간으로 자주 변경되지 않으므로, 별도 테이블에 저장하고 OPC UA 통신 부담을 줄입니다.
CREATE TABLE tag_metadata (
id SERIAL PRIMARY KEY,
base_tag TEXT NOT NULL, -- ficq-6101, xv-6124
attribute TEXT NOT NULL, -- desc, area, state0descriptor, ...
value TEXT, -- 실제 값
node_id TEXT, -- OPC UA node_id (재로드용)
loaded_at TIMESTAMP DEFAULT NOW(), -- 마지막 로드 시간
UNIQUE(base_tag, attribute)
);
CREATE INDEX idx_tag_metadata_base ON tag_metadata(base_tag);
데이터 분리 전략:
| 테이블 | 저장 내용 | OPC UA 구독 |
|---|---|---|
realtime_table |
pv, sp, op, instate0~7 등 실시간 데이터 | ✅ 지속 구독 |
tag_metadata |
desc, area, state0descriptor~7 등 메타데이터 | ❌ 한 번 로드 |
장점:
- OPC UA 통신 부담 대폭 감소 (메타데이터는 한 번만 읽음)
- 수천 개 태그에 대해 desc/area를 실시간 구독하지 않음
- UI에서 "재로드" 버튼으로 필요시 갱신 가능
4.2 메타데이터 로드/재로드 시나리오
초기 로드:
- UI에서 태그 선택 후 등록 시, 실시간 태그(pv, sp, op, instate0~7)는
realtime_table에 등록 - 동시에 메타데이터(desc, area, state0descriptor~7)는 OPC UA에서 한 번 읽어서
tag_metadata테이블 저장 - 읽은 메타데이터 정보로
node_map_master도 동시 갱신
재로드 (UI 버튼):
- UI에서 "메타데이터 재로드" 버튼 클릭
- Backend가 OPC UA 서버에 직접 연결
- 선택된 태그(또는 전체 등록 태그)의 메타데이터 재조회
tag_metadata테이블 UPDATE +node_map_master갱신
UI 재로드 버튼
↓
Backend API: POST /api/tags/metadata/reload
↓
OPC UA Read (desc, area, state0descriptor~7)
↓
tag_metadata UPDATE
node_map_master UPDATE (UPSERT)
↓
Response: 갱신된 태그 수
4.3 SQL 뷰 추가
realtime_table + tag_metadata를 JOIN하여 태그별 속성을 한눈에 보기 위한 뷰:
CREATE OR REPLACE VIEW v_tag_summary AS
SELECT
rt_base.base_tag,
pv_rt.livevalue AS pv,
sp_rt.livevalue AS sp,
op_rt.livevalue AS op,
instate0_rt.livevalue AS instate0,
instate1_rt.livevalue AS instate1,
instate2_rt.livevalue AS instate2,
desc_md.value AS description,
area_md.value AS area,
s0d_md.value AS state0_descriptor,
s1d_md.value AS state1_descriptor,
s2d_md.value AS state2_descriptor
FROM (SELECT DISTINCT split_part(tagname, '.', 1) AS base_tag FROM realtime_table) rt_base
LEFT JOIN realtime_table pv_rt ON pv_rt.tagname = rt_base.base_tag || '.pv'
LEFT JOIN realtime_table sp_rt ON sp_rt.tagname = rt_base.base_tag || '.sp'
LEFT JOIN realtime_table op_rt ON op_rt.tagname = rt_base.base_tag || '.op'
LEFT JOIN realtime_table instate0_rt ON instate0_rt.tagname = rt_base.base_tag || '.instate0'
LEFT JOIN realtime_table instate1_rt ON instate1_rt.tagname = rt_base.base_tag || '.instate1'
LEFT JOIN realtime_table instate2_rt ON instate2_rt.tagname = rt_base.base_tag || '.instate2'
LEFT JOIN tag_metadata desc_md ON desc_md.base_tag = rt_base.base_tag AND desc_md.attribute = 'desc'
LEFT JOIN tag_metadata area_md ON area_md.base_tag = rt_base.base_tag AND area_md.attribute = 'area'
LEFT JOIN tag_metadata s0d_md ON s0d_md.base_tag = rt_base.base_tag AND s0d_md.attribute = 'state0descriptor'
LEFT JOIN tag_metadata s1d_md ON s1d_md.base_tag = rt_base.base_tag AND s1d_md.attribute = 'state1descriptor'
LEFT JOIN tag_metadata s2d_md ON s2d_md.base_tag = rt_base.base_tag AND s2d_md.attribute = 'state2descriptor';
4.4 UI 변경
| 항목 | 현재 | 변경 |
|---|---|---|
| 최대 선택 태그 수 | 8개 | 10개 |
| 태그 검색 | name 기준 | name + node_id 기준 |
| 메타데이터 재로드 | 없음 | "메타데이터 재로드" 버튼 추가 |
| 속성 미리보기 | 없음 | 선택 시 하위 속성 표시 (선택사항) |
5. 변경 사항 요약
5.1 Database
- 신규 테이블:
tag_metadata(base_tag, attribute, value, node_id, loaded_at) - 신규 뷰:
v_tag_summary(realtime_table + tag_metadata JOIN)
5.2 Backend (src/Infrastructure/OpcUa/)
- 신규 서비스:
MetadataLoaderServiceLoadMetadataAsync(base_tags): OPC UA에서 desc, area, state0descriptor~7 읽어서tag_metadata저장ReloadMetadataAsync(base_tags): 재로드 시 OPC UA 재조회 +tag_metadataUPDATE +node_map_masterUPSERT
- 신규 API:
POST /api/tags/metadata/reload
5.3 Frontend (src/Web/wwwroot/js/app.js)
- 태그 선택 최대 수 8 → 10개로 변경
- "메타데이터 재로드" 버튼 추가
- (선택사항) 상위 Object 선택 시 하위 속성 자동 표시
5.4 Entity/DbContext
- 신규 Entity:
TagMetadataclass - DbContext:
DbSet<TagMetadata>추가,OnModelCreating설정 추가
5.5 사용자 작업
- UI에서 새로운 태그(Pump, XV)와 속성(pv, op, instate0~2) 선택하여 등록
- 메타데이터(desc, area, state descriptor)는 자동 로드
- 필요시 "메타데이터 재로드" 버튼으로 갱신
6. 대규모 리팩토링이 필요한 시나리오 (향후 고려사항)
현재 설계안은 기존 realtime_table 구조를 유지하므로 저위험입니다.
하지만 아래 기능들을 추가하려면 DB 스키마 변경 + 코드 리팩토링이 필요합니다.
6.1 자동 속성 제안 기능
문제: 현재 UI에서 각 태그의 속성(pv, sp, op, desc, area, instate0~2)을 수동으로 선택해야 함. Pump/XV 같은 디지털 장비는 7개 이상의 속성을 선택해야 하므로 번거로움.
해결 방안:
- 상위 Object (예:
xv-6124) 선택 시, node_map_master에서 하위 속성을 자동으로 조회하여 제안 - 장비 타입에 따라 기본 속성 조합 제안:
- 아날로그 (ficq-6101):
.pv,.sp,.op,.area - 디지털 XV (xv-6124):
.pv,.op,.desc,.area,.instate0,.instate1,.instate2 - Pump (p-6102):
.pv,.op,.desc,.area,.instate0,.instate1,.instate2
- 아날로그 (ficq-6101):
변경 범위:
- Frontend: 상위 Object 선택 시 하위 속성 자동 로딩 UI
- Backend:
/api/nodemap/children?parent=xv-6124API 추가 - node_map_master에서
node_id LIKE '%xv-6124.%'로 하위 속성 조회
리팩토링 위험: 낮음 (기존 API 추가만 필요)
6.2 tag_registry 테이블 도입
문제: BCD 상태 의미(instate0=Run/Stop, instate1=R/L 등)는 사용자 정의이므로 시스템에서 자동으로 해석할 수 없음. Text-to-SQL에서 "pump-6102 상태 알려줘" 같은 자연어 질문에 의미 있는 답변을 제공하려면 상태 의미 정보가 필요함.
해결 방안:
CREATE TABLE tag_registry (
id SERIAL PRIMARY KEY,
base_tag TEXT NOT NULL UNIQUE, -- xv-6124, p-6102
equipment_type TEXT, -- pump, xv, ai, ao, fi, ti
bit_count INT DEFAULT 1, -- 사용하는 instate 비트 수 (1~3)
state0_meaning TEXT, -- Run/Stop, Open/Close
state1_meaning TEXT, -- Remote/Local
state2_meaning TEXT, -- Trip/Normal, Fault/Normal
area TEXT, -- 소속 플랜트/Asset
description TEXT, -- 장비 설명
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
활용 시나리오:
- BCD 상태 의미 저장: 사용자가 UI에서 상태 의미 정의 → DB 저장
- 자동 속성 제안:
equipment_type에 따라 기본 속성 조합 제안 - Text-to-SQL: "pump-6102 현재 상태" → instate0~2를 Boolean으로 읽어서 상태 의미와 매핑하여 "Run, Remote, Normal" 표시
- P&ID 매핑: P&ID에서 추출한 장비와 Experion 태그를
tag_registry를 통해 연결
리팩토링 위험: 중간 (새 테이블 + Entity + Service 추가)
6.3 장비 타입별 다른 처리 로직
문제: 현재 realtime_table은 모든 태그를 동일하게 처리.
하지만 장비 타입에 따라 다른 처리가 필요할 수 있음:
- 아날로그: pv/sp/op를 숫자로 표시, 단위 표시
- 디지털 XV: instate0~2를 Boolean으로 읽어서 "Open/Close", "R/L"로 표시
- Pump: instate0~2를 Boolean으로 읽어서 "Run/Stop", "R/L", "Trip/Normal"로 표시
- 알람: alarmflags, alarmvalue를 파싱해서 알람 상태 표시
해결 방안:
tag_registry.equipment_type에 따라 UI에서 다른 컴포넌트 렌더링- Backend에서 장비 타입별 상태 해석 서비스 추가
- BCD 상태값을 Boolean 배열로 파싱하는 유틸리티 함수
변경 범위:
- Frontend: 장비 타입별 상태 표시 컴포넌트
- Backend:
EquipmentStateService추가 (BCD → Boolean 배열 → 상태 문자열) tag_registry테이블과 연동
리팩토링 위험: 높음 (Frontend 컴포넌트 + Backend 서비스 + DB 연동)
6.4 desc/area 별도 테이블
문제: desc/area는 자주 변경되지 않지만, 현재는 realtime_table에 실시간 데이터로 저장.
수천 개의 태그에 대해 desc/area를 OPC UA에서 계속 구독하면 불필요한 트래픽 발생.
해결 방안:
CREATE TABLE tag_metadata (
tagname TEXT PRIMARY KEY, -- ficq-6101.area, xv-6124.desc
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT NOW()
);
- 초기 로드 시 OPC UA에서 desc/area 한 번 읽어서 저장
- 변경 감지: 주기적 폴링 또는 이벤트 기반 업데이트
- UI 조회 시
tag_metadata에서 읽되, 변경 의심 시 OPC UA에서 재조회
리팩토링 위험: 중간 (새 테이블 + 폴링 서비스 추가)
7. 권장 진행 순서
Phase 1: 기본 확장 (현재 설계안)
- UI 변경: 태그 선택 최대 수 8 → 10개
- 테스트: xv-6124의 pv, op, desc, area, instate0~2 등록 및 OPC UA 구독 확인
- SQL 뷰:
v_tag_summary생성 (선택사항)
Phase 2: 자동화 (향후)
- 자동 속성 제안: 상위 Object 선택 시 하위 속성 자동 제안
- tag_registry 테이블: BCD 상태 의미 저장, 장비 타입 관리
Phase 3: 지능형 처리 (장기)
- 장비 타입별 처리: BCD 상태 해석, UI 컴포넌트별 렌더링
- desc/area 최적화: 별도 테이블 + 변경 감지 폴링
- Text-to-SQL 연동: 자연어 질문 → BCD 상태 해석 → 의미 있는 답변
8. NL2SQL 변경 사항
현재 NL2SQL worker(mcp-server/worker/nl2sql_worker.py)는 history_table과 realtime_table만 인식합니다.
새로운 테이블과 뷰, 태그 타입을 인식하도록 DB_SCHEMA를 업데이트해야 합니다.
8.1 DB_SCHEMA 업데이트 필요 항목
추가할 테이블 정의:
테이블: tag_metadata (태그 메타데이터 - 변경 드묾)
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor', ...)
value TEXT - 메타데이터 값
node_id TEXT - OPC UA 노드 ID
loaded_at TIMESTAMPTZ - 마지막 로드 시각
뷰: v_tag_summary (실시간 + 메타데이터 통합)
base_tag TEXT - 기본 태그명
pv TEXT - 현재 프로세스 값
sp TEXT - 설정값
op TEXT - 출력값
instate0 TEXT - 상태 비트 0 (true/false)
instate1 TEXT - 상태 비트 1 (true/false)
instate2 TEXT - 상태 비트 2 (true/false)
description TEXT - 장비 설명 (tag_metadata.desc)
area TEXT - 소속 플랜트 (tag_metadata.area)
state0_descriptor TEXT - 비트 0 의미 (예: "Run/Stop")
state1_descriptor TEXT - 비트 1 의미 (예: "Remote/Local")
state2_descriptor TEXT - 비트 2 의미 (예: "Trip/Normal")
8.2 NL2SQL 사용 예시 (새로운 시나리오)
| 자연어 질문 | 생성 SQL |
|---|---|
| "xv-6124의 현재 상태와 설명을 알려줘" | SELECT instate0, instate1, state0_descriptor, state1_descriptor, description FROM v_tag_summary WHERE base_tag = 'xv-6124' |
| "Unit-A에 있는 모든 pump의 상태를 보여줘" | SELECT base_tag, instate0, state0_descriptor, description FROM v_tag_summary WHERE area = 'Unit-A' AND base_tag LIKE 'p-%' |
| "ficq-6101의 현재 값과 설명은?" | SELECT pv, description FROM v_tag_summary WHERE base_tag = 'ficq-6101' |
| "모든 XV 중 instate0이 true인 것" | SELECT base_tag, instate0, state0_descriptor FROM v_tag_summary WHERE instate0 = 'True' AND base_tag LIKE 'xv-%' |
| "p-6102의 3개 상태 비트와 의미를 보여줘" | SELECT instate0, instate1, instate2, state0_descriptor, state1_descriptor, state2_descriptor FROM v_tag_summary WHERE base_tag = 'p-6102' |
8.3 변경 파일 목록
| 파일 | 변경 내용 |
|---|---|
mcp-server/worker/nl2sql_worker.py |
DB_SCHEMA에 tag_metadata, v_tag_summary 추가 |
mcp-server/server.py |
_DB_SCHEMA에 동일하게 추가 (동기용) |
8.4 DB_SCHEMA 추가 내용 예시
테이블: tag_metadata (태그 메타데이터 - 변경 드묾, OPC UA 폴링으로 갱신)
base_tag TEXT - 기본 태그명 (예: 'ficq-6101', 'xv-6124')
attribute TEXT - 속성명 ('desc', 'area', 'state0descriptor'~'state7descriptor')
value TEXT - 메타데이터 값
loaded_at TIMESTAMPTZ - 마지막 로드 시각
뷰: v_tag_summary (실시간값 + 메타데이터 통합 뷰)
base_tag, pv, sp, op, instate0~2, description, area, state0~2_descriptor
새로운 태그 타입:
- 아날로그: ficq-6101.pv/sp/op (Double), ficq-6101.area
- 디지털 XV: xv-6124.pv/op (Int32), xv-6124.instate0~7 (Boolean)
- Pump: p-6102.pv/op (Int32), p-6102.instate0~7 (Boolean)
- 메타데이터: desc (String), area (Enum), state0descriptor~7 (String)
BCD 상태 조회 팁:
- instate0~7은 Boolean (true/false)
- state0descriptor~7은 해당 비트의 의미 설명 (예: "Run/Stop")
- instate0=true이고 state0descriptor="Run/Stop"이면 → "Run" 상태
- v_tag_summary 뷰를 사용하면 실시간값+메타데이터 한 번에 조회 가능
9. Event & Alarm 테이블 (지능형 가동 상태 보고용)
9.1 필요성
"어제 18시부터 6차 플랜트의 가동상태 알려줘" 같은 지능형 보고를 하려면:
- 펌프/밸브 상태 변화 이력
- 알람 발생/해제 시점
- 비상정지(ESHUTDOWN) 발생/해제 시점
- 유량계 적산값(qv.value)으로 생산량 계산
이 정보들은 실시간 값이 아닌 이벤트 이력이므로 별도 테이블 필요.
9.2 테이블 구조
CREATE TABLE event_alarm_log (
id BIGSERIAL PRIMARY KEY,
base_tag TEXT NOT NULL, -- xv-6124, p-6102
event_type TEXT NOT NULL, -- state_change, alarm, shutdown
bit_index INT, -- instate0~7 중 어떤 비트 (state_change용)
old_value TEXT, -- 이전 값
new_value TEXT, -- 새 값
state_descriptor TEXT, -- state0descriptor 등 의미
alarm_priority TEXT, -- Urgent/High/Low/Journal
alarm_message TEXT, -- 알람 메시지
occurred_at TIMESTAMPTZ NOT NULL, -- 발생 시각
acknowledged_at TIMESTAMPTZ, -- 확인 시각
source TEXT DEFAULT 'opcua' -- opcua, manual
);
CREATE INDEX idx_eal_base_tag ON event_alarm_log(base_tag);
CREATE INDEX idx_eal_occurred ON event_alarm_log(occurred_at);
CREATE INDEX idx_eal_type ON event_alarm_log(event_type);
9.3 수집 방법
방법 1: OPC UA Event Subscription (권장)
- Experion HS의 OPC UA Event Mechanism 구독
- 알람 발생 시 실시간으로 이벤트 수신 → DB 저장
방법 2: 폴링 기반 상태 변화 감지
- instate0~7 값을 주기적 폴링
- 이전 값과 비교하여 변화 감지 → event_alarm_log INSERT
- alarmflags, alarmvalue 변화도 함께 감지
9.4 활용 시나리오
| 질문 | 필요한 데이터 |
|---|---|
| "p6 플랜트 가동상태 알려줘" | pump instate0 변화 이력 + shutdown 이벤트 |
| "00시에 왜 정지됐어?" | event_alarm_log WHERE event_type='shutdown' |
| "오늘 생산량?" | qv.value 적산 (history_table) |
| "xv-6124 알람 이력" | event_alarm_log WHERE base_tag='xv-6124' |
9.5 NL2SQL DB_SCHEMA 추가 내용
테이블: event_alarm_log (이벤트/알람 이력)
base_tag TEXT - 기본 태그명
event_type TEXT - state_change, alarm, shutdown
bit_index INT - instate 비트 인덱스 (0~7)
old_value TEXT - 이전 값
new_value TEXT - 새 값
state_descriptor TEXT - 상태 의미 (예: "Trip, Fault")
alarm_priority TEXT - Urgent/High/Low/Journal
occurred_at TIMESTAMPTZ - 발생 시각
area - 그룹명 (플랜트 'p1~10' 에 그룹 알람 정보 찾기 쉬움)
예시: "오늘 p6 플랜트 알람 목록"
SELECT base_tag, event_type, new_value, state_descriptor, alarm_priority, occurred_at
FROM event_alarm_log
WHERE base_tag IN (SELECT base_tag FROM tag_metadata WHERE attribute='area' AND value='p6')
AND occurred_at >= date_trunc('day', NOW())
ORDER BY occurred_at
future Plan 항목
이 디자인 플랜의 후순위
- 알람 관련 작업 :Experion HS 알람 구성 파악, 알람 테이블 긁어올 가능성 있는지 체크
- WebUI 도입관련 : 진정한 AX도입의 FINAL WORK가 될 것이며, 위의 모든 기능은 최종 AX화를 위한 걸 기본으로 설계되어야 한다.