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
PORTING_NOTES.md — Raspberry Pi(Python) → STM32F407(C/FreeRTOS) 이식 노트
원본 동작 기준: RPi sht30_monitor.py(SHT30 온습도, sensor_id=2) — 현재 저장소에는 없음; 동작만 이식.
근거 계획: docs/stm32f407_migration_plan.md.
핵심 원칙: 서버측 PHP API 는 변경하지 않는다. 펌웨어가 기존 요청 포맷/서명 규약을 그대로 재현하여
서버 무중단으로 RPi → STM32 전환을 달성한다(서버는 X-Signature 와 레거시 본문 서명을 이중 수용).
1. Python 함수 → C 펌웨어 모듈 매핑
1.1 SHT30 보드 (sht30_monitor.py)
| Python (sht30_monitor.py) | C 펌웨어 | 비고 |
|---|---|---|
read_sht30() (smbus2) |
sht30.h sht30_measure(*t, *rh) |
HAL I2C 로 0x2C06 → 20ms → 6B read |
crc8_sht3x(values) |
sht30_convert.h sht30_crc8() |
동일 알고리즘(poly 0x31, init 0xFF) |
| (raw → °C/% 변환, round 2) | sht30_parse(frame, *t, *rh) |
T=-45+175*raw/65535, RH=100*raw/65535. 소수 2자리는 jb_sht30_event 직렬화에서 |
classify_reading() |
(reporter 호출부) metric_status 계산 |
plausibility 범위는 APP_SHT30_TEMP/RH_LOW/HIGH. 경보 임계는 서버(§4) |
make_signature(data) |
sig.h sig_raw_body(key, body, len, hex) |
raw-body 서명 — float 서명 패리티 문제 원천 제거(§3) |
post_reading(reading, event) |
reporter.h report_sht30_event(event, t, rh, status) |
내부 본문 생성→서명→TLS POST→재시도 |
| (본문 dict 생성) | jsonbody.h jb_sht30_event(...) |
device_id/location/sensor_id/name/event_type/timestamp + metric_type="sht30", temperature_c, humidity_percent, metric_status, app_version |
| (requests.post + 재시도 루프) | httpapi.h api_post_once() + reporter 재시도 |
TLS 전송은 transport.h/tls.h |
run_loop() (while + sleep) |
Task_Report (board_sht30/main.c, app_sht30.c) |
주기 APP_SHT30_REPORT_INTERVAL_SEC(300s), 첫 성공=startup |
main() 시작 시 startup POST |
board_sht30/main.c 부팅 시퀀스 |
부팅 후 1회 report_sht30_event("startup", ...) |
read_once() / --read-once/--once CLI |
(해당 없음) | 베어메탈에는 CLI 없음. UART 명령은 옵션(미구현) |
setup_logger() |
applog.h applog_init() / LOGI/LOGW/LOGE |
파일 로테이션 → UART 콘솔만 |
env 설정(env_*) / config.py import |
app_config.h + board_config.h + secrets.h |
컴파일 타임 고정(런타임 env 없음) |
SimGPIO / SIMULATION |
(해당 없음) | 실제 하드웨어 타깃. 순수 로직은 test/host/ 게이트로 검증 |
1.2 인프라/공통 매핑
| RPi/OS 기능 | STM32 | 모듈 |
|---|---|---|
| WiFi | 유선 Ethernet(LAN8720 RMII) + LwIP | net.h |
| OS NTP | SNTP(LwIP) → RTC | timesync.h |
OpenSSL(requests) |
mbedTLS(TLS1.2, ECDHE-RSA+AES-GCM, 소프트웨어 SHA/AES) | tls.h |
| Python threads | FreeRTOS tasks/queue/timer (네이티브 API) | board_sht30/main.c |
| systemd 재시작 | IWDG 워치독 + 재연결 백오프 | watchdog.h |
| 로테이팅 파일 로그 | UART 콘솔 로그 | applog.h |
| 클럭/부팅 | HAL_Init + SystemClock(168MHz) + LED | bsp.h |
2. 동작 패리티 (값/타이밍/이벤트)
서버가 보는 동작은 RPi 와 동일하게 유지한다. 주요 상수는 app_config.h:
| 항목 | RPi 값 | STM32 매크로 | 일치 |
|---|---|---|---|
| 앱 버전(보고) | v2605-sht30 |
APP_VERSION = "v2606-sht30" |
의도적 갱신(STM32 식별) |
| SHT30 주기 | 300s (최소 30s) | APP_SHT30_REPORT_INTERVAL_SEC = 300 |
✔ |
| HTTP 타임아웃 | 10s | APP_HTTP_TIMEOUT_MS = 10000 |
✔ |
| HTTP 재시도 | 3회 | APP_HTTP_RETRY_COUNT = 3 |
✔ |
| 재시도 간격 | 5s | APP_HTTP_RETRY_DELAY_MS = 5000 |
✔ |
| SHT30 주소 | 0x44 | APP_SHT30_I2C_ADDR = 0x44 |
✔ |
| SHT30 온도 plausibility | -40~125°C | APP_SHT30_TEMP_LOW/HIGH_C |
✔ |
| SHT30 습도 plausibility | 0~100% | APP_SHT30_RH_LOW/HIGH |
✔ |
이벤트 종류 (서버 event_type, 변경 없음)
| 이벤트 | 트리거 | RPi | STM32 |
|---|---|---|---|
startup |
부팅 직후 1회 | main() |
board_sht30/main.c 부팅 시퀀스 |
periodic |
주기 만료(300s) | run_loop |
Task_Report |
SHT30 시나리오: startup → (300s 마다) periodic 측정 보고. 서버가 매 보고의 temperature_c/humidity_percent 를
config.php 의 METRIC_* 임계와 비교해 고온/저온/고습/저습 시 SMS 를 발송한다(§4).
본문 필드 패리티 (서버가 받는 JSON)
- SHT30:
device_id, device_location, sensor_id, sensor_name, event_type, timestamp+metric_type="sht30", temperature_c, humidity_percent, metric_status, app_version(jb_sht30_event). - 식별 헤더: RPi 는
X-Device-Id/User-Agent를 보냈다. 펌웨어는httpapi.hhttp_build_post()가X-Device-Id와X-Signature를 넣는다(서버 동작에 영향 없음).
3. 무엇이 바뀌었나 (의도적 변경)
| 변경 | RPi | STM32 | 이유 |
|---|---|---|---|
| 네트워크 | WiFi | 유선 Ethernet(LAN8720 RMII)+LwIP | 베어메탈, 유선 신뢰성 |
| HTTP 클라이언트 | requests(OpenSSL) |
mbedTLS + httpapi.c |
온칩 TLS 스택 |
| TLS 해시/암호 | OS OpenSSL | 소프트웨어(SHA-256/AES) | F407 에 HW 해시 가속기 없음. RSA-2048 핸드셰이크 ~1–2s 허용 |
| 동시성 | Python threads + Lock | FreeRTOS tasks/queue/timer (네이티브 API) | RTOS. 태스크 스택은 CCM(64KB) 배치 |
| 시간원 | OS NTP | SNTP→RTC (TLS 전에 수행) | UDP SNTP 는 TLS 불필요 → 닭-달걀 회피 |
| 서명 방식 | sha256(API_KEY + json.dumps(sort_keys, ensure_ascii=False, separators=(',',':'))) (본문에 signature 필드) |
sha256(API_KEY ‖ raw_body) → X-Signature 헤더 |
(중요) 키 정렬/float 포맷/슬래시 이스케이프 재현 불필요. SHT30 float 서명 패리티 문제 원천 제거(R1). 서버는 verify_signature_raw + 레거시 폴백 이중 모드 |
| 본문 필드(wire) | is_leak 포함(누수 시스템 잔재) |
is_leak 필드 제거 |
단일 SHT30 보드에 무의미. jb_sht30_event 가 더 이상 직렬화하지 않으며, 서버 sensor_data.php 도 is_leak 처리 제거 |
| 임계 판정/SMS | (서버측) | 서버 config.php METRIC_ 에서 판정* |
폐쇄망 재플래시 비용 회피. 펌웨어는 원값 + metric_status 만 보고(§4) |
| 설정 | env / config.py |
컴파일 타임 app_config.h/board_config.h + secrets.h |
런타임 env 없음(옵션: flash 설정/UART 명령 미구현) |
| 프로세스 관리 | systemd | IWDG(20s) + 재연결 백오프 | 베어메탈 |
| 로깅 | 로테이팅 파일 로그 | UART 콘솔만 | 파일시스템 없음 |
| TLS 연결 수명 | requests 가 관리 | 매 보고 connect→handshake→POST→close | RAM 절약(R2). 보고 주기 길어 성능 무관 |
서명 변경의 의미 (가장 중요한 단순화)
RPi 의 make_signature() 는 본문을 정규화(sort_keys, no-space, non-ascii 유지) 한 뒤 서명하고, 그 값을
본문 signature 필드에 넣었다. 펌웨어는 자신이 실제 전송하는 본문 바이트 그대로 서명만 하면 된다:
/* 펌웨어 절차 (httpapi.c::api_post_once 가 내부 수행) */
sig_raw_body(APP_API_KEY, body, strlen(body), sig_hex); /* sha256(key || body) */
/* HTTP 헤더에 "X-Signature: <sig_hex>", 본문은 body 바이트 그대로 전송 */
유일한 불변식: 서명한 body 바이트 == 실제 전송 body 바이트 (100% 동일).
문자열 필드에 "/\ 가 들어가면 JSON 이스케이프만 적용한다(서버는 그 바이트로 검증하므로 PHP json_encode
포맷 재현은 불필요). jb_sht30_event 직렬화가 이 불변식을 보장한다.
RPi 무중단 공존:
X-Signature헤더가 없으면 서버가 레거시 본문 필드 서명으로 폴백하므로, 같은 사이트에 RPi 와 STM32 가 공존 가능(롤백/점진 전환 안전).
4. 임계 판정 / SMS 흐름 (펌웨어 측정 → 서버 경보)
SHT30 측정에서 SMS 발송까지의 책임 분담:
- 펌웨어(측정·보고): 300s 마다 SHT30 을 읽어
temperature_c/humidity_percent원값(소수 2자리)을 계산하고, 센서 물리/타당성 범위(APP_SHT30_TEMP/RH_LOW/HIGH) 기준으로metric_status(normal/out_of_range)만 부여해sensor_data.php로 POST 한다. 펌웨어는 고온/저온/고습/저습 같은 운영 임계를 판정하지 않는다. - 서버(임계·SMS):
sensor_data.php가 본문을 DB 에 적재하고,config.php의METRIC_*임계와 비교해 고온/저온/고습/저습 조건이면 SMS 게이트웨이로 문자를 발송한다.
이 분리의 이유: 폐쇄망에서는 펌웨어 재플래시 비용이 크다. 임계값을 서버
config.php에 두면 현장에서 보드를 다시 굽지 않고도 경보 기준을 조정할 수 있다. 따라서 펌웨어APP_SHT30_*범위는 물리 타당성 한계로만 쓰고, 운영 경보 임계는 서버가 소유한다.
5. 열린 하드웨어/운영 결정 항목 — TODO(hw)
코드는 합리적 기본값으로 컴파일되지만, 아래는 사람이 사이트/보드에 맞춰 확정해야 한다. 핀 상세는
HARDWARE.md, 빌드/인증서/비밀값은BUILD_OFFLINE.md.
| # | TODO(hw) | 기본값/현황 | 결정 필요 |
|---|---|---|---|
| 1 | PA2 핀 충돌 (USART2_TX ↔ ETH_MDIO) | ✅ 코드 해소: 로그 UART = USART3(PD8/PD9, AF7) (applog.c/applog.h/app_config.h 반영) |
(HW만 남음) ST-Link VCP 불가 → 외부 3.3V USB-UART 어댑터를 PD8/PD9 에 연결 (HARDWARE.md §0) |
| 2 | 서버 호스트명/경로 | APP_API_HOST="your-domain.example", APP_API_PATH="/raspi_leck_detecter/api/sensor_data.php" |
실제 Cafe24 도메인/경로로 정정 |
| 3 | 서버 루트 CA PEM | certs/server_ca.c 자리표시자(헤더만 실제값) |
openssl s_client 로 체인 확인 후 실제 루트 CA 임베드(R3). LEN=sizeof(NUL 포함) |
| 4 | APP_API_KEY |
secrets.h.example = "change-this-api-key" |
서버 php/config.php 의 API_KEY 와 동일하게 secrets.h 작성(미커밋) |
| 5 | 네트워크 주소 | DHCP 우선, static fallback 192.168.0.50/24 |
사이트 망에 맞춰 static fallback/DNS 조정 |
| 6 | SNTP 대상 | APP_SNTP_SERVER="pool.ntp.org" |
폐쇄망에서 외부 NTP 도달 불가 → 내부 NTP 서버 IP/호스트로 변경 필수(R4) |
| 7 | RMII 50MHz REF_CLK 소싱 | RMII 모드 코드 기본 | PHY 50MHz 공급 vs 외부 OSC 중 보드 설계 확정(HARDWARE.md §1.1) |
| 8 | LAN8720 nRST 핀 | 미배정(RC 리셋 가정) | 소프트 리셋 원하면 GPIO 1개 배정(예 PE0) |
| 9 | SHT30 ADDR | 0x44 (ADDR→GND) | 모듈 결선 확인(0x45 면 코드 매크로 변경) |
| 10 | 서버 METRIC_* 임계 | 서버 config.php 소유 |
고온/저온/고습/저습 경보 기준을 사이트 운영에 맞춰 설정 |
| 11 | 상태 LED 핀 | PD12(디스커버리 가정) | 커스텀 PCB 면 임의 GPIO |
| 12 | RTC/LSE + VBAT 백업 | LSE 권장, 미실장 시 LSI + 매부팅 SNTP | 정전 시간 보존 필요 여부로 결정(R4) |
| 13 | 전원 급전 | 3.3V 레일 가정 | PoE/12V→3.3V 등 설치 환경 확정 |
| 14 | 벤더링 고정 버전 | third_party/ 미생성 |
CubeF4/FreeRTOS/lwip/mbedtls 버전 핀 + 해시 기록(BUILD_OFFLINE.md §1) |
| 15 | CMake/툴체인/linker | cmake//*.ld 확정 |
CCM 스택 배치 + 크기 최적화 + sht30_fw 타깃 정의 |
6. 검증 게이트 (이식 정확성 확인)
- 호스트 패리티:
firmware/test/host/parity_test.py+php_verify.php로sha256(API_KEY‖body)가 서버verify_signature_raw기대값과 일치하는지 대조(순수 로직: 서명/JSON/CRC/변환은 호스트·ARM 동일 다이제스트). - E2E: 스테이징 PHP 로
startup/periodic(SHT30) 200 수신,temperature_c/humidity_percent/metric_status적재 + 서버 임계 초과 시 SMS 트리거 확인. - 안정성: 24h + 망 단절 회복 + IWDG 동작(상세 BUILD_OFFLINE.md §6, 이식 계획 Phase 8).