uzbek-plus/b7/b7a/b7a-analyze.py
q 8d7721c775 B7: add network time bootstrap and persistent B7C cache
B7A: QMI DMS time vs VNIIFTRI NTP (DMS = UTC - 0.28 s, sigma 1 ms; NITZ = DMS
truncated to 1 s). B7B: a 1970 -> 2026 date -s step is harmless for the modem
stack. B7C: aurora-modem probes DMS/NITZ along the start flow and steps
CLOCK_REALTIME once at the first valid sample (REG registered/attached), never
RTC; background SNTP check (query only) after START OK. 3/3 class-A cold boots
PASS (residual +0.04/+0.36/+0.84 s), cache 14fe453a written (B7C_WRITE_VERIFIED)
and verified by a normal power-on (V1, +0.26 s).

Also publishes the prerequisite R2 (boot-hang / lk eMMC investigation, T4
telnet baseline that B7C builds on) and R3 (autonomous cold boot, NCM loss)
material, and extends tools/publish-sanitize.py to r2/, r3/, b7/.
Binary images, initramfs, busybox and raw logs stay out (see b7/*/SHA256SUMS).
2026-10-02 20:18:25 +03:00

54 lines
4.3 KiB
Python

#!/usr/bin/env python3
# B7A analysis: offsets of DMS / NITZ / NTP(vniiftri 1..3) vs the board's free-running CLOCK_REALTIME (1970-based), then DMS/NITZ - NTP.
import re, sys, datetime, statistics as st
L = open(sys.argv[1]).read().splitlines(); GPS = 315964800
dms, nitz, ntp = [], [], []
for l in L:
m = re.match(r'r(\d) DMS\d up=([\d.]+) t0=([\d.]+) t1=([\d.]+) (\d+)$', l)
if m: r,u,a,b,ms = int(m[1]),float(m[2]),float(m[3]),float(m[4]),int(m[5]); dms.append((r,u,(a+b)/2,(b-a)/2, ms/1000+GPS-(a+b)/2)); continue
m = re.match(r'r(\d) NITZ\d up=([\d.]+) t0=([\d.]+) t1=([\d.]+) (\S+) (\S+) tz=(\d+)', l)
if m:
r,u,a,b = int(m[1]),float(m[2]),float(m[3]),float(m[4])
e = datetime.datetime.fromisoformat(m[5]+'T'+m[6]+'+00:00').timestamp(); nitz.append((r,u,(a+b)/2,(b-a)/2,e-(a+b)/2)); continue
m = re.match(r'r(\d) NTP(\d) up=([\d.]+) t0=([\d.]+) t1=([\d.]+)', l)
if m: cur = (int(m[1]),int(m[2]),float(m[3]),float(m[4])); continue
m = re.match(r'r(\d) NTP(\d) ntpd: reply from \S+: offset:([+-][\d.]+) delay:([\d.]+)', l)
if m: ntp.append((int(m[1]),int(m[2]),float(m[3]),float(m[4])))
# NTP: best (min delay) reply per round/server; local time of the run start t0 approximates sample time (8 s window)
starts = {}
for l in L:
m = re.match(r'r(\d) NTP(\d) up=([\d.]+) t0=([\d.]+) t1=([\d.]+)', l)
if m: starts[(int(m[1]),int(m[2]))] = (float(m[4])+float(m[5]))/2
best = {}
for r,s,o,d in ntp:
if (r,s) not in best or d < best[(r,s)][1]: best[(r,s)] = (o,d)
# linear fit of NTP offset vs local time (all best samples, all servers) -> local clock drift
xs = [starts[k] for k in best]; ys = [best[k][0] for k in best]
mx, my = st.mean(xs), st.mean(ys); slope = sum((x-mx)*(y-my) for x,y in zip(xs,ys))/sum((x-mx)**2 for x in xs)
ntp_at = lambda t: my + slope*(t-mx)
print(f"NTP samples: {len(ntp)} replies, best per round/server: {len(best)}; local clock drift vs NTP: {slope*1e6:+.2f} ppm")
print("\nPer-round NTP best offsets (s, relative to board clock) and server spread:")
print("round ntp1(delay) ntp2(delay) ntp3(delay) spread_ms resid_vs_fit_ms")
for r in sorted({k[0] for k in best}):
row = [best.get((r,s)) for s in (1,2,3)]
os_ = [x[0] for x in row if x]
res = [ (best[(r,s)][0]-ntp_at(starts[(r,s)]))*1e3 for s in (1,2,3) if (r,s) in best]
print(f"r{r} " + " ".join(f"{x[0]:.6f}({x[1]*1e3:5.1f}ms)" if x else "-" for x in row) + f" {(max(os_)-min(os_))*1e3:7.2f} " + " ".join(f"{v:+.2f}" for v in res))
print("\nTable: uptime / DMS / NITZ / ntp1 / ntp2 / ntp3 (UTC at the sample's local midpoint; NTP = board clock + NTP offset at that instant)")
def utc(t): return datetime.datetime.fromtimestamp(t, datetime.timezone.utc).strftime('%H:%M:%S.%f')[:-3]
print("round uptime DMS_UTC NITZ_UTC ntp1 ntp2 ntp3 DMS-NTP_ms qmi_halfwin_ms")
dd = []
for r in sorted({x[0] for x in dms}):
d = [x for x in dms if x[0]==r]; n = [x for x in nitz if x[0]==r]
for x in d:
t = x[2]; diff = (x[4]-ntp_at(t))*1e3; dd.append((diff, x[3]*1e3))
no = [best[(r,s)][0] for s in (1,2,3) if (r,s) in best]
cols = [utc(t+best[(r,s)][0]) if (r,s) in best else '-' for s in (1,2,3)]
print(f"r{r} {x[1]:8.2f} {utc(t+x[4])} {utc(t+n[0][4])[:8] if n else '-':10} {cols[0]} {cols[1]} {cols[2]} {diff:+8.1f} {x[3]*1e3:5.1f}")
nd = [((x[4]-ntp_at(x[2]))*1e3, x[3]*1e3) for x in nitz]
print("\nDMS - NTP (ms): n=%d mean %+.1f median %+.1f min %+.1f max %+.1f stdev %.1f ; max |DMS-NTP| %.1f ms; qmicli half-window max %.1f ms" % (
len(dd), st.mean(v for v,_ in dd), st.median(v for v,_ in dd), min(v for v,_ in dd), max(v for v,_ in dd), st.pstdev([v for v,_ in dd]), max(abs(v) for v,_ in dd), max(h for _,h in dd)))
print("NITZ - NTP (ms, NITZ truncated to 1 s => expected in (-1000, 0]): n=%d min %+.1f max %+.1f" % (len(nd), min(v for v,_ in nd), max(v for v,_ in nd)))
for x in nitz: pass
print("NTP server spread (max-min of best offsets within a round): max %.2f ms" % max((max(best[(r,s)][0] for s in (1,2,3) if (r,s) in best)-min(best[(r,s)][0] for s in (1,2,3) if (r,s) in best))*1e3 for r in {k[0] for k in best}))
print("DMS formula check: Unix UTC = DMS_ms/1000 + 315964800 -> DMS-NTP mean above (0 = formula exact; ~ +18000 ms would mean GPS time without leap correction)")