# PORTING_NOTES.md — Raspberry Pi(Python) → STM32F407(C/FreeRTOS) 이식 노트 원본 동작 기준: RPi `sht30_monitor.py`(SHT30 온습도, sensor_id=2) — 현재 저장소에는 없음; 동작만 이식. 근거 계획: [`docs/stm32f407_migration_plan.md`](../../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`](../common/sht30.h) `sht30_measure(*t, *rh)` | HAL I2C 로 `0x2C06` → 20ms → 6B read | | `crc8_sht3x(values)` | [`sht30_convert.h`](../common/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`](../common/sig.h) `sig_raw_body(key, body, len, hex)` | raw-body 서명 — **float 서명 패리티 문제 원천 제거**(§3) | | `post_reading(reading, event)` | [`reporter.h`](../common/reporter.h) `report_sht30_event(event, t, rh, status)` | 내부 본문 생성→서명→TLS POST→재시도 | | (본문 dict 생성) | [`jsonbody.h`](../common/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`](../common/httpapi.h) `api_post_once()` + reporter 재시도 | TLS 전송은 [`transport.h`](../common/transport.h)/[`tls.h`](../common/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`](../common/applog.h) `applog_init()` / `LOGI/LOGW/LOGE` | 파일 로테이션 → UART 콘솔만 | | env 설정(`env_*`) / `config.py` import | [`app_config.h`](../common/app_config.h) + [`board_config.h`](../common/board_config.h) + `secrets.h` | 컴파일 타임 고정(런타임 env 없음) | | `SimGPIO` / SIMULATION | (해당 없음) | 실제 하드웨어 타깃. 순수 로직은 `test/host/` 게이트로 검증 | ### 1.2 인프라/공통 매핑 | RPi/OS 기능 | STM32 | 모듈 | |-------------|-------|------| | WiFi | 유선 Ethernet(LAN8720 RMII) + LwIP | [`net.h`](../common/net.h) | | OS NTP | SNTP(LwIP) → RTC | [`timesync.h`](../common/timesync.h) | | OpenSSL(`requests`) | mbedTLS(TLS1.2, ECDHE-RSA+AES-GCM, 소프트웨어 SHA/AES) | [`tls.h`](../common/tls.h) | | Python threads | FreeRTOS tasks/queue/timer (네이티브 API) | `board_sht30/main.c` | | systemd 재시작 | IWDG 워치독 + 재연결 백오프 | [`watchdog.h`](../common/watchdog.h) | | 로테이팅 파일 로그 | UART 콘솔 로그 | [`applog.h`](../common/applog.h) | | 클럭/부팅 | HAL_Init + SystemClock(168MHz) + LED | [`bsp.h`](../common/bsp.h) | --- ## 2. 동작 패리티 (값/타이밍/이벤트) 서버가 보는 동작은 RPi 와 동일하게 유지한다. 주요 상수는 [`app_config.h`](../common/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`](../common/jsonbody.h)). - 식별 헤더: RPi 는 `X-Device-Id` / `User-Agent` 를 보냈다. 펌웨어는 [`httpapi.h`](../common/httpapi.h) `http_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` 필드에 넣었다. 펌웨어는 **자신이 실제 전송하는 본문 바이트 그대로** 서명만 하면 된다: ```c /* 펌웨어 절차 (httpapi.c::api_post_once 가 내부 수행) */ sig_raw_body(APP_API_KEY, body, strlen(body), sig_hex); /* sha256(key || body) */ /* HTTP 헤더에 "X-Signature: ", 본문은 body 바이트 그대로 전송 */ ``` **유일한 불변식:** 서명한 `body` 바이트 == 실제 전송 `body` 바이트 (100% 동일). 문자열 필드에 `"`/`\` 가 들어가면 JSON 이스케이프만 적용한다(서버는 그 바이트로 검증하므로 PHP `json_encode` 포맷 재현은 불필요). `jb_sht30_event` 직렬화가 이 불변식을 보장한다. > RPi 무중단 공존: `X-Signature` 헤더가 없으면 서버가 레거시 본문 필드 서명으로 폴백하므로, 같은 사이트에 > RPi 와 STM32 가 공존 가능(롤백/점진 전환 안전). --- ## 4. 임계 판정 / SMS 흐름 (펌웨어 측정 → 서버 경보) SHT30 측정에서 SMS 발송까지의 책임 분담: 1. **펌웨어(측정·보고)**: 300s 마다 SHT30 을 읽어 `temperature_c`/`humidity_percent` **원값**(소수 2자리)을 계산하고, 센서 물리/타당성 범위(`APP_SHT30_TEMP/RH_LOW/HIGH`) 기준으로 `metric_status`(`normal`/`out_of_range`)만 부여해 `sensor_data.php` 로 POST 한다. 펌웨어는 **고온/저온/고습/저습 같은 운영 임계를 판정하지 않는다.** 2. **서버(임계·SMS)**: `sensor_data.php` 가 본문을 DB 에 적재하고, `config.php` 의 `METRIC_*` 임계와 비교해 고온/저온/고습/저습 조건이면 **SMS 게이트웨이로 문자**를 발송한다. > **이 분리의 이유:** 폐쇄망에서는 펌웨어 재플래시 비용이 크다. 임계값을 서버 `config.php` 에 두면 현장에서 > 보드를 다시 굽지 않고도 경보 기준을 조정할 수 있다. 따라서 펌웨어 `APP_SHT30_*` 범위는 **물리 타당성** > 한계로만 쓰고, **운영 경보 임계는 서버가 소유**한다. --- ## 5. 열린 하드웨어/운영 결정 항목 — `TODO(hw)` > 코드는 합리적 기본값으로 컴파일되지만, 아래는 사람이 사이트/보드에 맞춰 확정해야 한다. > 핀 상세는 [`HARDWARE.md`](HARDWARE.md), 빌드/인증서/비밀값은 [`BUILD_OFFLINE.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).