docs: pilot runbook + gated MoveNet smoke test
This commit is contained in:
parent
b0d1241da1
commit
a946d0bee1
2 changed files with 140 additions and 0 deletions
112
docs/pilot-runbook.md
Normal file
112
docs/pilot-runbook.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# P1 파일럿 런북 (노트북 웹캠 엔드투엔드)
|
||||
|
||||
이 문서는 노트북 웹캠 하나로 **엣지 → 클라우드 → 관리자 응답**의 최소 경로를 돌리는 절차다.
|
||||
실장비(Hailo·Pi5)·폐쇄망 전체 번들·드릴 게이트·SMS 연동은 후속(범위 밖, §6).
|
||||
|
||||
- 엣지: `python -m edge` (또는 `move-detect-edge`) — CPU MoveNet pose → 감지 → HMAC 서명 POST.
|
||||
- 클라우드: 기존 `cloud/` PHP 앱 — ingest + 대시보드 + 사고 상세 + 데스크 Web Audio 알림.
|
||||
|
||||
---
|
||||
|
||||
## 1. 빌드 머신 준비 (모델 · onnxruntime)
|
||||
|
||||
> **폐쇄망 원칙:** onnxruntime 휠과 `.onnx` 모델은 **빌드 머신에서 받아 번들**한다. 타깃(폐쇄망)에서
|
||||
> 인터넷을 받지 않는다. 노트북 파일럿은 빌드=타깃이 같은 머신이라 아래를 그대로 실행하면 된다.
|
||||
|
||||
```bash
|
||||
pip install onnxruntime # CPU 추론 런타임 (에어갭 타깃엔 오프라인 휠로 번들)
|
||||
python -m edge.tools.fetch_movenet # MoveNet SinglePose Lightning ONNX -> models/movenet_lightning.onnx
|
||||
```
|
||||
|
||||
`fetch_movenet.py`의 URL은 빌드 편의용이다. 실제 내려받은 모델의 입력 크기/자료형과 출력 레이아웃
|
||||
`[1,1,17,3]`(y,x,score)를 어댑터 기대값과 대조하라(다르면 `MoveNetPoseAdapter(input_size=...)` 조정;
|
||||
COCO-17 remap 로직은 그대로). 어댑터 remap은 `tests/test_movenet_pose.py`(fake session)로 고정돼 있고,
|
||||
실모델 경로는 `tests/hardware/test_movenet_smoke.py`가 모델이 번들됐을 때만 게이트로 검증한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 클라우드 로컬 기동
|
||||
|
||||
로컬 MySQL 8(스키마 CHECK 제약은 8.0.16+)과 PHP 8.2+ 필요. 상세 배포·전제조건(gd/argon2/HTTPS 등)은
|
||||
[cloud/docs/cloud-deploy.md](../cloud/docs/cloud-deploy.md) 참조 — 여기선 로컬 최소 절차만.
|
||||
|
||||
1. DB 생성 후 스키마 임포트: `mysql <db> < cloud/schema.sql` (10개 테이블).
|
||||
2. `cloud/config.php`를 `cloud/config.example.php`에서 복사, 로컬 DB DSN/계정 기입
|
||||
(`snapshot_dir`는 쓰기 가능 경로로).
|
||||
3. **관리자 계정 · 디바이스 시크릿 등록** — 앱에 셀프 가입 흐름이 없으므로 직접 넣는다.
|
||||
CLI 스니펫과 SQL은 cloud-deploy.md **§6(디바이스 시크릿)** · **§7(첫 관리자)** 그대로 사용:
|
||||
- 시크릿 생성: `php -r "echo bin2hex(random_bytes(32)), \"\n\";"`
|
||||
- `INSERT INTO devices (device_id, name, secret, config_version) VALUES ('pi-01','노트북','<시크릿>',1);`
|
||||
- 관리자 해시: `php -r "echo password_hash('바꿀-암구호', PASSWORD_ARGON2ID), \"\n\";"`
|
||||
- `INSERT INTO users (username, password_hash, role, ...) VALUES ('admin','<해시>','admin',NULL,0,0);`
|
||||
4. 기동: `php -S localhost:8000 -t cloud/public`
|
||||
- 세션 쿠키가 `cookie_secure`라 순수 HTTP에선 로그인 쿠키가 안 붙을 수 있다. 로컬은 대개
|
||||
`localhost`가 secure-context로 취급돼 동작하지만, 안 되면 cloud-deploy.md §8(HTTPS) 참조.
|
||||
|
||||
---
|
||||
|
||||
## 3. 엣지 환경변수
|
||||
|
||||
`edge/config.py`(`load_config`) + `edge/daemon.py`(`main`)가 읽는 실제 키:
|
||||
|
||||
| env | 의미 | 예시 |
|
||||
|-----|------|------|
|
||||
| `DEVICE_ID` | `devices.device_id`와 일치 | `pi-01` |
|
||||
| `DEVICE_SECRET` | `devices.secret`와 일치(HMAC) | `<§2에서 생성한 값>` |
|
||||
| `CAFE24_URL` | 클라우드 베이스 URL | `http://localhost:8000` |
|
||||
| `CAMERA_ID` | 이벤트/구역의 camera_id | `cam-01` |
|
||||
| `MOVENET_ONNX` | 모델 경로 **(필수, 없으면 즉시 KeyError)** | `models/movenet_lightning.onnx` |
|
||||
| `UVC_DEVICE` | OpenCV 캡처 인덱스 | `0` |
|
||||
| `TARGET_FPS` | 프레임 간격 = 1/fps | `12` |
|
||||
| `QUEUE_DB` | 오프라인 큐 sqlite 경로 | `queue.db` |
|
||||
|
||||
감지 임계값(선택, 데모 단축용): `FALL_PRONE_S`(기본 3.0), `FALL_NO_MOTION_S`(10.0),
|
||||
`IMMOBILE_S`(120.0), `GHOST_REAPPEAR_S`(10.0), `ZONE_DEBOUNCE_S`(0.75), `COOLDOWN_S`(30.0).
|
||||
|
||||
```bash
|
||||
export DEVICE_ID=pi-01 DEVICE_SECRET='<시크릿>' CAFE24_URL=http://localhost:8000 \
|
||||
CAMERA_ID=cam-01 MOVENET_ONNX=models/movenet_lightning.onnx UVC_DEVICE=0 TARGET_FPS=12
|
||||
python -m edge # 또는 move-detect-edge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 데모 시나리오
|
||||
|
||||
1. **낙상:** 웹캠 앞에서 눕는다 → `FALL_PRONE_S`(기본 3초) 후 `fall`(high) 이벤트.
|
||||
2. **위급 상승:** 계속 안 움직이면 `FALL_NO_MOTION_S`(기본 10초) 후 `no_motion`(critical)
|
||||
→ 대시보드에서 **데스크 비프 반복**(첫 클릭으로 오디오 언락 필요).
|
||||
3. **가시화:** 목록의 이벤트 유형(링크) 클릭 → `/events/{id}/view` 상세 → **스냅샷 이미지** 확인.
|
||||
4. **응답:** 상세/목록에서 **확인** → 미확인 critical이 사라지면 비프 정지.
|
||||
5. **역루프(구역):** 대시보드 `구역 설정`에서 폴리곤 저장 → 발끝 진입 → `zone_*` 이벤트.
|
||||
구역 변경은 엣지의 **다음 config 폴링** 때 적용된다 — 기본 cadence `sync_every=150` 프레임
|
||||
≈ 12fps에서 약 12초. 저장 후 ~15초 기다렸다가 진입할 것.
|
||||
6. **오프라인 큐:** 네트워크(또는 `php -S`) 중단 상태에서 이벤트 발생 → 큐에 적재 →
|
||||
복구 후 `flush_every=300` 프레임(≈25초)마다 flush로 밀린 이벤트 전송.
|
||||
7. **하트비트:** `heartbeat_every=900` 프레임(≈75초)마다 `POST /api/heartbeat`(서명).
|
||||
|
||||
> cadence(`sync_every`/`heartbeat_every`/`flush_every`)는 `daemon.main()` 기본값이며 env로
|
||||
> 노출돼 있지 않다. 데모에서 더 빠른 주기가 필요하면 `run_loop(...)` 호출 인자를 임시 조정한다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 검증(테스트)
|
||||
|
||||
```bash
|
||||
pytest -q # 엣지 유닛(하드웨어 스모크는 모델 없으면 skip)
|
||||
cd cloud && ./vendor/bin/phpunit --display-warnings --display-deprecations # 클라우드(0 warnings)
|
||||
```
|
||||
|
||||
모델을 번들한 뒤 실추론 경로까지 확인하려면 `models/movenet_lightning.onnx` 존재 상태로 `pytest -q` —
|
||||
`tests/hardware/test_movenet_smoke.py`가 skip 해제돼 실행된다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 파일럿 한계 (명시)
|
||||
|
||||
- **단일 카메라 · CPU pose(저 fps)** — 실장비 Hailo 가속은 후속.
|
||||
- **단일 프레임 스냅샷** — 前中後 링버퍼 다중 프레임은 P2.
|
||||
- **알림 1채널(데스크 Web Audio)** — critical SMS/webhook 실연동은 후속(게이트 정보 필요).
|
||||
- **폐쇄망 전체 번들 아님** — onnxruntime/모델 fetch만 제공. 오프라인 휠·차단 설치·`docker save`
|
||||
패키징은 별도 후속 태스크.
|
||||
- **드릴 게이트 · F14 컴플라이언스 · 감사 해시체인** — 범위 밖(P1-C/P3).
|
||||
28
tests/hardware/test_movenet_smoke.py
Normal file
28
tests/hardware/test_movenet_smoke.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"""Gated end-to-end smoke test for the real MoveNet ONNX model.
|
||||
|
||||
Skipped unless the model is bundled AND onnxruntime is installed, so the default
|
||||
CI/dev suite (no model, no onnxruntime) stays green. On a build/pilot machine that
|
||||
ran ``python -m edge.tools.fetch_movenet``, this exercises the full adapter path:
|
||||
frame -> onnxruntime inference -> COCO-17 remap -> list[Person].
|
||||
"""
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
MODEL = os.path.join(os.path.dirname(__file__), "..", "..", "models", "movenet_lightning.onnx")
|
||||
|
||||
|
||||
@pytest.mark.skipif(not os.path.exists(MODEL), reason="MoveNet model not bundled")
|
||||
def test_real_movenet_runs():
|
||||
pytest.importorskip("onnxruntime")
|
||||
from edge.adapters.movenet_pose import MoveNetPoseAdapter
|
||||
|
||||
adapter = MoveNetPoseAdapter(MODEL)
|
||||
persons = adapter.infer(np.full((480, 640, 3), 128, np.uint8))
|
||||
|
||||
# Runs end-to-end; a flat gray frame may yield 0 persons — the contract is
|
||||
# only that it returns a list and, when non-empty, each Person has 17 keypoints.
|
||||
assert isinstance(persons, list)
|
||||
for person in persons:
|
||||
assert len(person.keypoints) == 17
|
||||
Loading…
Reference in a new issue