# SHT30 온습도 모니터링 STM32F407 펌웨어

> Raspberry Pi SHT30 온습도 노드를 **STM32F407VGT6** 베어메탈 펌웨어로 대체한 프로젝트.
> 기존 서버측 PHP API / 서명 규약 / 요청 포맷을 **바이트 단위로 그대로 재현**하여
> 서버 무중단으로 RPi → STM32 전환을 목표로 한다.

- 대상 MCU: STM32F407VGT6 (Cortex-M4F @168MHz, Flash 1MB, SRAM 192KB + CCM 64KB)
- 네트워크: **유선 Ethernet** (LAN8720 RMII PHY) → LwIP + mbedTLS (HTTPS)
- 런타임: **FreeRTOS** (네이티브 API — `task.h`/`queue.h`/`timers.h`/`semphr.h`)
- 빌드: `arm-none-eabi-gcc` 15.2 + CMake + Ninja
- 배포: **폐쇄망(air-gapped)** — 모든 서드파티 의존성은 `third_party/`에 벤더링, 빌드·런타임에 외부 다운로드 0건

상세 이식 근거는 [`../docs/stm32f407_migration_plan.md`](../docs/stm32f407_migration_plan.md) 참고.

---

## 1. 시스템 아키텍처

물리 센서에서 SMS 발송까지의 전체 경로. **굵게** 표시한 구간이 본 펌웨어가 대체하는 부분이다.

```
┌──────────────┐    I2C1     ┌────────────────────────────┐  Ethernet(RMII)  ┌──────────────┐
│  SHT30 센서   │ ──────────▶ │      STM32F407VGT6          │ ───────────────▶ │  LAN8720 PHY  │
│  (온습도)     │  PB6/PB7    │  (FreeRTOS 펌웨어)           │                  │  (외부 PHY)   │
│  0x44        │  (SCL/SDA)  │                            │                  └──────┬───────┘
│              │             │  Task_Net   : 링크/DHCP/SNTP │                         │ UTP
│              │             │  Task_Sensor: 5분 주기 측정   │                         ▼
│              │             │  Task_Report: TLS POST       │                  ┌──────────────┐
└──────────────┘             │  Task_Watchdog: IWDG refresh │                  │   스위치/공유기 │
                             └────────────────────────────┘                  └──────┬───────┘
                                                                                    │
                          lowercase_hex(sha256(API_KEY || raw_body))  X-Signature 헤더 │ HTTPS/TLS1.2
                                                                                    ▼
                                  ┌────────────────────────────────────────────────────────┐
                                  │            Cafe24 호스팅 (변경 없음)                       │
                                  │  sensor_data.php  ──▶  MySQL (로그 저장)                  │
                                  │       │                                                  │
                                  │       └──▶ 서버 임계 판정(config.php METRIC_*) ──▶ SMS    │
                                  └────────────────────────────────────────────────────────┘
```

- 펌웨어는 매 보고마다 `connect → TLS 핸드셰이크 → POST → close` 를 수행한다(메모리 절약, 보고 주기가 길어 성능 무관).
- 서명은 본문 바이트 그대로 `sha256(API_KEY || body)` 를 계산해 **`X-Signature`** 헤더로 보낸다. 본문의 키 정렬/float 포맷 재현이 불필요하다(서버 `verify_signature_raw`).
- **임계(고온/저온/고습/저습) 판정과 SMS 발송은 서버(PHP)에서 수행**한다. 펌웨어는 온습도 원값과 plausibility 상태(`metric_status`)만 보고하고, 경보 임계는 서버 `config.php` 의 `METRIC_*` 상수가 결정한다(폐쇄망 재플래시 비용을 피하기 위함).

---

## 2. 보드 구성 (SHT30 단일 보드)

이 프로젝트는 **SHT30 온습도 보드 단일 구성**이다. CMake `sht30_fw` 타깃이 `-DBOARD_SHT30` 을 자동으로 주입한다.

| 빌드 매크로     | CMake 타깃 | 산출물          | sensor_id | device_id        | 센서          | 동작                                       |
|----------------|-----------|-----------------|-----------|------------------|---------------|--------------------------------------------|
| `-DBOARD_SHT30`| `sht30_fw`| `sht30_fw.bin`  | 2         | `stm32-sht30-01` | SHT30 온습도   | 주기(300s) I2C 측정 + 원값 보고            |

식별값 정의는 [`common/board_config.h`](common/board_config.h)(`BOARD_SHT30` 단일). 공통 튜너블은 [`common/app_config.h`](common/app_config.h).

---

## 3. 기능 패리티 (RPi 대비)

