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 제외.
13 KiB
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-gcc15.2 + CMake + Ninja - 배포: 폐쇄망(air-gapped) — 모든 서드파티 의존성은
third_party/에 벤더링, 빌드·런타임에 외부 다운로드 0건
상세 이식 근거는 ../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(BOARD_SHT30 단일). 공통 튜너블은 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 참고.
4. 빠른 시작 (빌드/플래시)
폐쇄망 전체 절차는
docs/BUILD_OFFLINE.md. 핀맵/배선은docs/HARDWARE.md.
# 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).