#include #include #include #include #include "ql_rtos.h" #include "ql_func.h" #include "ql_nw.h" #include "ql_rtc.h" #include "app_nitz_test.h" #include "app_sys_time_probe.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 #define SAMPLE_MS 3000U static ql_task_t test_task; static int test_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-NITZ-TEST] 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-NITZ-TEST] rejected stage=parse reason=%d\r\n", parsed); return -1; } /* A long blocking query makes the timestamp's age ambiguous. */ if (sample->ticks - began > 400U) return -1; printf("[APP-NITZ-TEST] 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-NITZ-TEST] 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-NITZ-TEST] 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-NITZ-TEST] 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-NITZ-TEST] 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-NITZ-TEST] FAIL stage=app_clock_verification\r\n"); return -1; } printf("[APP-NITZ-TEST] PASS source=nitz app_utc=verified " "wss_requires_separate_handshake_check=1\r\n"); return 0; } static void nitz_test_worker(void *arg) { nitz_sample first, second; int attempt; long long advance, elapsed, drift; unsigned int span; (void)arg; app_sys_time_probe_run(); printf("[APP-NITZ-TEST] begin version=2 source=operator_nitz ntp=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-NITZ-TEST] FAIL stage=sdk_api_unavailable\r\n"); goto done; } for (attempt = 1; attempt <= ATTEMPTS; ++attempt) { if (get_sample(attempt, 1, &first) == 0) { ql_rtos_task_sleep_ms(SAMPLE_MS); if (get_sample(attempt, 2, &second) == 0) { span = second.ticks - first.ticks; /* unsigned handles tick wrap */ elapsed = span / 200U; advance = (long long)second.utc - first.utc; drift = advance - elapsed; printf("[APP-NITZ-TEST] freshness advance_s=%lld elapsed_s=%lld " "drift_s=%lld\r\n", advance, elapsed, drift); if (span >= 400U && span <= 2000U && advance > 0 && drift >= -1 && drift <= 1 && first.zone_quarters == second.zone_quarters) { write_and_verify(&second); goto done; /* Never repeat an uncertain RTC write. */ } printf("[APP-NITZ-TEST] rejected stage=freshness " "cached_or_inconsistent_nitz=1 rtc_not_written=1\r\n"); } } if (attempt < ATTEMPTS) ql_rtos_task_sleep_ms(5000); } printf("[APP-NITZ-TEST] FAIL stage=no_fresh_nitz rtc_not_written=1\r\n"); done: printf("[APP-NITZ-TEST] finished one_test_per_boot=1\r\n"); ql_rtos_task_delete(NULL); } void app_nitz_test_start(void) { int rc; if (test_started) return; /* PDP management is the sole caller. */ test_started = 1; rc = ql_rtos_task_create(&test_task, 6 * 1024, 110, "app_nitz_test", nitz_test_worker, NULL); if (rc != 0) { test_started = 0; printf("[APP-NITZ-TEST] FAIL stage=create_task rc=%d\r\n", rc); } }