| 기능                  | RPi (Python)                     | STM32 펌웨어                              | 상태       |
|----------------------|----------------------------------|-------------------------------------------|-----------|
| SHT30 측정/CRC        | `smbus2` + `crc8_sht3x`          | HAL I2C + `sht30_crc8`/`sht30_parse`      | 유지       |
| 주기 측정/보고         | 300s (최소 30s)                  | `APP_SHT30_REPORT_INTERVAL_SEC` (300s)    | 유지       |
| metric_status 판정    | `classify_reading()` plausibility | reporter 호출부 `metric_status`           | 유지       |
| 임계 경보/SMS         | (서버측 처리)                    | **서버 `config.php` METRIC_* 에서 판정**  | 유지(서버)  |
| HTTP 재시도            | requests + 루프(3회/5s)          | `report_*` 내부 재시도(3회/5s)            | 유지       |
| TLS/HTTPS             | OpenSSL(`requests`)              | mbedTLS + LwIP                            | 변경(이식) |
| 네트워크              | WiFi                             | 유선 Ethernet(LAN8720 RMII)               | 변경(이식) |
| 동시성                | Python threads                   | FreeRTOS tasks                            | 변경(이식) |
| 시간원                | OS NTP                           | SNTP(LwIP) → RTC                          | 변경(이식) |
| 서명                  | 본문 필드 정규화 서명             | raw-body `X-Signature`                    | 변경(단순화)|
| 프로세스 관리          | systemd 재시작                   | IWDG 워치독 + 재연결                       | 변경(이식) |
| 로깅                  | 로테이팅 파일 로그                | UART 콘솔                                 | 변경(축소) |

> **임계 판정의 위치:** 펌웨어는 `temperature_c`/`humidity_percent` 원값과 `metric_status`(센서 물리/타당성 범위 기준 `normal`/`out_of_range`)만 보고한다. 고온/저온/고습/저습 경보와 SMS 트리거는 **서버 `config.php` 의 `METRIC_*` 임계**가 담당한다. 폐쇄망에서 임계값을 조정할 때 펌웨어 재플래시가 불필요하도록 의도된 분리다.

자세한 함수 단위 매핑은 [`docs/PORTING_NOTES.md`](docs/PORTING_NOTES.md) 참고.

---

## 4. 빠른 시작 (빌드/플래시)

> 폐쇄망 전체 절차는 [`docs/BUILD_OFFLINE.md`](docs/BUILD_OFFLINE.md). 핀맵/배선은 [`docs/HARDWARE.md`](docs/HARDWARE.md).

```bash
# 0) 비밀값 준비 (한 번)
cp firmware/common/secrets.h.example firmware/common/secrets.h
#   secrets.h 의 APP_API_KEY 를 서버 php/config.php 의 API_KEY 와 동일하게 채운다.

# 1) 서버 루트 CA 임베드 (한 번)
#   실제 Cafe24 인증서 체인의 루트 CA PEM 으로 certs/server_ca.c 를 생성한다.
#   (certs/server_ca.h 가 SERVER_CA_PEM / SERVER_CA_PEM_LEN 을 extern 으로 선언함)

# 2) 빌드 (sht30_fw 단일 타깃)
cmake -S firmware -B firmware/build -G Ninja \
      -DCMAKE_TOOLCHAIN_FILE=firmware/cmake/arm-none-eabi.cmake
cmake --build firmware/build --target sht30_fw     # → sht30_fw.bin/.hex/.elf

# 3) 플래시 (ST-Link) — 보드 파라미터 없이 sht30_fw 고정
pwsh firmware/scripts/flash.ps1
#   대안: st-flash write firmware/build/sht30_fw.bin 0x08000000
#   대안: openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
#               -c "program sht30_fw.elf verify reset exit"

# 4) 콘솔 확인 (USART3 PD8/PD9, 115200 8N1)
#   부팅 로그 → 링크업/DHCP/SNTP → 첫 startup POST 200 확인
```

> 참고: `cmake/`, `third_party/`, `board_sht30/`, `certs/server_ca.c` 는 별도 작업으로 생성/벤더링된다(아래 디렉터리 맵 참고). 위 명령은 그 산출물이 갖춰진 뒤의 표준 절차다.

---

## 5. `firmware/` 디렉터리 맵

