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 제외.
310 lines
11 KiB
C
310 lines
11 KiB
C
/* =============================================================================
|
|
* timesync.c - SNTP 시간 동기 + RTC 반영 (timesync.h 구현)
|
|
*
|
|
* 동작:
|
|
* - timesync_init(): LwIP apps/sntp 를 POLL 모드로 시작(서버=APP_SNTP_SERVER).
|
|
* - SNTP 응답 수신 시 lwipopts.h 의 SNTP_SET_SYSTEM_TIME(sec) 매크로가
|
|
* timesync_apply_epoch() 를 호출 -> Unix 초를 분해해 HAL_RTC_Set{Time,Date}.
|
|
* - timesync_now(): RTC 를 다시 읽어 Unix 초로 환산. (RTC 미설정 시 0)
|
|
*
|
|
* 시간대: 모든 시간은 UTC 로 다룬다. 서버 timestamp / TLS 인증서 유효기간 검증
|
|
* 모두 UTC 기준. RTC 에도 UTC 를 저장한다.
|
|
*
|
|
* RTC 클럭소스: 기본 LSI(저정밀, 외부 부품 불필요). 보드에 32.768kHz LSE 가
|
|
* 있으면 정밀도가 좋다. TODO(hw): 실제 보드에 맞춰 bsp 에서 RTC 클럭
|
|
* 소스를 LSE 로 전환(아래 RTC 초기화 가드 참고).
|
|
* ===========================================================================*/
|
|
#include "timesync.h"
|
|
#include "app_config.h"
|
|
#include "applog.h"
|
|
|
|
#include "stm32f4xx_hal.h"
|
|
|
|
/* LwIP SNTP 앱 */
|
|
#include "lwip/apps/sntp.h"
|
|
|
|
#include "FreeRTOS.h"
|
|
#include "task.h"
|
|
|
|
#include <string.h>
|
|
|
|
/* ── 내부 상태 ──────────────────────────────────────────────────────────── */
|
|
|
|
/* HAL RTC 핸들. bsp 가 RTC 클럭/도메인을 켠 뒤 여기서 Init 한다. */
|
|
static RTC_HandleTypeDef s_hrtc;
|
|
|
|
/* 한 번이라도 SNTP 동기가 끝났는지(volatile: 콜백/태스크 경합). */
|
|
static volatile int s_is_set = 0;
|
|
|
|
/* RTC 가 HAL_RTC_Init 까지 완료됐는지. */
|
|
static int s_rtc_ready = 0;
|
|
|
|
/* RTC 연도 기준(STM32 RTC 는 2자리 연도 BCD: 0..99). 2000년 기준 사용. */
|
|
#define TS_RTC_YEAR_BASE 2000
|
|
/* RTC 가 표현 가능한 최소 Unix epoch(2000-01-01 00:00:00 UTC). */
|
|
#define TS_EPOCH_2000 946684800UL
|
|
|
|
/* ── 시간 분해/합성 유틸 (gmtime/timegm 대체, UTC 고정) ─────────────────── */
|
|
|
|
static int ts_is_leap(int year)
|
|
{
|
|
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
|
|
}
|
|
|
|
/* Unix 초(UTC) -> 분해 시각. 윤년/월일 정확. */
|
|
typedef struct {
|
|
int year; /* 절대 연도, 예: 2026 */
|
|
int mon; /* 1..12 */
|
|
int mday; /* 1..31 */
|
|
int hour; /* 0..23 */
|
|
int min; /* 0..59 */
|
|
int sec; /* 0..59 */
|
|
int wday; /* 1..7, 월=1 ... 일=7 (HAL RTC_WeekDay 호환) */
|
|
} ts_tm_t;
|
|
|
|
static void ts_epoch_to_tm(uint32_t epoch, ts_tm_t *out)
|
|
{
|
|
static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
|
|
|
|
uint32_t days = epoch / 86400UL;
|
|
uint32_t rem = epoch % 86400UL;
|
|
|
|
out->hour = (int)(rem / 3600UL);
|
|
rem %= 3600UL;
|
|
out->min = (int)(rem / 60UL);
|
|
out->sec = (int)(rem % 60UL);
|
|
|
|
/* 1970-01-01 은 목요일(Thursday). HAL 의 RTC_WEEKDAY: 월=1..일=7.
|
|
* Unix wday(일=0..토=6) -> ISO(월=1..일=7) 로 변환. */
|
|
int unix_wday = (int)((days + 4) % 7); /* 0=Sun..6=Sat */
|
|
out->wday = (unix_wday == 0) ? 7 : unix_wday; /* 1=Mon..7=Sun */
|
|
|
|
int year = 1970;
|
|
while (1) {
|
|
int ydays = ts_is_leap(year) ? 366 : 365;
|
|
if (days >= (uint32_t)ydays) {
|
|
days -= (uint32_t)ydays;
|
|
year++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
out->year = year;
|
|
|
|
int mon = 0;
|
|
while (mon < 12) {
|
|
int dm = mdays[mon];
|
|
if (mon == 1 && ts_is_leap(year)) dm = 29;
|
|
if (days >= (uint32_t)dm) {
|
|
days -= (uint32_t)dm;
|
|
mon++;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
out->mon = mon + 1;
|
|
out->mday = (int)days + 1;
|
|
}
|
|
|
|
/* 분해 시각(UTC) -> Unix 초. */
|
|
static uint32_t ts_tm_to_epoch(int year, int mon, int mday,
|
|
int hour, int min, int sec)
|
|
{
|
|
static const int mdays[12] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
|
|
uint32_t days = 0;
|
|
|
|
for (int y = 1970; y < year; y++) {
|
|
days += ts_is_leap(y) ? 366 : 365;
|
|
}
|
|
for (int m = 0; m < (mon - 1); m++) {
|
|
int dm = mdays[m];
|
|
if (m == 1 && ts_is_leap(year)) dm = 29;
|
|
days += (uint32_t)dm;
|
|
}
|
|
days += (uint32_t)(mday - 1);
|
|
|
|
return days * 86400UL + (uint32_t)hour * 3600UL
|
|
+ (uint32_t)min * 60UL + (uint32_t)sec;
|
|
}
|
|
|
|
/* ── RTC 초기화 ─────────────────────────────────────────────────────────── */
|
|
|
|
/* RTC 페리페럴 초기화(1회). 클럭소스 선택 + 비동기/동기 프리스케일러로 1Hz.
|
|
* 반환 0 성공, 음수 실패. */
|
|
static int ts_rtc_init(void)
|
|
{
|
|
if (s_rtc_ready) {
|
|
return 0;
|
|
}
|
|
|
|
/* RTC 백업도메인 접근/클럭소스 설정.
|
|
* 기본은 LSI(~32kHz, 부품 불필요). LSE 사용 시 아래 가드를 LSE 로 전환.
|
|
* TODO(hw): 보드에 32.768kHz 크리스털이 있으면 LSE 경로를 활성화. */
|
|
RCC_OscInitTypeDef osc = {0};
|
|
RCC_PeriphCLKInitTypeDef pclk = {0};
|
|
|
|
/* 백업도메인 쓰기 보호 해제(PWR 클럭은 bsp 에서 켜졌다고 가정하나, 안전하게 한 번 더). */
|
|
__HAL_RCC_PWR_CLK_ENABLE();
|
|
HAL_PWR_EnableBkUpAccess();
|
|
|
|
#if defined(TS_USE_LSE) /* TODO(hw): LSE 보드면 빌드에 -DTS_USE_LSE 추가 */
|
|
osc.OscillatorType = RCC_OSCILLATORTYPE_LSE;
|
|
osc.PLL.PLLState = RCC_PLL_NONE;
|
|
osc.LSEState = RCC_LSE_ON;
|
|
if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
|
|
return -1;
|
|
}
|
|
pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
|
|
pclk.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
|
|
if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) {
|
|
return -2;
|
|
}
|
|
/* LSE 32768Hz -> async(127)+1, sync(255)+1 => 1Hz */
|
|
const uint32_t async_pre = 127;
|
|
const uint32_t sync_pre = 255;
|
|
#else
|
|
osc.OscillatorType = RCC_OSCILLATORTYPE_LSI;
|
|
osc.PLL.PLLState = RCC_PLL_NONE;
|
|
osc.LSIState = RCC_LSI_ON;
|
|
if (HAL_RCC_OscConfig(&osc) != HAL_OK) {
|
|
return -1;
|
|
}
|
|
pclk.PeriphClockSelection = RCC_PERIPHCLK_RTC;
|
|
pclk.RTCClockSelection = RCC_RTCCLKSOURCE_LSI;
|
|
if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) {
|
|
return -2;
|
|
}
|
|
/* LSI 약 32000Hz -> async(124)+1=125, sync(255)+1=256 => 125*256=32000 -> 1Hz.
|
|
* LSI 는 오차가 크므로 SNTP 가 주기적으로 보정한다(SNTP_UPDATE_DELAY). */
|
|
const uint32_t async_pre = 124;
|
|
const uint32_t sync_pre = 255;
|
|
#endif
|
|
|
|
__HAL_RCC_RTC_ENABLE();
|
|
|
|
s_hrtc.Instance = RTC;
|
|
s_hrtc.Init.HourFormat = RTC_HOURFORMAT_24;
|
|
s_hrtc.Init.AsynchPrediv = async_pre;
|
|
s_hrtc.Init.SynchPrediv = sync_pre;
|
|
s_hrtc.Init.OutPut = RTC_OUTPUT_DISABLE;
|
|
s_hrtc.Init.OutPutPolarity = RTC_OUTPUT_POLARITY_HIGH;
|
|
s_hrtc.Init.OutPutType = RTC_OUTPUT_TYPE_OPENDRAIN;
|
|
|
|
if (HAL_RTC_Init(&s_hrtc) != HAL_OK) {
|
|
return -3;
|
|
}
|
|
|
|
s_rtc_ready = 1;
|
|
return 0;
|
|
}
|
|
|
|
/* ── 공개 API ───────────────────────────────────────────────────────────── */
|
|
|
|
void timesync_init(void)
|
|
{
|
|
/* RTC 를 먼저 준비(아직 시간은 미설정). SNTP 응답이 와야 실제 시간이 채워진다. */
|
|
if (ts_rtc_init() != 0) {
|
|
LOGE("timesync: RTC init failed (계속: SNTP 동기 시 재시도 안 함)");
|
|
/* RTC 가 없어도 SNTP 자체는 동작하지만, 시간 보존 불가.
|
|
* apply_epoch 에서 다시 init 을 시도한다. */
|
|
}
|
|
|
|
sntp_setoperatingmode(SNTP_OPMODE_POLL);
|
|
sntp_setservername(0, APP_SNTP_SERVER);
|
|
sntp_init();
|
|
|
|
LOGI("timesync: SNTP 시작 server=%s", APP_SNTP_SERVER);
|
|
}
|
|
|
|
void timesync_apply_epoch(uint32_t unix_seconds)
|
|
{
|
|
/* 비정상적으로 작은 값(2000년 이전)은 무시: 잘못된 SNTP 응답 방지. */
|
|
if (unix_seconds < TS_EPOCH_2000) {
|
|
LOGW("timesync: epoch 무시(%lu < 2000) ", (unsigned long)unix_seconds);
|
|
return;
|
|
}
|
|
|
|
/* 콜백 컨텍스트(tcpip 스레드)에서 호출될 수 있음. RTC 미준비 시 1회 시도. */
|
|
if (!s_rtc_ready) {
|
|
if (ts_rtc_init() != 0) {
|
|
/* RTC 사용 불가: 플래그만 세워 timesync_is_set 은 만족시키되,
|
|
* timesync_now 는 RTC 미설정으로 0 을 줄 수 있음. */
|
|
s_is_set = 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
ts_tm_t tm;
|
|
ts_epoch_to_tm(unix_seconds, &tm);
|
|
|
|
RTC_TimeTypeDef t = {0};
|
|
RTC_DateTypeDef d = {0};
|
|
|
|
t.Hours = (uint8_t)tm.hour;
|
|
t.Minutes = (uint8_t)tm.min;
|
|
t.Seconds = (uint8_t)tm.sec;
|
|
t.TimeFormat = RTC_HOURFORMAT12_AM; /* 24h 모드에선 무시됨 */
|
|
t.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
|
|
t.StoreOperation = RTC_STOREOPERATION_RESET;
|
|
|
|
d.WeekDay = (uint8_t)tm.wday; /* RTC_WEEKDAY_MONDAY=1 .. SUNDAY=7 */
|
|
d.Month = (uint8_t)tm.mon; /* 1..12, BCD 변환은 HAL 이 처리(BIN 포맷) */
|
|
d.Date = (uint8_t)tm.mday;
|
|
d.Year = (uint8_t)(tm.year - TS_RTC_YEAR_BASE); /* 0..99 */
|
|
|
|
if (HAL_RTC_SetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK ||
|
|
HAL_RTC_SetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) {
|
|
LOGE("timesync: RTC set 실패");
|
|
/* 그래도 동기됐다고 표시: 최소한 한 번 시간이 들어왔음. */
|
|
s_is_set = 1;
|
|
return;
|
|
}
|
|
|
|
s_is_set = 1;
|
|
LOGI("timesync: 동기 완료 epoch=%lu (%04d-%02d-%02d %02d:%02d:%02d UTC)",
|
|
(unsigned long)unix_seconds,
|
|
tm.year, tm.mon, tm.mday, tm.hour, tm.min, tm.sec);
|
|
}
|
|
|
|
uint32_t timesync_now(void)
|
|
{
|
|
if (!s_is_set || !s_rtc_ready) {
|
|
return 0;
|
|
}
|
|
|
|
/* HAL_RTC_GetTime 을 먼저 호출해야 섀도 레지스터가 잠기고,
|
|
* 이후 GetDate 호출로 잠금이 풀린다(HAL 규약). 순서 준수 필수. */
|
|
RTC_TimeTypeDef t = {0};
|
|
RTC_DateTypeDef d = {0};
|
|
|
|
if (HAL_RTC_GetTime(&s_hrtc, &t, RTC_FORMAT_BIN) != HAL_OK) {
|
|
return 0;
|
|
}
|
|
if (HAL_RTC_GetDate(&s_hrtc, &d, RTC_FORMAT_BIN) != HAL_OK) {
|
|
return 0;
|
|
}
|
|
|
|
int year = TS_RTC_YEAR_BASE + (int)d.Year;
|
|
return ts_tm_to_epoch(year, (int)d.Month, (int)d.Date,
|
|
(int)t.Hours, (int)t.Minutes, (int)t.Seconds);
|
|
}
|
|
|
|
int timesync_is_set(void)
|
|
{
|
|
return s_is_set ? 1 : 0;
|
|
}
|
|
|
|
int timesync_wait(uint32_t timeout_ms)
|
|
{
|
|
const uint32_t step_ms = 100;
|
|
uint32_t waited = 0;
|
|
|
|
while (!s_is_set) {
|
|
if (timeout_ms != 0 && waited >= timeout_ms) {
|
|
return -1;
|
|
}
|
|
vTaskDelay(pdMS_TO_TICKS(step_ms));
|
|
waited += step_ms;
|
|
}
|
|
return 0;
|
|
}
|