Files
webflash/frontend/index.html
root 88094afe06 fix: auto-select firmware after upload, guard flash button without selection
- Auto-select uploaded firmware immediately after upload completes
- Auto-select single firmware on list load (no manual click needed)
- Hide flash button and show warning when no firmware is selected
- Re-apply manifest on list reload to avoid stale state
- Restore warning banner on firmware delete

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 05:39:23 +09:00

416 lines
19 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESP32 Web Flasher</title>
<link rel="stylesheet" href="css/style.css" />
<!--
esp-web-tools (Espressif 공식)
Web Serial API를 통해 ESP32 시리얼 플래시 수행
Chrome 89+ / Edge 89+ 필요
-->
<script
type="module"
src="https://unpkg.com/esp-web-tools@10/dist/web/install-button.js">
</script>
</head>
<body>
<!-- ═══════════════════════════════════════════════════════════════
HEADER
══════════════════════════════════════════════════════════════════ -->
<header>
<a class="logo" href="#">
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="#00c896" fill-opacity=".15"/>
<rect x="6" y="6" width="20" height="20" rx="3" stroke="#00c896" stroke-width="2"/>
<rect x="10" y="10" width="5" height="5" fill="#00c896"/>
<rect x="17" y="10" width="5" height="5" fill="#00c896" fill-opacity=".5"/>
<rect x="10" y="17" width="5" height="5" fill="#00c896" fill-opacity=".5"/>
<rect x="17" y="17" width="5" height="5" fill="#00c896"/>
<line x1="2" y1="12" x2="6" y2="12" stroke="#00c896" stroke-width="1.5"/>
<line x1="2" y1="20" x2="6" y2="20" stroke="#00c896" stroke-width="1.5"/>
<line x1="26" y1="12" x2="30" y2="12" stroke="#00c896" stroke-width="1.5"/>
<line x1="26" y1="20" x2="30" y2="20" stroke="#00c896" stroke-width="1.5"/>
</svg>
ESP32 Web Flasher
</a>
<span class="header-badge">ESP / ESP32</span>
</header>
<!-- ═══════════════════════════════════════════════════════════════
MAIN
══════════════════════════════════════════════════════════════════ -->
<main>
<!-- 브라우저 경고 배너 -->
<div id="browser-warning" class="alert alert-warn" style="display:none;">
<span style="font-size:18px;"></span>
<div>
<strong>지원되지 않는 브라우저</strong><br>
Web Serial API는 <strong>Chrome 89+</strong> 또는 <strong>Edge 89+</strong>에서만 동작합니다.
현재 브라우저에서는 시리얼 연결 및 플래시 기능을 사용할 수 없습니다.
</div>
</div>
<!-- ─────────────────────────────────────────────────────────────
STEP 1: 시리얼 연결 확인
──────────────────────────────────────────────────────────────── -->
<div class="card">
<div class="card-header">
<div class="step-badge" id="step1-badge">1</div>
<h2>시리얼 연결 확인</h2>
</div>
<div class="card-body">
<div class="alert alert-info" style="margin-bottom:16px;">
<span></span>
<div>
ESP 보드를 USB 케이블로 PC에 연결한 후 아래 버튼을 눌러 연결 상태를 확인하세요.
<br><small style="color:var(--text-muted);">내장 USB 포트 또는 USB-UART 변환기(CP2102, CH340, FTDI) 모두 지원</small>
</div>
</div>
<div class="status-row">
<div class="dot dot-idle" id="conn-dot"></div>
<span id="conn-text">연결 안 됨</span>
<span id="device-info" style="margin-left:auto;font-size:12px;color:var(--text-muted);"></span>
</div>
<div style="display:flex;gap:10px;margin-top:14px;">
<button id="btn-connect" class="btn btn-primary needs-serial">
🔌 포트 연결
</button>
<button id="btn-disconnect" class="btn btn-secondary" style="display:none;">
⏏ 연결 해제
</button>
</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────────────
STEP 2 & 3: 펌웨어 업로드 / 플래시 (탭)
──────────────────────────────────────────────────────────────── -->
<div class="card">
<div class="card-header">
<div class="step-badge" id="step2-badge">2</div>
<h2>펌웨어 관리 및 플래시</h2>
</div>
<!-- 탭 바 -->
<div class="tab-bar">
<button class="tab-btn active" data-tab="tab-upload">📤 펌웨어 업로드</button>
<button class="tab-btn" data-tab="tab-flash">⚡ 플래시</button>
</div>
<!-- ── 탭: 펌웨어 업로드 ──────────────────────────────── -->
<div class="tab-panel active" id="tab-upload">
<div class="alert alert-info" style="margin-bottom:16px;">
<span></span>
<div>
Arduino IDE <strong>Sketch → Export Compiled Binary</strong> 후 빌드 폴더에서 파일을 선택하세요.
<br><small style="color:var(--text-muted);">
병합 바이너리: <code>*.merged.bin</code> 하나만 업로드 (권장) |
분리 파일: bootloader + partitions + boot_app0 + app
</small>
</div>
</div>
<!-- 업로드 방식 선택 -->
<div style="display:flex;gap:10px;margin-bottom:18px;">
<label id="lbl-merged" style="flex:1;display:flex;align-items:center;gap:10px;padding:12px 16px;border:2px solid var(--accent);border-radius:8px;cursor:pointer;background:rgba(0,200,150,.06);">
<input type="radio" name="upload-mode" id="mode-merged" value="merged" checked style="accent-color:var(--accent);">
<div>
<div style="font-size:13px;font-weight:600;">병합 바이너리 <span style="color:var(--accent);font-size:11px;">권장</span></div>
<div style="font-size:11px;color:var(--text-muted);">*.merged.bin 하나 업로드 — offset 0x0</div>
</div>
</label>
<label id="lbl-split" style="flex:1;display:flex;align-items:center;gap:10px;padding:12px 16px;border:2px solid var(--border);border-radius:8px;cursor:pointer;">
<input type="radio" name="upload-mode" id="mode-split" value="split" style="accent-color:var(--accent);">
<div>
<div style="font-size:13px;font-weight:600;">분리 파일</div>
<div style="font-size:11px;color:var(--text-muted);">bootloader + partitions + boot_app0 + app</div>
</div>
</label>
</div>
<form id="upload-form">
<div class="form-grid">
<div class="form-group">
<label>펌웨어 이름</label>
<input id="fw-name" type="text" placeholder="예: MyProduct v1.0" />
</div>
<div class="form-group">
<label>버전</label>
<input id="fw-version" type="text" placeholder="예: 1.0.0" />
</div>
</div>
<div class="form-group">
<label>설명 (선택)</label>
<input id="fw-desc" type="text" placeholder="간단한 설명" />
</div>
<div class="form-group">
<label>칩 패밀리</label>
<select id="fw-chip">
<optgroup label="ESP32 시리즈">
<option value="ESP32-S3" selected>ESP32-S3</option>
<option value="ESP32-S2">ESP32-S2</option>
<option value="ESP32-S3-BOX">ESP32-S3-BOX</option>
</optgroup>
<optgroup label="ESP32-C 시리즈 (RISC-V)">
<option value="ESP32-C3">ESP32-C3</option>
<option value="ESP32-C6">ESP32-C6</option>
<option value="ESP32-C2">ESP32-C2 (ESP8684)</option>
</optgroup>
<optgroup label="ESP32-H 시리즈 (RISC-V)">
<option value="ESP32-H2">ESP32-H2</option>
</optgroup>
<optgroup label="ESP32 오리지널">
<option value="ESP32">ESP32</option>
</optgroup>
<optgroup label="ESP8266 시리즈">
<option value="ESP8266">ESP8266</option>
</optgroup>
</select>
</div>
<!-- 드롭존: 펌웨어 (필수) -->
<div class="form-group" id="zone-firmware">
<label id="lbl-firmware">펌웨어 바이너리 <span style="color:var(--danger);">*</span></label>
<div class="drop-zone" id="drop-firmware">
<input type="file" id="file-firmware" accept=".bin" />
<div class="icon">📦</div>
<div class="drop-label" id="firmware-hint">
<span id="firmware-hint-text">*.merged.bin 파일을 드래그하거나 클릭하세요</span>
<div style="font-size:11px;margin-top:4px;color:var(--text-muted);">(최대 32 MB)</div>
</div>
</div>
</div>
<!-- 분리 파일 섹션 (mode-split 선택 시 표시) -->
<div id="split-files" style="display:none;flex-direction:column;gap:12px;margin-bottom:14px;">
<div class="form-group" style="margin:0;">
<label>① 부트로더 — offset 0x0000 <span style="color:var(--danger);">*</span></label>
<div class="drop-zone" id="drop-bootloader" style="padding:16px;">
<input type="file" id="file-bootloader" accept=".bin" />
<div class="drop-label">*.bootloader.bin 드래그 또는 클릭</div>
</div>
</div>
<div class="form-group" style="margin:0;">
<label>② 파티션 테이블 — offset 0x8000 <span style="color:var(--danger);">*</span></label>
<div class="drop-zone" id="drop-partitions" style="padding:16px;">
<input type="file" id="file-partitions" accept=".bin" />
<div class="drop-label">*.partitions.bin 드래그 또는 클릭</div>
</div>
</div>
<div class="form-group" style="margin:0;">
<label>③ Boot App0 — offset 0xe000 <span style="color:var(--text-muted);font-size:11px;">(OTA 사용 시 필요)</span></label>
<div class="drop-zone" id="drop-boot-app0" style="padding:16px;">
<input type="file" id="file-boot-app0" accept=".bin" />
<div class="drop-label">boot_app0.bin 드래그 또는 클릭</div>
</div>
</div>
<div class="form-group" style="margin:0;">
<label>④ 애플리케이션 — offset 0x10000 <span style="color:var(--danger);">*</span></label>
<div class="drop-zone" id="drop-app" style="padding:16px;">
<input type="file" id="file-app" accept=".bin" />
<div class="drop-label">*.ino.bin 드래그 또는 클릭</div>
</div>
</div>
</div>
<div id="upload-progress" style="display:none;margin-bottom:14px;">
<div class="progress-wrap"><div class="progress-bar" id="upload-bar"></div></div>
<div class="progress-label">
<span id="upload-pct">0%</span>
</div>
</div>
<button type="submit" class="btn btn-primary">
📤 서버에 업로드
</button>
</form>
</div>
<!-- ── 탭: 플래시 ─────────────────────────────────────── -->
<div class="tab-panel" id="tab-flash">
<div class="alert alert-info" style="margin-bottom:16px;">
<span></span>
<div>
업로드된 펌웨어를 선택 후 <strong>플래시 실행</strong> 버튼을 누르세요.
<br><small style="color:var(--text-muted);">
esp-web-tools가 시리얼 포트 선택 대화상자를 표시하고 자동으로 플래싱합니다.
</small>
</div>
</div>
<!-- 업로드된 펌웨어 목록 -->
<div style="margin-bottom:16px;">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
<span style="font-size:14px;font-weight:600;">업로드된 펌웨어</span>
<button class="btn btn-secondary btn-sm" onclick="loadFirmwareList()">🔄 새로고침</button>
</div>
<div id="fw-list" class="fw-list"></div>
</div>
<!-- 선택 상태 & 플래시 버튼 -->
<div style="border-top:1px solid var(--border);padding-top:16px;margin-top:4px;">
<div class="status-row" style="margin-bottom:12px;">
<span>💾</span>
<span id="selected-fw-info" style="font-size:14px;">선택된 펌웨어 없음 — 위 목록에서 클릭하여 선택하세요</span>
</div>
<!-- 펌웨어 미선택 경고 -->
<div id="no-fw-warning" class="alert alert-warn" style="margin-bottom:12px;">
<span></span>
<div style="font-size:13px;">
<strong>펌웨어를 먼저 선택하세요.</strong>
위 목록에서 플래시할 펌웨어를 클릭하면 이 버튼이 활성화됩니다.
</div>
</div>
<!-- 플래시 버튼 (펌웨어 선택 후 표시) -->
<div id="flash-btn-wrap" style="display:none;">
<esp-web-install-button id="esp-install-btn">
<button
slot="activate"
class="btn btn-primary needs-serial"
style="width:100%;justify-content:center;padding:12px;"
>
⚡ 플래시 실행
</button>
<span slot="unsupported" style="color:var(--danger);font-size:13px;">
⚠ 이 브라우저는 Web Serial을 지원하지 않습니다 (Chrome/Edge 필요)
</span>
</esp-web-install-button>
</div>
<div class="alert alert-warn" style="margin-top:14px;">
<span></span>
<div style="font-size:12px;">
<strong>플래시 전 확인 사항:</strong>
<ul style="margin:4px 0 0 16px;line-height:1.8;">
<li>ESP 보드가 USB로 연결되어 있어야 합니다</li>
<li>다른 프로그램(Arduino IDE, esptool)이 해당 포트를 사용 중이면 안 됩니다</li>
<li>플래시 중 USB 케이블을 분리하지 마세요</li>
<li>일부 보드는 BOOT 버튼을 누른 상태로 연결해야 합니다</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- ─────────────────────────────────────────────────────────────
시리얼 모니터 / 로그
──────────────────────────────────────────────────────────────── -->
<div class="card">
<div class="card-header" style="justify-content:space-between;">
<div style="display:flex;align-items:center;gap:10px;">
<span>🖥</span>
<h2>로그 / 상태</h2>
</div>
<button id="btn-clear-log" class="btn btn-secondary btn-sm">지우기</button>
</div>
<div class="card-body" style="padding:12px;">
<pre id="monitor"></pre>
</div>
</div>
<!-- ─────────────────────────────────────────────────────────────
도움말
──────────────────────────────────────────────────────────────── -->
<div class="card">
<div class="card-header">
<span>📖</span>
<h2>사용 방법 & 플래시 파일 준비</h2>
</div>
<div class="card-body">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
<div>
<h3 style="font-size:14px;margin-bottom:10px;color:var(--accent);">Arduino IDE에서 .bin 내보내기</h3>
<ol style="font-size:13px;line-height:2;color:var(--text-muted);padding-left:18px;">
<li>Arduino IDE 2.x 실행</li>
<li>사용할 ESP 보드 선택 후 코드 작성</li>
<li><strong>Sketch → Export Compiled Binary</strong></li>
<li>스케치 폴더에 <code>.bin</code> 파일 생성됨</li>
<li>위 업로드 탭에서 해당 파일 업로드</li>
</ol>
</div>
<div>
<h3 style="font-size:14px;margin-bottom:10px;color:var(--accent);">병합 바이너리(merged.bin) 생성</h3>
<pre style="font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow:auto;color:var(--text-muted);"># ESP32-S3 / S2 / C3 / C6 / H2
esptool.py --chip esp32s3 merge_bin \
-o merged.bin \
0x0 bootloader.bin \
0x8000 partitions.bin \
0x10000 app.bin
# ESP32 (오리지널)
esptool.py --chip esp32 merge_bin \
-o merged.bin \
0x1000 bootloader.bin \
0x8000 partitions.bin \
0x10000 app.bin
# ESP8266
esptool.py --chip esp8266 \
-o merged.bin 0x0 app.bin</pre>
<p style="font-size:12px;color:var(--text-muted);margin-top:8px;">
병합 후 merged.bin 하나만 업로드하면 됩니다.
</p>
</div>
<div>
<h3 style="font-size:14px;margin-bottom:10px;color:var(--accent);">Flash 암호화 (보안 강화)</h3>
<pre style="font-size:11px;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:10px;overflow:auto;color:var(--text-muted);"># Flash Encryption 활성화
espefuse.py --chip esp32s3 \
burn_efuse DIS_DOWNLOAD_PLAIN_DECRYPT</pre>
<p style="font-size:12px;color:var(--text-muted);margin-top:8px;">
활성화 후 덤프 시 암호화된 데이터만 읽힙니다.
</p>
</div>
<div>
<h3 style="font-size:14px;margin-bottom:10px;color:var(--accent);">지원 환경</h3>
<table style="font-size:12px;width:100%;border-collapse:collapse;color:var(--text-muted);">
<tr>
<td style="padding:4px 0;">Chrome 89+</td>
<td style="color:var(--accent);">✓ 지원</td>
</tr>
<tr>
<td style="padding:4px 0;">Edge 89+</td>
<td style="color:var(--accent);">✓ 지원</td>
</tr>
<tr>
<td style="padding:4px 0;">Firefox</td>
<td style="color:var(--danger);">✗ 미지원</td>
</tr>
<tr>
<td style="padding:4px 0;">Safari</td>
<td style="color:var(--danger);">✗ 미지원</td>
</tr>
<tr>
<td style="padding:4px 0;">HTTP (non-localhost)</td>
<td style="color:var(--danger);">✗ HTTPS 필요</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</main>
<script src="js/app.js"></script>
</body>
</html>