You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

132 lines
6.8 KiB

# 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 + `<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.