온도프로파일/PV일관성/PointBuilder/history 작업지시, 신호태그·스팀유량 진단, 베이직아키텍처 재설계, MSDS, LLM채팅 구조 등. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.9 KiB
작업지시: 신호점 PV 명명 일관화(.PV) 전체 롤아웃
2026-06-09.
build_register_map_from_sinam.py는 이미 수정 완료(검증: bare 태그 0). 이 문서는 DB/데이터/다운스트림까지 일관 적용하는 절차. 깨진 중간상태 방지를 위해 모든 단계를 함께 수행할 것.
진단 결과 (diagnosis-checklist.md 기준)
| STEP | 결과 |
|---|---|
| STEP 1-2 | Python 스크립트(build_register_map_from_sinam.py) + C# 서비스(Realtime/DigitalEvent/DbContext) |
| STEP 3 | 전체 파일 읽음 (670행 Python, 266행 RealtimeService, 199행 DigitalEventDetector, 3144행 DbContext) |
| STEP 4 | 호출 계층: DB → RealtimeService(폴링) → DigitalEventDetector(1s 감지) |
| STEP 5 | 🔴 HIGH 1건 발견 (아래 참조) |
| STEP 6 | 교차검증 통과 — 실제 코드에서 재확인 |
| STEP 7-8 | 아래 1건 보고 |
[1]. GetDigitalTagNamesAsync 가 bare 태그명 반환 → 디지털 이벤트 감지 실패 (HIGH)
문제: GetDigitalTagNamesAsync() (Hc900DbContext.cs:2273) 는 tag_metadata에서 base_tag(예: LI-6100) 를 반환하고, GetDigitalPointsAsync() (Hc900DbContext.cs:2303) 는 이 bare 명으로 realtime_table을 조회한다. 그러나 PV 일관화 롤아웃 후 realtime_table의 태그명은 LI-6100.PV이므로 매칭 실패 → 디지털 태그 0개 감지 → 이벤트 기록 완전 마비.
근거:
Hc900DbContext.cs:2278-2281:SELECT DISTINCT BaseTag FROM tag_metadata WHERE attribute LIKE 'state%'→ bare 명 반환Hc900DbContext.cs:2303-2304:WHERE tagname IN (bare 명들)→LI-6100.PV과 매칭 안 됨Hc900DigitalEventDetectorService.cs:71-73:GetDigitalTagNamesAsync()결과로_previousStates초기화Hc900DigitalEventDetectorService.cs:103:GetRealtimeRecordsByTagNamesAsync(queryTags)→ 빈 결과Hc900RealtimeService.cs:251-253:FormatValue는baseTag = tagname.split('.')[0]로 이미.PV대응 완료 → 수정 불필요
영향: 롤아웃 후 디지털 이벤트(TRIP/RUN/ALARM)가 전부 기록되지 않음. 운전원 알람 미수신, 이벤트 로그 공백.
수정: GetDigitalPointsAsync() 조회 시 bare + .PV 명 모두 매칭. GetDigitalTagPairsAsync() 반환값에 .PV 붙임.
// Hc900DbContext.cs:2295-2308 수정
public async Task<IEnumerable<RealtimePoint>> GetDigitalPointsAsync()
{
var digitalTagNames = await GetDigitalTagNamesAsync();
var tagSet = new HashSet<string>(digitalTagNames);
if (tagSet.Count == 0)
return Enumerable.Empty<RealtimePoint>();
// bare 명(LI-6100) + .PV 명(LI-6100.PV) 모두 매칭 — PV 일관화 롤아웃 대응
return await _ctx.RealtimePoints
.Where(p => tagSet.Contains(p.TagName)
|| (p.TagName.LastIndexOf('.') > 0 && tagSet.Contains(p.TagName.Substring(0, p.TagName.LastIndexOf('.')))))
.ToListAsync();
}
// Hc900DbContext.cs:2742-2759 수정
private async Task<HashSet<(string, string)>> GetDigitalTagPairsAsync()
{
var fromMetadata = await _ctx.TagMetadata
.Where(m => m.Attribute.StartsWith("state") && m.Value != null && m.Value != "")
.Select(m => new { m.ControllerId, m.BaseTag })
.Distinct()
.ToListAsync();
if (fromMetadata.Any())
return fromMetadata.Select(m => (m.ControllerId, m.BaseTag + ".PV")).ToHashSet();
var fromRealtime = await _ctx.RealtimePoints
.Where(p => p.LiveValue != null && p.LiveValue.StartsWith("{"))
.Select(p => new { p.ControllerId, p.TagName })
.Distinct()
.ToListAsync();
return fromRealtime.Select(p => (p.ControllerId, p.TagName)).ToHashSet();
}
GetDigitalTagPairsAsync() 수정으로 GetDigitalTagPairsCachedAsync() 사용처(Hc900DbContext.cs:1863) 의 디지털 제외 필터도 자연 해결 → history_table에 디지털 쓰레기 데이터 적재 방지.
무엇이 바뀌나
기존: 신호/상태/아날로그 점의 PV가 bare(LI-6100, AG-3202), 그 외 속성만 suffix
(AG-3202.OP). → 같은 계기 안에서 PV만 suffix 없는 불일치.
변경: 모든 PV를 .PV suffix로 → 전 레지스터가 {base}.{param}.
LI-6100→LI-6100.PVAG-3202→AG-3202.PV(+.OP/.MD)- 변수
VP-8117→.PV/.OP/.MD, SIGTI-8117HSET→.PV/.SP - 루프(FICQ 등)는 이미
.PV— 영향 없음.
스크립트 변경점: build_registers 의 점 PV 엔트리 add_entry(item_name, ...) →
add_entry(f'{item_name}.PV', ...) (이미 반영됨).
추가: 지시계 흡수(dedup) — 이미 반영됨
짧은 지시계 prefix(≥2자, FI/LI/TI/PI…)가 **같은 번호의 더 긴 prefix(그 prefix로 시작하는 컨트롤러/적산계, 동일 측정)**에 흡수되어 중복 제거된다.
- 예)
FI-6101 → FICQ-6101,FI-5115 → FIQ-5115,LI-6128 → LICA-6128,PI/TI → PICA/TICA. - 대응 없는 독립 지시계(
FI-3203/3401/3402/6128등)는 유지. P-(펌프, 1자),AG-/XV-/VP-(교반/밸브/변수)는 ≥2자 규칙 + prefix-startswith 로 보호(흡수 안 됨).- 검증(C3): 흡수 61개(FI 28, TI 18, PI 8, LI 7), 비지시계 흡수 0. 레지스터 2185→2123.
- 구현:
build_registers말미 후처리 — register 필터 + db_conn 시 흡수 base를 map_master/ tag_metadata 에서 DELETE(split_part(...,'.',1)=base).
롤아웃 순서 (반드시 함께)
1) register-map + map_master + metadata 재생성 (4 컨트롤러)
cd /home/windpacer/projects/hc900_ax
DSN="host=localhost port=5432 dbname=iiot_platform user=postgres password=postgres options=-csearch_path=hc900"
for C in C1 C2 C3 C4; do
python3 scripts/build_register_map_from_sinam.py --controller $C \
--sinam docs/Sinam_Tag_all.xlsx -o docs/register-map-${C,,}.json --db-conn "$DSN"
done
→ map_master 가 새 .PV 태그로 갱신됨. 단, 이전 bare 태그가 map_master에 잔존 (upsert는 삭제 안 함). 아래로 정리:
# 각 컨트롤러: register-map JSON 에 없는 map_master 태그 비활성/삭제 (고아 = 옛 bare명)
python3 - <<'PY'
import json, subprocess
for c in ['C1','C2','C3','C4']:
js={r['tag'] for r in json.load(open(f'docs/register-map-{c.lower()}.json'))['registers']}
out=subprocess.run(["docker","exec","iiot-timescaledb","psql","-U","postgres","-d","iiot_platform","-At","-c",
f"SELECT tagname FROM hc900.hc900_map_master WHERE controller_id='{c}'"],capture_output=True,text=True).stdout
orphan=set(out.split())-js
print(c,'orphan',len(orphan))
if orphan:
vals=",".join("'"+t.replace("'","''")+"'" for t in orphan)
subprocess.run(["docker","exec","iiot-timescaledb","psql","-U","postgres","-d","iiot_platform","-c",
f"DELETE FROM hc900.hc900_map_master WHERE controller_id='{c}' AND tagname IN ({vals})"])
PY
2) 기존 realtime/history 데이터 마이그레이션 (bare → .PV)
옛 bare명 데이터를 새 .PV명으로 rename(아날로그 신호점 한정 — 디지털 PV는 history에 없음). map_master의 새 .PV 태그 중 옛 bare가 realtime/history에 있던 것만.
-- realtime_table: bare → bare||'.PV' (단, 이미 .PV/기타 suffix 없는 신호점만)
-- 안전을 위해 map_master 의 .PV 태그에서 base 도출해 매핑.
-- (대량 작업 — 백업 후 실행 권장)
대안(권장): 데이터 마이그레이션 대신 realtime_table/history_table의 옛 bare 행은 폐기하고 게이트웨이 재시작 후 새 .PV명으로 자연 재적재. history 트렌드 연속성이 필요하면 rename, 아니면 폐기가 단순.
3) 다운스트림 코드 (C# src/Infrastructure/Database/Hc900DbContext.cs)
디지털 검출: GetDigitalTagNamesAsync/GetDigitalTagPairsAsync 는 state 라벨 보유
base_tag(예: P-9114)를 반환하는데, 이제 realtime 태그명은 P-9114.PV다.
→ 반환값을 base || '.PV' 로 매핑하도록 수정(또는 detector가 base.PV로 조회).
Hc900RealtimeService.FormatValue는baseTag = tagname.split('.')[0]라.PV여도 state 라벨 적용은 정상(수정 불필요).Hc900DigitalEventDetectorService는 위 검출목록으로 realtime 조회 → 목록이.PV면 동작.
4) 게이트웨이/앱 재시작 + 검증
pkill -f Hc900Crawler; pkill -f hc900_gateway; sleep 3
cd src/Hc900Crawler && setsid nohup dotnet run --no-build > /tmp/hc900_app.log 2>&1 < /dev/null &
# 검증: 디지털 태그 N개 로드 / 에러 0 / realtime 에 .PV 명 적재
검증 체크리스트
- register-map: bare(suffix 없는) 태그 0 (모두
{base}.{param}). - map_master == register-map JSON (고아 0).
- 디지털 검출:
디지털 태그 N개 로드로그, 이벤트 정상. - 이력조회 드롭다운:
.PV명으로 표시(예:LI-6100.PV).
참고
- 관련 메모리:
sinam-xlsx-multisection-parsing,tag-naming-and-map-master. - 이 변경은 doc §5.1(AnalogPoint PV=bare)을 개정한다 — 일관성 위해 PV도 항상 suffix.