2026-05-07 09:01:32 +01:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""
|
|
|
|
|
|
flicker_watch.py — Continuous LP capture during video on/off cycles.
|
|
|
|
|
|
|
|
|
|
|
|
Operator watches the display. Script keeps cycling the video stream on/off
|
|
|
|
|
|
and triggering LP captures in the background. Files accumulate on the scope
|
|
|
|
|
|
without being transferred (fast).
|
|
|
|
|
|
|
|
|
|
|
|
Keys (no Enter needed):
|
|
|
|
|
|
f — flicker observed: transfer + archive + analyse recent captures
|
|
|
|
|
|
g — good baseline: transfer + archive recent captures (no analysis)
|
|
|
|
|
|
q — quit
|
|
|
|
|
|
|
|
|
|
|
|
Captures are organised under data/flicker/{event_ts}/ or data/good/{event_ts}/.
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import select
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import termios
|
|
|
|
|
|
import time
|
|
|
|
|
|
import tty
|
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
import vxi11
|
|
|
|
|
|
|
|
|
|
|
|
import ai_mgmt
|
|
|
|
|
|
from csv_preprocessor import analyze_lp_file
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Config
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
SCOPE_IP = "192.168.45.4"
|
|
|
|
|
|
DEVICE_BASE = "http://192.168.45.8:5000"
|
|
|
|
|
|
VIDEO_URL = f"{DEVICE_BASE}/video"
|
|
|
|
|
|
|
|
|
|
|
|
DATA_DIR = Path(__file__).parent / "data"
|
|
|
|
|
|
FLICKER_DIR = DATA_DIR / "flicker"
|
|
|
|
|
|
GOOD_DIR = DATA_DIR / "good"
|
|
|
|
|
|
|
|
|
|
|
|
# LP capture parameters (matched to mipi_test_interactive.py)
|
|
|
|
|
|
LP_SCALE = 1e-6 # 1 µs/div → 20 µs window
|
|
|
|
|
|
LP_POINTS = 200_000
|
|
|
|
|
|
LP_TRIG_OFFSET = 9e-6 # 1 µs pre / 19 µs post-trigger
|
|
|
|
|
|
LP_V_SCALE = 0.2
|
|
|
|
|
|
LP_V_OFFSET = 0.6
|
|
|
|
|
|
LP_TRIG_LEVEL = 0.6
|
|
|
|
|
|
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# Segmented memory: capture N back-to-back LP triggers per :DIGitize, then
|
|
|
|
|
|
# dump the whole acquisition as a single H5 file. Massively higher coverage
|
|
|
|
|
|
# than single-shot CSV captures.
|
|
|
|
|
|
SEGMENT_COUNT = 100
|
|
|
|
|
|
SAVE_FORMAT = "H5" # Keysight native multi-segment format
|
|
|
|
|
|
|
2026-05-07 09:01:32 +01:00
|
|
|
|
CYCLE_S = 10.0 # seconds video is on per cycle
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# Filling N segments takes ~N × LP-trigger period. LP triggers fire roughly
|
|
|
|
|
|
# at line rate (≈48 kHz) so 100 segments fill in ms, but allow margin.
|
|
|
|
|
|
TRIG_TIMEOUT_S = max(SEGMENT_COUNT * 0.020 + 5.0, 10.0)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Scope setup
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
scope = vxi11.Instrument(SCOPE_IP)
|
|
|
|
|
|
scope.timeout = 30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_scope() -> None:
|
|
|
|
|
|
"""One-shot scope init — channels, math, default trigger."""
|
|
|
|
|
|
print("CONFIGURING SCOPE...")
|
|
|
|
|
|
cmds = [
|
|
|
|
|
|
"*RST", ":RUN", ":STOP",
|
|
|
|
|
|
":CHANnel1:DISPlay ON", ":CHANnel1:INPut DC50", ":CHANnel1:PROBe 19.2",
|
|
|
|
|
|
":CHANnel1:LABel 'CLK+'",
|
|
|
|
|
|
":CHANnel2:DISPlay ON", ":CHANnel2:INPut DC50", ":CHANnel2:PROBe 19.2",
|
|
|
|
|
|
":CHANnel2:LABel 'CLK-'",
|
|
|
|
|
|
":CHANnel3:DISPlay ON", ":CHANnel3:INPut DC50", ":CHANnel3:PROBe 19.2",
|
|
|
|
|
|
":CHANnel3:LABel 'DAT0+'",
|
|
|
|
|
|
":CHANnel4:DISPlay ON", ":CHANnel4:INPut DC50", ":CHANnel4:PROBe 19.2",
|
|
|
|
|
|
":CHANnel4:LABel 'DAT0-'",
|
|
|
|
|
|
":TIMebase:REFerence CENTer",
|
|
|
|
|
|
":TRIGger:MODE EDGE",
|
|
|
|
|
|
":ACQuire:MODE RTIMe", ":ACQuire:INTerpolate ON",
|
|
|
|
|
|
":DISPlay:LAYout STACKED",
|
|
|
|
|
|
]
|
|
|
|
|
|
for c in cmds:
|
|
|
|
|
|
scope.write(c)
|
|
|
|
|
|
time.sleep(0.05)
|
|
|
|
|
|
print("SCOPE READY.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def configure_for_lp() -> None:
|
2026-05-07 12:10:02 +01:00
|
|
|
|
"""LP-mode + segmented memory: N back-to-back LP triggers per acquisition."""
|
2026-05-07 09:01:32 +01:00
|
|
|
|
for ch in (1, 2, 3, 4):
|
|
|
|
|
|
scope.write(f":CHANnel{ch}:SCALe {LP_V_SCALE:.3f}")
|
|
|
|
|
|
scope.write(f":CHANnel{ch}:OFFSet {LP_V_OFFSET:.3f}")
|
|
|
|
|
|
scope.write(":TRIGger:EDGE:SOURce CHANnel3")
|
|
|
|
|
|
scope.write(":TRIGger:EDGE:SLOPe NEGative")
|
|
|
|
|
|
scope.write(f":TRIGger:EDGE:LEVel {LP_TRIG_LEVEL:.3f}")
|
|
|
|
|
|
scope.write(":TRIGger:SWEep NORMal")
|
|
|
|
|
|
scope.write(f":TIMebase:SCALe {LP_SCALE:.3E}")
|
|
|
|
|
|
scope.write(f":ACQuire:POINts {LP_POINTS}")
|
|
|
|
|
|
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# Segmented memory: fill N segments per :DIGitize.
|
|
|
|
|
|
scope.write(":ACQuire:MODE SEGMented")
|
|
|
|
|
|
scope.write(f":ACQuire:SEGMented:COUNt {SEGMENT_COUNT}")
|
|
|
|
|
|
time.sleep(0.5)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def arm_and_wait(timeout_s: float) -> bool:
|
|
|
|
|
|
""":DIGitize + *OPC?. Returns True if trigger fired within timeout."""
|
|
|
|
|
|
global scope
|
|
|
|
|
|
prev = scope.timeout
|
|
|
|
|
|
try:
|
|
|
|
|
|
scope.timeout = timeout_s + 2
|
|
|
|
|
|
scope.write(":DIGitize")
|
|
|
|
|
|
return scope.ask("*OPC?").strip() == "1"
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
# Trigger timed out or scope locked up — reconnect.
|
|
|
|
|
|
try:
|
|
|
|
|
|
scope.close()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
time.sleep(1.0)
|
|
|
|
|
|
scope = vxi11.Instrument(SCOPE_IP)
|
|
|
|
|
|
scope.timeout = 30
|
|
|
|
|
|
try:
|
|
|
|
|
|
scope.write(":STOP")
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
return False
|
|
|
|
|
|
finally:
|
|
|
|
|
|
try:
|
|
|
|
|
|
scope.timeout = prev
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def save_lp(base_name: str) -> None:
|
2026-05-07 12:10:02 +01:00
|
|
|
|
"""Save all N segments of Ch1 (CLK+) and Ch3 (DAT0+) as a single H5 each."""
|
2026-05-07 09:01:32 +01:00
|
|
|
|
base = f"C:\\TEMP\\{base_name}"
|
2026-05-07 12:10:02 +01:00
|
|
|
|
ext = SAVE_FORMAT.lower()
|
|
|
|
|
|
scope.write(f':DISK:SAVE:WAVeform CHANnel1,"{base}_clk.{ext}",{SAVE_FORMAT}')
|
|
|
|
|
|
time.sleep(3.0)
|
|
|
|
|
|
scope.write(f':DISK:SAVE:WAVeform CHANnel3,"{base}_dat.{ext}",{SAVE_FORMAT}')
|
|
|
|
|
|
time.sleep(3.0)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Non-blocking keyboard
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class KeyReader:
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
|
self.fd = sys.stdin.fileno()
|
|
|
|
|
|
self.old = termios.tcgetattr(self.fd)
|
|
|
|
|
|
tty.setcbreak(self.fd)
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def get_key(self) -> str | None:
|
|
|
|
|
|
if select.select([sys.stdin], [], [], 0)[0]:
|
|
|
|
|
|
return sys.stdin.read(1).lower()
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *_):
|
|
|
|
|
|
termios.tcsetattr(self.fd, termios.TCSADRAIN, self.old)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Video control
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def video_start() -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
requests.put(VIDEO_URL,
|
|
|
|
|
|
json={"action": "start", "mode": "static-pink"},
|
|
|
|
|
|
timeout=3)
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
print(f" VIDEO START failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def video_stop() -> None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
requests.put(VIDEO_URL, json={"action": "stop"}, timeout=3)
|
|
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
|
|
|
|
print(f" VIDEO STOP failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# H5 transfer (ai_mgmt only handles CSV — segmented mode produces .h5)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _transfer_h5_files() -> int:
|
|
|
|
|
|
"""SMB-pull every .h5 from the scope share into DATA_DIR; delete on scope."""
|
|
|
|
|
|
from smb.SMBConnection import SMBConnection
|
|
|
|
|
|
import socket
|
|
|
|
|
|
conn = SMBConnection(
|
|
|
|
|
|
ai_mgmt.USERNAME, ai_mgmt.PASSWORD,
|
|
|
|
|
|
socket.gethostname(), ai_mgmt.SERVER_NAME,
|
|
|
|
|
|
use_ntlm_v2=True, is_direct_tcp=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not conn.connect(ai_mgmt.SERVER, 445):
|
|
|
|
|
|
print(" H5 transfer: could not connect to scope share")
|
|
|
|
|
|
return 0
|
|
|
|
|
|
count = 0
|
|
|
|
|
|
try:
|
|
|
|
|
|
h5_paths: list[str] = []
|
|
|
|
|
|
def walk(path: str) -> None:
|
|
|
|
|
|
for entry in conn.listPath(ai_mgmt.SHARE, path):
|
|
|
|
|
|
if entry.filename in (".", ".."):
|
|
|
|
|
|
continue
|
|
|
|
|
|
full = f"{path}/{entry.filename}"
|
|
|
|
|
|
if entry.isDirectory:
|
|
|
|
|
|
walk(full)
|
|
|
|
|
|
elif entry.filename.lower().endswith(".h5"):
|
|
|
|
|
|
h5_paths.append(full)
|
|
|
|
|
|
walk("/")
|
|
|
|
|
|
for remote in h5_paths:
|
|
|
|
|
|
local = DATA_DIR / Path(remote).name
|
|
|
|
|
|
try:
|
|
|
|
|
|
with open(local, "wb") as fh:
|
|
|
|
|
|
conn.retrieveFile(ai_mgmt.SHARE, remote, fh)
|
|
|
|
|
|
conn.deleteFiles(ai_mgmt.SHARE, remote)
|
|
|
|
|
|
count += 1
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" H5 transfer failed for {Path(remote).name}: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
conn.close()
|
|
|
|
|
|
return count
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-07 09:01:32 +01:00
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Register snapshot from device (DSIM PHY + SN65DSI83)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def fetch_registers_snapshot(target_dir: Path, event_ts: str) -> None:
|
|
|
|
|
|
"""GET /registers + /sn65_registers, print key indicators, save JSON."""
|
|
|
|
|
|
combined: dict = {}
|
|
|
|
|
|
for endpoint, key in [("/registers", "dsim"),
|
|
|
|
|
|
("/sn65_registers", "sn65")]:
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = requests.get(f"{DEVICE_BASE}{endpoint}", timeout=5)
|
|
|
|
|
|
r.raise_for_status()
|
|
|
|
|
|
combined[key] = r.json()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" REGISTERS: {endpoint} failed — {e}")
|
|
|
|
|
|
combined[key] = None
|
|
|
|
|
|
|
|
|
|
|
|
# Quick-look indicators
|
|
|
|
|
|
sn65 = combined.get("sn65") or {}
|
|
|
|
|
|
regs = sn65.get("registers", {}) if isinstance(sn65, dict) else {}
|
|
|
|
|
|
csr_0a = regs.get("csr_0a", {}) or {}
|
|
|
|
|
|
csr_e5 = regs.get("csr_e5", {}) or {}
|
|
|
|
|
|
|
|
|
|
|
|
if csr_0a:
|
|
|
|
|
|
pll_str = "LOCKED" if csr_0a.get("pll_lock") else "*** UNLOCKED ***"
|
|
|
|
|
|
clk_str = "detected" if csr_0a.get("clk_det") else "NOT detected"
|
|
|
|
|
|
print(f" SN65: PLL {pll_str} CLK {clk_str} (CSR 0x0A = {csr_0a.get('value')})")
|
|
|
|
|
|
|
|
|
|
|
|
if csr_e5:
|
|
|
|
|
|
flags = [
|
|
|
|
|
|
("pll_unlock", "PLL_UNLOCK"),
|
|
|
|
|
|
("cha_sot_bit_err", "SOT_BIT_ERR"),
|
|
|
|
|
|
("cha_llp_err", "LLP_ERR"),
|
|
|
|
|
|
("cha_ecc_err", "ECC_ERR"),
|
|
|
|
|
|
("cha_lp_err", "LP_ERR"),
|
|
|
|
|
|
("cha_crc_err", "CRC_ERR"),
|
|
|
|
|
|
]
|
|
|
|
|
|
active = [label for k, label in flags if csr_e5.get(k)]
|
|
|
|
|
|
if active:
|
|
|
|
|
|
print(f" SN65: *** ERROR FLAGS: {', '.join(active)} "
|
|
|
|
|
|
f"(CSR 0xE5 = {csr_e5.get('value')}) ***")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" SN65: no error flags (CSR 0xE5 = {csr_e5.get('value')})")
|
|
|
|
|
|
|
|
|
|
|
|
out = target_dir / f"{event_ts}_registers.json"
|
|
|
|
|
|
try:
|
|
|
|
|
|
out.write_text(json.dumps(combined, indent=2))
|
|
|
|
|
|
print(f" registers → {out.relative_to(DATA_DIR.parent)}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" REGISTERS save failed: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Event handling: archive recent captures and (for flicker) analyse
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def archive_and_analyse(event: str, since_iso: str) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
Pull every CSV from the scope, move into data/{event}/{event_ts}/.
|
|
|
|
|
|
For flicker events, run csv_preprocessor on each LP capture and print a
|
|
|
|
|
|
summary table. Always pulls a register snapshot from the device too.
|
|
|
|
|
|
"""
|
|
|
|
|
|
event_ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
target = (FLICKER_DIR if event == "flicker" else GOOD_DIR) / event_ts
|
|
|
|
|
|
target.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
print(f"\n *** {event.upper()} EVENT @ {event_ts} ***")
|
|
|
|
|
|
|
|
|
|
|
|
# Register snapshot first (fast, before scope transfer which takes longer)
|
|
|
|
|
|
fetch_registers_snapshot(target, event_ts)
|
|
|
|
|
|
|
|
|
|
|
|
print(f" Transferring scope → {target} ...")
|
|
|
|
|
|
try:
|
|
|
|
|
|
copied, failed = ai_mgmt.transfer_csv_files()
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" TRANSFER ERROR: {e}")
|
|
|
|
|
|
return
|
|
|
|
|
|
print(f" {copied} file(s) transferred ({failed} failed)")
|
|
|
|
|
|
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# ai_mgmt only fetches CSVs. H5 (segmented) files need a separate pass.
|
|
|
|
|
|
h5_count = _transfer_h5_files()
|
|
|
|
|
|
if h5_count:
|
|
|
|
|
|
print(f" {h5_count} H5 file(s) transferred")
|
|
|
|
|
|
|
|
|
|
|
|
# Move just-arrived files (csv + h5) out of data/ (flat) into the event folder.
|
2026-05-07 09:01:32 +01:00
|
|
|
|
moved = 0
|
2026-05-07 12:10:02 +01:00
|
|
|
|
for f in list(DATA_DIR.glob("*.csv")) + list(DATA_DIR.glob("*.h5")):
|
|
|
|
|
|
if f.is_file():
|
|
|
|
|
|
shutil.move(str(f), target / f.name)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
moved += 1
|
|
|
|
|
|
print(f" {moved} file(s) archived to {target.relative_to(DATA_DIR.parent)}")
|
|
|
|
|
|
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# Explode each H5 into per-segment CSVs so csv_preprocessor can analyse them.
|
|
|
|
|
|
from explode_h5 import explode
|
|
|
|
|
|
h5_files = sorted(target.glob("*_lp_*.h5"))
|
|
|
|
|
|
seg_csv_count = 0
|
|
|
|
|
|
for h5 in h5_files:
|
|
|
|
|
|
try:
|
|
|
|
|
|
csvs = explode(h5)
|
|
|
|
|
|
seg_csv_count += len(csvs)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" EXPLODE error on {h5.name}: {e}")
|
|
|
|
|
|
if h5_files:
|
|
|
|
|
|
print(f" exploded {len(h5_files)} H5 file(s) → {seg_csv_count} segment CSV(s)")
|
|
|
|
|
|
|
2026-05-07 09:01:32 +01:00
|
|
|
|
if event != "flicker":
|
|
|
|
|
|
return
|
|
|
|
|
|
|
2026-05-07 12:10:02 +01:00
|
|
|
|
# Analyse every segment CSV. Flag outliers.
|
|
|
|
|
|
print("\n Per-segment LP analysis:")
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for f in sorted(target.glob("*_lp_*_dat.csv")):
|
2026-05-07 09:01:32 +01:00
|
|
|
|
try:
|
|
|
|
|
|
m = analyze_lp_file(f)
|
2026-05-07 12:10:02 +01:00
|
|
|
|
rows.append({
|
|
|
|
|
|
"file": f.name,
|
|
|
|
|
|
"lp_low": float(m.lp_low_duration_ns) if m.lp_low_duration_ns is not None else None,
|
|
|
|
|
|
"hs_amp": float(m.hs_amplitude_mv) if m.hs_amplitude_mv is not None else None,
|
|
|
|
|
|
"hs_dur": float(m.hs_burst_dur_ns) if m.hs_burst_dur_ns is not None else None,
|
|
|
|
|
|
"n_burst": int(m.n_hs_bursts) if m.n_hs_bursts is not None else None,
|
|
|
|
|
|
"sus": bool(m.flicker_suspect),
|
|
|
|
|
|
})
|
2026-05-07 09:01:32 +01:00
|
|
|
|
except Exception as e:
|
2026-05-07 12:10:02 +01:00
|
|
|
|
rows.append({"file": f.name, "error": str(e)})
|
|
|
|
|
|
|
|
|
|
|
|
n_total = len(rows)
|
|
|
|
|
|
n_sus = sum(1 for r in rows if r.get("sus"))
|
|
|
|
|
|
print(f" {n_total} segments analysed ({n_sus} flagged as flicker_suspect)")
|
|
|
|
|
|
|
|
|
|
|
|
# Outlier search across the segments themselves.
|
|
|
|
|
|
def _outliers(field: str, lo_thresh: float | None = None,
|
|
|
|
|
|
hi_thresh: float | None = None) -> list[dict]:
|
|
|
|
|
|
vals = sorted(r[field] for r in rows if r.get(field) is not None)
|
|
|
|
|
|
if not vals:
|
|
|
|
|
|
return []
|
|
|
|
|
|
med = vals[len(vals) // 2]
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for r in rows:
|
|
|
|
|
|
v = r.get(field)
|
|
|
|
|
|
if v is None: continue
|
|
|
|
|
|
far = (lo_thresh is not None and v < lo_thresh) or \
|
|
|
|
|
|
(hi_thresh is not None and v > hi_thresh)
|
|
|
|
|
|
if far:
|
|
|
|
|
|
out.append({"file": r["file"], field: v, "median": med})
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
print("\n Anomalies vs segment-set median:")
|
|
|
|
|
|
for label, field, lo, hi in [
|
|
|
|
|
|
("very-short LP-low (<50 ns)", "lp_low", 50, None),
|
|
|
|
|
|
("very-low HS amplitude (<50 mV)", "hs_amp", 50, None),
|
|
|
|
|
|
("very-high HS amplitude (>140 mV)","hs_amp", None, 140),
|
|
|
|
|
|
("short HS burst (<8000 ns)", "hs_dur", 8000, None),
|
|
|
|
|
|
]:
|
|
|
|
|
|
ax = _outliers(field, lo, hi)
|
|
|
|
|
|
if ax:
|
|
|
|
|
|
print(f" {label}: {len(ax)} segment(s)")
|
|
|
|
|
|
for x in ax[:8]:
|
|
|
|
|
|
print(f" {x['file']} {field}={x[field]:.1f} "
|
|
|
|
|
|
f"(set median={x['median']:.1f})")
|
|
|
|
|
|
if len(ax) > 8:
|
|
|
|
|
|
print(f" ... +{len(ax) - 8} more")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" {label}: none")
|
2026-05-07 09:01:32 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Main loop
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
|
DATA_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
FLICKER_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
GOOD_DIR.mkdir(exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
setup_scope()
|
|
|
|
|
|
configure_for_lp()
|
|
|
|
|
|
|
|
|
|
|
|
print("\n" + "=" * 64)
|
|
|
|
|
|
print(" FLICKER WATCH — keys: f=flicker g=good q=quit")
|
|
|
|
|
|
print("=" * 64 + "\n")
|
|
|
|
|
|
|
|
|
|
|
|
cycle = 0
|
|
|
|
|
|
try:
|
|
|
|
|
|
with KeyReader() as keys:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
cycle += 1
|
|
|
|
|
|
cycle_ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
cycle_caps = []
|
|
|
|
|
|
cycle_end = time.time() + CYCLE_S
|
|
|
|
|
|
|
|
|
|
|
|
video_start()
|
|
|
|
|
|
print(f"\n[cycle {cycle:03d} {cycle_ts}] video ON "
|
2026-05-07 12:10:02 +01:00
|
|
|
|
f"({CYCLE_S:.0f}s window, {SEGMENT_COUNT} segs/acquire)",
|
|
|
|
|
|
flush=True)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
|
|
|
|
|
|
event = None
|
|
|
|
|
|
last_tick = 0.0
|
|
|
|
|
|
while time.time() < cycle_end:
|
|
|
|
|
|
seq = len(cycle_caps) + 1
|
|
|
|
|
|
base = f"{cycle_ts}_lp_c{cycle:03d}_{seq:02d}"
|
|
|
|
|
|
remaining = lambda: max(0, cycle_end - time.time())
|
|
|
|
|
|
|
|
|
|
|
|
if arm_and_wait(TRIG_TIMEOUT_S):
|
|
|
|
|
|
try:
|
|
|
|
|
|
save_lp(base)
|
|
|
|
|
|
cycle_caps.append(base)
|
2026-05-07 12:10:02 +01:00
|
|
|
|
print(f" + acq {seq:02d} ({SEGMENT_COUNT} segs) "
|
|
|
|
|
|
f"[{remaining():4.1f}s left]", flush=True)
|
2026-05-07 09:01:32 +01:00
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f" save error: {e}", flush=True)
|
|
|
|
|
|
else:
|
|
|
|
|
|
# Trigger timed out — print a heartbeat at most every 2s
|
|
|
|
|
|
if time.time() - last_tick > 2.0:
|
|
|
|
|
|
print(f" ... waiting for trigger "
|
|
|
|
|
|
f"[{remaining():4.1f}s left]", flush=True)
|
|
|
|
|
|
last_tick = time.time()
|
|
|
|
|
|
|
|
|
|
|
|
key = keys.get_key()
|
|
|
|
|
|
if key in ("f", "g", "q"):
|
|
|
|
|
|
event = key
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
video_stop()
|
|
|
|
|
|
if event is None:
|
|
|
|
|
|
print(f"[cycle {cycle:03d}] ended "
|
2026-05-07 12:10:02 +01:00
|
|
|
|
f"({len(cycle_caps)} acq(s) ≈ "
|
|
|
|
|
|
f"{len(cycle_caps) * SEGMENT_COUNT} segments, no event)",
|
2026-05-07 09:01:32 +01:00
|
|
|
|
flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
if event == "f":
|
|
|
|
|
|
archive_and_analyse("flicker", cycle_ts)
|
|
|
|
|
|
elif event == "g":
|
|
|
|
|
|
archive_and_analyse("good", cycle_ts)
|
|
|
|
|
|
elif event == "q":
|
|
|
|
|
|
print("\nQUIT requested.")
|
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
|
|
# Brief pause before next cycle so video stop settles.
|
|
|
|
|
|
time.sleep(0.5)
|
|
|
|
|
|
|
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
|
print("\nInterrupted (Ctrl+C).")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
try:
|
|
|
|
|
|
video_stop()
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|