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 제외.
218 lines
9.6 KiB
C
218 lines
9.6 KiB
C
/* =============================================================================
|
|
* app_sht30.c - SHT30 온습도 보드 애플리케이션 태스크
|
|
*
|
|
* RPi sht30_monitor.py 의 동작을 STM32 + FreeRTOS 로 이식한다.
|
|
*
|
|
* - 측정 태스크(sht30_measure_task):
|
|
* net 업 대기 -> SNTP 시간 동기 -> 주기적으로 SHT30 측정 후 서버 보고.
|
|
* sht30_monitor.py 의 first_success 로직과 동일하게, "최초 성공" 측정은
|
|
* event_type="startup", 이후는 "periodic" 으로 보고한다.
|
|
* 온습도가 정상 범위를 벗어나면 metric_status="out_of_range" 로 분류한다.
|
|
* 측정 실패 시 크래시하지 않고 로그만 남긴 뒤 다음 주기에 재시도한다.
|
|
*
|
|
* - 헬스 태스크(health_task):
|
|
* IWDG 워치독을 주기적으로 refresh 하고, 태스크 스택 여유를 로깅한다.
|
|
* (RPi systemd watchdog / 헬스 체크 대응)
|
|
*
|
|
* 두 태스크의 분리 이유:
|
|
* 네트워크 I/O(TLS 핸드셰이크/전송)는 수 초간 블로킹될 수 있으므로, 워치독
|
|
* refresh 를 측정 태스크에 의존시키면 통신 지연이 곧 리셋으로 이어질 수 있다.
|
|
* 따라서 워치독은 독립적인 고우선순위 헬스 태스크에서 갱신한다.
|
|
* ===========================================================================*/
|
|
#include <stdint.h>
|
|
#include <math.h>
|
|
|
|
#include "FreeRTOS.h"
|
|
#include "task.h"
|
|
|
|
#include "app_config.h" /* 주기/범위/버전 + board_config.h + secrets.h */
|
|
#include "applog.h"
|
|
#include "watchdog.h"
|
|
#include "net.h"
|
|
#include "timesync.h"
|
|
#include "sht30.h"
|
|
#include "reporter.h"
|
|
#include "bsp.h"
|
|
|
|
/* ── 내부 타이밍 상수 ─────────────────────────────────────────────────────── */
|
|
|
|
/* net 업 / 시간 동기 대기 타임아웃. 실패해도 무한 재시도(폐쇄망 부팅 대비). */
|
|
#define APP_NET_WAIT_TIMEOUT_MS (APP_NET_DHCP_TIMEOUT_MS + 10000u)
|
|
#define APP_TIMESYNC_TIMEOUT_MS 30000u
|
|
|
|
/* 헬스(워치독) 태스크 주기. 워치독 타임아웃의 1/4 이하로 충분히 여유 있게. */
|
|
#define HEALTH_TICK_MS (APP_WATCHDOG_TIMEOUT_MS / 4u)
|
|
|
|
/* 측정 실패 후 짧은 재시도 간격 (정상 주기와 별도). 센서 글리치 복구용. */
|
|
#define MEASURE_RETRY_MS 5000u
|
|
|
|
/* 스택 여유 로깅 주기 (헬스 틱 N회마다 1번). */
|
|
#define STACK_LOG_EVERY_N_TICKS 12u
|
|
|
|
/* 측정 태스크 핸들 (스택 워터마크 로깅용으로 헬스 태스크가 참조) */
|
|
static TaskHandle_t s_measure_task = NULL;
|
|
|
|
/* ── 헬퍼: 온습도 정상 범위 분류 (sht30_monitor.py metric_status) ─────────── */
|
|
static const char *classify_metric_status(double temp_c, double rh)
|
|
{
|
|
/* NaN/Inf 방어: 비정상 부동소수도 out_of_range 로 취급 */
|
|
if (!isfinite(temp_c) || !isfinite(rh)) {
|
|
return "out_of_range";
|
|
}
|
|
if (temp_c < APP_SHT30_TEMP_LOW_C || temp_c > APP_SHT30_TEMP_HIGH_C) {
|
|
return "out_of_range";
|
|
}
|
|
if (rh < APP_SHT30_RH_LOW || rh > APP_SHT30_RH_HIGH) {
|
|
return "out_of_range";
|
|
}
|
|
return "normal";
|
|
}
|
|
|
|
/* ── 헬퍼: 네트워크 업 + 시간 동기 보장 (무한 재시도) ─────────────────────── */
|
|
static void ensure_network_and_time(void)
|
|
{
|
|
/* 1) 링크 업 + IP 확보까지 대기. 폐쇄망에서 케이블/스위치 지연이 있을 수
|
|
* 있으므로 타임아웃 시 경고만 남기고 계속 재시도한다. */
|
|
while (net_wait_up(APP_NET_WAIT_TIMEOUT_MS) != 0) {
|
|
LOGW("net not up yet, retrying...");
|
|
/* 워치독은 헬스 태스크가 별도로 refresh 하므로 여기서는 단순 대기 */
|
|
vTaskDelay(pdMS_TO_TICKS(2000));
|
|
}
|
|
|
|
char ip[16];
|
|
net_ip_str(ip, sizeof(ip));
|
|
LOGI("network up, ip=%s", ip);
|
|
|
|
/* 2) SNTP 시작 후 최초 동기까지 대기. TLS 인증서 유효기간 검증과 보고
|
|
* timestamp 필드에 정확한 Unix 시간이 필요하므로 동기 완료를 보장한다.
|
|
* (SNTP 는 UDP 라 TLS 불필요 -> TLS 연결보다 먼저 수행) */
|
|
timesync_init();
|
|
while (timesync_wait(APP_TIMESYNC_TIMEOUT_MS) != 0) {
|
|
LOGW("time sync pending, retrying...");
|
|
vTaskDelay(pdMS_TO_TICKS(APP_SNTP_RETRY_DELAY_MS));
|
|
}
|
|
LOGI("time synced, epoch=%lu", (unsigned long)timesync_now());
|
|
}
|
|
|
|
/* ── 측정 태스크 ─────────────────────────────────────────────────────────── */
|
|
/* RPi sht30_monitor.py main loop 대응:
|
|
* while True:
|
|
* reading = read_sht30()
|
|
* if reading is None: 로그 후 continue
|
|
* event_type = "startup" if first_success else "periodic"
|
|
* post_reading(...); first_success = False
|
|
* sleep(REPORT_INTERVAL)
|
|
*/
|
|
static void sht30_measure_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
/* 부팅 시 네트워크/시간 준비 (sht30_monitor.py 의 시작 전 OS NTP 대응) */
|
|
ensure_network_and_time();
|
|
|
|
/* first_success: 첫 성공 측정만 "startup" 으로 보고 (RPi 동일 로직) */
|
|
int first_success = 1;
|
|
|
|
LOGI("sht30 measure task started (interval=%us)",
|
|
(unsigned)APP_SHT30_REPORT_INTERVAL_SEC);
|
|
|
|
for (;;) {
|
|
double temp_c = 0.0;
|
|
double rh = 0.0;
|
|
|
|
int rc = sht30_measure(&temp_c, &rh);
|
|
if (rc != 0) {
|
|
/* 측정 실패: 크래시 금지. 로그 후 짧게 쉬고 다음 주기에 재시도.
|
|
* rc == -1: I2C 통신 오류, rc == -2: CRC 오류 */
|
|
LOGE("sht30_measure failed (rc=%d), retry in %ums",
|
|
rc, (unsigned)MEASURE_RETRY_MS);
|
|
vTaskDelay(pdMS_TO_TICKS(MEASURE_RETRY_MS));
|
|
continue;
|
|
}
|
|
|
|
const char *metric_status = classify_metric_status(temp_c, rh);
|
|
const char *event_type = first_success ? "startup" : "periodic";
|
|
|
|
/* 운영 가시성: 측정값/상태 로깅 (소수 2자리로 표시) */
|
|
LOGI("sht30 %s: T=%.2fC RH=%.2f%% status=%s",
|
|
event_type, temp_c, rh, metric_status);
|
|
|
|
/* 서버 보고. reporter 가 본문 생성 -> 서명 -> TLS POST -> 재시도 처리.
|
|
* 반환 0 성공(HTTP 200), 음수 실패. */
|
|
int prc = report_sht30_event(event_type, temp_c, rh, metric_status);
|
|
if (prc == 0) {
|
|
/* 최초 성공 보고 이후에는 startup -> periodic 으로 전환 */
|
|
if (first_success) {
|
|
first_success = 0;
|
|
}
|
|
bsp_led_toggle(); /* 성공 보고 표시 */
|
|
} else {
|
|
/* 보고 실패: reporter 내부 재시도까지 모두 소진한 경우.
|
|
* 다음 정상 주기에 다시 시도한다. first_success 는 유지하여
|
|
* 최초 보고가 반드시 "startup" 으로 도달하도록 한다. */
|
|
LOGW("report_sht30_event failed (rc=%d), will retry next cycle", prc);
|
|
}
|
|
|
|
/* 정상 보고 주기 대기 (sht30_monitor.py REPORT_INTERVAL). */
|
|
vTaskDelay(pdMS_TO_TICKS((TickType_t)APP_SHT30_REPORT_INTERVAL_SEC * 1000u));
|
|
}
|
|
}
|
|
|
|
/* ── 헬스 태스크 ─────────────────────────────────────────────────────────── */
|
|
/* 워치독 refresh + 주기적 스택 여유 로깅. 네트워크/측정 블로킹과 독립적으로
|
|
* 동작해야 하므로 측정 태스크보다 높은 우선순위로 생성된다(main.c). */
|
|
static void health_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
|
|
uint32_t tick = 0;
|
|
|
|
for (;;) {
|
|
/* IWDG 갱신: 이 호출이 APP_WATCHDOG_TIMEOUT_MS 내에 끊기면 MCU 리셋 */
|
|
watchdog_refresh();
|
|
|
|
/* 주기적 스택 워터마크 로깅 (개발/운영 진단용) */
|
|
if ((tick % STACK_LOG_EVERY_N_TICKS) == 0u) {
|
|
UBaseType_t health_free = uxTaskGetStackHighWaterMark(NULL);
|
|
UBaseType_t measure_free = (s_measure_task != NULL)
|
|
? uxTaskGetStackHighWaterMark(s_measure_task)
|
|
: 0;
|
|
LOGI("health: heap_free=%u stack_min[health]=%u stack_min[measure]=%u",
|
|
(unsigned)xPortGetFreeHeapSize(),
|
|
(unsigned)health_free,
|
|
(unsigned)measure_free);
|
|
}
|
|
|
|
tick++;
|
|
vTaskDelay(pdMS_TO_TICKS(HEALTH_TICK_MS));
|
|
}
|
|
}
|
|
|
|
/* ── 태스크 생성 진입점 (main.c 에서 호출) ───────────────────────────────── */
|
|
/* 스택 크기: TLS 핸드셰이크는 mbedTLS 가 측정 태스크 문맥에서 수행되므로
|
|
* 측정 태스크에 넉넉한 스택을 준다(reporter 가 동일 태스크에서 호출됨). */
|
|
#define MEASURE_TASK_STACK_WORDS 3072u /* 12 KB (word=4B): mbedTLS 핸드셰이크 피크 여유 */
|
|
#define HEALTH_TASK_STACK_WORDS 256u /* 1 KB: 로깅/워치독만 */
|
|
|
|
/* 우선순위: 헬스(워치독) > 측정. 워치독 갱신이 통신 지연에 막히지 않게 한다. */
|
|
#define MEASURE_TASK_PRIO (tskIDLE_PRIORITY + 2)
|
|
#define HEALTH_TASK_PRIO (tskIDLE_PRIORITY + 3)
|
|
|
|
void app_sht30_start(void)
|
|
{
|
|
BaseType_t ok;
|
|
|
|
ok = xTaskCreate(sht30_measure_task, "sht30",
|
|
MEASURE_TASK_STACK_WORDS, NULL,
|
|
MEASURE_TASK_PRIO, &s_measure_task);
|
|
if (ok != pdPASS) {
|
|
bsp_fatal("failed to create sht30 measure task");
|
|
}
|
|
|
|
ok = xTaskCreate(health_task, "health",
|
|
HEALTH_TASK_STACK_WORDS, NULL,
|
|
HEALTH_TASK_PRIO, NULL);
|
|
if (ok != pdPASS) {
|
|
bsp_fatal("failed to create health task");
|
|
}
|
|
}
|