```
firmware/
├─ README.md                  ← (이 문서)
├─ CMakeLists.txt             빌드 루트: sht30_fw 타깃, third_party 통합
├─ cmake/
│   └─ arm-none-eabi.cmake    크로스 툴체인 파일 (gcc 15.2)
├─ scripts/
│   ├─ vendor.*               빌드 머신에서 고정 버전 의존성 벤더링(폐쇄망 준비)
│   ├─ build.ps1              cmake configure + sht30_fw 빌드
│   └─ flash.ps1              sht30_fw 플래싱(보드 파라미터 없이 고정)
├─ common/                    공유 코드/계약 헤더
│   ├─ app_config.h           튜너블: API 호스트/포트/경로, 주기, 재시도, SNTP, 망주소, 워치독, 핀, SHT30 범위
│   ├─ board_config.h         BOARD_DEVICE_ID/LOCATION/SENSOR_ID/SENSOR_NAME (BOARD_SHT30 단일)
│   ├─ secrets.h.example      APP_API_KEY 템플릿 (실 secrets.h 는 운영자가 생성, 미커밋)
│   ├─ bsp.{h,c}              클럭/HAL/LED 초기화 (HSE 8MHz → 168MHz)
│   ├─ net.{h,c}              ETH MAC + LAN8720 RMII + LwIP, DHCP/static
│   ├─ timesync.{h,c}         SNTP → RTC
│   ├─ tls.{h,c}              mbedTLS over LwIP socket (transport_t 제공)
│   ├─ transport.h            바이트 스트림 전송 추상화(connect/send/recv/close)
│   ├─ httpapi.{h,c}          HTTP/1.1 POST 작성 + X-Signature + 상태 파싱  (검증된 코어)
│   ├─ sig.{h,c}              raw-body 서명 sig_raw_body()                  (검증된 코어)
│   ├─ jsonbody.{h,c}         본문 JSON 생성 jb_sht30_event                 (검증된 코어)
│   ├─ sha256_backend.h       SHA-256 추상 인터페이스
│   ├─ sha256_sw.c            자급식 SHA-256 (공개 도메인)                   (검증된 코어)
│   ├─ hexutil.{h,c}          바이트 ↔ 소문자 hex                           (검증된 코어)
│   ├─ sht30.h                SHT30 I2C 드라이버 계약
│   ├─ sht30_convert.{h,c}    SHT30 CRC8/변환 (이식성 순수 로직)             (검증된 코어)
│   ├─ reporter.h             이벤트 보고 헬퍼(본문→서명→POST→재시도) 계약
│   ├─ watchdog.h             IWDG 계약
│   └─ applog.h               UART 로그 계약
├─ board_sht30/               SHT30 보드(sensor_id=2) 진입점 + 드라이버
│   ├─ main.c                 FreeRTOS 태스크 구성 + 부팅
│   ├─ app_sht30.c            주기 측정 → metric_status → 보고
│   └─ sht30.c                HAL I2C 측정
├─ certs/
│   ├─ server_ca.h            SERVER_CA_PEM / SERVER_CA_PEM_LEN extern 선언
│   └─ server_ca.c            실제 Cafe24 루트 CA PEM (운영자가 교체)  ← TODO(hw)
├─ third_party/               벤더링(폐쇄망): CMSIS, STM32F4_HAL, FreeRTOS-Kernel, lwip, mbedtls
├─ test/host/                 호스트 빌드 패리티 테스트 (PHP 기대값 대조)
│   ├─ parity_test.py
│   ├─ php_verify.php
│   └─ reference.py
├─ build/                     CMake/Ninja 산출물 (armgate: 호스트 컴파일 게이트 .o)
└─ docs/
    ├─ HARDWARE.md            SHT30 보드 핀맵/배선/핀 충돌 해소
    ├─ BUILD_OFFLINE.md       폐쇄망 빌드·전달·플래시·롤백 절차
    └─ PORTING_NOTES.md       Python → C 매핑 / 동작 패리티 / 변경점 / TODO(hw)
```

> 위 맵 중 `CMakeLists.txt`, `cmake/`, `scripts/`, `board_sht30/`, `third_party/`, `certs/server_ca.c` 는 별도 작업으로 채워진다. 본 문서·`common/`·`certs/server_ca.h`·`test/`·`build/` 산출물은 이미 존재한다.

---

## 6. 핵심 제약 (반드시 인지)

- **하드웨어 해시 가속기 없음**: STM32F407 에는 CRYP/HASH 블록이 없다(F415/F417 전용). SHA-256/AES 는 **소프트웨어**로 수행한다. RSA-2048 핸드셰이크는 ~1–2s 소요(보고 주기가 길어 허용).
- **하드웨어 RNG 있음**: TLS 엔트로피 소스로 사용한다.
- **SRAM 예산이 최대 리스크**: mbedTLS record buffer 를 `MBEDTLS_SSL_IN/OUT_CONTENT_LEN ≈ 4096` 으로 축소하고, FreeRTOS 태스크 스택을 CCM(64KB)에 배치한다(linker script).
- **폐쇄망**: 빌드·설치·런타임 모두 인터넷 접근 불가를 가정한다. 모든 의존성·인증서·SNTP 대상 IP 까지 사전에 준비한다(자세히 [`docs/BUILD_OFFLINE.md`](docs/BUILD_OFFLINE.md)).
