Files
wadamesh/test/test_sntp_clock_push.cpp
Kaj SchittecatandClaude Opus 5.5 1ee32105de touch(clock): Central Africa and 24 more zones, GPS wake no longer sets a stale time, every SNTP sync reaches the clock
Discord (gadgeteerza, ThinkNode M9, beta_100): no Central Africa zone, the
clock often half an hour behind on a Custom UTC offset, and "Sync clock from
system" with Wi-Fi on not always setting it.

Zones: the saved setting is an index into the zone table, and Custom used to
be "one past the list", so appending zones would have moved it. Custom now
keeps its id (16) forever and the new zones (Central/South/East/West Africa,
Egypt, Eastern/Western Europe, Moscow, Turkey, Arabia, Iran, Gulf, Pakistan,
Nepal, Bangladesh, Indochina, Singapore, Perth, Korea, Adelaide, Brisbane,
New Zealand, Argentina, Mexico City, Newfoundland) sit after it. The picker
shows its own west-to-east order and opens on the current zone.

Half an hour behind: not the offset. WadaNmeaLocationProvider::begin() kept
the parser's pre-rest sentence, so after a GPS saver rest the first loop pass
saw a "valid" fix, the 30-minute re-sync was due, and the clock was set to
the moment the receiver went to rest. begin() now clears the parser, and
UITask::setGpsPower(true) calls syncTime() so the core provider (V4 and
others) gets the same. test_gps_time_sync runs the real provider against a
receiver sending true UTC: the pre-fix provider set the clock 1801 s behind
after a 30-minute rest; now every set is within 1 s.

