This commit is contained in:
david rice
2026-05-07 12:10:02 +01:00
5 changed files with 390 additions and 35 deletions

View File

@@ -49,8 +49,16 @@ LP_V_SCALE = 0.2
LP_V_OFFSET = 0.6
LP_TRIG_LEVEL = 0.6
# 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
CYCLE_S = 10.0 # seconds video is on per cycle
TRIG_TIMEOUT_S = 2.0 # per-capture trigger wait
# 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)
# ---------------------------------------------------------------------------
# Scope setup
@@ -84,7 +92,7 @@ def setup_scope() -> None:
def configure_for_lp() -> None:
"""LP-mode: widen vertical range, falling-edge trigger on Ch3."""
"""LP-mode + segmented memory: N back-to-back LP triggers per acquisition."""
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}")
@@ -95,7 +103,10 @@ def configure_for_lp() -> None:
scope.write(f":TIMebase:SCALe {LP_SCALE:.3E}")
scope.write(f":ACQuire:POINts {LP_POINTS}")
scope.write(f":TIMebase:POSition {LP_TRIG_OFFSET:.2E}")
time.sleep(0.3)
# Segmented memory: fill N segments per :DIGitize.
scope.write(":ACQuire:MODE SEGMented")
scope.write(f":ACQuire:SEGMented:COUNt {SEGMENT_COUNT}")
time.sleep(0.5)
def arm_and_wait(timeout_s: float) -> bool:
@@ -128,12 +139,13 @@ def arm_and_wait(timeout_s: float) -> bool:
def save_lp(base_name: str) -> None:
"""Save Ch1 (CLK+) and Ch3 (DAT0+) as CSV to scope's C:\\TEMP\\."""
"""Save all N segments of Ch1 (CLK+) and Ch3 (DAT0+) as a single H5 each."""
base = f"C:\\TEMP\\{base_name}"
scope.write(f':DISK:SAVE:WAVeform CHANnel1,"{base}_clk.csv",CSV')
time.sleep(2.5)
scope.write(f':DISK:SAVE:WAVeform CHANnel3,"{base}_dat.csv",CSV')
time.sleep(2.5)
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)
# ---------------------------------------------------------------------------
@@ -174,6 +186,48 @@ def video_stop() -> None:
print(f" VIDEO STOP failed: {e}")
# ---------------------------------------------------------------------------
# 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
# ---------------------------------------------------------------------------
# Register snapshot from device (DSIM PHY + SN65DSI83)
# ---------------------------------------------------------------------------
@@ -251,37 +305,90 @@ def archive_and_analyse(event: str, since_iso: str) -> None:
return
print(f" {copied} file(s) transferred ({failed} failed)")
# Move just-arrived CSVs out of data/ (flat) into the event folder.
# 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.
moved = 0
for csv in DATA_DIR.glob("*.csv"):
if csv.is_file():
shutil.move(str(csv), target / csv.name)
for f in list(DATA_DIR.glob("*.csv")) + list(DATA_DIR.glob("*.h5")):
if f.is_file():
shutil.move(str(f), target / f.name)
moved += 1
print(f" {moved} file(s) archived to {target.relative_to(DATA_DIR.parent)}")
# 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)")
if event != "flicker":
return
# Analyse the LP captures we just archived.
print("\n LP analysis (csv_preprocessor):")
print(" " + "-" * 78)
print(f" {'file':<46} {'lp_low_ns':>10} {'hs_amp_mV':>10} {'flicker?':>9}")
print(" " + "-" * 78)
lp_files = sorted(target.glob("*_lp_*_dat.csv"))
for f in lp_files:
# Analyse every segment CSV. Flag outliers.
print("\n Per-segment LP analysis:")
rows = []
for f in sorted(target.glob("*_lp_*_dat.csv")):
try:
m = analyze_lp_file(f)
lp_low = getattr(m, "lp_low_duration_ns", None)
hs_amp = getattr(m, "hs_amp_mV", None)
sus = getattr(m, "flicker_suspect", False)
print(f" {f.name:<46} "
f"{(f'{lp_low:.1f}' if lp_low is not None else '?'):>10} "
f"{(f'{hs_amp:.1f}' if hs_amp is not None else '?'):>10} "
f"{('YES' if sus else 'no'):>9}")
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),
})
except Exception as e:
print(f" {f.name:<46} ERROR: {e}")
print(" " + "-" * 78)
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")
# ---------------------------------------------------------------------------
@@ -310,7 +417,8 @@ def main() -> None:
video_start()
print(f"\n[cycle {cycle:03d} {cycle_ts}] video ON "
f"({CYCLE_S:.0f}s window)", flush=True)
f"({CYCLE_S:.0f}s window, {SEGMENT_COUNT} segs/acquire)",
flush=True)
event = None
last_tick = 0.0
@@ -323,8 +431,8 @@ def main() -> None:
try:
save_lp(base)
cycle_caps.append(base)
print(f" + cap {seq:02d} [{remaining():4.1f}s left]",
flush=True)
print(f" + acq {seq:02d} ({SEGMENT_COUNT} segs) "
f"[{remaining():4.1f}s left]", flush=True)
except Exception as e:
print(f" save error: {e}", flush=True)
else:
@@ -342,7 +450,8 @@ def main() -> None:
video_stop()
if event is None:
print(f"[cycle {cycle:03d}] ended "
f"({len(cycle_caps)} cap(s), no event)",
f"({len(cycle_caps)} acq(s) ≈ "
f"{len(cycle_caps) * SEGMENT_COUNT} segments, no event)",
flush=True)
if event == "f":