#include #include #include #include #include "ql_rtos.h" #include "ql_func.h" #include "ql_nw.h" #include "ql_rtc.h" #include "app_time_calibrate.h" /* SDK wrapper uses the same PMIC timezone as time(); no libc TZ assumptions. */ extern struct tm *ql_localtime_r(const time_t *, struct tm *); #define MIN_EPOCH 1704067200LL #define MAX_EPOCH 2147483647LL #define ATTEMPTS 3 static ql_task_t calibration_task; static int calibration_started; typedef struct { time_t utc; unsigned int ticks; int zone_quarters; } nitz_sample; static int leap(int year) { return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); } /* Explicit Gregorian conversion: no mktime() or APP clock dependency. */ static int calendar_epoch(int year, int month, int day, int hour, int minute, int second, long long *result) { static const int days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; long long total = 0; int y, m; if (year < 1970 || year > 2038 || month < 1 || month > 12 || day < 1 || day > days[month - 1] + (month == 2 && leap(year)) || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) return -1; for (y = 1970; y < year; ++y) total += 365 + leap(y); for (m = 1; m < month; ++m) total += days[m - 1] + (m == 2 && leap(year)); *result = ((total + day - 1) * 24 + hour) * 3600 + minute * 60 + second; return 0; } static int two_digits(const char *s) { if (s[0] < '0' || s[0] > '9' || s[1] < '0' || s[1] > '9') return -1; return (s[0] - '0') * 10 + s[1] - '0'; } /* Accept actual "YY/MM/DD HH:MM:SS 32 0", documented signed format, and compact * "YY/MM/DD,HH:MM:SS+32". NITZ UT is UTC; zone is metadata, not subtracted. * abs_time/leap_sec have insufficient documented semantics to use as epoch. */ static int parse_nitz(const char *s, time_t *epoch, int *zone) { size_t n = strlen(s), pos = 17; int yy, mo, dd, hh, mm, ss, q, dst, negative = 0; size_t start; long long value; if (n < 20 || s[2] != '/' || s[5] != '/' || (s[8] != ' ' && s[8] != ',') || s[11] != ':' || s[14] != ':') return -1; yy = two_digits(s); mo = two_digits(s + 3); dd = two_digits(s + 6); hh = two_digits(s + 9); mm = two_digits(s + 12); ss = two_digits(s + 15); if (yy < 0 || calendar_epoch(2000 + yy, mo, dd, hh, mm, ss, &value) != 0 || value < MIN_EPOCH || value > MAX_EPOCH) return -2; /* Actual SDK output is also "26/10/26 08:00:10 32 0". */ if (s[pos] == ' ') ++pos; else if (s[pos] != '+' && s[pos] != '-') return -3; if (s[pos] == '+' || s[pos] == '-') negative = s[pos++] == '-'; start = pos; q = 0; while (pos < n && s[pos] >= '0' && s[pos] <= '9' && pos - start < 2) q = q * 10 + s[pos++] - '0'; if (pos == start || q > 56 || (negative && q > 48)) return -3; *zone = negative ? -q : q; if (pos < n) { if (s[pos++] != ' ') return -4; start = pos; dst = 0; while (pos < n && s[pos] >= '0' && s[pos] <= '9' && pos - start < 2) dst = dst * 10 + s[pos++] - '0'; if (pos == start || pos != n || dst > 2) return -4; } *epoch = (time_t)value; return 0; } static int get_sample(int attempt, int index, nitz_sample *sample) { QL_NW_NITZ_TIME_INFO_T info; unsigned int began = ql_rtos_get_systicks(); int rc, parsed; memset(&info, 0, sizeof(info)); rc = ql_nw_get_nitz_time_info(&info); sample->ticks = ql_rtos_get_systicks(); printf("[APP-TIME] sample attempt=%d index=%d rc=%d raw=\"%.*s\" " "abs_time=%lu leap_sec=%u query_ms=%lu\r\n", attempt, index, rc, (int)sizeof(info.nitz_time), info.nitz_time, info.abs_time, (unsigned int)info.leap_sec, (unsigned long)(sample->ticks - began) * 5UL); if (rc != QL_NW_SUCCESS || memchr(info.nitz_time, '\0', sizeof(info.nitz_time)) == NULL) return -1; parsed = parse_nitz(info.nitz_time, &sample->utc, &sample->zone_quarters); if (parsed != 0) { printf("[APP-TIME] rejected stage=parse reason=%d\r\n", parsed); return -1; } /* Bound query delay; this does not establish the age of a cached NITZ. */ if (sample->ticks - began > 400U) { printf("[APP-TIME] rejected stage=slow_query rtc_not_written=1\r\n"); return -1; } printf("[APP-TIME] parsed utc=%ld zone_quarters=%d zone_minutes=%d\r\n", (long)sample->utc, sample->zone_quarters, sample->zone_quarters * 15); return 0; } static int write_and_verify(const nitz_sample *reference) { struct tm local; ql_rtc_time_t target, actual; time_t epoch, before = time(NULL), after; long long expected, local_epoch, delta, actual_local = 0; int rc, read_rc, calendar_ok; expected = (long long)reference->utc + (ql_rtos_get_systicks() - reference->ticks) / 200U; if (expected > MAX_EPOCH) { printf("[APP-TIME] FAIL stage=epoch_range\r\n"); return -1; } epoch = (time_t)expected; memset(&local, 0, sizeof(local)); if (ql_localtime_r(&epoch, &local) == NULL || calendar_epoch(local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, local.tm_hour, local.tm_min, local.tm_sec, &local_epoch) != 0 || local_epoch < MIN_EPOCH || local_epoch > MAX_EPOCH || local_epoch - expected < -43200 || local_epoch - expected > 50400) { printf("[APP-TIME] FAIL stage=local_calendar\r\n"); return -1; } memset(&target, 0, sizeof(target)); target.tm_year = local.tm_year + 1900; target.tm_mon = local.tm_mon + 1; target.tm_mday = local.tm_mday; target.tm_hour = local.tm_hour; target.tm_min = local.tm_min; target.tm_sec = local.tm_sec; target.tm_wday = local.tm_wday; printf("[APP-TIME] set_begin app_before=%ld expected_utc=%ld " "pmic_zone_minutes=%ld app_calendar=%04d-%02d-%02dT%02d:%02d:%02d\r\n", (long)before, (long)epoch, (long)((local_epoch - expected) / 60), target.tm_year, target.tm_mon, target.tm_mday, target.tm_hour, target.tm_min, target.tm_sec); rc = ql_rtc_set_time(&target); memset(&actual, 0, sizeof(actual)); read_rc = ql_rtc_get_time(&actual); after = time(NULL); expected = (long long)reference->utc + (ql_rtos_get_systicks() - reference->ticks) / 200U; delta = (long long)after - expected; calendar_ok = calendar_epoch(actual.tm_year, actual.tm_mon, actual.tm_mday, actual.tm_hour, actual.tm_min, actual.tm_sec, &actual_local) == 0; actual_local -= local_epoch + (expected - (long long)epoch); printf("[APP-TIME] readback set_rc=%d rtc_rc=%d app_after=%ld " "expected_utc=%lld delta_s=%lld calendar=%04d-%02d-%02dT%02d:%02d:%02d\r\n", rc, read_rc, (long)after, expected, delta, actual.tm_year, actual.tm_mon, actual.tm_mday, actual.tm_hour, actual.tm_min, actual.tm_sec); if (rc != 0 || read_rc != 0 || after < MIN_EPOCH || delta < -3 || delta > 3 || !calendar_ok || actual_local < -3 || actual_local > 3) { printf("[APP-TIME] FAIL stage=app_clock_verification\r\n"); return -1; } printf("[APP-TIME] PASS source=nitz app_utc=verified " "wss_requires_separate_handshake_check=1\r\n"); return 0; } static void time_calibrate_worker(void *arg) { nitz_sample sample; int attempt; (void)arg; printf("[APP-TIME] begin version=1 source=operator_nitz ntp=off " "policy=first_valid_after_pdp sys_probe=off\r\n"); if (!m_get_api_ptr || !m_get_api_ptr("ql_nw_get_nitz_time_info") || !m_get_api_ptr("ql_localtime_r") || !m_get_api_ptr("ql_rtc_set_time") || !m_get_api_ptr("ql_rtc_get_time")) { printf("[APP-TIME] FAIL stage=sdk_api_unavailable\r\n"); goto done; } /* Use the first valid operator snapshot promptly after * PDP activation. A snapshot need not advance between API reads. Its actual * network reception time is unknown; readback alone cannot establish age. */ for (attempt = 1; attempt <= ATTEMPTS; ++attempt) { if (get_sample(attempt, 1, &sample) == 0) { printf("[APP-TIME] snapshot_selected attempt=%d utc=%ld " "age=unknown first_valid_after_pdp=1 max_rtc_writes=1\r\n", attempt, (long)sample.utc); write_and_verify(&sample); goto done; /* No repeated write, even on setter/readback failure. */ } if (attempt < ATTEMPTS) ql_rtos_task_sleep_ms(5000); } printf("[APP-TIME] FAIL stage=no_valid_nitz rtc_not_written=1\r\n"); done: printf("[APP-TIME] finished once_per_boot=1\r\n"); ql_rtos_task_delete(NULL); } void app_time_calibrate_start(void) { int rc; if (calibration_started) return; /* PDP management is the sole caller. */ calibration_started = 1; rc = ql_rtos_task_create(&calibration_task, 6 * 1024, 110, "app_time_calibrate", time_calibrate_worker, NULL); if (rc != 0) { calibration_started = 0; printf("[APP-TIME] FAIL stage=create_task rc=%d\r\n", rc); } }