/* ============================================================================= * 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 #include #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"); } }