2번 모듈 신규(raspberry_pi4_sht30/): SHT30 온습도 → cafe24, raw-body X-Signature HMAC, 폐쇄망 오프라인 설치(vendor/install), systemd, CRC/포맷 자가점검. firmware sht30_fw wire 포맷과 byte-parity 검증. 1번 모듈 통합(raspberry_pi4/): 누수+카메라를 동일 서버로. 레거시 sha256(key+json) body-sig → X-Signature HMAC 전환, 사진 canonical=device_id|sensor_id|log_id|timestamp. 서버 통합(php/): api/sensor_data.php 누수 이벤트 수신, api/photo_upload.php 신규(HMAC·finfo·서버생성 파일명·nonce 멱등·sensor 결속), photo.php 인증 사진서빙, dashboard.php 누수 패널+사진, sms_send.php 누수 SMS, config.php 태그/요약. DB: sql/migration_add_leak.sql(가산·멱등), schema_sht30.sql 통합 스키마. 보안 하드닝(외부 펜테스트 대비, 확정 CRITICAL/HIGH 0): cron_heartbeat api_key fail-closed, setup_mfa 부트스트랩 토큰 post-enrollment 잠금, var/ PHP 실행차단, HSTS. 문서/테스트: SYSTEM_OVERVIEW.md, security_hardening_test.php 갱신. 참고: 사용자 요청으로 세션 외 기존 미커밋 변경도 함께 포함. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
658 lines
44 KiB
HTML
658 lines
44 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>서버실 온습도(SHT30) 모니터링 v2606 — 설치 가이드 (Cafe24 → STM32)</title>
|
||
<style>
|
||
/* 폐쇄망 적합: 외부 CDN/폰트/스크립트 0. 인라인 스타일만 사용. */
|
||
:root{
|
||
--bg:#f1f5f9; --card:#fff; --ink:#0f172a; --muted:#64748b; --line:#e2e8f0;
|
||
--brand:#1a56db; --brandd:#1347b8; --ok:#166534; --okbg:#f0fdf4; --okln:#bbf7d0;
|
||
--warn:#92400e; --warnbg:#fffbeb; --warnln:#fde68a; --crit:#991b1b; --critbg:#fef2f2; --critln:#fecaca;
|
||
--mono:'D2Coding','Consolas','Courier New',monospace;
|
||
}
|
||
*{box-sizing:border-box;}
|
||
body{margin:0;font-family:'Noto Sans KR','Malgun Gothic',system-ui,sans-serif;background:var(--bg);color:var(--ink);line-height:1.65;}
|
||
a{color:var(--brand);text-decoration:none;} a:hover{text-decoration:underline;}
|
||
header.top{background:linear-gradient(135deg,#1a56db,#0f766e);color:#fff;padding:34px 24px;}
|
||
header.top .wrap{max-width:980px;margin:0 auto;}
|
||
header.top h1{margin:0 0 6px;font-size:1.7rem;}
|
||
header.top p{margin:2px 0;color:rgba(255,255,255,.92);font-size:.95rem;}
|
||
.badges{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;}
|
||
.badge{background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.32);border-radius:999px;padding:3px 12px;font-size:.8rem;font-weight:600;}
|
||
.layout{max-width:980px;margin:0 auto;padding:24px 18px 80px;}
|
||
nav.toc{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px 20px;margin-bottom:24px;}
|
||
nav.toc h2{margin:0 0 10px;font-size:1rem;color:var(--brand);}
|
||
nav.toc ol{margin:0;padding-left:20px;columns:2;column-gap:28px;font-size:.9rem;}
|
||
@media(max-width:640px){nav.toc ol{columns:1;}}
|
||
nav.toc li{margin:3px 0;break-inside:avoid;}
|
||
section{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:24px 26px;margin-bottom:22px;box-shadow:0 1px 4px rgba(15,23,42,.05);}
|
||
section>h2{margin:0 0 4px;font-size:1.35rem;color:var(--brand);border-bottom:2px solid var(--line);padding-bottom:10px;}
|
||
.part-tag{display:inline-block;font-size:.78rem;font-weight:700;color:#fff;background:var(--brandd);border-radius:6px;padding:2px 10px;margin-bottom:8px;}
|
||
h3{margin:26px 0 8px;font-size:1.08rem;}
|
||
h4{margin:18px 0 6px;font-size:.98rem;color:#334155;}
|
||
p{margin:8px 0;}
|
||
code{font-family:var(--mono);background:#f1f5f9;border:1px solid var(--line);border-radius:4px;padding:1px 6px;font-size:.88em;color:#0b3b8c;}
|
||
pre{background:#0f172a;color:#e2e8f0;border-radius:10px;padding:16px 18px;overflow:auto;font-family:var(--mono);font-size:.85rem;line-height:1.55;}
|
||
pre code{background:none;border:none;color:inherit;padding:0;}
|
||
table{width:100%;border-collapse:collapse;margin:12px 0;font-size:.9rem;}
|
||
th,td{border:1px solid var(--line);padding:8px 10px;text-align:left;vertical-align:top;}
|
||
th{background:#f8fafc;font-weight:700;}
|
||
tbody tr:nth-child(even){background:#fafcff;}
|
||
.step{counter-increment:step;position:relative;padding-left:46px;margin:22px 0;}
|
||
.step::before{content:counter(step);position:absolute;left:0;top:-2px;width:32px;height:32px;border-radius:50%;background:var(--brand);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center;font-size:.95rem;}
|
||
.steps{counter-reset:step;}
|
||
.step h3{margin-top:0;}
|
||
/* 콜아웃 */
|
||
.note,.warn,.crit,.ok{border-radius:10px;padding:12px 16px;margin:14px 0;font-size:.9rem;border:1px solid;}
|
||
.note{background:#eff6ff;border-color:#bfdbfe;color:#1e3a8a;}
|
||
.warn{background:var(--warnbg);border-color:var(--warnln);color:var(--warn);}
|
||
.crit{background:var(--critbg);border-color:var(--critln);color:var(--crit);}
|
||
.ok{background:var(--okbg);border-color:var(--okln);color:var(--ok);}
|
||
.note b,.warn b,.crit b,.ok b{font-weight:800;}
|
||
/* 캡처 자리표시 */
|
||
figure{margin:18px 0;}
|
||
.shot{border:2px dashed #94a3b8;border-radius:12px;background:repeating-linear-gradient(45deg,#f8fafc,#f8fafc 12px,#f1f5f9 12px,#f1f5f9 24px);padding:26px 20px;text-align:center;}
|
||
.shot .cam{font-size:1.6rem;}
|
||
.shot .ph-title{font-weight:800;color:#334155;margin:6px 0 4px;font-size:.95rem;}
|
||
.shot .ph-what{color:#475569;font-size:.86rem;}
|
||
.shot .ph-what b{color:var(--brand);}
|
||
figure>figcaption{margin-top:8px;font-size:.82rem;color:var(--muted);text-align:center;font-style:italic;}
|
||
figure svg{max-width:100%;height:auto;display:block;margin:0 auto;background:#fff;border:1px solid var(--line);border-radius:12px;}
|
||
.kbd{font-family:var(--mono);background:#fff;border:1px solid #cbd5e1;border-bottom-width:2px;border-radius:5px;padding:1px 6px;font-size:.82em;}
|
||
hr{border:none;border-top:1px solid var(--line);margin:26px 0;}
|
||
.pin{font-family:var(--mono);font-weight:700;color:#0b3b8c;}
|
||
.small{font-size:.84rem;color:var(--muted);}
|
||
@media print{
|
||
body{background:#fff;} section{box-shadow:none;border-color:#cbd5e1;break-inside:avoid;}
|
||
header.top{background:#1a56db !important;-webkit-print-color-adjust:exact;print-color-adjust:exact;}
|
||
nav.toc{display:none;}
|
||
}
|
||
.cap{display:block;max-width:100%;height:auto;margin:6px auto 0;border:1px solid var(--line);border-radius:12px;box-shadow:0 1px 6px rgba(15,23,42,.08);}
|
||
.fname{font-family:var(--mono);font-size:.78rem;color:#475569;font-weight:600;}
|
||
.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(230px,1fr));gap:16px;margin:14px 0;}
|
||
.gallery figure{margin:0;}
|
||
.gallery img{width:100%;height:auto;border:1px solid var(--line);border-radius:8px;background:#fff;}
|
||
.gallery figcaption{font-size:.78rem;color:var(--muted);margin-top:4px;text-align:center;}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="top">
|
||
<div class="wrap">
|
||
<h1>서버실 온습도(SHT30) 모니터링 시스템 v2606 — 설치 가이드</h1>
|
||
<p>Cafe24 PHP/MySQL 서버부터 STM32F407 MCU 펌웨어까지, 폐쇄망(air-gapped) 배포 절차</p>
|
||
<div class="badges">
|
||
<span class="badge">서버: Cafe24 PHP / MySQL</span>
|
||
<span class="badge">MCU: STM32F407VGT6 (sht30_fw)</span>
|
||
<span class="badge">센서: SHT30 I²C 0x44</span>
|
||
<span class="badge">전송: HMAC-SHA256 + TLS 1.2</span>
|
||
<span class="badge">폐쇄망 · 인터넷 없음</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="layout">
|
||
|
||
<div class="note">
|
||
<b>이 가이드의 이미지에 대하여.</b> 본 문서의 <b>아키텍처·배선 다이어그램은 코드/<code>HARDWARE.md</code> 기준으로 정확히 생성</b>한 SVG입니다.
|
||
Cafe24 관리자 콘솔·phpMyAdmin·플래싱 툴·실제 대시보드 화면처럼 <b>운영 환경에서만 캡처할 수 있는 화면은
|
||
“📷 캡처 위치” 자리표시</b>로 두었습니다. 각 자리표시의 캡션이 가리키는 화면을 직접 캡처해 그 자리에 넣으면 됩니다.
|
||
(저장소의 기존 캡처는 구버전 v2605 누수감지 UI라 현재 화면과 다릅니다 — <a href="#appendix">부록</a> 참조.)
|
||
</div>
|
||
|
||
<nav class="toc">
|
||
<h2>목차</h2>
|
||
<ol>
|
||
<li><a href="#overview">시스템 개요 & 아키텍처</a></li>
|
||
<li><a href="#prep">설치 전 준비물</a></li>
|
||
<li><a href="#A-files">A. Cafe24 — 파일 업로드</a></li>
|
||
<li><a href="#A-config">A. Cafe24 — config.local.php</a></li>
|
||
<li><a href="#A-db">A. Cafe24 — DB 설치(phpMyAdmin)</a></li>
|
||
<li><a href="#A-admin">A. Cafe24 — 권한 & 관리자/MFA</a></li>
|
||
<li><a href="#A-cron">A. Cafe24 — cron(오프라인 감지)</a></li>
|
||
<li><a href="#B-wiring">B. STM32 — 하드웨어 배선</a></li>
|
||
<li><a href="#B-build">B. STM32 — 폐쇄망 빌드</a></li>
|
||
<li><a href="#B-flash">B. STM32 — 플래시 & 콘솔 확인</a></li>
|
||
<li><a href="#verify">최종 검증(종단)</a></li>
|
||
<li><a href="#trouble">문제 해결</a></li>
|
||
<li><a href="#secrets">운영자 보관 값</a></li>
|
||
<li><a href="#appendix">부록 — 레거시 자산</a></li>
|
||
</ol>
|
||
</nav>
|
||
|
||
<!-- ============================== 개요 ============================== -->
|
||
<section id="overview">
|
||
<h2>1. 시스템 개요 & 아키텍처</h2>
|
||
<p>단일 STM32F407 보드(<code>sht30_fw</code>, <code>sensor_id=2</code>)가 SHT30 센서를 I²C로 5분마다 읽어,
|
||
본문을 <b>HMAC-SHA256으로 서명</b>해 <b>TLS 1.2</b>로 Cafe24 PHP 서버에 POST합니다. 서버가 임계를 <b>재판정</b>하여
|
||
고온/저온/고습/저습·결로·오프라인 상황을 <b>SMS</b>로 알리고 <b>웹 대시보드/보고서</b>를 제공합니다.</p>
|
||
|
||
<figure>
|
||
<svg viewBox="0 0 940 250" role="img" aria-label="시스템 아키텍처 다이어그램">
|
||
<defs>
|
||
<marker id="ah" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">
|
||
<path d="M0,0 L8,3 L0,6 Z" fill="#334155"/>
|
||
</marker>
|
||
<style>
|
||
.b{fill:#fff;stroke:#1a56db;stroke-width:2;rx:10;}
|
||
.t{font-family:'Noto Sans KR',sans-serif;font-size:14px;fill:#0f172a;font-weight:700;text-anchor:middle;}
|
||
.s{font-family:'Noto Sans KR',sans-serif;font-size:11px;fill:#64748b;text-anchor:middle;}
|
||
.ln{stroke:#334155;stroke-width:2;marker-end:url(#ah);fill:none;}
|
||
.lbl{font-family:'Noto Sans KR',sans-serif;font-size:10.5px;fill:#0f766e;text-anchor:middle;font-weight:700;}
|
||
</style>
|
||
</defs>
|
||
<rect x="20" y="92" width="120" height="66" rx="10" fill="#ecfeff" stroke="#0f766e" stroke-width="2"/>
|
||
<text class="t" x="80" y="120">SHT30</text><text class="s" x="80" y="140">온·습도 센서</text>
|
||
<rect x="200" y="80" width="170" height="90" rx="10" fill="#eff6ff" stroke="#1a56db" stroke-width="2"/>
|
||
<text class="t" x="285" y="112">STM32F407</text><text class="s" x="285" y="132">sht30_fw</text><text class="s" x="285" y="150">FreeRTOS+mbedTLS</text>
|
||
<rect x="430" y="80" width="180" height="90" rx="10" fill="#eff6ff" stroke="#1a56db" stroke-width="2"/>
|
||
<text class="t" x="520" y="108">Cafe24 PHP</text><text class="s" x="520" y="128">api/sensor_data.php</text><text class="s" x="520" y="146">임계 재판정 + 저장</text>
|
||
<rect x="670" y="40" width="160" height="60" rx="10" fill="#f0fdf4" stroke="#166534" stroke-width="2"/>
|
||
<text class="t" x="750" y="66">MySQL</text><text class="s" x="750" y="84">4 테이블</text>
|
||
<rect x="670" y="150" width="160" height="62" rx="10" fill="#fffbeb" stroke="#92400e" stroke-width="2"/>
|
||
<text class="t" x="750" y="176">SMS · 대시보드</text><text class="s" x="750" y="194">경보/보고서</text>
|
||
<line class="ln" x1="142" y1="125" x2="196" y2="125"/><text class="lbl" x="170" y="116">I²C 0x44</text>
|
||
<line class="ln" x1="372" y1="125" x2="426" y2="125"/><text class="lbl" x="399" y="112">HMAC</text><text class="lbl" x="399" y="146">TLS 1.2</text>
|
||
<line class="ln" x1="612" y1="110" x2="666" y2="80"/>
|
||
<line class="ln" x1="612" y1="140" x2="666" y2="172"/>
|
||
</svg>
|
||
<figcaption>그림 1. 전체 데이터 흐름 — SHT30 → STM32(서명/TLS) → Cafe24(재판정/저장) → MySQL + SMS/대시보드. (코드 기준 생성)</figcaption>
|
||
</figure>
|
||
|
||
<table>
|
||
<thead><tr><th>구분</th><th>설치 위치</th><th>역할</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Cafe24 서버</td><td><code>public_html/raspi_leck_detecter/</code></td><td>센서 API 수신, DB 저장, 임계 판정, SMS, 대시보드/보고서</td></tr>
|
||
<tr><td>Cafe24 MySQL</td><td>Cafe24 DB</td><td>센서 로그 · 온습도 측정값 · SMS 로그 · 장비 상태</td></tr>
|
||
<tr><td>STM32F407VGT6</td><td>펌웨어 플래시 (<code>sht30_fw</code>)</td><td>SHT30 I²C 측정, 5분 주기 보고, raw-body HMAC 서명</td></tr>
|
||
<tr><td>서버 cron</td><td>Cafe24 cron 또는 외부 스케줄러</td><td><code>cron_heartbeat.php</code> 주기 실행(오프라인 감지·헬스·에스컬레이션)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="warn"><b>웹 루트 경로 고정.</b> 폴더명 <code>raspi_leck_detecter</code> 는 레거시 이름이지만 펌웨어
|
||
<code>app_config.h</code> 의 <code>APP_API_PATH</code> 와 <b>반드시 일치</b>해야 합니다. 폴더명을 바꾸려면 펌웨어도 함께 바꿔 재빌드해야 합니다.</div>
|
||
</section>
|
||
|
||
<!-- ============================== 준비물 ============================== -->
|
||
<section id="prep">
|
||
<h2>2. 설치 전 준비물</h2>
|
||
<table>
|
||
<thead><tr><th>서버측(Cafe24)</th><th>장치측(STM32)</th></tr></thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>
|
||
• Cafe24 PHP 호스팅<br>
|
||
• Cafe24 MySQL DB(이름/계정/비밀번호)<br>
|
||
• Cafe24 SMS 서비스 계정 + secure key + 발신번호<br>
|
||
• 운영 담당자 SMS 수신번호<br>
|
||
• HTTPS 도메인/경로 + Cafe24 루트 CA
|
||
</td>
|
||
<td>
|
||
• STM32F407VGT6 보드(DISCOVERY 계열/커스텀 PCB)<br>
|
||
• SHT30 모듈(한진데이터 P4422-3, I²C)<br>
|
||
• LAN8720 Ethernet PHY(RMII) + LAN 케이블<br>
|
||
• I²C 풀업 4.7kΩ × 2 (모듈 내장 시 생략)<br>
|
||
• 외부 3.3V USB-UART 어댑터(USART3 콘솔)<br>
|
||
• 폐쇄망 빌드 머신(arm-none-eabi-gcc, CMake, Ninja, ST-Link 툴)
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="note"><b>핵심 비밀값 한 쌍.</b> 서버 <code>config.local.php</code> 의 <code>API_KEY</code> 와 펌웨어
|
||
<code>secrets.h</code> 의 <code>APP_API_KEY</code> 는 <b>바이트 단위로 동일</b>해야 합니다. 서명
|
||
<code>X-Signature = lowercase_hex(HMAC-SHA256(key=API_KEY, msg=요청본문))</code> 검증의 기준입니다.</div>
|
||
</section>
|
||
|
||
<!-- ====================================================================
|
||
PART A — CAFE24 서버
|
||
===================================================================== -->
|
||
<section id="A-files">
|
||
<span class="part-tag">PART A · Cafe24 서버</span>
|
||
<h2>3. 서버 파일 업로드</h2>
|
||
<p>Cafe24 FTP(또는 파일 관리자)로 웹 루트 아래에 PHP 트리를 업로드합니다.</p>
|
||
<pre><code>public_html/raspi_leck_detecter/
|
||
.htaccess config.php config.local.php ← 서버에서만 생성
|
||
config.local.example.php
|
||
login.php dashboard.php setup_wizard.php
|
||
setup_mfa.php admin_security.php ops_checks.php
|
||
sms_send.php cron_heartbeat.php monthly_report.php
|
||
security_evidence.php retention_cleanup.php setup_hash.php
|
||
blocked.php test_mobile.php
|
||
api/
|
||
sensor_data.php
|
||
var/
|
||
.gitkeep ← PHP가 쓰기 가능해야 함(상태/감사/카운터 파일)</code></pre>
|
||
<div class="crit"><b>업로드 금지.</b> <code>config.local.php</code> 와 펌웨어 <code>secrets.h</code> 는 비밀값이므로
|
||
저장소/공유에 올리지 않습니다. <code>php/var/</code> 는 <code>.htaccess</code> 로 외부 접근이 차단됩니다(확인 필수).</div>
|
||
<figure>
|
||
<img class="cap" src="screenshots/02-server-files-upload.png" alt="그림 2 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 2 <span class="fname">→ screenshots/02-server-files-upload.png</span></div>
|
||
<div class="ph-what">Cafe24 <b>FTP/파일 관리자</b>에서 <b><code>public_html/raspi_leck_detecter/</code> 업로드 완료된 파일 목록</b> 화면</div>
|
||
</div>
|
||
<figcaption>그림 2. 서버 파일 업로드 결과(웹 루트 트리)</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<section id="A-config">
|
||
<span class="part-tag">PART A · Cafe24 서버</span>
|
||
<h2>4. 운영 설정 — <code>config.local.php</code></h2>
|
||
<p><code>config.local.example.php</code> 를 복사해 <code>config.local.php</code> 를 만들고 실제 운영 값으로 교체합니다.</p>
|
||
<pre><code><?php
|
||
return [
|
||
'DB_HOST' => 'localhost',
|
||
'DB_PORT' => 3306,
|
||
'DB_NAME' => 'your_db_name',
|
||
'DB_USER' => 'your_db_user',
|
||
'DB_PASS' => 'your_db_password',
|
||
|
||
'API_KEY' => 'replace-with-long-random-secret', // 펌웨어 APP_API_KEY 와 동일!
|
||
'ALLOW_LEGACY_BODY_SIGNATURE' => false, // HMAC 전용(레거시 RPi 없으면 false)
|
||
|
||
'SMS_USER_ID' => 'your-cafe24-sms-user-id',
|
||
'SMS_SECURE' => 'your-cafe24-sms-secure-key',
|
||
'SMS_SENDER' => '01000000000',
|
||
'SMS_RECIPIENTS' => ['01000000000'],
|
||
'SMS_RECIPIENTS_TIER2' => [], // 미복귀 30분 시 2차(야간) 통지(opt-in)
|
||
|
||
'ADMIN_USER' => 'admin',
|
||
'ADMIN_PASSWORD_HASH' => '', // setup_hash.php 출력으로 채움(빈값=로그인 차단)
|
||
'ADMIN_TOTP_SECRET' => '', // setup_mfa.php 등록 후 채움(빈값=로그인 차단)
|
||
'MFA_SETUP_TOKEN' => 'replace-with-temporary-random-token', // 등록 후 비울 것
|
||
|
||
// 온습도 임계(서버 판정 → SMS). 기본값은 config.php 의 METRIC_*
|
||
'METRIC_TEMP_HIGH_C' => 30, 'METRIC_TEMP_LOW_C' => 10,
|
||
'METRIC_RH_HIGH' => 70, 'METRIC_RH_LOW' => 20,
|
||
];</code></pre>
|
||
<div class="warn"><b>fail-closed 기본값.</b> <code>ADMIN_PASSWORD_HASH</code> 또는 <code>ADMIN_TOTP_SECRET</code> 가
|
||
비어 있으면 관리자 로그인이 <b>차단</b>됩니다(보안). 4·6단계에서 채웁니다.</div>
|
||
</section>
|
||
|
||
<section id="A-db">
|
||
<span class="part-tag">PART A · Cafe24 서버</span>
|
||
<h2>5. 데이터베이스 설치 (phpMyAdmin)</h2>
|
||
<div class="steps">
|
||
<div class="step">
|
||
<h3>Cafe24 phpMyAdmin 접속 → 대상 DB 선택</h3>
|
||
<p>Cafe24 호스팅 관리 → MySQL → phpMyAdmin. 좌측에서 운영 DB(<code>DB_NAME</code>)를 클릭합니다.</p>
|
||
</div>
|
||
<div class="step">
|
||
<h3>스키마 실행</h3>
|
||
<p><b>신규 설치</b>는 <code>sql/schema_sht30.sql</code> 전체를 복사해 <span class="kbd">SQL</span> 탭에 붙여넣고 <span class="kbd">실행</span>.
|
||
생성 테이블: <code>sensor_log</code>, <code>sensor_status</code>, <code>sensor_metric</code>, <code>sms_log</code>
|
||
(+ 재전송 방어용 <code>ingest_nonce</code>).</p>
|
||
<table>
|
||
<thead><tr><th>SQL 파일</th><th>적용 상황</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>sql/schema_sht30.sql</code></td><td>신규 설치(통합 스키마)</td></tr>
|
||
<tr><td><code>sql/migration_drop_leak.sql</code></td><td>기존 누수(v2605) 설치 → 온습도 전용 전환. <b>비가역 — 실행 전 DB 전체 백업</b></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="crit"><b>중복 실행 주의.</b> 이미 적용한 마이그레이션을 다시 실행하면 오류가 날 수 있습니다.
|
||
기존 설치는 <code>setup_wizard.php</code> 점검 결과를 먼저 확인하세요.</div>
|
||
</div>
|
||
</div>
|
||
<figure>
|
||
<img class="cap" src="screenshots/03-db-phpmyadmin.png" alt="그림 3 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 3 <span class="fname">→ screenshots/03-db-phpmyadmin.png</span></div>
|
||
<div class="ph-what">phpMyAdmin <b>SQL 탭에 <code>schema_sht30.sql</code> 붙여넣고 [실행]한 결과</b>(“쿼리 실행 성공” + 좌측 4~5개 테이블 생성된 화면)</div>
|
||
</div>
|
||
<figcaption>그림 3. DB 스키마 실행 결과 (sensor_log/sensor_status/sensor_metric/sms_log/ingest_nonce)</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<section id="A-admin">
|
||
<span class="part-tag">PART A · Cafe24 서버</span>
|
||
<h2>6. 권한 · 관리자 비밀번호 · MFA 등록</h2>
|
||
<div class="steps">
|
||
<div class="step">
|
||
<h3>상태 파일 폴더 쓰기 권한</h3>
|
||
<p><code>raspi_leck_detecter/var/</code> 가 PHP에서 쓰기 가능해야 합니다(브루트포스/하트비트/감사로그/거부 카운터 파일 저장).</p>
|
||
</div>
|
||
<div class="step">
|
||
<h3>관리자 비밀번호 해시 생성(서버 CLI)</h3>
|
||
<pre><code>php setup_hash.php "새관리자비밀번호"</code></pre>
|
||
<p>출력된 bcrypt 해시를 <code>config.local.php</code> 의 <code>ADMIN_PASSWORD_HASH</code> 에 넣습니다.</p>
|
||
</div>
|
||
<div class="step">
|
||
<h3>MFA(TOTP) 등록</h3>
|
||
<p><code>MFA_SETUP_TOKEN</code> 을 임시 설정한 뒤 아래 주소로 접속해 인증 앱(Google/Microsoft Authenticator, Authy)에 등록합니다.</p>
|
||
<pre><code>https://your-domain.example/raspi_leck_detecter/setup_mfa.php?token=임시토큰</code></pre>
|
||
<p>화면의 Base32 키를 인증 앱에 등록 → 6자리 코드 검증 → 표시된 <code>ADMIN_TOTP_SECRET</code> 줄을
|
||
<code>config.local.php</code> 에 반영 → <b>등록 후 <code>MFA_SETUP_TOKEN</code> 을 빈 값으로</b> 바꿉니다.</p>
|
||
<figure>
|
||
<img class="cap" src="screenshots/04-mfa-setup.png" alt="그림 4 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 4 <span class="fname">→ screenshots/04-mfa-setup.png</span></div>
|
||
<div class="ph-what"><b><code>setup_mfa.php</code> 등록 화면</b>(Base32 수동 입력 키 + 6자리 코드 “확인됨” 상태). 비밀키 노출 주의 — 캡처 시 키 일부 가림 권장</div>
|
||
</div>
|
||
<figcaption>그림 4. 관리자 MFA(TOTP) 등록 화면</figcaption>
|
||
</figure>
|
||
</div>
|
||
<div class="step">
|
||
<h3>설치 점검 화면 확인</h3>
|
||
<pre><code>https://your-domain.example/raspi_leck_detecter/setup_wizard.php</code></pre>
|
||
<p>확인 항목: DB 연결 · 필수 테이블 · API 키 교체 · 관리자 해시 · 관리자 TOTP · SMS 수신자 ·
|
||
<code>var/</code> 쓰기 권한 · <b>테스트 SMS 발송</b>.</p>
|
||
<figure>
|
||
<img class="cap" src="screenshots/05-setup-wizard.png" alt="그림 5 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 5 <span class="fname">→ screenshots/05-setup-wizard.png</span></div>
|
||
<div class="ph-what"><b><code>setup_wizard.php</code> 점검 결과</b>(모든 항목 ✅ + 테스트 SMS “발송 성공”). 수신번호는 마스킹되어 표시됨</div>
|
||
</div>
|
||
<figcaption>그림 5. 설치 점검(setup_wizard) 통과 화면</figcaption>
|
||
</figure>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="A-cron">
|
||
<span class="part-tag">PART A · Cafe24 서버</span>
|
||
<h2>7. cron 설정 — 오프라인 감지 · 헬스 · 에스컬레이션</h2>
|
||
<p><code>cron_heartbeat.php</code> 를 <b>1분 간격</b>으로 실행합니다(오프라인 감지 + 일일 헬스 SMS + 미복귀 에스컬레이션).</p>
|
||
<h4>방법 A — 서버 CLI cron</h4>
|
||
<pre><code>* * * * * php /home/hosting_user/public_html/raspi_leck_detecter/cron_heartbeat.php</code></pre>
|
||
<h4>방법 B — HTTP 호출(외부 스케줄러)</h4>
|
||
<pre><code>https://your-domain.example/raspi_leck_detecter/cron_heartbeat.php?key=<CRON_SECRET></code></pre>
|
||
<p class="small"><code>CRON_SECRET = HMAC-SHA256("cron", API_KEY)</code> 입니다(코드가 파생). 절대 경로는 Cafe24 파일 관리자/<code>phpinfo()</code>로 확인.
|
||
마지막 수신 후 <code>HEARTBEAT_TIMEOUT_SEC</code>(기본 1200초=20분) 초과 시 오프라인 판정.</p>
|
||
<div class="note"><b>cron이 없으면?</b> 다른 페이지(ingest/대시보드) 접속 시 5분 쓰로틀로 piggyback 실행됩니다. 단,
|
||
완전 유휴 상태에선 오프라인 감지가 늦어질 수 있으니 <b>cron 등록을 권장</b>합니다.</div>
|
||
<figure>
|
||
<img class="cap" src="screenshots/06-cron-register.png" alt="그림 6 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 6 <span class="fname">→ screenshots/06-cron-register.png</span></div>
|
||
<div class="ph-what">Cafe24 <b>크론탭(예약작업) 등록 화면</b> — <code>cron_heartbeat.php</code> 가 1분 주기로 등록된 목록</div>
|
||
</div>
|
||
<figcaption>그림 6. Cafe24 cron(예약작업) 등록</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ====================================================================
|
||
PART B — STM32 MCU
|
||
===================================================================== -->
|
||
<section id="B-wiring">
|
||
<span class="part-tag">PART B · STM32 MCU</span>
|
||
<h2>8. 하드웨어 배선 (STM32F407VGT6)</h2>
|
||
<p>LQFP-100 기준. SHT30 I²C, LAN8720 RMII, USART3 콘솔, 상태 LED를 결선합니다.</p>
|
||
|
||
<figure>
|
||
<svg viewBox="0 0 920 430" role="img" aria-label="STM32F407 SHT30 보드 배선도">
|
||
<defs><style>
|
||
.mcu{fill:#0f172a;}
|
||
.blk{fill:#fff;stroke-width:2;rx:10;}
|
||
.tt{font-family:'Noto Sans KR',sans-serif;font-size:13px;font-weight:700;fill:#0f172a;text-anchor:middle;}
|
||
.ss{font-family:'Noto Sans KR',sans-serif;font-size:10.5px;fill:#475569;text-anchor:middle;}
|
||
.pn{font-family:'Consolas',monospace;font-size:11px;font-weight:700;fill:#0b3b8c;}
|
||
.wt{font-family:'Consolas',monospace;font-size:10px;fill:#334155;}
|
||
.w{stroke-width:2.4;fill:none;}
|
||
</style></defs>
|
||
<!-- MCU -->
|
||
<rect x="360" y="120" width="200" height="190" rx="14" class="mcu"/>
|
||
<text x="460" y="150" text-anchor="middle" font-family="Noto Sans KR" font-size="15" font-weight="700" fill="#fff">STM32F407VGT6</text>
|
||
<text x="460" y="170" text-anchor="middle" font-family="Noto Sans KR" font-size="11" fill="#cbd5e1">168MHz · sht30_fw</text>
|
||
<text x="460" y="300" text-anchor="middle" font-family="Consolas" font-size="10" fill="#94a3b8">HSE 8MHz→PLL · IWDG 20s · LSE RTC</text>
|
||
<!-- SHT30 (left) -->
|
||
<rect x="40" y="150" width="150" height="120" class="blk" stroke="#0f766e"/>
|
||
<text class="tt" x="115" y="178">SHT30</text><text class="ss" x="115" y="196">P4422-3 · I²C 0x44</text>
|
||
<text class="pn" x="58" y="222">VDD</text><text class="pn" x="58" y="240">SCL</text><text class="pn" x="58" y="258">SDA</text><text class="pn" x="150" y="258">GND</text>
|
||
<!-- wires SHT30 -> MCU (PB6/PB7) -->
|
||
<path class="w" stroke="#f97316" d="M150,218 H 300 V 200 H 360"/><text class="wt" x="250" y="194">3.3V</text>
|
||
<path class="w" stroke="#22c55e" d="M150,236 H 320 V 225 H 360"/><text class="wt" x="300" y="219">PB6 SCL</text>
|
||
<path class="w" stroke="#3b82f6" d="M150,256 H 335 V 250 H 360"/><text class="wt" x="305" y="266">PB7 SDA</text>
|
||
<path class="w" stroke="#111827" d="M150,272 H 300 V 280 H 360"/><text class="wt" x="250" y="296">GND</text>
|
||
<!-- pullups -->
|
||
<text x="250" y="150" text-anchor="middle" font-family="Consolas" font-size="10" fill="#b45309">4.7kΩ↑3.3V (SCL/SDA)</text>
|
||
<!-- LAN8720 (top) -->
|
||
<rect x="610" y="40" width="270" height="92" class="blk" stroke="#1a56db"/>
|
||
<text class="tt" x="745" y="66">LAN8720 PHY (RMII · AF11)</text>
|
||
<text class="wt" x="745" y="86" text-anchor="middle">PA1 REF_CLK · PA2 MDIO · PC1 MDC · PA7 CRS_DV</text>
|
||
<text class="wt" x="745" y="102" text-anchor="middle">PC4/PC5 RXD0/1 · PB11 TXEN · PB12/PB13 TXD0/1</text>
|
||
<text class="ss" x="745" y="122">MDIO 1.5kΩ↑3.3V · 50MHz REF_CLK</text>
|
||
<path class="w" stroke="#1a56db" d="M610,110 H 540 V 120"/>
|
||
<!-- USART3 + LED (bottom) -->
|
||
<rect x="610" y="330" width="270" height="80" class="blk" stroke="#92400e"/>
|
||
<text class="tt" x="745" y="356">콘솔 & 상태 LED</text>
|
||
<text class="wt" x="745" y="376" text-anchor="middle">USART3: PD8 TX / PD9 RX (AF7, 115200 8N1)</text>
|
||
<text class="wt" x="745" y="392" text-anchor="middle">외부 3.3V USB-UART · 상태 LED = PD12</text>
|
||
<path class="w" stroke="#92400e" d="M610,360 H 540 V 300 H 560"/>
|
||
<!-- 범례 -->
|
||
<text x="40" y="300" font-family="Noto Sans KR" font-size="10.5" fill="#64748b">※ PB6/PB7(I²C)와 PB11/12/13(RMII)은 같은 PB포트지만 충돌 없음.</text>
|
||
<text x="40" y="316" font-family="Noto Sans KR" font-size="10.5" fill="#991b1b">※ PA2는 ETH_MDIO 전용 — USART2 사용 금지(콘솔은 USART3 PD8/PD9).</text>
|
||
</svg>
|
||
<figcaption>그림 7. STM32F407 + SHT30 + LAN8720 + USART3 콘솔 배선 (HARDWARE.md 핀맵 기준 생성)</figcaption>
|
||
</figure>
|
||
|
||
<h3>8.1 SHT30 I²C1</h3>
|
||
<table>
|
||
<thead><tr><th>SHT30</th><th>STM32 핀</th><th>비고</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>VDD</td><td>3.3V</td><td rowspan="4">SCL/SDA 각각 <b>4.7kΩ → 3.3V</b> 풀업(모듈 내장 시 생략). 100kHz 권장. 명령 <code>0x2C06</code> → ~20ms → 6바이트 read</td></tr>
|
||
<tr><td>SCL</td><td class="pin">PB6</td></tr>
|
||
<tr><td>SDA</td><td class="pin">PB7</td></tr>
|
||
<tr><td>GND / ADDR</td><td>GND</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<h3>8.2 LAN8720 RMII (AF11)</h3>
|
||
<table>
|
||
<thead><tr><th>STM32 핀</th><th>RMII 신호</th><th>STM32 핀</th><th>RMII 신호</th></tr></thead>
|
||
<tbody>
|
||
<tr><td class="pin">PA1</td><td>ETH_REF_CLK (50MHz 입력)</td><td class="pin">PC4</td><td>ETH_RXD0</td></tr>
|
||
<tr><td class="pin">PA2</td><td>ETH_MDIO ⚠️ (USART2 금지)</td><td class="pin">PC5</td><td>ETH_RXD1</td></tr>
|
||
<tr><td class="pin">PC1</td><td>ETH_MDC</td><td class="pin">PB11</td><td>ETH_TX_EN</td></tr>
|
||
<tr><td class="pin">PA7</td><td>ETH_CRS_DV</td><td class="pin">PB12/PB13</td><td>ETH_TXD0 / TXD1</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<h3>8.3 콘솔 · 상태 LED · 클럭</h3>
|
||
<ul>
|
||
<li>콘솔 UART: <span class="pin">PD8</span>(TX)/<span class="pin">PD9</span>(RX), USART3 AF7, <b>115200 8N1</b>, 외부 3.3V USB-UART 어댑터.</li>
|
||
<li>상태 LED: <span class="pin">PD12</span> — 부팅=점등 / 정상보고=느린 토글 / 망단절·TLS실패=빠른 점멸.</li>
|
||
<li>클럭: HSE 8MHz→168MHz, RMII REF_CLK 50MHz, RTC <b>LSE 32.768kHz</b>(권장; 정전 시 VBAT 시간 보존), IWDG=LSI 20s.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section id="B-build">
|
||
<span class="part-tag">PART B · STM32 MCU</span>
|
||
<h2>9. 폐쇄망 펌웨어 빌드</h2>
|
||
<div class="crit"><b>폐쇄망 원칙.</b> 다운로드는 <b>인터넷 가능한 빌드 머신에서만</b> 일어나고, 결과물 <code>.bin</code> 이 에어갭을
|
||
넘어 전달됩니다. 설치 호스트는 어떤 것도 직접 가져오지 않습니다.</div>
|
||
<div class="steps">
|
||
<div class="step">
|
||
<h3>의존성 벤더링(빌드 머신, 외부 다운로드는 여기서만)</h3>
|
||
<pre><code>bash firmware/scripts/vendor.sh # 또는 vendor.ps1
|
||
# 릴리스 빌드는 핀(SHA-256) 강제:
|
||
VENDOR_STRICT=1 bash firmware/scripts/vendor.sh</code></pre>
|
||
<p class="small">STM32CubeF4 HAL/CMSIS · FreeRTOS-Kernel · lwip(+sntp) · mbedtls 를 <code>firmware/third_party/</code> 에 고정 버전으로 배치. 빌드·런타임 외부 fetch 0건.</p>
|
||
</div>
|
||
<div class="step">
|
||
<h3>비밀값 — <code>secrets.h</code></h3>
|
||
<pre><code>cp firmware/common/secrets.h.example firmware/common/secrets.h
|
||
# APP_API_KEY 를 서버 config.local.php 의 API_KEY 와 바이트 단위로 동일하게 채운다.</code></pre>
|
||
<div class="warn"><code>secrets.h</code> 는 커밋 금지. 산출 <code>.bin</code> 에 키가 임베드되므로 <code>.bin</code> 자체를 비밀로 취급합니다.</div>
|
||
</div>
|
||
<div class="step">
|
||
<h3>서버 루트 CA 임베드 — <code>certs/server_ca.c</code> <span style="color:var(--crit)">(필수)</span></h3>
|
||
<pre><code>openssl s_client -connect your-domain.example:443 -servername your-domain.example -showcerts </dev/null
|
||
# → 루트 CA 를 PEM 으로 저장 후 certs/server_ca.c 의 SERVER_CA_PEM 에 반영
|
||
# (SERVER_CA_PEM_LEN = sizeof(...) , 종결 NUL 포함)</code></pre>
|
||
<div class="crit"><b>자리표시자 CA로는 부팅 불가.</b> 기본 <code>server_ca.c</code> 는 PLACEHOLDER라 <code>tls_init()</code>가
|
||
실패해 부팅이 정지(fail-closed)합니다. <code>build.ps1</code> 이 PLACEHOLDER를 감지하면 빌드를 중단합니다 — 반드시 실제 CA로 교체하세요.</div>
|
||
</div>
|
||
<div class="step">
|
||
<h3>설정값 확인 — <code>app_config.h</code></h3>
|
||
<table>
|
||
<thead><tr><th>설정</th><th>값</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>APP_API_HOST</code> / <code>APP_API_PATH</code></td><td>운영 도메인 / <code>/raspi_leck_detecter/api/sensor_data.php</code></td></tr>
|
||
<tr><td><code>APP_SNTP_SERVER</code> ⚠️</td><td><b>폐쇄망 LAN NTP 서버 IP</b>(기본 <code>192.0.2.123</code> 자리표시자 — 반드시 교체)</td></tr>
|
||
<tr><td><code>APP_SHT30_I2C_ADDR</code> / <code>APP_SHT30_REPORT_INTERVAL_SEC</code></td><td>0x44 / 300(5분)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="crit"><b>SNTP는 폐쇄망 전면장애 지점.</b> 인터넷 NTP(<code>pool.ntp.org</code>)는 폐쇄망에서 도달 불가 →
|
||
시간 미동기 → <code>timestamp=0</code> → TLS 차단 + 서버 전량 거부. <b>반드시 사내 LAN NTP IP</b>로 설정하세요.</div>
|
||
</div>
|
||
<div class="step">
|
||
<h3>빌드 (cmake + ninja)</h3>
|
||
<pre><code>cmake -S firmware -B firmware/build -G Ninja \
|
||
-DCMAKE_TOOLCHAIN_FILE=firmware/cmake/arm-none-eabi-toolchain.cmake
|
||
cmake --build firmware/build --target sht30_fw # → sht30_fw.elf / .bin / .hex</code></pre>
|
||
<p class="small">(선택) 호스트 패리티 게이트로 C↔Python↔PHP 서명 일치 확인: <code>python3 firmware/test/host/parity_test.py</code></p>
|
||
</div>
|
||
</div>
|
||
<figure>
|
||
<img class="cap" src="screenshots/08-firmware-build.png" alt="그림 8 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 8 <span class="fname">→ screenshots/08-firmware-build.png</span></div>
|
||
<div class="ph-what">빌드 머신 터미널의 <b><code>cmake --build … --target sht30_fw</code> 성공 로그</b>(산출물 <code>sht30_fw.bin/.hex/.elf</code> + <code>arm-none-eabi-size</code> 출력)</div>
|
||
</div>
|
||
<figcaption>그림 8. 펌웨어 빌드 성공 로그</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<section id="B-flash">
|
||
<span class="part-tag">PART B · STM32 MCU</span>
|
||
<h2>10. 에어갭 전달 · 플래시 · 콘솔 확인</h2>
|
||
<div class="steps">
|
||
<div class="step">
|
||
<h3>무결성 해시 + 신뢰 매체 전달</h3>
|
||
<pre><code>sha256sum firmware/build/sht30_fw.bin > FW_HASHES.txt # 빌드 머신
|
||
# USB 등 신뢰 매체로 격리 호스트에 전달 후 재계산 대조</code></pre>
|
||
</div>
|
||
<div class="step">
|
||
<h3>ST-Link 플래시 (격리 호스트)</h3>
|
||
<pre><code>pwsh firmware/scripts/flash.ps1 # 권장(자동 탐지)
|
||
# 대안:
|
||
st-flash write sht30_fw.bin 0x08000000
|
||
STM32_Programmer_CLI -c port=SWD -w sht30_fw.hex -v -rst</code></pre>
|
||
<figure>
|
||
<img class="cap" src="screenshots/09-flash.png" alt="그림 9 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 9 <span class="fname">→ screenshots/09-flash.png</span></div>
|
||
<div class="ph-what"><b>플래싱 툴 화면</b>(STM32CubeProgrammer 또는 <code>st-flash</code>의 “Flash written / verified” 결과)</div>
|
||
</div>
|
||
<figcaption>그림 9. STM32 펌웨어 플래시 완료</figcaption>
|
||
</figure>
|
||
</div>
|
||
<div class="step">
|
||
<h3>콘솔 로그 확인 (USART3 PD8/PD9, 115200 8N1)</h3>
|
||
<p>외부 USB-UART 어댑터를 연결하고 부팅 시퀀스를 관찰합니다:</p>
|
||
<pre><code>bsp_init → 링크업 → DHCP(또는 static) → SNTP 시간 동기
|
||
→ TLS 핸드셰이크 → 첫 startup POST → 서버 HTTP 200</code></pre>
|
||
<p class="small">I²C1 0x44 응답 · 첫 온/습도 측정 · 상태 LED(PD12) 패턴을 함께 확인. 서명 불일치(403)면
|
||
<code>API_KEY</code>↔<code>APP_API_KEY</code>, TLS 실패면 <code>server_ca.c</code>/SNTP를 재점검.</p>
|
||
<figure>
|
||
<img class="cap" src="screenshots/10-serial-console.png" alt="그림 10 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 10 <span class="fname">→ screenshots/10-serial-console.png</span></div>
|
||
<div class="ph-what"><b>시리얼 터미널(USART3) 부팅 로그</b> — SNTP 동기 → TLS OK → 서버 200 응답 라인이 보이는 화면</div>
|
||
</div>
|
||
<figcaption>그림 10. STM32 콘솔 부팅 로그(정상 보고)</figcaption>
|
||
</figure>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ============================== 검증 ============================== -->
|
||
<section id="verify">
|
||
<h2>11. 최종 검증 (종단)</h2>
|
||
<p>서버에서 아래 화면을 확인합니다.</p>
|
||
<pre><code>https://your-domain.example/raspi_leck_detecter/setup_wizard.php
|
||
https://your-domain.example/raspi_leck_detecter/dashboard.php
|
||
https://your-domain.example/raspi_leck_detecter/monthly_report.php
|
||
https://your-domain.example/raspi_leck_detecter/security_evidence.php</code></pre>
|
||
<div class="ok">
|
||
<b>정상 설치 기준</b>
|
||
<ul style="margin:6px 0 0;">
|
||
<li><code>setup_wizard.php</code> 필수 점검 항목 통과 + 테스트 SMS 수신</li>
|
||
<li><code>dashboard.php</code> 에 최신 온/습도와 센서 상태(<code>sensor_id=2</code>) 표시</li>
|
||
<li><code>sensor_metric</code> 에 주기 측정값(periodic) 저장 · STM32 콘솔에 서버 200 기록</li>
|
||
<li>임계 초과 테스트 시 <code>metric_status</code> 경보 + SMS 수신 / 월간 보고서에 내역 조회</li>
|
||
</ul>
|
||
</div>
|
||
<figure>
|
||
<img class="cap" src="screenshots/11-dashboard.png" alt="그림 11 캡처" onerror="this.style.display='none';this.nextElementSibling.hidden=false;">
|
||
<div class="shot" hidden>
|
||
<div class="cam">📷</div>
|
||
<div class="ph-title">캡처 위치 — 그림 11 <span class="fname">→ screenshots/11-dashboard.png</span></div>
|
||
<div class="ph-what"><b>운영 <code>dashboard.php</code> 화면</b>(상단 “정상 감시 중” + 2번 센서 SHT30 온/습도 카드 + 30일 요약). 데스크톱·모바일 2장 권장</div>
|
||
</div>
|
||
<figcaption>그림 11. 운영 대시보드(v2606 온습도) — 데스크톱 / 모바일</figcaption>
|
||
</figure>
|
||
</section>
|
||
|
||
<!-- ============================== 문제해결 ============================== -->
|
||
<section id="trouble">
|
||
<h2>12. 문제 해결</h2>
|
||
<table>
|
||
<thead><tr><th>증상</th><th>확인 지점</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>API 403</td><td>서버 <code>API_KEY</code> ↔ 펌웨어 <code>APP_API_KEY</code> 일치, raw-body 서명</td></tr>
|
||
<tr><td>API 500</td><td>Cafe24 PHP 오류 로그, DB 접속 정보, 테이블 생성 여부</td></tr>
|
||
<tr><td>로그인 불가</td><td><code>ADMIN_PASSWORD_HASH</code>, <code>ADMIN_TOTP_SECRET</code>, 인증 앱 시간 동기</td></tr>
|
||
<tr><td>SMS 미발송</td><td>Cafe24 SMS 계정·secure key·발신번호 등록·<b>잔액</b>, <code>sms_log</code>. 대시보드 ‘수집 거부/발송 실패’ 배너 확인</td></tr>
|
||
<tr><td>온습도 미표시</td><td>I²C 배선(PB6/PB7, 0x44), 4.7kΩ 풀업, <code>sensor_metric</code>, 펌웨어 측정 로그</td></tr>
|
||
<tr><td>TLS 실패</td><td><code>server_ca.c</code> 실제 CA 반영, <b>SNTP 시간 동기</b>(<code>APP_SNTP_SERVER</code>=LAN NTP)</td></tr>
|
||
<tr><td>데이터 전혀 안 들어옴</td><td>대시보드 <b>수집 거부 사유 카운터</b>(서명불일치→키/펌웨어, 시각오차→SNTP/RTC)로 원인 분별</td></tr>
|
||
<tr><td>오프라인 알림 과다/지연</td><td><code>HEARTBEAT_TIMEOUT_SEC</code>(1200초), cron 주기</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</section>
|
||
|
||
<!-- ============================== 보관값 ============================== -->
|
||
<section id="secrets">
|
||
<h2>13. 운영자가 별도 보관할 값</h2>
|
||
<div class="warn">아래 값은 저장소에 넣지 말고 운영자만 안전하게 보관합니다.</div>
|
||
<ul>
|
||
<li>Cafe24 DB 이름 / 계정 / 비밀번호</li>
|
||
<li><code>API_KEY</code> = 펌웨어 <code>APP_API_KEY</code></li>
|
||
<li>Cafe24 SMS user id / secure key / 발신번호 / 수신자 번호</li>
|
||
<li>관리자 계정·원문 비밀번호·<code>ADMIN_TOTP_SECRET</code></li>
|
||
<li>서버 업로드 경로, LAN NTP 서버 IP, 장비 ID(<code>stm32-sht30-01</code>)와 설치 위치</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<!-- ============================== 부록 ============================== -->
|
||
<section id="appendix">
|
||
<h2>부록 — 저장소 내 기존 이미지 자산 (레거시 · 참고용)</h2>
|
||
<div class="warn"><b>주의:</b> 아래 캡처는 <b>구버전 v2605 누수감지(카메라/사진) UI</b> 또는 <b>Raspberry Pi</b> 기준이라
|
||
현재 v2606 STM32/온습도 시스템 화면과 <b>다릅니다</b>. 화면 구성 참고용으로만 사용하고, 위 본문의 “📷 캡처 위치”에는
|
||
<b>현재 운영 시스템에서 직접 캡처한 이미지</b>를 넣으세요.</div>
|
||
<p>본 HTML 파일은 <code>docs/</code> 폴더 기준 상대경로로 아래 자산을 참조할 수 있습니다(폴더 동반 시 표시됨):</p>
|
||
<table>
|
||
<thead><tr><th>경로</th><th>내용</th><th>상태</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>examples/captures/admin_dashboard_overview.png</code></td><td>관리자 대시보드 개요</td><td>⚠️ v2605 누수 UI</td></tr>
|
||
<tr><td><code>examples/captures/admin_dashboard_mobile.png</code></td><td>대시보드 모바일</td><td>⚠️ v2605</td></tr>
|
||
<tr><td><code>examples/captures/admin_dashboard_incident.png</code></td><td>경보/사고 카드</td><td>⚠️ v2605(사고/사진)</td></tr>
|
||
<tr><td><code>evidence/mfa_setup_page_capture.png</code></td><td>MFA 등록 페이지</td><td>참고(개념 동일)</td></tr>
|
||
<tr><td><code>examples/assets/sht30_sensor2_wiring.svg</code></td><td>배선도</td><td>⚠️ Raspberry Pi 기준</td></tr>
|
||
<tr><td><code>assets/security_plan/generated/security_plan_target_architecture.png</code></td><td>목표 아키텍처</td><td>보안계획 다이어그램</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="gallery">
|
||
<figure><img src="examples/captures/admin_dashboard_overview.png" alt="v2605 대시보드 개요"><figcaption>⚠️ v2605 대시보드 개요(누수 UI)</figcaption></figure>
|
||
<figure><img src="examples/captures/admin_dashboard_mobile.png" alt="v2605 모바일"><figcaption>⚠️ v2605 모바일</figcaption></figure>
|
||
<figure><img src="examples/captures/admin_dashboard_incident.png" alt="v2605 사고 카드"><figcaption>⚠️ v2605 사고/사진 카드</figcaption></figure>
|
||
<figure><img src="evidence/mfa_setup_page_capture.png" alt="MFA 등록 페이지"><figcaption>MFA 등록 페이지(참고·개념 동일)</figcaption></figure>
|
||
<figure><img src="examples/assets/sht30_sensor2_wiring.svg" alt="RPi 배선도"><figcaption>⚠️ 배선도(Raspberry Pi 기준)</figcaption></figure>
|
||
<figure><img src="assets/security_plan/generated/security_plan_target_architecture.png" alt="목표 아키텍처"><figcaption>목표 아키텍처(보안계획 다이어그램)</figcaption></figure>
|
||
</div>
|
||
<p class="small">현재 시스템의 정확한 아키텍처·배선은 본문 그림 1·7(생성 SVG)을 기준으로 하세요.</p>
|
||
</section>
|
||
|
||
<p class="small" style="text-align:center;margin-top:30px;">
|
||
서버실 온습도(SHT30) 모니터링 시스템 v2606 · 설치 가이드 · 폐쇄망(air-gapped) 배포 · 인쇄(<span class="kbd">Ctrl/Cmd</span>+<span class="kbd">P</span>) 지원
|
||
</p>
|
||
</div>
|
||
</body>
|
||
</html>
|