Updated
This commit is contained in:
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user