Files
HC900-Crawler/docs/작업지시-PV일관성-롤아웃.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

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: FormatValuebaseTag = 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-6100LI-6100.PV
  • AG-3202AG-3202.PV (+ .OP/.MD)
  • 변수 VP-8117.PV/.OP/.MD, SIG TI-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.FormatValuebaseTag = 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.