# Firmware OTA 서브시스템 가이드 > 살아있는 문서(living guide) — 앱 **v1.11.10** 기준. 코드: `src/firmware/*.py`. > 아키텍처 전체는 [`architecture.md`](architecture.md) §3.1.2. 웹 UI 에서 펌웨어 ZIP 을 올려 dpworldapp 의 FW-MMI 채널(**TCP 8990**)로 플래시한다. 플래시 전 config 백업, 재부팅 후 복원 검사까지 한 흐름으로 오케스트레이션한다. 오케스트레이터는 `FirmwareController`(단일 인스턴스, thread-safe). --- ## 1. staging → flash 흐름 ``` upload(ZIP) → stage(추출·sha256) → preflight(게이트) → flash(백업→전송→commit) → reboot → restore-check ``` 1. **upload** — 요청 본문(펌웨어 ZIP)을 디스크 임시파일에 1MB 청크로 스트리밍. 버퍼는 **반드시 디스크 백엔드**(`/opt/fw_upload`) — `MemoryMax=48M`·no-swap 환경에서 tmpfs(`/tmp`)에 수백 MB 를 버퍼하면 cgroup OOM 으로 서비스가 죽는다. 2. **stage** — ZIP 추출 → 확장자로 컴포넌트 자동 식별 → 각 파일 sha256 계산 → `/opt/fw_staging`. zip-bomb/디스크 채움 가드(컴포넌트당·전체 압축 해제 크기 상한, 중복 파일명 거부, free-space 확인). sidecar(`.fw_build_dates.json`)에 sha256+build_date 를 증분 기록 → 서비스 재시작 시 `_recover_staging` 가 sha256 재검증으로 안전 복구(불일치/미기록 파일은 거부·삭제). 3. **preflight** — 게이트 체크리스트. critical 전부 통과해야 `ok`. 4. **flash** — 백그라운드 워커가 백업 → 컴포넌트 전송 → commit → reboot 트리거. 5. **restore-check** — 재부팅·재접속 후 운영자가 호출 → reseed 감지 시 config 복원 + dpworldapp 재시작. ### 컴포넌트 식별 (확장자 → wire signature) | 확장자 | role | signature | |--------|------|-----------| | `.rom` | bootloader | `BTL` | | `.img` | kernel | `KRN` | | `.ext4` | rootfs | `RTF` | | `.dtb` | dtb | `DTB` | commit 은 별도 message-only 프레임(`CPU` signature, payload 0). --- ## 2. phase stepper (UI 상태) `fw_controller._PHASES` — 순서 의미 있음. 각 phase 는 `pending`/`active`/`done`/`error`, tier 는 `na`/`warn`/`ok`/`error` 로 매핑. | key | label | 의미 | |-----|-------|------| | `upload` | Upload | ZIP 수신 | | `verify` | Verify | sha256 계산/검증 | | `preflight` | Pre-flight | 게이트 체크 | | `backup` | Backup | 플래시 전 config 백업 | | `flash` | Flash | 컴포넌트 전송 | | `commit` | Commit | 슬롯 기록·커밋(마지막 컴포넌트 100% 시 시작) | | `reboot` | Reboot | 장치 재부팅(~10s) | | `config` | Config check | 재부팅 후 복원 검사 | 상태머신: `idle`→`staging`→`staged`→`flashing`→`rebooting`→`done` (실패 시 `failed`). `flashing`/`staging`/`rebooting`/`done` 중에는 새 upload/flash 를 거부(race 방지). ### preflight 게이트 항목 - `staging`(컴포넌트 staged, **critical**) - `space`(여유공간 headroom 128MB, **critical**) — staged 바이트는 이미 디스크에 있으므로 중복 계상 안 함 - `fw_port`(dpworldapp FW 포트 8990 도달, **critical**) - `db`(config DB 존재, non-critical — flash 는 되지만 restore-check 에 필요) - `slot`(활성 A/B 슬롯, informational) --- ## 3. config 안전 (config_safety) - **플래시 전 백업**: `backup_config` 가 `device_config`/`protocol_config`(+ DB)를 `/opt/config_backups/<타임스탬프>/` 로 저장. `backup` phase 에서 수행. - **재부팅 후 복원 검사**(`detect_and_restore`): 펌웨어 flash 가 config 를 factory default 로 reseed 했는지 **필드 단위로 감지**한다. "reseed" 판정 = 사용자 구별 필드가 대거 factory default 로 되돌아갔고(≥80%, 최소 2개) **새 편집 흔적이 전혀 없을 때**(do-no-harm). 이 경우에만 백업본을 **단일 트랜잭션**(BEGIN IMMEDIATE)으로 원자 복원하고 dpworldapp 을 재시작한다. 애매하면 복원하지 않음(fail-safe toward no-restore). - DB 가 읽기 불가/잠금/비어 있어도 500 으로 죽지 않고 사유를 보고한다. - restore-check 응답은 **정직성** 원칙: dpworldapp 재시작이 실제 실패하면 `restart_error` 를 명시하고 "재시작됨" 으로 거짓 보고하지 않는다(v1.5.4.5 STAB-2). --- ## 4. TCP 8990 dpworldapp 채널 (wire protocol) `fw_client` 가 `protocol` 프레이밍으로 8990 에 전송한다(`server.py`: `FW_HOST`/`FW_PORT`, 기본 `127.0.0.1:8990`). dpworldapp 유닛 상태와 무관하게 서빙되는 포트다. - **프레임** = ASCII signature + 8바이트 little-endian size + `` payload. message-only step 은 size 0, payload 없음. - **ACK**: 성공 `SUCCESS`, 실패 `FW_FAIL`(deployed firmware behavior 확인). `classify_ack` 가 success 우선 분류. - **per-step ACK 타임아웃(ms)**: BTL/KRN/RTF 20000, DTB 10000, CPU(commit) 120000. --- ## 5. A/B rootfs 슬롯 + `/opt` 영속 (flash 생존) - 장치는 **A/B rootfs 슬롯**(p5=A, p6=B)을 가지며 flash 마다 flip 한다. 활성 슬롯은 `/proc/cmdline` 의 `root=` 로 best-effort 탐지(`_detect_slot`). status 에 `slot.before/after` 노출. - commit 성공 후 staging(`/opt/fw_staging`)은 공간 회수를 위해 삭제(슬롯에 이미 기록됨). config 백업(`/opt/config_backups`)은 **보존**. - `/opt`(p12)·`/home/root`(p11)는 flash 후에도 **영속** — 업로드 버퍼·staging·config 백업·DB 가 여기에 있어 flash 를 견딘다. 단, web-configurator/AP 의 systemd 유닛 등 rootfs(`/lib`) 자산은 flash 마다 wipe 된다(§6). --- ## 6. ⚠ flash 생존 caveat 펌웨어 flash 는 rootfs 슬롯을 통째로 교체하므로 rootfs(`/lib/systemd` 유닛, `/usr/bin` 스크립트)에 설치된 web-configurator·Wi-Fi AP·network-apply 자산이 **매번 wipe** 된다. flash 후에는 이들을 재설치해야 한다. 근본 자동화는 BSP 이미지 베이킹이 정답(백로그) — 메모리 `web-survives-flash` 참조. --- ## 7. API | Method | Path | 비고 | |--------|------|------| | `GET` | `/api/firmware/status` | phase stepper + 컴포넌트 진행률 + slot + backup + message/error | | `POST` | `/api/firmware/preflight` | 게이트 체크리스트(`{ok, checks}`) | | `POST` | `/api/firmware/upload` | ZIP 스트리밍 업로드(per-read 소켓 타임아웃) → stage. `{ok, components}` | | `POST` | `/api/firmware/flash` | staged 컴포넌트 플래시 시작. 진행 중이면 409 | | `POST` | `/api/firmware/restore-check` | reseed 감지 → 복원 + dpworldapp 재시작 | 오류는 sanitize 되어 내부 fs 경로/예외 내부를 노출하지 않는다(공간부족 507, 잘못된 ZIP 400 등). > **API 인증 없음**: :9090 은 현재 미인증(문서화된 threat boundary). 운영자 로그인은 설계 > 백로그 — [`architecture.md`](architecture.md) §10.