/* ============================================================================= * reporter.c - 이벤트 보고 헬퍼 구현 (본문 생성 -> 서명 -> TLS POST + 재시도) * * SHT30 온습도 보드가 사용한다. RPi 의 post_reading() 의 * "본문 구성 -> requests.post -> 실패 시 재시도" 흐름을 그대로 옮긴다. * * 동작 개요(report_sht30_event): * 1) jsonbody.c 로 JSON 본문 생성 (BOARD_* 식별값 + timesync_now() timestamp) * 2) tls_transport_init() 로 TLS 전송(transport_t) 획득 * 3) httpapi.c::api_post_once() 로 1회 POST (서명/연결/송수신/상태파싱/종료 포함) * 4) HTTP 200 이면 성공(0), 아니면 APP_HTTP_RETRY_DELAY_MS 대기 후 재시도 * (총 시도 = 1 + APP_HTTP_RETRY_COUNT) * * 동시성: * 현재 보고는 측정 태스크 단독에서 호출되지만, 향후 다중 호출자에 대비해 * mbedTLS 컨텍스트/LwIP 소켓 세션을 정적 뮤텍스로 직렬화한다(한 번에 하나의 * TLS 세션만 수립). * ===========================================================================*/ #include "reporter.h" #include "app_config.h" /* APP_API_HOST/PORT/PATH, APP_HTTP_*, APP_VERSION */ #include "board_config.h" /* BOARD_DEVICE_ID/LOCATION/SENSOR_ID/SENSOR_NAME */ #include "jsonbody.h" /* jb_sht30_event */ #include "httpapi.h" /* api_post_once */ #include "tls.h" /* tls_transport_init */ #include "timesync.h" /* timesync_now / timesync_is_set */ #include "applog.h" /* LOGI / LOGW / LOGE */ #include "FreeRTOS.h" #include "task.h" /* vTaskDelay, xTaskGetSchedulerState */ #include "semphr.h" /* xSemaphoreCreateMutexStatic 등 */ /* ── 상수 ───────────────────────────────────────────────────────────────── */ #define REPORT_BODY_BUFSZ 512u /* 본문 버퍼 (SHT30 본문에 충분) */ #define HTTP_STATUS_OK 200 /* 성공으로 간주하는 HTTP 상태 코드 */ /* ── TLS 세션 직렬화용 정적 뮤텍스 ───────────────────────────────────────── * 정적 할당(xSemaphoreCreateMutexStatic)을 사용해 힙/초기화 순서 의존성을 * 없앤다. report_init() 호출 없이도 첫 사용 시 lazy 생성한다. */ static StaticSemaphore_t s_tls_mutex_storage; static SemaphoreHandle_t s_tls_mutex = NULL; /* 뮤텍스 lazy 초기화. 스케줄러 시작 전 호출되어도 안전하도록 임계구역으로 * 보호한다(동시 생성 방지). 생성 실패는 NULL 로 남으며, 이후 락 단계에서 * "락 없이 진행"으로 폴백한다. */ static SemaphoreHandle_t reporter_mutex(void) { if (s_tls_mutex == NULL) { taskENTER_CRITICAL(); if (s_tls_mutex == NULL) { s_tls_mutex = xSemaphoreCreateMutexStatic(&s_tls_mutex_storage); } taskEXIT_CRITICAL(); } return s_tls_mutex; } /* 보고 절차 진입: 뮤텍스 확보. 스케줄러가 동작 중일 때만 블로킹 take 한다. * (스케줄러 시작 전 부팅 단계 보고에서는 동시성이 없으므로 락을 건너뛴다.) */ static void reporter_lock(void) { SemaphoreHandle_t m = reporter_mutex(); if (m != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { (void)xSemaphoreTake(m, portMAX_DELAY); } } static void reporter_unlock(void) { SemaphoreHandle_t m = s_tls_mutex; if (m != NULL && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING) { (void)xSemaphoreGive(m); } } /* ── 본문 1개를 재시도 포함하여 POST ─────────────────────────────────────── * body 는 이미 완성된 JSON 본문. what 은 로그용 라벨(예: "sht30/periodic"). * 반환: HTTP 200 이면 0, 그 외/통신 실패는 음수. */ static int report_post_body(const char *what, const char *body) { /* 총 시도 횟수 = 최초 1회 + 재시도 APP_HTTP_RETRY_COUNT (RPi 재시도 루프와 동일) */ const unsigned max_attempts = 1u + APP_HTTP_RETRY_COUNT; int last_rc = -1; reporter_lock(); for (unsigned attempt = 1u; attempt <= max_attempts; ++attempt) { /* 매 시도마다 새 transport 를 바인딩한다(connect 시 핸드셰이크). */ transport_t t; int init_rc = tls_transport_init(&t); if (init_rc != 0) { last_rc = init_rc; LOGE("report %s: TLS init 실패 rc=%d (시도 %u/%u)", what, init_rc, attempt, max_attempts); } else { int code = api_post_once(&t, APP_API_HOST, APP_API_PORT, APP_API_PATH, BOARD_DEVICE_ID, APP_API_KEY, body, APP_HTTP_TIMEOUT_MS); if (code == HTTP_STATUS_OK) { LOGI("report %s: HTTP 200 OK (시도 %u/%u)", what, attempt, max_attempts); reporter_unlock(); return 0; /* 성공 */ } if (code > 0) { /* 서버 응답은 받았으나 비-200 (예: 4xx/5xx) */ last_rc = -code; LOGW("report %s: HTTP %d (시도 %u/%u)", what, code, attempt, max_attempts); } else { /* 전송 계층 오류(연결/송수신/응답없음) */ last_rc = code; LOGW("report %s: 전송 오류 rc=%d (시도 %u/%u)", what, code, attempt, max_attempts); } } /* 마지막 시도가 아니면 대기 후 재시도 */ if (attempt < max_attempts) { vTaskDelay(pdMS_TO_TICKS(APP_HTTP_RETRY_DELAY_MS)); } } reporter_unlock(); LOGE("report %s: 모든 시도 실패 (last rc=%d)", what, last_rc); return (last_rc < 0) ? last_rc : -1; } /* 현재 timestamp 획득. 미동기 시 0 이며 경고를 남기되 그대로 전송한다 * (서버가 수신 시각으로 보정할 수 있도록). */ static uint32_t reporter_timestamp(const char *what) { if (!timesync_is_set()) { LOGW("report %s: 시간 미동기(timestamp=0) 상태로 전송", what); } return timesync_now(); } /* ── 공개 API ───────────────────────────────────────────────────────────── */ int report_sht30_event(const char *event_type, double temp_c, double rh, const char *metric_status) { char body[REPORT_BODY_BUFSZ]; uint32_t ts = reporter_timestamp("sht30"); /* SHT30 본문에는 app_version(APP_VERSION) 을 포함한다. */ int n = jb_sht30_event(body, sizeof(body), BOARD_DEVICE_ID, BOARD_DEVICE_LOCATION, BOARD_SENSOR_ID, BOARD_SENSOR_NAME, event_type, ts, temp_c, rh, metric_status, APP_VERSION); if (n < 0) { LOGE("report sht30: 본문 버퍼 부족(event=%s)", event_type ? event_type : "?"); return -1; } LOGI("report sht30: event=%s status=%s ts=%lu (%d bytes)", event_type ? event_type : "?", metric_status ? metric_status : "?", (unsigned long)ts, n); return report_post_body(event_type ? event_type : "sht30", body); }