SungjuNewPrime/NILM_Agent/AgentNILM_MQTTClient(Delphi13 VCL)/Docs/Generate Separate Manuals.py
2026-09-04 11:15:47 +09:00

412 lines
24 KiB
Python

import os
import docx
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_TABLE_ALIGNMENT
from docx.oxml import parse_xml, OxmlElement
from docx.oxml.ns import nsdecls, qn
def set_cell_shading(cell, color_hex):
shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color_hex}"/>')
cell._tc.get_or_add_tcPr().append(shading_elm)
def set_cell_margins(cell, top=100, bottom=100, left=150, right=150):
tcPr = cell._tc.get_or_add_tcPr()
tcMar = OxmlElement('w:tcMar')
for m, val in [('top', top), ('bottom', bottom), ('left', left), ('right', right)]:
node = OxmlElement(f'w:{m}')
node.set(qn('w:w'), str(val))
node.set(qn('w:type'), 'dxa')
tcMar.append(node)
tcPr.append(tcMar)
def init_doc():
doc = Document()
for section in doc.sections:
section.top_margin = Inches(1.0)
section.bottom_margin = Inches(1.0)
section.left_margin = Inches(1.0)
section.right_margin = Inches(1.0)
style_normal = doc.styles['Normal']
font = style_normal.font
font.name = 'Malgun Gothic'
font.size = Pt(10)
font.color.rgb = RGBColor(0x33, 0x33, 0x33)
return doc
def add_title(doc, title, subtitle, version, date):
title_p = doc.add_paragraph()
title_p.paragraph_format.space_before = Pt(12)
title_p.paragraph_format.space_after = Pt(4)
title_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run_title = title_p.add_run(title)
run_title.font.name = 'Malgun Gothic'
run_title.font.size = Pt(22)
run_title.font.bold = True
run_title.font.color.rgb = RGBColor(0x1B, 0x36, 0x5D)
sub_p = doc.add_paragraph()
sub_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
sub_p.paragraph_format.space_after = Pt(20)
run_sub = sub_p.add_run(subtitle)
run_sub.font.name = 'Malgun Gothic'
run_sub.font.size = Pt(12)
run_sub.font.color.rgb = RGBColor(0x66, 0x66, 0x66)
table_meta = doc.add_table(rows=2, cols=4)
table_meta.alignment = WD_TABLE_ALIGNMENT.CENTER
meta_headers = ["문서 구분", "문서 버전", "작성 일자", "대상 시스템"]
meta_values = [title.split('\n')[-1].strip(), version, date, "AgentNILM MQTT Client"]
for c_idx in range(4):
cell_h = table_meta.cell(0, c_idx)
cell_h.text = meta_headers[c_idx]
set_cell_shading(cell_h, "1B365D")
p = cell_h.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.runs[0].font.bold = True
p.runs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
p.runs[0].font.size = Pt(9.5)
set_cell_margins(cell_h, 80, 80, 100, 100)
cell_v = table_meta.cell(1, c_idx)
cell_v.text = meta_values[c_idx]
set_cell_shading(cell_v, "F4F6F9")
p = cell_v.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
p.runs[0].font.size = Pt(9.5)
set_cell_margins(cell_v, 80, 80, 100, 100)
doc.add_paragraph().paragraph_format.space_after = Pt(12)
def add_h1(doc, text):
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(18)
p.paragraph_format.space_after = Pt(6)
p.paragraph_format.keep_with_next = True
run = p.add_run(text)
run.font.name = 'Malgun Gothic'
run.font.size = Pt(14)
run.font.bold = True
run.font.color.rgb = RGBColor(0x1B, 0x36, 0x5D)
return p
def add_h2(doc, text):
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(12)
p.paragraph_format.space_after = Pt(4)
p.paragraph_format.keep_with_next = True
run = p.add_run(text)
run.font.name = 'Malgun Gothic'
run.font.size = Pt(11.5)
run.font.bold = True
run.font.color.rgb = RGBColor(0x2B, 0x54, 0x7E)
return p
def add_h3(doc, text):
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(8)
p.paragraph_format.space_after = Pt(2)
p.paragraph_format.keep_with_next = True
run = p.add_run(text)
run.font.name = 'Malgun Gothic'
run.font.size = Pt(10.5)
run.font.bold = True
run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
return p
def add_p(doc, text, bold_prefix=None, space_after=4):
p = doc.add_paragraph()
p.paragraph_format.space_after = Pt(space_after)
p.paragraph_format.line_spacing = 1.2
if bold_prefix:
r_bold = p.add_run(bold_prefix)
r_bold.font.bold = True
r_bold.font.color.rgb = RGBColor(0x1B, 0x36, 0x5D)
run = p.add_run(text)
run.font.name = 'Malgun Gothic'
return p
def add_bullet(doc, text, bold_prefix=None):
p = doc.add_paragraph(style='List Bullet')
p.paragraph_format.space_after = Pt(3)
p.paragraph_format.line_spacing = 1.15
if bold_prefix:
r_bold = p.add_run(bold_prefix)
r_bold.font.bold = True
run = p.add_run(text)
run.font.name = 'Malgun Gothic'
return p
def add_code_block(doc, text):
p = doc.add_paragraph()
p.paragraph_format.space_before = Pt(4)
p.paragraph_format.space_after = Pt(8)
run = p.add_run(text)
run.font.name = 'Consolas'
run.font.size = Pt(8.5)
run.font.color.rgb = RGBColor(0x00, 0x33, 0x66)
return p
# ─────────────────────────────────────────────────────────
# 1. 운영 매뉴얼 (User & Operation Manual)
# ─────────────────────────────────────────────────────────
def generate_operation_manual(output_path):
doc = init_doc()
add_title(doc, "AgentNILM MQTT Client\n시스템 운영자 매뉴얼", "현장 운영자 및 관리자를 위한 기능별 조작, 3P3W/3P4W 모니터링 및 유지보수 가이드", "v1.6", "2026-08-20")
# 1. Overview
add_h1(doc, "1. 시스템 소개 및 운영 목적")
add_p(doc, "AgentNILM MQTT Client는 비침입형 부하 모니터링(NILM: Non-Intrusive Load Monitoring) 센서 및 스마트 전력 미터로부터 계측 데이터를 실시간 수신하여 공장 내 주요 설비의 전력 상태(전압, 전류, 유효/무효/피상전력, 역률, 누적전력량 등)를 감시하고, 설비 가동 상태(가동/미가동/꺼짐)를 자동 판정하여 데이터베이스 및 표준 OPC Server(Kepware 등)로 전달하는 통합 운영 관제 프로그램입니다.")
add_bullet(doc, " 3상 3선식(3P3W, 2전력계법) 및 3상 4선식(3P4W) 혼재 환경을 완벽 지원합니다.", "다양한 결선 지원:")
add_bullet(doc, " 실시간 측정값 외에 노드 상태(/status), 알람(/event), 게이트웨이 상태(/gateway)를 통합 관리합니다.", "토픽별 다중 관제:")
add_bullet(doc, " 34개 장비 전체에 대해 총 1,870개 태그를 OPC Server에 실시간으로 동기화합니다.", "OPC Server 자동 연동:")
# 2. Getting Started
add_h1(doc, "2. 프로그램 실행 및 초기 환경 설정")
add_h2(doc, "2.1 프로그램 시작 및 자동 연결")
add_bullet(doc, " AgentNILM_MQTTClient.exe를 실행합니다.", "프로그램 실행:")
add_bullet(doc, " 상단 Broker IP, Port(기본 1883), Client ID를 입력하고 [Connect] 버튼을 클릭하여 브로커에 접속합니다.", "브로커 접속:")
add_bullet(doc, " 프로그램 시작 시 자동으로 MQTT 브로커, PostgreSQL DB, OPC Server에 동시 연결하고 등록된 모든 장비 토픽을 자동 구독합니다.", "Auto Start (전체 시작):")
add_bullet(doc, " 수신된 계측 데이터를 PostgreSQL DB(nilm_data 및 nilm_data_history)에 실시간 자동 저장합니다.", "DB 자동 저장 (chkAutoSaveDB):")
add_bullet(doc, " 화면 하단 통신 로그 창 표시 여부 및 일자별 파일 로그 저장 여부를 제어합니다.", "Log View / Log Save:")
# 3. Screen Guide
add_h1(doc, "3. 화면별 조작 및 모니터링 가이드")
add_h2(doc, "3.1 [탭 1] 실시간 전력 모니터링 (grdMQTTMonitor)")
add_p(doc, "현장에 설치된 모든 NILM 디바이스(1~34번)의 최신 측정값을 실시간 표시하는 메인 대시보드 화면입니다. 3P3W(3상 3선식) 결선 장비의 경우 B상(S상) 열은 자동으로 '-' (미사용)으로 표시됩니다.")
col_info = [
("No", "화면 표시 순번"),
("Device ID", "디바이스 고유 식별 번호 (1 ~ 34)"),
("Device Name", "설비/센서 등록 명칭"),
("Comm Status", "통신 상태 코드 (0: 정상 수신, 255: 통신 두절)"),
("Phase Type", "전력 결선 방식 (3P3W / 3P4W / Single)"),
("RSSI (dBm)", "무선 통신 수신 신호 강도 (LoRa 수신감도)"),
("Total_W (W)", "설비 전체 유효전력 합계 (3P3W: PA+PC, 3P4W: PA+PB+PC)"),
("Total_PF", "설비 전체 역률 (Total Power Factor)"),
("PF_A / PF_B / PF_C", "A, B, C 상별 역률 (3P3W 시 B상은 '-')"),
("Vrms_A / Vrms_B / Vrms_C", "A, B, C 상별 RMS 전압 (3P3W: 선간전압, 3P4W: 상전압)"),
("Irms_A / Irms_B / Irms_C", "A, B, C 상별 RMS 전류 (A 단위, 3P3W 시 B상은 '-')"),
("Active_A / Active_B / Active_C", "A, B, C 상별 유효전력 소비량 (W 단위)"),
("wh (Wh)", "센서 노드 자체 누적 적산 유효전력량 (Wh)"),
("Temp (°C)", "디바이스 내부 센서 계측 온도"),
("Energy_Day (Wh)", "당일 00:00부터 현재까지의 수치적분 누적 유효 전력량 (Wh)"),
("Last Received", "마지막 데이터 패킷 수신 일시 (YYYY-MM-DD HH:NN:SS)")
]
t_grid = doc.add_table(rows=len(col_info)+1, cols=2)
t_grid.alignment = WD_TABLE_ALIGNMENT.CENTER
t_grid.cell(0, 0).text = "컬럼명"
t_grid.cell(0, 1).text = "상세 설명 및 운영 기준"
set_cell_shading(t_grid.cell(0, 0), "1B365D")
set_cell_shading(t_grid.cell(0, 1), "1B365D")
for cell in (t_grid.cell(0, 0), t_grid.cell(0, 1)):
p = cell.paragraphs[0]
p.runs[0].font.bold = True
p.runs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
set_cell_margins(cell, 60, 60, 100, 100)
for idx, (cname, cdesc) in enumerate(col_info):
r_cell0 = t_grid.cell(idx+1, 0)
r_cell1 = t_grid.cell(idx+1, 1)
r_cell0.text = cname
r_cell1.text = cdesc
if idx % 2 == 1:
set_cell_shading(r_cell0, "F8F9FA")
set_cell_shading(r_cell1, "F8F9FA")
set_cell_margins(r_cell0, 50, 50, 80, 80)
set_cell_margins(r_cell1, 50, 50, 80, 80)
doc.add_paragraph().paragraph_format.space_after = Pt(6)
add_h2(doc, "3.2 [탭 2] NILM 장비 설정 및 MQTT Topic 관리")
add_p(doc, "신규 센서 등록, 기존 센서 정보 수정/삭제 및 설비 가동 상태 판단 파라미터를 관리합니다.")
add_h3(doc, "1) 디바이스 기본 정보 및 토픽 입력 규칙")
add_bullet(doc, " 중복되지 않는 1 이상의 정수 번호를 입력합니다. (예: 1 ~ 34)", "장비 번호 (Device ID):")
add_bullet(doc, " 현장 설비명(예: 1번기_성형기) 및 배전반 위치를 입력합니다.", "장비 명칭 & 위치:")
add_bullet(doc, " QST/SUNGJOO/NEWPRIME/{게이트웨이ID}/{장비번호} 형식으로 입력합니다. (예: QST/SUNGJOO/NEWPRIME/NE002/1)", "MQTT 토픽:")
add_bullet(doc, " 3P3W(3상 3선식), 3P4W(3상 4선식), Single(단상) 선택 시 센서 템플릿 목록이 자동으로 재구성됩니다.", "상수 타입 (Phase Type):")
add_h3(doc, "2) 설비 가동 상태(OpStatus) 판정 임계값 설정")
add_bullet(doc, " 판정에 사용할 상 선택 (L1, L2, L3, 또는 3상 평균 AVG). 3P3W 설비는 AVG 선택 시 A/C상 평균으로 계산됩니다.", "대상 상 (Target Phase):")
add_bullet(doc, " 전류가 이 값 이하이면 '전원 꺼짐(0)'으로 판정하는 기준 전류(A).", "꺼짐 기준 전류 (Off Current):")
add_bullet(doc, " 전류가 이 값 이상이면 '가동 중(2)'으로 판정하는 기준 전류(A). (미만 시 '미가동/대기(1)')", "가동 기준 전류 (Run Current):")
add_bullet(doc, " 가동 판정 시 보조 지표로 활용할 최소 역률 기준값.", "가동 기준 PF:")
add_h2(doc, "3.3 [탭 3] 트렌드 분석 (시계열 차트)")
add_p(doc, "선택한 기간 동안 특정 설비의 전력 파라미터 변화 추이를 시계열 그래프로 조회합니다.")
add_bullet(doc, " 분석할 디바이스를 콤보박스에서 선택하고 기간을 설정합니다.", "1단계 (장비 및 기간):")
add_bullet(doc, " L1, L2, L3 중 분석하고자 하는 상(Phase)을 선택합니다. (3P3W 설비는 L1: A선, L3: C선)", "2단계 (대상 상 선택):")
add_bullet(doc, " pf(역률), voltage(전압), current(전류), Var(무효전력), Va(피상전력), W(유효전력) 체크박스 선택 후 [조회] 클릭", "3단계 (항목 선택 및 조회):")
add_h2(doc, "3.4 [탭 4] 히스토리 및 이벤트 로그 검색")
add_p(doc, "데이터베이스에 축적된 시계열 원시 데이터(결선, RSSI, Total 전력/역률, 상별 전력, wh 등)와 알람 이벤트 로그를 검색하고 [CSV 내보내기]를 통해 파일로 저장할 수 있습니다.")
# 4. Routine & Troubleshooting
add_h1(doc, "4. 일상 운영 점검 및 트러블슈팅")
add_h2(doc, "4.1 일상 점검 체크리스트")
add_bullet(doc, " MQTT Log에 'Broker Connected' 및 구독 성공 메시지가 정상 표시되는지 확인", "1. 브로커 연결 확인:")
add_bullet(doc, " 모니터링 화면의 'Last Received' 일시가 1~5초 주기로 정상 갱신되는지 확인", "2. 수신 주기 확인:")
add_bullet(doc, " DB Log 창에 에러 없이 정상 트랜잭션이 발생하는지 확인", "3. DB 저장 상태:")
add_bullet(doc, " OPC Server에 SYSTEM.{DeviceID}/total_p 등 신규 태그가 정상 쓰기되는지 확인", "4. OPC 통신 상태:")
add_h2(doc, "4.2 주요 이상 상황별 대처 요령")
add_bullet(doc, " 게이트웨이 전원 및 LoRa 안테나 상태를 확인하고, 브로커 IP/Port를 확인 후 재연결합니다.", "현상 1: 특정 장비 Comm Status가 255(통신두절)로 표시될 때:")
add_bullet(doc, " PostgreSQL 서비스 상태 및 AgentNILM.ini 파일 내 DB 접속 정보(Host, Port, User, Pass)를 점검합니다.", "현상 2: DB Log에 Connection Error 발생 시:")
add_bullet(doc, " Kepware 등 OPC Server가 실행 중인지 확인하고, DXP 파일에 해당 장비 태그가 등록되어 있는지 확인합니다.", "현상 3: OPC Log에 태그 쓰기 오류 발생 시:")
doc.save(output_path)
print(f"Operation Manual created at: {output_path}")
# ─────────────────────────────────────────────────────────
# 2. 개발 및 유지보수 매뉴얼 (Development Manual)
# ─────────────────────────────────────────────────────────
def generate_development_manual(output_path):
doc = init_doc()
add_title(doc, "AgentNILM MQTT Client\n개발 및 유지보수 매뉴얼", "소프트웨어 엔지니어 및 유지보수 담당자를 위한 3P3W/3P4W 아키텍처, 코드, DB 및 OPC 연동 가이드", "v1.6", "2026-08-20")
# 1. Project Architecture
add_h1(doc, "1. 프로젝트 아키텍처 및 소스 구조")
add_p(doc, "AgentNILM 프로젝트는 Delphi 13 (Athens) VCL 기반의 비동기 소켓, 멀티스레드 안전성 및 고성능 전력 분석 엔진으로 구성되어 있습니다.")
unit_info = [
("AgentNILM_MQTTClient.dpr", "프로젝트 메인 엔트리포인트 (초기화 및 폼 생성)"),
("uMain.pas / .dfm", "메인 화면 UI, MQTT 메시지 라우팅, 실시간 모니터링, OPC Server 쓰기 제어, 가동 판정"),
("uNILMManager.pas", "PostgreSQL DB 액세스 전담 레이어 (Connection Pool, UPSERT, 스키마 마이그레이션, 이벤트 기록)"),
("uNILMDeviceForm.pas / .dfm", "디바이스 추가/수정 모달 폼 (3P3W/3P4W 센서 템플릿 동기화 및 메모리 독립 Deep Copy 적용)"),
("uNILMTypes.pas", "3P3W/3P4W 측정, 상태, 게이트웨이, 이벤트 페이로드 구조체 및 센서 템플릿 빌더"),
("UMQTTClient.pas", "MQTT 프로토콜 비동기 소켓 통신 클라이언트 컴포넌트"),
("uLogManagerThread.pas", "비동기 파일 로그 기록을 위한 백그라운드 스레드 매니저"),
("Docs/sungju_mqtt_1_34.dxp", "1~34번 장비 1,870개 OPC 태그 설정 XML 파일"),
("Docs/NILM_OPC_서버_태그_사양서_및_연동가이드.docx", "타사 연동용 OPC 태그 사양서 및 클라이언트 개발 가이드")
]
t_units = doc.add_table(rows=len(unit_info)+1, cols=2)
t_units.alignment = WD_TABLE_ALIGNMENT.CENTER
t_units.cell(0, 0).text = "유닛/파일 명칭"
t_units.cell(0, 1).text = "역할 및 핵심 구현 내용"
set_cell_shading(t_units.cell(0, 0), "1B365D")
set_cell_shading(t_units.cell(0, 1), "1B365D")
for cell in (t_units.cell(0, 0), t_units.cell(0, 1)):
p = cell.paragraphs[0]
p.runs[0].font.bold = True
p.runs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
set_cell_margins(cell, 60, 60, 100, 100)
for idx, (uname, udesc) in enumerate(unit_info):
r_cell0 = t_units.cell(idx+1, 0)
r_cell1 = t_units.cell(idx+1, 1)
r_cell0.text = uname
r_cell1.text = udesc
if idx % 2 == 1:
set_cell_shading(r_cell0, "F8F9FA")
set_cell_shading(r_cell1, "F8F9FA")
set_cell_margins(r_cell0, 50, 50, 80, 80)
set_cell_margins(r_cell1, 50, 50, 80, 80)
doc.add_paragraph().paragraph_format.space_after = Pt(6)
# 2. Protocols & Schema
add_h1(doc, "2. 통신 프로토콜 및 라우팅 아키텍처")
add_h2(doc, "2.1 토픽 분류 및 처리 흐름 (ProcessIncomingMQTTMessage)")
add_bullet(doc, " QST/SUNGJOO/NEWPRIME/{GWID}/{NDID} -> NILM_SaveMQTTData (전압, 전류, 전력, 역률, wh 실시간 처리)", "1. 실시간 계측 토픽:")
add_bullet(doc, " .../{NDID}/status -> ProcessNodeStatusMessage (노드 온라인 여부 및 손실률 -> COMM 태그 쓰기)", "2. 노드 상태 토픽 (Retain):")
add_bullet(doc, " .../{NDID}/event -> ProcessEventMessage (과전압/과전류 알람 -> nilm_event_log 기록)", "3. 알람 이벤트 토픽:")
add_bullet(doc, " .../{GWID}/gateway -> ProcessGatewayMessage (게이트웨이 온라인 노드 수 및 주기 관리)", "4. 게이트웨이 상태 토픽 (Retain):")
add_h2(doc, "2.2 MQTT JSON 페이로드 상세 스펙")
add_p(doc, "최신 표준 규격(3P3W / 3P4W 채널 배열 구조):")
json_spec = (
'{\n'
' "gw": "NE002", "dev": "NE002-01",\n'
' "seq": 1024, "COMM": 0, "rssi": -65,\n'
' "wire": "3P3W", // 결선: "3P3W" (0) 또는 "3P4W" (1)\n'
' "freq": 60.01,\n'
' "wh": 1254890, // 센서 적산 유효전력량 (Wh)\n'
' "ch": [\n'
' { "v": 220.5, "i": 12.3, "p": 2700.0, "q": 300.0, "s": 2716.5, "pf": 0.99 }, // ch[0]: A선\n'
' null, // ch[1]: B선 (3P3W 시 null)\n'
' { "v": 220.8, "i": 11.9, "p": 2620.0, "q": 280.0, "s": 2634.9, "pf": 0.99 } // ch[2]: C선\n'
' ],\n'
' "total": { "p": 5320.0, "q": 580.0, "s": 5351.4, "pf": 0.99 }\n'
'}'
)
add_code_block(doc, json_spec)
add_h2(doc, "2.3 전력 결선 방식별 연산 공식")
add_bullet(doc, " 2전력계법 적용. ch[0](A선)과 ch[2](C선) 계측. Total P = PA + PC, Total Q = QA + QC, Total S = √(P² + Q²), Total PF = P / S", "3상 3선식 (3P3W):")
add_bullet(doc, " 3상 개별 계측 및 합산. Total P = PA + PB + PC, Total Q = QA + QB + QC", "3상 4선식 (3P4W):")
add_h2(doc, "2.4 OPC Server 태그 쓰기 사양 (Kepware)")
add_bullet(doc, " 네임스페이스: SYSTEM.{DeviceID}/{TagName}", "태그 경로:")
add_bullet(doc, " total_p, total_q, total_s, total_pf, wh, freq, rssi, wire, COMM, OpStatus 및 상별 vrms, irms, active_power 등", "주요 태그:")
add_bullet(doc, " WriteOPCValueIfChanged를 통해 이전 값과 달라진 경우에만 OPC Server에 기록하여 불필요한 COM 호출 및 부하 최소화", "최적화 메커니즘:")
# 3. Database Architecture
add_h1(doc, "3. 데이터베이스 설계 및 DDL 마이그레이션 (PostgreSQL)")
add_h2(doc, "3.1 스키마 자동 마이그레이션")
add_p(doc, "uNILMManager.EnsureTables 실행 시 기존 테이블에 신규 컬럼을 자동 추가합니다:")
sql_ddl = (
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS wire VARCHAR(10);\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS rssi INTEGER;\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS wh BIGINT;\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS total_p DOUBLE PRECISION;\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS total_q DOUBLE PRECISION;\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS total_s DOUBLE PRECISION;\n'
'ALTER TABLE nilm_data ADD COLUMN IF NOT EXISTS total_pf DOUBLE PRECISION;\n'
'\n'
'CREATE TABLE IF NOT EXISTS nilm_gateway_status (\n'
' gw VARCHAR(50) PRIMARY KEY, online BOOLEAN, nodes_total INT, nodes_online INT,\n'
' cycle_ms INT, uptime_s BIGINT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP\n'
');'
)
add_code_block(doc, sql_ddl)
add_h2(doc, "3.2 실시간 측정 데이터 UPSERT 메커니즘 (SaveMeasurementData)")
add_p(doc, "nilm_data 테이블에 결선별 최신 전력 데이터를 UPSERT 갱신하고, nilm_data_history에 이력을 누적합니다:")
sql_upsert = (
'INSERT INTO nilm_data (\n'
' device_id, seq, pre_seq, comm_status, wire, rssi, wh, total_p, total_q, total_s, total_pf,\n'
' power_factor_a, vrms_a, irms_a, active_power_a, reactive_power_a, apparent_power_a,\n'
' power_factor_b, vrms_b, irms_b, active_power_b, reactive_power_b, apparent_power_b,\n'
' power_factor_c, vrms_c, irms_c, active_power_c, reactive_power_c, apparent_power_c,\n'
' received_at, payload_json\n'
') VALUES (\n'
' :did, :seq, :preseq, :comm, :wire, :rssi, :wh, :totp, :totq, :tots, :totpf,\n'
' :pfa, :vrmsa, :irmsa, :apa, :rpa, :appa,\n'
' :pfb, :vrmsb, :irmsb, :apb, :rpb, :appb,\n'
' :pfc, :vrmsc, :irmsc, :apc, :rpc, :appc,\n'
' NOW(), CAST(:pjson AS JSONB)\n'
') ON CONFLICT (device_id) DO UPDATE SET\n'
' seq=EXCLUDED.seq, pre_seq=EXCLUDED.pre_seq, comm_status=EXCLUDED.comm_status,\n'
' wire=EXCLUDED.wire, rssi=EXCLUDED.rssi, wh=EXCLUDED.wh,\n'
' total_p=EXCLUDED.total_p, total_q=EXCLUDED.total_q, total_s=EXCLUDED.total_s, total_pf=EXCLUDED.total_pf,\n'
' power_factor_a=EXCLUDED.power_factor_a, vrms_a=EXCLUDED.vrms_a, irms_a=EXCLUDED.irms_a,\n'
' active_power_a=EXCLUDED.active_power_a, received_at=NOW();'
)
add_code_block(doc, sql_upsert)
# 4. Build & Maintenance
add_h1(doc, "4. 빌드, 배포 및 메모리 안전성 가이드")
add_h2(doc, "4.1 핵심 트러블슈팅 및 메모리 관리 지침")
add_bullet(doc, " TfNILMDeviceForm에서 SensorList를 전달받을 때 포인터를 직접 대입하지 않고 Deep Copy를 수행하여 Form 소멸 시 Double Free 에러(Invalid pointer operation)를 완벽히 차단함.", "1. 다이얼로그 SensorList 메모리 독립성 보장:")
add_bullet(doc, " 델파이 익명 메서드 큐잉 시 TThread.ForceQueue(nil, procedure begin ... end)를 사용하여 오버로드 모호성(E2250)을 방지함.", "2. TThread.ForceQueue UI 동기화:")
add_bullet(doc, " 3P3W 설비의 경우 B상 관련 태그 값이 0이므로 UI 표출 시 '-'로 대체하여 작업자 혼선을 방지함.", "3. 3P3W B상 예외 처리:")
doc.save(output_path)
print(f"Development Manual created at: {output_path}")
if __name__ == "__main__":
docs_dir = r"c:\Users\MyName\Desktop\antigravity\AgentNILM_MQTTClient(Delphi13 VCL)\Docs"
op_file = os.path.join(docs_dir, "AgentNILM_운영_매뉴얼.docx")
dev_file = os.path.join(docs_dir, "AgentNILM_개발_및_유지보수_매뉴얼.docx")
generate_operation_manual(op_file)
generate_development_manual(dev_file)