SHT30 온습도 모니터링 시스템 전체 소스(서버 PHP, STM32 펌웨어, SQL, 테스트). 전체 코드리뷰에서 도출된 보안 하드닝 10건 반영: - 요청 서명 HMAC-SHA256 전환(펌웨어 sig.c/서버 config.php/호스트 패리티 동시) - 재전송 방어 + 기본 API_KEY fail-closed + 디바이스 문자열 정제(api/sensor_data.php) - 오프라인 SMS 중복 발송 경합 제거(cron_heartbeat.php, 원자적 선점) - CSV 수식 주입 방지(monthly_report.php), 감사로그 회전 락(retention_cleanup.php) - 브루트포스 카운터 원자화(login.php), 예시 TOTP 비밀키 무효화, 마이그레이션 멱등화 _backup/(하드코딩 실 비밀값 포함)·config.local.php·런타임 상태는 .gitignore 제외.
175 lines
13 KiB
Markdown
175 lines
13 KiB
Markdown
# 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)).
|