feat: support merged.bin, boot_app0, upload mode toggle + flash guide

- server.js: add boot_app0 field at 0xe000, raise file limit 8→32 MB
- index.html: add 병합/분리 mode toggle, boot_app0 drop zone, numbered split zones
- app.js: dynamic mode logic, remove hardcoded flashAddress 0x10000,
  server now auto-selects 0x0 (merged) or 0x10000 (split)
- flash-guide.html: step-by-step Korean flash guide with file table,
  method A/B walkthrough, flash_args explanation, troubleshooting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-05-19 05:26:53 +09:00
parent cb9df54abb
commit 3a662affb6
4 changed files with 842 additions and 47 deletions

View File

@@ -110,14 +110,32 @@
<div class="alert alert-info" style="margin-bottom:16px;">
<span></span>
<div>
Arduino IDE <strong>Sketch → Export Compiled Binary</strong> 로 생성한 .bin 파일을 업로드하세요.
<br>병합 바이너리(merged.bin)는 '펌웨어' 하나만 업로드해도 됩니다.
Arduino IDE <strong>Sketch → Export Compiled Binary</strong> 후 빌드 폴더에서 파일을 선택하세요.
<br><small style="color:var(--text-muted);">
분리 파일 사용 시: bootloader(0x0000) + partition-table(0x8000) + app(0x10000)
병합 바이너리: <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">
@@ -161,40 +179,49 @@
</div>
<!-- 드롭존: 펌웨어 (필수) -->
<div class="form-group">
<label>펌웨어 바이너리 <span style="color:var(--danger);">*</span></label>
<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">
펌웨어 .bin 파일을 드래그하거나 클릭하세요
<div style="font-size:11px;margin-top:4px;color:var(--text-muted);">(최대 8 MB)</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>
<!-- 분리 파일 섹션 (선택) -->
<details style="margin-bottom:14px;">
<summary style="cursor:pointer;font-size:13px;color:var(--text-muted);margin-bottom:8px;">
▸ 분리 바이너리 파일 추가 (선택)
</summary>
<div style="padding-top:10px;display:flex;flex-direction:column;gap:12px;">
<div class="form-group" style="margin:0;">
<label>부트로더 (bootloader.bin) — offset 0x0000</label>
<div class="drop-zone" id="drop-bootloader" style="padding:16px;">
<input type="file" id="file-bootloader" accept=".bin" />
<div class="drop-label">부트로더 .bin (선택사항)</div>
</div>
</div>
<div class="form-group" style="margin:0;">
<label>파티션 테이블 (partition-table.bin) — offset 0x8000</label>
<div class="drop-zone" id="drop-partitions" style="padding:16px;">
<input type="file" id="file-partitions" accept=".bin" />
<div class="drop-label">파티션 테이블 .bin (선택사항)</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>
</details>
<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>

View File

@@ -190,29 +190,71 @@ function setupDropZone(zoneId, inputId) {
setupDropZone('drop-firmware', 'file-firmware');
setupDropZone('drop-bootloader', 'file-bootloader');
setupDropZone('drop-partitions', 'file-partitions');
setupDropZone('drop-boot-app0', 'file-boot-app0');
setupDropZone('drop-app', 'file-app');
// ── 업로드 모드 토글 ─────────────────────────────────────────
const modeMerged = $('#mode-merged');
const modeSplit = $('#mode-split');
const splitFiles = $('#split-files');
const lblMerged = $('#lbl-merged');
const lblSplit = $('#lbl-split');
const firmwareHintText = $('#firmware-hint-text');
function applyUploadMode() {
const isMerged = modeMerged.checked;
splitFiles.style.display = isMerged ? 'none' : 'flex';
lblMerged.style.border = isMerged ? '2px solid var(--accent)' : '2px solid var(--border)';
lblMerged.style.background = isMerged ? 'rgba(0,200,150,.06)' : '';
lblSplit.style.border = isMerged ? '2px solid var(--border)' : '2px solid var(--accent)';
lblSplit.style.background = isMerged ? '' : 'rgba(0,200,150,.06)';
firmwareHintText.textContent = isMerged
? '*.merged.bin 파일을 드래그하거나 클릭하세요'
: '앱 바이너리 (*.ino.bin)를 드래그하거나 클릭하세요';
}
modeMerged.addEventListener('change', applyUploadMode);
modeSplit.addEventListener('change', applyUploadMode);
uploadForm.addEventListener('submit', async e => {
e.preventDefault();
const fwFile = $('#file-firmware').files[0];
if (!fwFile) {
alert('펌웨어(.bin) 파일을 선택하세요.');
return;
const isMerged = modeMerged.checked;
const fwFile = $('#file-firmware').files[0];
if (isMerged) {
if (!fwFile) { alert('merged.bin 파일을 선택하세요.'); return; }
} else {
const appFile = $('#file-app').files[0];
if (!fwFile && !appFile) { alert('앱 바이너리(.bin) 파일을 선택하세요.'); return; }
// 분리 모드에서는 app 드롭존 파일을 firmware로 사용
if (!fwFile && appFile) {
const dt = new DataTransfer();
dt.items.add(appFile);
$('#file-firmware').files = dt.files;
}
}
const finalFwFile = $('#file-firmware').files[0] || $('#file-app').files[0];
const fd = new FormData();
fd.append('name', $('#fw-name').value || fwFile.name.replace('.bin',''));
fd.append('version', $('#fw-version').value || '1.0.0');
fd.append('name', $('#fw-name').value || finalFwFile.name.replace(/\.bin$/i,''));
fd.append('version', $('#fw-version').value || '1.0.0');
fd.append('description', $('#fw-desc').value);
fd.append('chipFamily', $('#fw-chip').value);
fd.append('flashAddress', '0x10000');
fd.append('firmware', fwFile);
fd.append('chipFamily', $('#fw-chip').value);
fd.append('firmware', finalFwFile);
// flashAddress 미전송 → 서버가 bootloader 유무로 자동 결정 (merged:0x0 / split:0x10000)
const blFile = $('#file-bootloader').files[0];
if (blFile) fd.append('bootloader', blFile);
if (!isMerged) {
const blFile = $('#file-bootloader').files[0];
if (blFile) fd.append('bootloader', blFile);
const ptFile = $('#file-partitions').files[0];
if (ptFile) fd.append('partitions', ptFile);
const ptFile = $('#file-partitions').files[0];
if (ptFile) fd.append('partitions', ptFile);
const baFile = $('#file-boot-app0').files[0];
if (baFile) fd.append('boot_app0', baFile);
}
progressWrap.style.display = 'block';
progressBar.style.width = '0%';