# 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` 확장자 및 32 MB 크기 제한 - XSS 방지를 위한 HTML 이스케이프 처리 - 외부 배포 시 HTTPS 적용 필수 - ESP32 Flash Encryption 활성화 권장 (펌웨어 덤프 방지) --- ## 동시 접속 및 상업 서비스 확장성 ### 아키텍처 특성 — 서버는 플래싱에 관여하지 않음 플래싱은 **브라우저가 Web Serial API를 통해 USB로 직접** 처리합니다. 서버는 manifest JSON과 펌웨어 .bin 파일을 제공하는 역할만 담당합니다. ``` 사용자 브라우저 → 서버에서 manifest + .bin 다운로드 (수초) 사용자 브라우저 → USB → ESP32 (플래싱, 서버 무관) ``` ### 병목 지점 분석 | 구분 | 서버 부하 | 한계 | |------|---------|------| | HTML/JS/CSS 서빙 | Nginx — 매우 가벼움 | 수천 명 동시 가능 | | Manifest JSON | ~1 KB, 순간적 | 수천 건/초 | | **펌웨어 .bin 다운로드** | **← 실질 병목** | 네트워크 대역폭 | | 플래시 실행 | 서버 무관 (브라우저-USB) | 무제한 | ### 대역폭 기준 동시 플래시 추정 ``` merged.bin 16 MB 기준 (다운로드 시간 ~수초): 내부망 (1 Gbps) : 125 MB/s ÷ 16 MB ≈ 동시 7~8명 다운로드 다운로드는 수초면 끝나므로 실질 50~100명 처리 가능 인터넷 (100 Mbps 업로드): 12.5 MB/s ÷ 16 MB ≈ 동시 1명 한계 인터넷 (1 Gbps 업로드) : 125 MB/s ÷ 16 MB ≈ 동시 7~8명 app.bin 단독 (~1 MB) 사용 시 약 16배 여유 ``` ### 현재 구조의 한계 (MVP 수준) | 항목 | 현재 상태 | 문제점 | |------|---------|------| | 메타데이터 | `_metadata.json` 파일 | 동시 쓰기 시 데이터 손상 위험 | | 인증 | 없음 | 누구나 업로드·삭제 가능 | | HTTPS | 없음 | 외부망에서 Web Serial API 불가 | | 펌웨어 저장 | 로컬 Docker 볼륨 | 서버 장애 시 소실 가능 | | Rate Limiting | 없음 | 무차별 다운로드 가능 | ### 상업 서비스 전환 로드맵 #### 단기 — 필수 적용 1. **HTTPS 적용** — 외부 접속 시 Web Serial API 동작 필수 조건 2. **인증 추가** — 구매자에게만 플래시 토큰 발급 3. **Rate Limiting** — IP당 다운로드 횟수 제한 (nginx `limit_req`) #### 중기 — 확장성 확보 4. **메타데이터 DB화** — JSON 파일 → SQLite 또는 PostgreSQL 5. **펌웨어 스토리지** — 로컬 볼륨 → S3 호환 오브젝트 스토리지 (CDN 연동 가능) 6. **플래시 이력 로깅** — 판매 증거, 환불 분쟁 대응용 #### 장기 — 라이선스 모델 7. **1회용 플래시 토큰** — 구매 1건 = 플래시 1회 8. **MAC 주소 바인딩** — 기기별 라이선스 잠금 (`esp_efuse_mac_get_default()`) 9. **결제 연동** — Stripe / 토스페이먼츠 #### 권장 최종 아키텍처 ``` [결제] → [라이선스 서버] → 1회용 토큰 발급 ↓ [웹 플래셔] → Web Serial → ESP32 ↓ [플래시 로그] → MAC 바인딩 → 라이선스 활성화 ``` ### 결론 > 현재 구조(내부망)로는 **50~100명 동시** 처리 무리 없음. > 인터넷 상업 서비스는 **HTTPS + 인증 + 스토리지 개선** 3가지 선행 후 운영 권장. --- ## 라이선스 MIT