diff --git a/README.md b/README.md index e69de29..b4bed1c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,203 @@ +# ESP32 Web Flasher + +브라우저에서 ESP32 계열 마이컴에 펌웨어를 플래시하는 Docker 기반 웹 애플리케이션입니다. +별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와 USB 케이블만으로 펌웨어를 구워 넣을 수 있습니다. + +## 주요 특징 + +- **Web Serial API** — 드라이버·CLI 도구 없이 브라우저 직접 시리얼 통신 +- **esp-web-tools v10** (Espressif 공식) — 안정적인 웹 컴포넌트 기반 플래싱 +- **다중 바이너리 지원** — bootloader / partition table / application 개별 또는 병합 바이너리 +- **펌웨어 관리** — 업로드·목록·삭제, UUID 기반 REST API +- **실시간 로그** — 색상 구분 시리얼 모니터 내장 +- **Docker Compose** — 단일 명령으로 백엔드(Node.js) + 프론트엔드(Nginx) 기동 + +## 지원 칩 + +| 칩 패밀리 | 지원 여부 | +|----------|---------| +| ESP32-S3 | ✓ 권장 (내장 USB-OTG 지원) | +| ESP32-S2 | ✓ | +| ESP32-C3 | ✓ | +| ESP32 | ✓ | + +## 브라우저 호환성 + +| 브라우저 | 지원 | +|--------|------| +| Chrome 89+ | ✓ | +| Edge 89+ | ✓ | +| Firefox | ✗ (Web Serial 미지원) | +| Safari | ✗ (Web Serial 미지원) | + +> **참고:** HTTP(localhost 제외)에서는 Web Serial API가 동작하지 않습니다. 외부 배포 시 HTTPS가 필수입니다. + +## 빠른 시작 + +### 사전 요구사항 + +- Docker Engine 20.10+ +- Docker Compose v2+ +- Chrome 89+ 또는 Edge 89+ + +### 실행 + +```bash +git clone https://gitea.byunc.com/byun/webflash.git +cd webflash +docker compose up -d --build +``` + +브라우저에서 `http://localhost:3100` 접속 + +### 중지 + +```bash +docker compose down +``` + +펌웨어 데이터까지 삭제하려면: + +```bash +docker compose down -v +``` + +## 사용 방법 + +### 1단계 — 시리얼 연결 확인 + +1. ESP32를 USB 케이블로 PC에 연결 +2. **포트 연결** 버튼 클릭 → 브라우저 포트 선택 대화상자에서 포트 선택 +3. VID/PID가 표시되고 Espressif(0x303A) 장치 여부 확인 + +### 2단계 — 펌웨어 업로드 + +**펌웨어 업로드** 탭에서: + +- 이름, 버전, 칩 패밀리 입력 +- `.bin` 파일 드래그&드롭 또는 클릭하여 선택 + - 병합 바이너리(merged.bin): 펌웨어 하나만 업로드 + - 분리 파일: bootloader(0x0000) + partitions(0x8000) + app(0x10000) +- **서버에 업로드** 클릭 + +### 3단계 — 플래시 + +**플래시** 탭에서: + +1. 목록에서 펌웨어 선택 +2. **ESP32S3 플래시 실행** 버튼 클릭 +3. esp-web-tools 대화상자에서 포트 선택 후 플래시 진행 + +## 플래시 파일 준비 + +### Arduino IDE에서 내보내기 + +``` +Sketch → Export Compiled Binary +→ 스케치 폴더에 .bin 파일 생성 +``` + +### 병합 바이너리 생성 (권장) + +```bash +esptool.py --chip esp32s3 merge_bin \ + -o merged.bin \ + 0x0 bootloader.bin \ + 0x8000 partitions.bin \ + 0x10000 app.bin +``` + +### PlatformIO 사용 시 + +```bash +pio run # 개별 바이너리 빌드 +pio run --target mergebin # 병합 바이너리 생성 +``` + +## 프로젝트 구조 + +``` +webflash/ +├── backend/ +│ ├── Dockerfile # Node.js 20 Alpine 이미지 +│ ├── package.json +│ └── server.js # Express REST API 서버 +├── frontend/ +│ ├── Dockerfile # Nginx Alpine 이미지 +│ ├── nginx.conf # 리버스 프록시 설정 +│ ├── index.html # 단일 페이지 앱 (한국어 UI) +│ ├── css/style.css # 다크 테마 스타일 +│ └── js/app.js # Web Serial API 로직 +└── docker-compose.yml +``` + +## REST API + +| 메서드 | 경로 | 설명 | +|--------|------|------| +| `GET` | `/api/health` | 헬스 체크 | +| `GET` | `/api/firmware` | 펌웨어 목록 조회 | +| `POST` | `/api/firmware/upload` | 펌웨어 업로드 | +| `GET` | `/api/firmware/:id/manifest` | esp-web-tools 매니페스트 생성 | +| `DELETE` | `/api/firmware/:id` | 펌웨어 삭제 | +| `GET` | `/firmware/files/:filename` | 바이너리 파일 서빙 | + +### 업로드 요청 예시 + +```bash +curl -X POST http://localhost:3100/api/firmware/upload \ + -F "name=My Firmware" \ + -F "version=1.0.0" \ + -F "chipFamily=ESP32-S3" \ + -F "firmware=@app.bin" \ + -F "bootloader=@bootloader.bin" \ + -F "partitions=@partitions.bin" +``` + +### 매니페스트 응답 예시 + +```json +{ + "name": "My Firmware", + "version": "1.0.0", + "new_install_prompt_erase": true, + "builds": [{ + "chipFamily": "ESP32-S3", + "parts": [ + { "path": "/firmware/files/xxxx-bootloader.bin", "offset": 0 }, + { "path": "/firmware/files/xxxx-partitions.bin", "offset": 32768 }, + { "path": "/firmware/files/xxxx-app.bin", "offset": 65536 } + ] + }] +} +``` + +## 환경 변수 + +| 변수 | 기본값 | 설명 | +|------|--------|------| +| `PORT` | `3000` | 백엔드 포트 | +| `NODE_ENV` | `production` | 실행 환경 | +| `ALLOWED_ORIGIN` | `*` | CORS 허용 오리진 | + +## 기술 스택 + +| 영역 | 기술 | +|------|------| +| 백엔드 | Node.js 20, Express 4, Multer, UUID | +| 프론트엔드 | Vanilla JS, HTML5, CSS3 | +| 웹 서버 | Nginx Alpine (리버스 프록시) | +| 플래싱 | esp-web-tools v10, Web Serial API | +| 컨테이너 | Docker, Docker Compose | +| 저장소 | 파일 시스템 + JSON 메타데이터 | + +## 보안 고려사항 + +- 파일 업로드는 `.bin` 확장자 및 8 MB 크기 제한 +- XSS 방지를 위한 HTML 이스케이프 처리 +- 외부 배포 시 HTTPS 적용 필수 +- ESP32 Flash Encryption 활성화 권장 (펌웨어 덤프 방지) + +## 라이선스 + +MIT diff --git a/report.html b/report.html new file mode 100644 index 0000000..cb40050 --- /dev/null +++ b/report.html @@ -0,0 +1,1067 @@ + + +
+ + +
+ 브라우저 기반 ESP32 펌웨어 플래시 시스템
+ Docker 컨테이너 아키텍처 기술 분석 보고서
+
+ ESP32 Web Flasher는 ESP32 계열 마이크로컨트롤러에 펌웨어를 플래시하는 작업을 + 웹 브라우저에서 수행할 수 있게 해주는 Docker 기반 웹 애플리케이션입니다. + Arduino IDE, esptool.py 등 별도 소프트웨어 설치 없이 Chrome/Edge 브라우저와 + USB 케이블만으로 동작합니다. +
+ +http://localhost:3100| 칩 | USB 방식 | 비고 |
|---|---|---|
| ESP32-S3 | 내장 USB-OTG (드라이버 불필요) | 권장 |
| ESP32-S2 | 내장 USB-OTG | 지원 |
| ESP32-C3 | USB-CDC | 지원 |
| ESP32 | USB-UART 변환기 (CP2102 등) | 변환칩 필요 |
+ 두 개의 Docker 컨테이너가 Docker Compose로 오케스트레이션됩니다. + Nginx가 리버스 프록시 역할을 하여 정적 파일 서빙과 API 라우팅을 모두 담당합니다. +
+ +| 컨테이너 | 이미지 | 내부 포트 | 외부 포트 | 역할 |
|---|---|---|---|---|
esp32-flasher-frontend |
+ nginx:alpine | +80 | +3100 | +정적 파일 서빙 + 리버스 프록시 | +
esp32-flasher-backend |
+ node:20-alpine | +3000 | +비공개 (내부망) | +REST API + 파일 관리 | +
location /api/ → proxy_pass http://backend:3000/api/ +location /firmware/ → proxy_pass http://backend:3000/firmware/ +location / → try_files $uri /index.html (SPA 라우팅)+ +
백엔드는 외부에 포트를 노출하지 않으며, Docker 내부 DNS(backend:3000)를 통해서만 접근됩니다.
+ Express.js 기반 REST API 서버. Multer로 멀티파트 업로드 처리, + UUID로 펌웨어 식별자 생성, 파일시스템 기반 JSON 메타데이터 관리. + esp-web-tools가 요구하는 manifest.json을 동적으로 생성하여 플래싱 파라미터를 제공합니다. +
++ Vanilla JavaScript로 작성된 SPA 로직. Web Serial API로 ESP32 연결을 확인하고, + XHR 기반 업로드 진행률 추적, 드래그&드롭 파일 선택, + esp-web-install-button 웹 컴포넌트 manifest 동적 주입을 담당합니다. +
++ SPA 라우팅(try_files), API 역방향 프록시, 펌웨어 파일 서빙을 + 하나의 Nginx 설정으로 처리합니다. 백엔드는 Docker DNS를 통해 참조됩니다. +
++ backend/frontend 두 서비스를 정의합니다. backend 헬스체크(30초 간격), + frontend는 backend depends_on, 펌웨어는 Named Volume으로 영속 저장됩니다. +
+| 구성요소 | 기술 | 버전 | 용도 |
|---|---|---|---|
| 컨테이너 런타임 | Docker Engine | 20.10+ | 컨테이너 빌드·실행 |
| 오케스트레이션 | Docker Compose | v2 | 멀티서비스 관리 |
| 스토리지 | Docker Named Volume | — | 펌웨어 바이너리 영속 저장 |
| 메타데이터 DB | JSON 파일 (_metadata.json) | — | 펌웨어 정보 레지스트리 |
+ multipart/form-data 방식으로 최대 3개의 .bin 파일(firmware, bootloader, partitions)을 동시 업로드합니다.
+ 각 파일은 타임스탬프-랜덤4바이트.bin 형태로 저장되고,
+ UUID 기반 메타데이터가 JSON 파일에 기록됩니다.
+
{
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "My Product v1.0",
+ "version": "1.0.0",
+ "chipFamily": "ESP32-S3",
+ "createdAt": "2025-05-17T10:00:00.000Z",
+ "parts": [
+ { "file": "1747123456-ab12.bin", "offset": "0x0000", "label": "Bootloader" },
+ { "file": "1747123456-cd34.bin", "offset": "0x8000", "label": "Partition Table" },
+ { "file": "1747123456-ef56.bin", "offset": "0x10000", "label": "Application" }
+ ]
+}
+
+ + 백엔드가 저장된 메타데이터를 esp-web-tools 표준 manifest 형식으로 변환합니다. + offset은 16진수 문자열에서 정수로 자동 변환됩니다. +
+GET /api/firmware/:id/manifest
+
+{
+ "name": "My Product v1.0",
+ "version": "1.0.0",
+ "new_install_prompt_erase": true,
+ "builds": [{
+ "chipFamily": "ESP32-S3",
+ "parts": [
+ { "path": "/firmware/files/xxxx.bin", "offset": 0 },
+ { "path": "/firmware/files/yyyy.bin", "offset": 32768 },
+ { "path": "/firmware/files/zzzz.bin", "offset": 65536 }
+ ]
+ }]
+}
+
+
+ 플래시 전에 ESP32가 올바르게 연결되었는지 VID/PID로 확인합니다.
+ Espressif의 USB Vendor ID는 0x303A로, 이 값을 감지하면 ESP32 장치로 판단합니다.
+
| VID | 장치 | 비고 |
|---|---|---|
0x303A | Espressif ESP32 (내장 USB) | Espressif 확인 |
0x10C4 | Silicon Labs CP210x | USB-UART 변환기 |
0x1A86 | CH340/CH341 | USB-UART 변환기 |
| 기타 | 알 수 없는 장치 | 경고 표시 |
+ DataTransfer API를 이용한 드래그&드롭 인터페이스와 XHR 기반 업로드 진행률 추적을 구현합니다.
+ fetch()는 업로드 진행률을 지원하지 않으므로 XMLHttpRequest를 직접 사용합니다.
+
| 타입 | CSS 클래스 | 색상 | 용도 |
|---|---|---|---|
| ok | line-ok | teal (#00c896) | 성공 메시지 |
| err | line-err | red (#f85149) | 오류 메시지 |
| warn | line-warn | yellow (#e3b341) | 경고 메시지 |
| info | line-info | muted (#8b949e) | 정보 메시지 |
| 메서드 | 경로 | 설명 | 응답 |
|---|---|---|---|
| GET | +/api/health |
+ 헬스 체크 (Docker 프로브용) | +{ status: "ok", timestamp } |
+
| GET | +/api/firmware |
+ 펌웨어 목록 조회 | +메타데이터 배열 JSON | +
| POST | +/api/firmware/upload |
+ 펌웨어 업로드 (multipart) | +{ success, id, firmware } |
+
| GET | +/api/firmware/:id/manifest |
+ esp-web-tools manifest 생성 | +manifest JSON | +
| DELETE | +/api/firmware/:id |
+ 펌웨어 삭제 (파일 + 메타데이터) | +{ success: true } |
+
| GET | +/firmware/files/:filename |
+ 바이너리 파일 정적 서빙 | +application/octet-stream | +
| 필드명 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
firmware | file (.bin) | 필수 | — | 애플리케이션 바이너리 |
bootloader | file (.bin) | 선택 | — | 부트로더 (offset 0x0000) |
partitions | file (.bin) | 선택 | — | 파티션 테이블 (offset 0x8000) |
name | string | 선택 | 파일명 | 펌웨어 이름 |
version | string | 선택 | 1.0.0 | 버전 문자열 |
chipFamily | string | 선택 | ESP32-S3 | 칩 패밀리 |
flashAddress | string (hex) | 선택 | 0x10000 | 앱 플래시 주소 |
+ 플래싱 단계에서 .bin 파일은 서버에서 브라우저로 다운로드된 후, + 브라우저가 직접 Web Serial API를 통해 ESP32에 씁니다. + 서버는 플래시 과정에 관여하지 않으므로 네트워크 지연의 영향이 최소화됩니다. +
+| 항목 | 구현 여부 | 방법 |
|---|---|---|
| 파일 유형 제한 | ✓ 구현 | Multer fileFilter — .bin 확장자만 허용 |
| 파일 크기 제한 | ✓ 구현 | 8 MB 상한 (limits.fileSize) |
| XSS 방지 | ✓ 구현 | escHtml() 함수로 사용자 입력 이스케이프 |
| CORS 제어 | ✓ 구현 | ALLOWED_ORIGIN 환경변수로 설정 가능 |
| 안전한 파일명 | ✓ 구현 | 원본 파일명 미사용, 타임스탬프+랜덤 생성 |
| 인증/인가 | 미구현 | 현재 MVP 단계, 토큰 인증 추가 필요 |
| Rate Limiting | 미구현 | 무차별 업로드 공격 방지 필요 |
| HTTPS | 미구현 | 외부 배포 시 필수 (현재 HTTP) |
# HTTPS (Nginx SSL 설정 예시)
+server {
+ listen 443 ssl;
+ ssl_certificate /etc/ssl/cert.pem;
+ ssl_certificate_key /etc/ssl/key.pem;
+ ...
+}
+
+# Flash Encryption 활성화 (esptool)
+espefuse.py --chip esp32s3 burn_efuse DIS_DOWNLOAD_PLAIN_DECRYPT
+ | 항목 | 백엔드 | 프론트엔드 |
|---|---|---|
| 베이스 이미지 | node:20-alpine | nginx:alpine |
| 예상 이미지 크기 | ~400 MB | ~40 MB |
| 노출 포트 | 없음 (내부 전용) | 3100:80 |
| 재시작 정책 | unless-stopped | unless-stopped |
| 헬스 체크 | 30초 간격 (3회 재시도) | 없음 |
| 볼륨 | firmware_uploads:/app/uploads | 없음 |
| 변수명 | 기본값 | 설명 |
|---|---|---|
PORT | 3000 | 백엔드 리스닝 포트 |
NODE_ENV | production | Node.js 실행 환경 |
ALLOWED_ORIGIN | * (전체 허용) | CORS 허용 오리진 |
# 빌드 및 시작 +docker compose up -d --build + +# 로그 확인 +docker compose logs -f + +# 재시작 +docker compose restart + +# 중지 (데이터 유지) +docker compose down + +# 완전 초기화 (데이터 삭제) +docker compose down -v+
| 단계 | 기능 | 우선순위 |
|---|---|---|
| v1.1 | +HTTPS 지원 (Nginx SSL/TLS), 업로드 인증 토큰 | +높음 | +
| v1.2 | +Rate Limiting, 플래시 이력 로깅, MAC 주소 기반 기기 인증 | +중간 | +
| v2.0 | +결제 연동 (Stripe), 1회용 플래시 토큰, 라이선스 서버 | +계획 | +
| v2.1 | +S3 호환 스토리지 연동, CDN 배포, 다국어 UI | +계획 | +
| v3.0 | +OTA 업데이트 통합, 구독 모델, 관리자 대시보드 | +계획 | +
+ Espressif 공식 esp-web-tools를 기반으로 하므로 기술 안정성이 높습니다. + ESP32-S3의 내장 USB-OTG 지원으로 별도 USB-UART 변환 칩 없이 플래싱이 가능하고, + 이는 제품 원가 절감으로 이어집니다. + MAC 주소 기반 라이선스 바인딩(ESP32의 eFuse MAC은 위변조 난이도 높음)과 + 결합하면 펌웨어 유료 판매 플랫폼으로 확장할 수 있습니다. +
++ 현재 MVP 아키텍처(파일 기반 JSON 메타데이터)는 소규모 운영에 적합하며, + 수백~수천 건 이상의 트래픽이 예상되면 PostgreSQL 등 관계형 DB와 S3 호환 스토리지로 + 마이그레이션이 권장됩니다. +
++ ESP32 Web Flasher 기술 분석 보고서 — 2025년 5월 작성 +
+ +