This commit is contained in:
david rice
2026-04-27 13:58:09 +01:00
parent a1b66906e9
commit 9c75598728
4 changed files with 604 additions and 56 deletions

View File

@@ -21,7 +21,6 @@ AUTHOR: D. RICE 16/04/2026
import csv as _csv_mod
import html
import json
import subprocess
import time
import sys
import requests
@@ -38,7 +37,6 @@ import vxi11
from dotenv import load_dotenv
import ai_mgmt
import rigol_scope
from csv_preprocessor import (analyze_lp_file, LPMetrics,
HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS)
@@ -420,7 +418,6 @@ except Exception as e:
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
sys.exit(1)
rigol_scope.connect()
# ---------------------------------------------------------------------------
# Scope configuration (identical to mipi_test.py)
@@ -646,7 +643,7 @@ def _fetch_registers(ts: str, iteration: int) -> None:
print(f" REGISTERS: SN65DSI83 error — {e}")
combined["sn65"] = None
# SN65DSI83 post-restart settling poll
# SN65DSI83 post-restart settling poll + register snapshots
try:
resp = requests.get(f"{DEVICE_BASE}/sn65_settling", timeout=10)
resp.raise_for_status()
@@ -656,13 +653,14 @@ def _fetch_registers(ts: str, iteration: int) -> None:
n = settling.get("n_readings", 0)
n_err = settling.get("n_error", 0)
dur = settling.get("duration_s", 0)
# ── csr_e5 error summary ──────────────────────────────────────────
if n_err:
# Print the first and last error readings for quick diagnosis
err_readings = [r for r in settling.get("readings", []) if r.get("any_error")]
times = [r["t_ms"] for r in err_readings]
print(f" SN65 SETTLING: *** {n_err}/{n} readings had csr_e5 errors "
f"over {dur:.1f} s (t={times[0]:.0f}{times[-1]:.0f} ms) ***")
for r in err_readings[:3]: # show up to first 3 error readings
for r in err_readings[:3]:
print(f" t={r['t_ms']:6.1f} ms csr_0a={r['csr_0a']} "
f"csr_e5={r['csr_e5']} "
f"pll={'Y' if r['pll_lock'] else 'N'} "
@@ -672,6 +670,29 @@ def _fetch_registers(ts: str, iteration: int) -> None:
if r.get("clk_det") is False)
print(f" SN65 SETTLING: no csr_e5 errors in {n} readings over {dur:.1f} s"
+ (f" ({clk_false} readings with clk_det=False)" if clk_false else ""))
# ── Register snapshot: print start values and flag any changes ───
snap_start = settling.get("snapshot_start") or {}
changed = settling.get("changed_regs") or {}
if snap_start:
print(f" SN65 REGS (t=0):", end="")
# Print a compact one-liner of key config registers
_key = ["0x0d", "0x10", "0x11", "0x18", "0x19", "0x1a", "0x1b",
"0x3c", "0xe0", "0xe1"]
parts = []
for r in _key:
info = snap_start.get(r, {})
parts.append(f"{info.get('name','?')}={info.get('value','?')}")
print(" " + " ".join(parts))
if changed:
print(f" SN65 REGS CHANGED during settling window ({len(changed)} registers):")
for reg, diff in changed.items():
print(f" {reg} {diff['name']:16s} {diff['start']}{diff['end']}")
elif snap_start:
print(f" SN65 REGS: stable (no register changes between t=0 and t={dur:.1f}s)")
except requests.exceptions.RequestException as e:
print(f" REGISTERS: settling poll fetch failed — {e}")
combined["sn65_settling"] = None
@@ -713,21 +734,11 @@ def dual_capture(iteration: int) -> str:
_configure_for_lp()
_set_timebase(LP_SCALE, LP_POINTS)
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
if rigol_scope.is_connected():
rigol_scope.arm()
if _arm_and_wait(timeout=30):
_save_pass_channels("lp", iteration, ts)
else:
print(" SKIPPING LP SAVE.")
scope.write(":TIMebase:POSition 0") # restore centred for subsequent passes
if rigol_scope.is_connected():
DATA_DIR.mkdir(exist_ok=True)
v18_path = DATA_DIR / f"{ts}_pwr_{iteration:04d}_1v8.csv"
n = rigol_scope.read_waveform_csv(v18_path)
if n:
print(f" SAVED: {v18_path.name} ({n} samples)")
else:
print(" RIGOL CH1: waveform read failed — check connection and probe.")
_restore_hs_config()
# ── Pass 2: HS signal quality ──────────────────────────────────────────
@@ -997,8 +1008,6 @@ def _lp_followup_capture(iteration: int) -> tuple[str, list[str], list[LPMetrics
ts_fu = datetime.now().strftime("%Y%m%d_%H%M%S")
_configure_for_lp()
_set_timebase(LP_SCALE, LP_POINTS)
if rigol_scope.is_connected():
rigol_scope.arm()
if _arm_and_wait(timeout=10):
_save_pass_channels("lp", iteration, ts_fu)
else:
@@ -1520,6 +1529,127 @@ def run_interactive_test() -> None:
f"({len(events)} total suspect(s) assessed)")
# ---------------------------------------------------------------------------
# Continuous capture mode (periodic flicker — no kiosk restart)
# ---------------------------------------------------------------------------
def run_continuous_test() -> None:
"""
Continuous LP capture loop — no kiosk restart between iterations.
Designed for periodic flicker that repeats roughly every second once the
display pipeline has started. The kiosk is started once; the scope
re-arms on the NORMAL LP trigger (VBLANK LP-11 → LP-01 falling edge on
Ch3) after each capture, effectively sampling one random display frame
every ~7 s.
With flicker on ~1/60 frames the expected time to first catch is
~60 × 7 s ≈ 7 minutes of unattended running.
When the LP rule-based detector flags a suspect:
• The LP file already on disk (10 GSa/s, 100 ps/sample) is decoded
directly using single-ended CLK+/DAT0+ thresholds — no extra capture.
• proto_decoder checks the HS-SYNC byte position (misalignment) and the
Lane 0 pixel content (corruption).
• compare_lp_captures() shows byte-level diffs vs the last clean capture.
Press Ctrl+C to stop. No report is written (raw LP/proto CSVs are kept).
"""
import proto_decoder as _pd
print("\n===== CONTINUOUS CAPTURE MODE =====")
print("Kiosk starts once. Scope re-arms on each VBLANK trigger (no restart).")
print("LP-only per iteration; LP bit decode fires directly on LP suspect files.")
print("Press Ctrl+C to stop.\n")
_start_video()
print("Waiting 5 s for display pipeline to stabilise...")
time.sleep(5.0)
iteration = 1
clean_count = 0
flicker_count = 0
last_clean_iter: int | None = None
try:
while True:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
# ── LP capture ──────────────────────────────────────────────────
_configure_for_lp()
_set_timebase(LP_SCALE, LP_POINTS)
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
ok = _arm_and_wait(timeout=5)
scope.write(":TIMebase:POSition 0")
_restore_hs_config()
if not ok:
print(f" [{iteration:04d}] LP trigger timeout — retrying")
time.sleep(0.5)
continue
_save_pass_channels("lp", iteration, ts)
# ── Transfer LP files ────────────────────────────────────────────
try:
ai_mgmt.transfer_csv_files()
except Exception as e:
print(f" [{iteration:04d}] transfer error: {e}")
iteration += 1
continue
# ── LP analysis (quiet) ──────────────────────────────────────────
lp_summaries, suspects = _analyze_lp_files(ts, iteration)
if not suspects:
clean_count += 1
last_clean_iter = iteration
print(f" [{iteration:04d}] clean "
f"({clean_count} clean {flicker_count} flicker)")
iteration += 1
continue
# ── Flicker detected ─────────────────────────────────────────────
flicker_count += 1
_play_alarm()
print(f"\n[{iteration:04d}] *** FLICKER SUSPECT #{flicker_count} ***")
for s in lp_summaries:
print(s)
# ── MIPI bit decode from LP files ────────────────────────────────
# LP files are already local (transferred above). At 10 GSa/s
# (100 ps/sample, ~23 samples/bit at 432 Mbps) they have sufficient
# resolution to decode the HS bit stream directly using single-ended
# CLK+ / DAT0+ thresholds. No separate proto pass needed.
print("\n --- MIPI BIT DECODE (from LP capture) ---")
try:
result = _pd.decode_lp_capture(iteration, DATA_DIR, verbose=True)
anomaly = _pd.analyse_for_anomalies(result)
if anomaly["anomalous"]:
print(f"\n *** BIT-LEVEL ANOMALIES: "
f"{', '.join(anomaly['flags'])} ***")
else:
print(f"\n Bit decode: no structural or content anomalies "
f"(sync OK, packet type OK, pixel content OK)")
if result and last_clean_iter is not None:
print()
_pd.compare_lp_captures(last_clean_iter, iteration, DATA_DIR)
except Exception as e:
print(f" bit decode error: {e}")
print()
iteration += 1
except KeyboardInterrupt:
print("\n\nContinuous test stopped (Ctrl+C).")
_stop_video()
total = clean_count + flicker_count
print(f"\nSummary: {total} iterations — {clean_count} clean, "
f"{flicker_count} flicker suspect(s) caught and decoded.")
# ---------------------------------------------------------------------------
# Menu
# ---------------------------------------------------------------------------
@@ -1531,23 +1661,18 @@ def main_menu() -> None:
print("2. SETUP SCOPE (RUN FIRST)")
print("3. CONFIGURE PSU (DEFAULT 24V / 1.5A)")
print("4. PSU OUTPUT ON/OFF (CH1)")
print("5. START INTERACTIVE FLICKER TEST")
print("6. EXIT")
print("5. START INTERACTIVE FLICKER TEST (kiosk restart per iteration)")
print("6. START CONTINUOUS CAPTURE TEST (no restart; LP bit decode on flicker)")
print("7. EXIT")
choice = input("\nSELECT OPTION (1-6): ").strip()
choice = input("\nSELECT OPTION (1-7): ").strip()
if choice == '1':
print(f"PSU : {psu.ask('*IDN?').strip()}")
print(f"SCOPE: {scope.ask('*IDN?').strip()}")
if rigol_scope.is_connected():
print(f"RIGOL: {rigol_scope.rigol.ask('*IDN?').strip()}")
else:
print("RIGOL: NOT CONNECTED")
elif choice == '2':
setup_scope()
if rigol_scope.is_connected():
rigol_scope.configure()
elif choice == '3':
psu.write('CH1:VOLT 24.0')
@@ -1566,14 +1691,16 @@ def main_menu() -> None:
run_interactive_test()
elif choice == '6':
run_continuous_test()
elif choice == '7':
psu.close()
scope.close()
rigol_scope.disconnect()
print("INSTRUMENTS CLOSED. BYE.")
break
else:
print("INVALID ENTRY. PLEASE CHOOSE 1-6.")
print("INVALID ENTRY. PLEASE CHOOSE 1-7.")
if __name__ == "__main__":