Files
webflash/backend/server.js
root 3a662affb6 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>
2026-05-19 05:26:53 +09:00

197 lines
6.2 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const cors = require('cors');
const crypto = require('crypto');
const { v4: uuidv4 } = require('uuid');
const app = express();
const PORT = process.env.PORT || 3000;
const FIRMWARE_DIR = path.join(__dirname, 'uploads');
const META_FILE = path.join(FIRMWARE_DIR, '_metadata.json');
const ALLOWED_ORIGIN = process.env.ALLOWED_ORIGIN || '*';
if (!fs.existsSync(FIRMWARE_DIR)) {
fs.mkdirSync(FIRMWARE_DIR, { recursive: true });
}
// ── Middleware ──────────────────────────────────────────────────────────────
app.use(cors({ origin: ALLOWED_ORIGIN }));
app.use(express.json());
// 펌웨어 바이너리 정적 서빙 (nginx 없이 직접 접근 시 사용)
app.use('/firmware/files', express.static(FIRMWARE_DIR));
// ── Firmware metadata (파일 기반, MVP용) ────────────────────────────────────
function loadMeta() {
if (!fs.existsSync(META_FILE)) return [];
try { return JSON.parse(fs.readFileSync(META_FILE, 'utf8')); }
catch { return []; }
}
function saveMeta(list) {
fs.writeFileSync(META_FILE, JSON.stringify(list, null, 2));
}
// ── Multer 설정 ─────────────────────────────────────────────────────────────
const storage = multer.diskStorage({
destination: FIRMWARE_DIR,
filename: (_req, file, cb) => {
const safe = Date.now() + '-' + crypto.randomBytes(4).toString('hex');
cb(null, safe + path.extname(file.originalname));
},
});
const upload = multer({
storage,
fileFilter: (_req, file, cb) => {
if (path.extname(file.originalname).toLowerCase() === '.bin') {
cb(null, true);
} else {
cb(new Error('.bin 파일만 업로드 가능합니다'));
}
},
limits: { fileSize: 32 * 1024 * 1024 }, // 32 MB
});
// ── 라우트 ──────────────────────────────────────────────────────────────────
// 헬스 체크
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 펌웨어 목록
app.get('/api/firmware', (_req, res) => {
res.json(loadMeta());
});
// 펌웨어 업로드
// 지원 필드: firmware(필수), bootloader(선택), partitions(선택), boot_app0(선택)
app.post(
'/api/firmware/upload',
upload.fields([
{ name: 'firmware', maxCount: 1 },
{ name: 'bootloader', maxCount: 1 },
{ name: 'partitions', maxCount: 1 },
{ name: 'boot_app0', maxCount: 1 },
]),
(req, res) => {
try {
const files = req.files || {};
if (!files.firmware) {
return res.status(400).json({ error: '펌웨어 파일(firmware)이 필요합니다' });
}
const { name, version, description, chipFamily, flashAddress } = req.body;
const list = loadMeta();
const id = uuidv4();
const parts = [];
if (files.bootloader) {
parts.push({
file: files.bootloader[0].filename,
offset: '0x0000',
label: 'Bootloader',
});
}
if (files.partitions) {
parts.push({
file: files.partitions[0].filename,
offset: '0x8000',
label: 'Partition Table',
});
}
if (files.boot_app0) {
parts.push({
file: files.boot_app0[0].filename,
offset: '0xe000',
label: 'Boot App0',
});
}
// 부트로더가 따로 없으면(병합 바이너리) 0x0, 있으면 0x10000
const appOffset = flashAddress
|| (files.bootloader ? '0x10000' : '0x0000');
parts.push({
file: files.firmware[0].filename,
offset: appOffset,
label: 'Application',
size: files.firmware[0].size,
});
const entry = {
id,
name: name || '이름 없음',
version: version || '1.0.0',
description: description || '',
chipFamily: chipFamily || 'ESP32-S3',
createdAt: new Date().toISOString(),
parts,
};
list.unshift(entry);
saveMeta(list);
res.json({ success: true, id, firmware: entry });
} catch (err) {
res.status(500).json({ error: err.message });
}
}
);
// esp-web-tools manifest.json 생성
app.get('/api/firmware/:id/manifest', (req, res) => {
const firmware = loadMeta().find(f => f.id === req.params.id);
if (!firmware) return res.status(404).json({ error: '펌웨어를 찾을 수 없습니다' });
// esp-web-tools manifest 스펙
const manifest = {
name: firmware.name,
version: firmware.version,
new_install_prompt_erase: true,
builds: [{
chipFamily: firmware.chipFamily,
parts: firmware.parts.map(p => ({
path: `/firmware/files/${p.file}`,
offset: parseInt(p.offset, 16),
})),
}],
};
// CORS 허용 헤더 (esp-web-tools가 fetch할 때 필요)
res.setHeader('Access-Control-Allow-Origin', '*');
res.json(manifest);
});
// 단일 펌웨어 삭제
app.delete('/api/firmware/:id', (req, res) => {
const list = loadMeta();
const index = list.findIndex(f => f.id === req.params.id);
if (index === -1) return res.status(404).json({ error: '펌웨어를 찾을 수 없습니다' });
list[index].parts.forEach(p => {
const fp = path.join(FIRMWARE_DIR, p.file);
if (fs.existsSync(fp)) fs.unlinkSync(fp);
});
list.splice(index, 1);
saveMeta(list);
res.json({ success: true });
});
// ── 오류 핸들러 ─────────────────────────────────────────────────────────────
app.use((err, _req, res, _next) => {
console.error(err.message);
res.status(400).json({ error: err.message });
});
app.listen(PORT, () => {
console.log(`ESP32 Flasher Backend → http://localhost:${PORT}`);
});