Sync from system: on the M9 the system clock is the device clock, so the
button copied a clock onto itself. And the Wi-Fi loop wrote only the FIRST
SNTP result into the mesh clock: SNTP's hourly re-syncs set the system clock
only, the PCF8563 kept its old value, and the hourly chip resync (#583)
pulled the clock back to it. Every completed sync is now written through
(SntpClockPush.h, test_sntp_clock_push), and the button with Wi-Fi up
restarts SNTP and says "Clock synced" when the answer lands.

test_time_zones: the legacy ids keep their zones, every zone's offset in
January/July/October, Egypt's DST edges, and all 65536 saved
(tz_zone, time_offs) byte pairs through the exact prefs path to localtime().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:19:39 +01:00

97 lines
4.9 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
//
// Host test for src/helpers/esp32/SntpClockPush.h (Discord, ThinkNode M9: "sync clock from
// system with Wi-Fi on does not always set it"). Simulates the Wi-Fi loop of src/main.cpp:
// SNTP results arrive once at association and then every hour; a clock with an RTC chip
// (the M9's PCF8563) pulls the system clock back to the chip once an hour (#583); the chip is
// written only when the mesh clock is set. Checks that every SNTP result now reaches the mesh
// clock (the old loop pushed the first one only), so the hourly chip resync can no longer
// undo a correction, and that the "Sync clock from system" request restarts SNTP and reports.
//
// c++ -std=c++17 -Wall -Wextra -I src test/test_sntp_clock_push.cpp -o /tmp/t && /tmp/t
#include <assert.h>
#include <stdio.h>
#include "helpers/esp32/SntpClockPush.h"
static const uint32_t MIN_VALID = 1772323200u, MAX_OK = 1893456000u;
// The M9 clock stack, as far as this question goes. All three clocks run at the same rate;
// what differs is who writes which.
struct M9Clock {
long truth = 0; // real UTC
long sys = 0; // ESP32 system clock (the UI shows this; SNTP sets it)
long chip = 0; // PCF8563 (written by rtc_clock.setCurrentTime only)
void advance(long dt) { truth += dt; sys += dt; chip += dt; }
void meshSet(long t) { sys = t; chip = t; } // HardwareRtcClock::setCurrentTime
void sntpSet() { sys = truth; } // lwIP SNTP: settimeofday only
void hourlyChipResync() { sys = chip; } // HardwareRtcClock::resyncFromChip (|delta| >= 2 s)
};
// One association, `hours` long: an SNTP result at the top of every hour, the chip resync at
// half past. In hour `stale_at_h`, ten minutes in, a stale set (the pre-fix GPS wake) moves
// the mesh clock 30 min back. Returns how far behind the clock ends, in seconds, right after
// the final chip resync. `old_loop` = the pre-fix rule (push the first result only).
static long simulate(bool old_loop, int hours, int stale_at_h) {
M9Clock c;
c.truth = 1793017200; // 2026-10-26 12:20Z
c.meshSet(c.truth - 3000); // came up 50 min behind (stale floor after a power cut)
SntpClockPush p;
bool old_pushed = false;
const bool kicked = p.wantKick();
assert(kicked);
for (int h = 0; h <= hours; ++h) {
c.sntpSet(); // SNTP result
if (old_loop) { if (!old_pushed) { c.meshSet(c.sys); old_pushed = true; } }
else if (p.shouldPush(true, (uint32_t)c.sys, MIN_VALID, MAX_OK, nullptr)) c.meshSet(c.sys);
c.advance(600);
if (h == stale_at_h) c.meshSet(c.truth - 1800);
c.advance(1200);
c.hourlyChipResync();
if (h < hours) c.advance(1800);
}
return c.truth - c.sys;
}
int main() {
// ---- the state machine ----
{
SntpClockPush p;
bool report = true;
assert(!p.shouldPush(true, MIN_VALID + 10, MIN_VALID, MAX_OK, &report) && !report); // not started
assert(p.wantKick() && !p.wantKick()); // one start per association
assert(!p.shouldPush(false, MIN_VALID + 10, MIN_VALID, MAX_OK, &report)); // no result yet
assert(!p.shouldPush(true, MIN_VALID, MIN_VALID, MAX_OK, &report)); // the unset seed: never
assert(!p.shouldPush(true, MAX_OK + 1, MIN_VALID, MAX_OK, &report)); // garbage future: never
assert(p.shouldPush(true, MIN_VALID + 10, MIN_VALID, MAX_OK, &report) && !report); // first result
assert(p.shouldPush(true, MIN_VALID + 3610, MIN_VALID, MAX_OK, &report) && !report); // hourly: again
p.linkDown(); // after a result: SNTP keeps going
assert(!p.wantKick());
// The user's "Sync clock from system": restart now, and the next result is reported.
p.rekick = true;
assert(p.wantKick() && !p.wantKick());
assert(!p.shouldPush(false, MIN_VALID + 3700, MIN_VALID, MAX_OK, &report) && !report);
assert(p.shouldPush(true, MIN_VALID + 3720, MIN_VALID, MAX_OK, &report) && report);
assert(p.shouldPush(true, MIN_VALID + 7300, MIN_VALID, MAX_OK, &report) && !report); // once
}
{
SntpClockPush p;
assert(p.wantKick());
p.linkDown(); // dropped before any result: start again next time
assert(p.wantKick());
}
// ---- the M9 timeline ----
// Pre-fix loop: the first result is pushed, a later stale set puts the chip 30 min behind,
// and every hour the chip resync pulls the SNTP-corrected system clock back to it.
const long old_lag = simulate(true, 6, 2);
// Now: the next hourly result writes through to the chip, so the clock ends on time.
const long new_lag = simulate(false, 6, 2);
printf(" M9, stale set in hour 2, 6 h on Wi-Fi: pre-fix loop ends %ld s behind, now %ld s\n", old_lag, new_lag);
assert(old_lag == 1800);
assert(new_lag == 0);
assert(simulate(false, 1, -1) == 0 && simulate(true, 1, -1) == 0); // no stale set: both fine
printf("test_sntp_clock_push: all passed\n");
return 0;
}