From e718a936678fb45bc3598deac70d7156e6a49cbc Mon Sep 17 00:00:00 2001 From: david rice Date: Mon, 20 Apr 2026 10:34:42 +0100 Subject: [PATCH] Updates --- __pycache__/csv_preprocessor.cpython-312.pyc | Bin 41319 -> 41617 bytes csv_preprocessor.py | 193 +++++++++++-- mipi_test_interactive.py | 278 ++++++++++++++++--- reports/20260420_074657_interactive.html | 131 +++++++++ reports/20260420_091026_interactive.html | 135 +++++++++ reports/flicker_log.csv | 7 + reports/interactive_log.csv | 6 + rigol_scope.py | 62 +++-- 8 files changed, 735 insertions(+), 77 deletions(-) create mode 100644 reports/20260420_074657_interactive.html create mode 100644 reports/20260420_091026_interactive.html diff --git a/__pycache__/csv_preprocessor.cpython-312.pyc b/__pycache__/csv_preprocessor.cpython-312.pyc index 3950fb9864c8ca1e473f6caee1a7ae4343b891ff..dd87b3c56ecbfc7a9df99d884816de3f4c851cb5 100644 GIT binary patch delta 876 zcmY*XZAep57(Va4yUT5Nw#{Ad%sH>ATOT@i{eUEbl1inFmE@1Yf|`aTvvybZv6WfY zq>JkeRC+ zcj!>SjfacvxH#LWt4E7HI8jwbD#HW7?eo(T*t=h>sH^qYO06xy-ICJM+7t{+A3J)b zV4JkwUo6W-8nm{x7fEd)sis~EG=-aXhMNL-(f%CrWnT}#XA*7y1;rWS>x`vi8B1@j zeqCNYR$e`B5iauQ`0>o#&;x>O^0tYaG!8T za#?tR3y(cY^O+DjC0|6?w<{Mv@SMk&JD0I?RF5Bd1^l(s4l<5aS25i(ep&6%V8`*_ zAd?*@vVcmFE)R_(UHJw%;>@D=?3PKQt5B1EoQw)`BrlR5;56Y7-LnQ9J2gS|Juxvy z{yV~e`4D~58w@r5^O2F*X4ahsC{o0u0}x^-y*b?9;cMzhjjv8S(#0@ z4*%?R7(1x;igu$6ldKDN4!qdgzvZX{87yZjM$;TA#29kMEqMt(FU3-tv^W#IQ=Nx6 zi^|WLj0R(p7gK1SIX!8~Pa&PvmJ~eK$`eA#n9!IgTl1=>akQ*4g+TC(i*D75IQw@e z;)N8l^7=O}kIGW~;99J56u;qj{nv9^bjVyfJ-HT8xgQjsw*Q2k-@x0Rv*rdHxiZiW z(18~WcEbfSGPsTr{Q#1Rv^dEk#-uVciDs-HS|zS#LEl%PA`OR@PtKsaK~J^~8Q>2= C^Z;uB delta 634 zcmXv~QAkr!7(V~KyJNfa+P0>5V{Na$6QMvsgI? z4wQeDFcW==sF&(cU@vmTC~4UiLiQ%T1VM0fzSp_xemLLvpa1*#4(Ffy!2Md|>OV`8 zNLa0iKd(J0zOH{Z0z>re$U2W-5(Nn2W}*?5QNa~TDW=V=xT|Q1E2#9#g4s3Ui)5>- zIn{ii9_lT)lkAhjX`x7-axdXP12W^aka#ODs~%MSqU9p#Xv1>7^_E3|w!#KT84?(0_3 zK}c4^rO9SoZJ)#InKQgLVMYB!1HR9AK*O(@a|}xZUM{Dh`rsdERDI|JHXE2~XQ*do zIgkp}K9keZ$9(LPN2W_YEfuN7q&reA zwVu7JXB({tc6uWh52A$~7UuDUed=GR|Js;%ZFWs5Fz(W^pD`cQ95oK#kTxK!J zE?gaMz}~1ItB?HRB%7Dn+LB|```9Bu8>+=i-HpWVoIPE!d3T9m7q{y|yF6pVO6rxR zQ)Q|Bn-nWYdbWCFl}K!tfE3z35;Pn{@a(zqPjL+f@zsLI7K^$!cqw+4mKJUUOyc>) w92Ds0;vgpv0VoDBvC0$9VQ~9{E2u5^$+tML&W84*&oF diff --git a/csv_preprocessor.py b/csv_preprocessor.py index d83f162..fc89b1f 100644 --- a/csv_preprocessor.py +++ b/csv_preprocessor.py @@ -52,6 +52,9 @@ HS_OSC_STD_V = 0.045 # V — rolling-std threshold above which a region i # LP-low plateau below this → SoT sequence too brief for receiver to detect → flicker risk FLICKER_LP_LOW_MAX_NS = 50.0 # ns +# CLK lane LP-00 minimum for SN65DSI83 CLK lane lock (TCLK_PREPARE + TCLK_ZERO ≥ 300 ns) +CLK_LP_LOW_MIN_NS = 300.0 + # HS burst amplitude below this (single-ended p-p / 2, mV) → HS burst absent after LP transition. # On this hardware normal HS = 105–122 mV; confirmed flicker = 14–32 mV (DC / LP-11 recovery). # Captures where LP-01/LP-00 completed normally but the bridge never entered HS mode show @@ -576,6 +579,98 @@ def analyze_reg_file(path: Path) -> "RegDump": ) +# --------------------------------------------------------------------------- +# SN65DSI83 IRQ pin analysis (Rigol CH2 — CMOS output, active HIGH) +# --------------------------------------------------------------------------- + +# IRQ is a CMOS output (Table 5-1). Default state (IRQ_EN=0): high-impedance → reads ~0 V. +# When IRQ_EN=1 (CSR 0xE0.0): driven LOW (~0 V) when no error, HIGH (≥1.25 V) on error. +# No pull-up required. 0 V is normal. Assertion requires IRQ_EN=1 + error bits in CSR 0xE1. +INT_ASSERTED_HIGH_V = 1.0 # V — IRQ considered asserted (error) above this + + +@dataclass +class INTMetrics: + timestamp: str + capture_num: int + + sample_rate_mhz: float + duration_us: float + n_samples: int + + mean_v: float + min_v: float + max_v: float + + int_asserted: bool # True if IRQ went above INT_ASSERTED_HIGH_V + asserted_duration_us: Optional[float] # total assertion time, or None if not asserted + + warnings: list = field(default_factory=list) + + def summary(self) -> str: + ok = lambda c: "✓" if c else "✗" + lines = [ + f"Capture {self.capture_num:04d} {self.timestamp} [int/irq]", + f" IRQ mean/min/max : {self.mean_v:.3f} V / {self.min_v:.3f} V / {self.max_v:.3f} V", + ] + if self.int_asserted: + dur_str = (f" ({self.asserted_duration_us:.2f} µs)" + if self.asserted_duration_us else "") + lines.append( + f" IRQ status : *** ASSERTED HIGH — bridge flagged error{dur_str} *** ✗" + ) + else: + lines.append(f" IRQ status : not asserted (no bridge error) ✓") + for w in self.warnings: + lines.append(f" WARNING: {w}") + return "\n".join(lines) + + +def analyze_int_file(path: Path) -> "INTMetrics": + """Analyse a Rigol CH2 IRQ pin CSV file.""" + m = re.match(r"(\d{8}_\d{6})_int_(\d+)\.csv", path.name, re.IGNORECASE) + if not m: + raise ValueError(f"Filename does not match int pattern: {path.name}") + timestamp, cap_str = m.groups() + capture_num = int(cap_str) + + times, volts = _read_csv(path) + dt = float(np.diff(times).mean()) + sample_rate = 1.0 / dt + duration_us = (float(times[-1]) - float(times[0])) * 1e6 + + mean_v = float(volts.mean()) + min_v = float(volts.min()) + max_v = float(volts.max()) + + asserted_mask = volts > INT_ASSERTED_HIGH_V + int_asserted = bool(asserted_mask.any()) + asserted_duration_us = None + if int_asserted: + asserted_duration_us = round(float(asserted_mask.sum()) * dt * 1e6, 3) + + warnings = [] + if max_v < 0.1 and mean_v < 0.1: + warnings.append( + f"IRQ pin reads ~0 V throughout — likely high-impedance (IRQ_EN=0, default). " + f"Set CSR 0xE0.0=1 and enable error bits in CSR 0xE1 to activate IRQ output." + ) + + return INTMetrics( + timestamp = timestamp, + capture_num = capture_num, + sample_rate_mhz = round(sample_rate / 1e6, 1), + duration_us = round(duration_us, 2), + n_samples = len(times), + mean_v = round(mean_v, 3), + min_v = round(min_v, 3), + max_v = round(max_v, 3), + int_asserted = int_asserted, + asserted_duration_us = asserted_duration_us, + warnings = warnings, + ) + + def group_captures(data_dir: Path) -> dict[tuple[str, int], dict[str, Path]]: """ Scan data_dir and group CSV files by (timestamp, capture_number). @@ -639,6 +734,12 @@ class LPMetrics: lp_transition_valid: bool # LP-11 → LP-low → HS sequence present + # CLK lane startup check (only set when CLK LP-11 is captured — i.e. startup was caught) + # None = CLK was in continuous HS when triggered (startup not visible in this capture) + # True = CLK LP-00 duration ≥ 300 ns (SN65DSI83 CLK lock spec met) + # False = CLK LP-00 too short → bridge may fail to lock CLK lane + clk_lp_startup_ok: Optional[bool] = None + # Flicker detection # A capture is flagged when the LP-low plateau is absent or shorter than # FLICKER_LP_LOW_MAX_NS. Normal captures show ~340 ns; flicker shows 0–50 ns. @@ -666,7 +767,19 @@ class LPMetrics: f"(spec ≥{LP_LOW_DUR_MIN_NS:.0f} ns) {ok(ok_exit)}" ) if self.lp_low_duration_ns is not None: - lines.append(f" LP-low plateau : {self.lp_low_duration_ns:.0f} ns") + if self.channel == "clk": + ok_clk = self.lp_low_duration_ns >= CLK_LP_LOW_MIN_NS + lines.append( + f" LP-00 (CLK) : {self.lp_low_duration_ns:.0f} ns " + f"(spec ≥{CLK_LP_LOW_MIN_NS:.0f} ns for bridge CLK lock) " + f"{'✓' if ok_clk else '✗'}" + ) + else: + lines.append(f" LP-low plateau : {self.lp_low_duration_ns:.0f} ns") + if self.clk_lp_startup_ok is not None: + lines.append( + f" CLK startup : {'ok ✓' if self.clk_lp_startup_ok else '*** SHORT — bridge may not lock CLK ✗'}" + ) lines.append( f" LP→HS sequence : {'valid ✓' if self.lp_transition_valid else 'NOT DETECTED ✗'}" ) @@ -676,7 +789,11 @@ class LPMetrics: if self.hs_amplitude_mv is not None: lines.append(f" HS amplitude : {self.hs_amplitude_mv:.0f} mV (single-ended p-p/2)") if self.flicker_suspect: - if (self.hs_amplitude_mv is not None + if not self.lp_transition_valid and not self.lp11_voltage_v: + lines.append( + f" *** FLICKER SUSPECT: MIPI link silent — no LP-11, LP-low, or HS detected ***" + ) + elif (self.hs_amplitude_mv is not None and self.hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV and self.lp11_to_hs_ns is not None and self.lp11_to_hs_ns >= LP_LOW_DUR_MIN_NS): @@ -842,8 +959,17 @@ def analyze_lp_file(path: Path) -> "LPMetrics": if n_hs_bursts == 0: warnings.append("No HS bursts detected after LP transition") - # Flicker suspect: either the LP-low plateau is absent/short, OR the HS burst - # amplitude is too low. Two confirmed failure modes on this hardware: + # CLK lane startup check — only relevant when CLK LP-11 was captured (startup visible) + clk_lp_startup_ok: Optional[bool] = None + if channel == "clk" and lp11_regions and lp_low_duration_ns is not None: + clk_lp_startup_ok = lp_low_duration_ns >= CLK_LP_LOW_MIN_NS + if not clk_lp_startup_ok: + warnings.append( + f"CLK LP-00 {lp_low_duration_ns:.0f} ns < {CLK_LP_LOW_MIN_NS:.0f} ns " + f"(TCLK_PREPARE+TCLK_ZERO minimum) — SN65DSI83 may fail to lock CLK lane" + ) + + # Flicker suspect: three confirmed failure modes on this hardware: # # A) Normal LP-low (~342–380 ns) → bridge misses SoT → returns to LP-11 # Signature: lp11_to_hs fires at real LP-low end (~347 ns), hs_amplitude ≈ 15–30 mV. @@ -853,10 +979,15 @@ def analyze_lp_file(path: Path) -> "LPMetrics": # B) Short LP-low (50–200 ns, vs nominal ~342–380 ns) → marginal SoT timing # → HS burst starts but is weak, hs_amplitude ≈ 40–60 mV (vs normal 100–122 mV). # Signature: lp_low anomalously short, lp11_to_hs fires at noise spike (~3 ns). - # The lp11_to_hs guard cannot be used here (noise spike looks the same as mode A - # false positives), so LP-low duration itself gates the amplitude check. + # The lp11_to_hs guard cannot be used here, so LP-low duration gates the check. # Confirmed example: capture 0120 (lp_low=108 ns, lp11_to_hs=1.7 ns, amp=49 mV). # + # C) No LP-11 detected at all → MIPI link silent or stuck + # Signature: no LP-11 region found, lp_transition_valid=False, no LP or HS seen. + # This is the most severe failure: the bridge or DSI IP has stopped outputting. + # Confirmed: follow-up of capture 13 (no LP-11, no HS) while display was flickering. + # Guard: only flag DAT lane (not CLK which is normally in continuous HS mode). + # # Only flag DAT lane (CLK is continuous HS — LP states not expected). _lp_low_short = ( lp_low_duration_ns is not None @@ -872,32 +1003,44 @@ def analyze_lp_file(path: Path) -> "LPMetrics": or _lp_low_short ) ) + # Mode C: no LP-11 at all → link silent (but exclude CLK which is always HS) + link_silent = ( + channel == "dat" + and not continuous_hs_clk + and not lp11_regions + ) flicker_suspect = ( channel == "dat" - and lp_transition_valid and ( - (lp_low_duration_ns is None or lp_low_duration_ns < FLICKER_LP_LOW_MAX_NS) - or hs_burst_absent + link_silent + or ( + lp_transition_valid + and ( + (lp_low_duration_ns is None or lp_low_duration_ns < FLICKER_LP_LOW_MAX_NS) + or hs_burst_absent + ) + ) ) ) return LPMetrics( - timestamp = timestamp, - capture_num = capture_num, - channel = channel, - sample_rate_gsps = round(sample_rate / 1e9, 1), - duration_us = round(duration_us, 2), - n_samples = len(times), - lp11_voltage_v = lp11_voltage_v, - lp11_duration_us = lp11_duration_us, - lp11_to_hs_ns = lp11_to_hs_ns, - lp_low_duration_ns = lp_low_duration_ns, - n_hs_bursts = n_hs_bursts, - hs_burst_dur_ns = hs_burst_dur_ns, - hs_amplitude_mv = hs_amplitude_mv, - lp_transition_valid = lp_transition_valid, - flicker_suspect = flicker_suspect, - warnings = warnings, + timestamp = timestamp, + capture_num = capture_num, + channel = channel, + sample_rate_gsps = round(sample_rate / 1e9, 1), + duration_us = round(duration_us, 2), + n_samples = len(times), + lp11_voltage_v = lp11_voltage_v, + lp11_duration_us = lp11_duration_us, + lp11_to_hs_ns = lp11_to_hs_ns, + lp_low_duration_ns = lp_low_duration_ns, + n_hs_bursts = n_hs_bursts, + hs_burst_dur_ns = hs_burst_dur_ns, + hs_amplitude_mv = hs_amplitude_mv, + lp_transition_valid = lp_transition_valid, + clk_lp_startup_ok = clk_lp_startup_ok, + flicker_suspect = flicker_suspect, + warnings = warnings, ) diff --git a/mipi_test_interactive.py b/mipi_test_interactive.py index a1106c5..8414eac 100644 --- a/mipi_test_interactive.py +++ b/mipi_test_interactive.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +ni w#!/usr/bin/env python3 """ MIPI TEST APPLICATION - MIPI_TEST_INTERACTIVE.PY Interactive flicker confirmation test. @@ -37,7 +37,8 @@ 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) + HS_BURST_AMPLITUDE_MIN_MV, FLICKER_LP_LOW_MAX_NS, + analyze_int_file, CLK_LP_LOW_MIN_NS) load_dotenv(Path(__file__).parent / ".env") @@ -555,6 +556,85 @@ def _restore_hs_config(): time.sleep(0.1) +def _arm_scope_for_clk_startup() -> None: + """ + Configure scope for CLK lane LP startup capture and arm in single-shot mode. + Must be called BEFORE display ON — the CLK+ LP-11 falling edge (start of CLK + lane SoT preamble) triggers it. The DAT0+-triggered LP pass would miss this + because CLK is already in continuous HS by the time DAT0+ falls. + """ + 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}") + time.sleep(0.05) + scope.write(":TRIGger:EDGE:SOURce CHANnel1") # CLK+ — fires before DAT0+ + 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}") + time.sleep(0.3) + scope.write(":SINGle") # arm without blocking — display ON happens next + time.sleep(0.1) + + +def _collect_clk_startup(ts: str, iteration: int, + timeout: float = 10.0) -> list[str]: + """ + Wait for the CLK startup trigger fired by _arm_scope_for_clk_startup(), + save CLK+ and DAT0+ channels, and return LP analysis summaries. + Restores HS config before returning. + """ + print(" CLK STARTUP: waiting for trigger...") + deadline = time.time() + timeout + triggered = False + while time.time() < deadline: + try: + status = scope.ask(":TRIGger:STATus?").strip().upper() + if status in ("STOP", "TD"): + triggered = True + break + except Exception: + pass + time.sleep(0.1) + + if not triggered: + print(" CLK STARTUP: trigger timeout — CLK LP startup not captured.") + _restore_hs_config() + return [] + + _save_pass_channels("lp", iteration, ts) + _restore_hs_config() + + try: + copied, _ = ai_mgmt.transfer_csv_files() + print(f" CLK STARTUP: {copied} file(s) transferred.") + except Exception as e: + print(f" CLK STARTUP TRANSFER ERROR: {e}") + + summaries = [] + for channel in ("clk", "dat"): + path = DATA_DIR / f"{ts}_lp_{iteration:04d}_{channel}.csv" + if not path.exists(): + continue + try: + m = analyze_lp_file(path) + summaries.append(m.summary()) + if channel == "clk": + if m.clk_lp_startup_ok is False: + print(f"\n *** CLK STARTUP SUSPECT: capture {iteration:04d} " + f"CLK LP-00={m.lp_low_duration_ns:.0f} ns " + f"< {CLK_LP_LOW_MIN_NS:.0f} ns — bridge may not lock CLK ***\n") + elif m.clk_lp_startup_ok is True: + print(f" CLK startup: LP-00={m.lp_low_duration_ns:.0f} ns ✓") + else: + print(" CLK startup: CLK already in continuous HS at trigger point.") + except Exception as e: + print(f" CLK STARTUP ANALYSIS ERROR ({channel}): {e}") + + return summaries + + def _fetch_registers(ts: str, iteration: int) -> None: """GET /registers from device server and save to data/ as JSON.""" try: @@ -603,7 +683,14 @@ def dual_capture(iteration: int) -> str: if n: print(f" SAVED: {v18_path.name} ({n} samples)") else: - print(" RIGOL: Waveform read failed — check connection and probe.") + print(" RIGOL: 1V8 waveform read failed.") + # CH2 — INTB pin (read after CH1; scope already stopped) + int_path = DATA_DIR / f"{ts}_int_{iteration:04d}.csv" + n_int = rigol_scope.read_int_csv(int_path) + if n_int: + print(f" SAVED: {int_path.name} ({n_int} samples)") + else: + print(" RIGOL: INTB waveform read failed.") _restore_hs_config() # ── Pass 2: HS signal quality ────────────────────────────────────────── @@ -662,10 +749,13 @@ def _build_system_prompt(config: dict | None = None) -> str: def _build_claude_prompt(ts: str, iteration: int, lp_summaries: list[str], suspects: list[LPMetrics], - config: dict | None = None) -> str: + config: dict | None = None, + followup_summaries: list[str] | None = None) -> str: """ Build a concise prompt asking Claude to assess a single capture. The rule-based pre-filter has already flagged at least one LP suspect. + If followup_summaries is provided it contains the next-frame LP capture taken + immediately after the suspect — the frame the operator will actually observe. """ suspect_lines = "\n".join( f" channel={m.channel} lp_low_plateau={m.lp_low_duration_ns} ns " @@ -686,13 +776,26 @@ def _build_claude_prompt(ts: str, iteration: int, f"({t['byte_period_ns']:.3f} ns/byte), UI {t['ui_ns']:.3f} ns." ) + followup_text = "" + if followup_summaries: + followup_text = ( + f"\n\nFOLLOW-UP CAPTURE (next display frame — what the operator sees " + f"on screen while assessing):\n" + f"Note: due to the display pipeline lag, the visual flicker caused by " + f"the electrical event above appears one frame later. If the follow-up " + f"frame looks electrically normal, the flicker observed by the operator " + f"was caused by the preceding capture, not this one.\n\n" + + "\n\n".join(followup_summaries) + ) + return ( f"SINGLE-CAPTURE FLICKER ASSESSMENT — capture {iteration:04d} [{ts}]\n\n" f"The rule-based LP pre-processor has flagged the following measurements as " f"potential flicker suspects because the LP-low plateau is absent or shorter " f"than 50 ns:\n{suspect_lines}\n\n" f"Full LP capture summaries:\n{summaries_text}" - f"{config_text}\n\n" + f"{config_text}" + f"{followup_text}\n\n" f"Based solely on these LP timing metrics, do you believe this capture " f"represents a genuine screen flicker event — i.e., was the SoT sequence " f"too brief for the SN65DSI83 bridge to detect start-of-transmission, " @@ -718,21 +821,13 @@ def _append_flicker_log(ts: str, iteration: int, m: LPMetrics) -> None: ]) -def analyze_lp_and_ask_claude( - ts: str, iteration: int, config: dict | None = None -) -> tuple[bool, str, list[LPMetrics]]: +def _analyze_lp_files( + ts: str, iteration: int +) -> tuple[list[str], list[LPMetrics]]: """ - Analyse the LP files for this iteration. - - 1. Run csv_preprocessor on lp_clk and lp_dat. - 2. If any file is flagged as a flicker suspect by the rule-based detector, - call the Claude API for a focused single-capture assessment. - 3. Parse Claude's YES/NO response. - - Returns: - claude_says_flicker — True if Claude opened with YES - reasoning — Claude's full response text (or "" if not called) - suspects — list of LPMetrics objects that were flagged + Run rule-based LP analysis for one iteration. + Returns (lp_summaries, suspects). Logs suspects and prints alerts. + Does NOT call Claude. """ lp_summaries: list[str] = [] suspects: list[LPMetrics] = [] @@ -748,11 +843,14 @@ def analyze_lp_and_ask_claude( if m.flicker_suspect: suspects.append(m) _append_flicker_log(ts, iteration, m) - if (m.hs_amplitude_mv is not None + if not m.lp_transition_valid and not m.lp11_voltage_v: + reason = "MIPI link silent (no LP-11/LP-low/HS detected)" + elif (m.hs_amplitude_mv is not None and m.hs_amplitude_mv < HS_BURST_AMPLITUDE_MIN_MV and m.lp11_to_hs_ns is not None and m.lp11_to_hs_ns >= FLICKER_LP_LOW_MAX_NS): - reason = f"HS burst absent ({m.hs_amplitude_mv:.0f} mV, lp11_to_hs={m.lp11_to_hs_ns:.0f} ns)" + reason = (f"HS burst absent ({m.hs_amplitude_mv:.0f} mV, " + f"lp11_to_hs={m.lp11_to_hs_ns:.0f} ns)") else: reason = f"lp_low={m.lp_low_duration_ns} ns" print(f"\n *** FLICKER SUSPECT: capture {iteration:04d} " @@ -760,36 +858,119 @@ def analyze_lp_and_ask_claude( except Exception as e: print(f" LP ANALYSIS ERROR ({channel}): {e}") - if not suspects: - return False, "", [] + return lp_summaries, suspects - # ── Call Claude ──────────────────────────────────────────────────────── + +def _analyze_int_file(ts: str, iteration: int) -> str | None: + """ + Analyse the INTB pin CSV for this iteration. + Returns a summary string, or None if the file is missing. + """ + path = DATA_DIR / f"{ts}_int_{iteration:04d}.csv" + if not path.exists(): + return None + try: + m = analyze_int_file(path) + if m.int_asserted: + print(f"\n *** INTB ASSERTED: capture {iteration:04d} " + f"bridge flagged error ({m.asserted_duration_us:.2f} µs) ***\n") + return m.summary() + except Exception as e: + print(f" INTB ANALYSIS ERROR: {e}") + return None + + +def _lp_followup_capture(iteration: int) -> tuple[str, list[str], list[LPMetrics]]: + """ + LP-only follow-up capture taken immediately after a suspect is detected. + Captures the next display frame — the one the operator will actually see + when asked to assess whether the screen is flickering. + + Returns (ts_followup, lp_summaries, suspects). + Returns ("", [], []) silently if the scope is not reachable. + """ + print(" FOLLOW-UP CAPTURE: acquiring next frame for display-lag context...") + try: + 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: + print(" FOLLOW-UP: trigger timeout — skipping.") + _restore_hs_config() + return "", [], [] + _restore_hs_config() + + # Transfer the new files from scope + try: + copied, _ = ai_mgmt.transfer_csv_files() + print(f" FOLLOW-UP: {copied} file(s) transferred.") + except Exception as e: + print(f" FOLLOW-UP TRANSFER ERROR: {e}") + + summaries, suspects = _analyze_lp_files(ts_fu, iteration) + return ts_fu, summaries, suspects + + except Exception as e: + print(f" FOLLOW-UP CAPTURE ERROR: {e}") + return "", [], [] + + +def _call_claude( + ts: str, iteration: int, + lp_summaries: list[str], + suspects: list[LPMetrics], + config: dict | None = None, + followup_summaries: list[str] | None = None, +) -> tuple[bool, str]: + """ + Call the Claude API to assess whether the flagged capture is a flicker event. + Returns (claude_says_flicker, response_text). + """ print(" CALLING CLAUDE FOR ASSESSMENT...") try: client = anthropic.Anthropic() message = client.messages.create( model = CLAUDE_MODEL, - max_tokens = 512, + max_tokens = 600, system = _build_system_prompt(config), messages = [{"role": "user", "content": _build_claude_prompt(ts, iteration, lp_summaries, - suspects, config)}], + suspects, config, + followup_summaries)}], ) response = message.content[0].text.strip() - # Parse YES/NO from the first line first_line = response.splitlines()[0].strip().upper() claude_says_flicker = first_line.startswith("YES") label = "FLICKER" if claude_says_flicker else "NOT FLICKER" print(f" CLAUDE: {label} ({message.usage.input_tokens} in / " f"{message.usage.output_tokens} out tokens)") - return claude_says_flicker, response, suspects + return claude_says_flicker, response except Exception as e: print(f" CLAUDE API ERROR: {e}") - # Fall back to the rule-based result so the operator still gets asked fallback = (f"(Claude API unavailable: {e})\n" f"Rule-based detector flagged LP-low plateau < 50 ns — " f"treat as potential flicker suspect.") - return True, fallback, suspects + return True, fallback + + +def analyze_lp_and_ask_claude( + ts: str, iteration: int, config: dict | None = None +) -> tuple[bool, str, list[LPMetrics]]: + """ + Analyse the LP files for this iteration (rule-based + Claude if suspect). + Kept for backwards compatibility — used by report generation and tests. + Does not perform a follow-up capture; call the test loop directly for that. + """ + lp_summaries, suspects = _analyze_lp_files(ts, iteration) + if not suspects: + return False, "", [] + claude_flicker, response = _call_claude(ts, iteration, lp_summaries, + suspects, config) + return claude_flicker, response, suspects # --------------------------------------------------------------------------- @@ -1118,12 +1299,22 @@ def run_interactive_test() -> None: try: while True: + # ── Arm scope for CLK lane startup (BEFORE display ON) ───────── + # CLK+ LP-11 falls before DAT0+, so we must arm here. + # The regular LP pass (triggered on DAT0+) would miss CLK startup + # because CLK is already in continuous HS by the time DAT0+ falls. + ts_startup = datetime.now().strftime("%Y%m%d_%H%M%S") + _arm_scope_for_clk_startup() + # ── Display ON ───────────────────────────────────────────────── try: requests.put(URL, json={"state": "on"}, timeout=2) except requests.exceptions.RequestException as e: print(f" WARNING: display ON failed: {e}") + # ── Collect CLK startup capture ──────────────────────────────── + clk_startup_summaries = _collect_clk_startup(ts_startup, iteration) + # ── Three-pass capture ───────────────────────────────────────── ts = dual_capture(iteration) @@ -1135,9 +1326,28 @@ def run_interactive_test() -> None: except Exception as e: print(f" TRANSFER ERROR: {e}") - # ── Analyse LP + ask Claude if suspect ───────────────────────── - claude_flicker, reasoning, suspects = analyze_lp_and_ask_claude( - ts, iteration, config) + # ── INTB pin analysis ────────────────────────────────────────── + _analyze_int_file(ts, iteration) + + # ── Rule-based LP analysis ───────────────────────────────────── + lp_summaries, suspects = _analyze_lp_files(ts, iteration) + + followup_summaries: list[str] = [] + if suspects: + # Take an LP-only follow-up capture before calling Claude. + # The visual flicker caused by a missed SoT appears one display + # frame later (pipeline lag), so the operator observes flicker + # on the frame AFTER the electrical event. Including the next + # frame gives Claude — and the operator — the correct context. + _, followup_summaries, _ = _lp_followup_capture(iteration) + + # ── Call Claude if any rule-based suspect was found ──────────── + claude_flicker = False + reasoning = "" + if suspects: + claude_flicker, reasoning = _call_claude( + ts, iteration, lp_summaries, suspects, config, + followup_summaries or None) if claude_flicker: # ── Keep display ON — ask operator ───────────────────────── @@ -1181,7 +1391,7 @@ def run_interactive_test() -> None: else: print(" NOT FLICKERING — false alarm logged. Continuing.\n") - elif suspects: + if suspects and not claude_flicker: # Rule-based suspect but Claude said no — record for reference _log_interaction(events, ts, iteration, suspects, False, None, reasoning) diff --git a/reports/20260420_074657_interactive.html b/reports/20260420_074657_interactive.html new file mode 100644 index 0000000..e63a109 --- /dev/null +++ b/reports/20260420_074657_interactive.html @@ -0,0 +1,131 @@ + + + + +MIPI Interactive Flicker Test — 2026-04-20 07:46:57 + + + + +

MIPI Interactive Flicker Test Report

+

+ Generated: 2026-04-20 07:46:57  |  + Model: claude-opus-4-6 +

+ +
+ Stop reason: Test interrupted by operator (Ctrl+C) +
+ +
+
0 confirmed flicker(s)
+
2 false alarm(s)
+
0 Claude said no
+
+ +

D-PHY Configuration

+

+ Pixel clock: 72.0 MHz  |  + Bit rate: 432.0 Mbit/s per lane  |  + Byte clock: 54.000 MHz + (18.519 ns/byte)  |  + UI: 2.315 ns +

+ + + + + + + + + + + + + + + + +
FieldSpec (ns)Rnd BestRnd UpExtraFinalActual (ns)Status
lpx≥ 50.033+0355.56
hs_prepare49.3 – 98.933+1474.07
hs_zero≥ 94.156+06111.11
hs_trail≥ 69.344+1592.59
hs_exit≥ 100.056+06111.11
clk_prepare38.0 – 95.023+0355.56
clk_zero≥ 244.41314+317314.81
clk_post≥ 180.41010+010185.19
clk_trail≥ 60.034+0474.07
+ +

✓ All D-PHY v1.1 Table 14 constraints satisfied.

+ +

Samsung DSIM Registers

+ + + + + + + + + + + + + + + + + +
RegisterAddressValueField breakdown
PHY_TIMING0xb40x00000306lpx=3   hs_exit=6
PHY_TIMING10xb80x03110a04clk_prepare=3   clk_zero=17   + clk_post=10   clk_trail=4
PHY_TIMING20xbc0x00040605hs_prepare=4   hs_zero=6   + hs_trail=5
+ +

u-boot Commands

+
# D-PHY PHY timing registers (pixel clock 72.0 MHz, 432.0 Mbit/s, byte clock 54.000 MHz)
+#
+# PHY_TIMING  (0xb4) = 0x00000306   lpx=3  hs_exit=6
+# PHY_TIMING1 (0xb8) = 0x03110a04   clk_prepare=3  clk_zero=17  clk_post=10  clk_trail=4
+# PHY_TIMING2 (0xbc) = 0x00040605   hs_prepare=4  hs_zero=6  hs_trail=5
+
+# Enable Round-Up rounding (dsi-tweak bit 2)
+setenv flb_dtovar "${flb_dtovar} dsi-tweak=4"
+
+# Extra PHY cycles above Round-Up minimum
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-hs-prepare=1"
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-hs-trail=1"
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-clk-zero=3"
+
+saveenv
+boot
+ +

Event Log

+ + + + + + + +
CaptureTimestampChannelLP-low plateauLP exit→HSLP-11 voltageClaude: flicker?Outcome
000220260420_074452dat107.8 ns3.1 ns1.017 VYES✓ FALSE ALARM
000420260420_074554dat107.4 ns1.2 ns1.016 VYES✓ FALSE ALARM
+ +

Claude Assessments

Capture 0002 [20260420_074452] — FALSE ALARM

YES
+
+The LP-low plateau at ~108 ns exceeds the 50 ns minimum, but the critical failure here is the **LP exit → HS transition of only 3 ns**, far below the 50 ns specification minimum. This means the LP-01 and LP-00 states that constitute the SoT preamble are essentially absent or too brief for the SN65DSI83 to reliably detect. Additionally, the **HS amplitude of 30 mV** is well below the normal 105–122 mV range and falls under the 50 mV "absent" threshold, indicating the bridge likely never locked onto the HS data. Together, the collapsed LP-exit timing and effectively absent HS signaling strongly indicate a missed SoT event that would produce visible flicker.

Capture 0004 [20260420_074554] — FALSE ALARM

YES
+
+The HS amplitude of only 32 mV (well below the 50 mV "absent" threshold and far from the normal 105–122 mV range) indicates the HS data burst was essentially not received by the SN65DSI83, even though the LP-low plateau at 107 ns nominally meets the ≥50 ns requirement. Critically, the LP exit → HS transition time of only 1 ns (spec ≥50 ns) means the LP-01/LP-00 states were not properly held long enough for the bridge to recognize the SoT preamble — the pre-processor itself flagged this as below spec. The combination of a collapsed LP-exit duration and an effectively absent HS swing strongly indicates the bridge missed start-of-transmission on this frame, which would produce visible flicker.
+ + + diff --git a/reports/20260420_091026_interactive.html b/reports/20260420_091026_interactive.html new file mode 100644 index 0000000..56ed5fd --- /dev/null +++ b/reports/20260420_091026_interactive.html @@ -0,0 +1,135 @@ + + + + +MIPI Interactive Flicker Test — 2026-04-20 09:10:26 + + + + +

MIPI Interactive Flicker Test Report

+

+ Generated: 2026-04-20 09:10:26  |  + Model: claude-opus-4-6 +

+ +
+ Stop reason: Test interrupted by operator (Ctrl+C) +
+ +
+
0 confirmed flicker(s)
+
4 false alarm(s)
+
0 Claude said no
+
+ +

D-PHY Configuration

+

+ Pixel clock: 72.0 MHz  |  + Bit rate: 432.0 Mbit/s per lane  |  + Byte clock: 54.000 MHz + (18.519 ns/byte)  |  + UI: 2.315 ns +

+ + + + + + + + + + + + + + + + +
FieldSpec (ns)Rnd BestRnd UpExtraFinalActual (ns)Status
lpx≥ 50.033+0355.56
hs_prepare49.3 – 98.933+1474.07
hs_zero≥ 94.156+06111.11
hs_trail≥ 69.344+1592.59
hs_exit≥ 100.056+06111.11
clk_prepare38.0 – 95.023+0355.56
clk_zero≥ 244.41314+317314.81
clk_post≥ 180.41010+010185.19
clk_trail≥ 60.034+0474.07
+ +

✓ All D-PHY v1.1 Table 14 constraints satisfied.

+ +

Samsung DSIM Registers

+ + + + + + + + + + + + + + + + + +
RegisterAddressValueField breakdown
PHY_TIMING0xb40x00000306lpx=3   hs_exit=6
PHY_TIMING10xb80x03110a04clk_prepare=3   clk_zero=17   + clk_post=10   clk_trail=4
PHY_TIMING20xbc0x00040605hs_prepare=4   hs_zero=6   + hs_trail=5
+ +

u-boot Commands

+
# D-PHY PHY timing registers (pixel clock 72.0 MHz, 432.0 Mbit/s, byte clock 54.000 MHz)
+#
+# PHY_TIMING  (0xb4) = 0x00000306   lpx=3  hs_exit=6
+# PHY_TIMING1 (0xb8) = 0x03110a04   clk_prepare=3  clk_zero=17  clk_post=10  clk_trail=4
+# PHY_TIMING2 (0xbc) = 0x00040605   hs_prepare=4  hs_zero=6  hs_trail=5
+
+# Enable Round-Up rounding (dsi-tweak bit 2)
+setenv flb_dtovar "${flb_dtovar} dsi-tweak=4"
+
+# Extra PHY cycles above Round-Up minimum
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-hs-prepare=1"
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-hs-trail=1"
+setenv flb_dtovar "${flb_dtovar} dsi-phy-extra-clk-zero=3"
+
+saveenv
+boot
+ +

Event Log

+ + + + + + + +
CaptureTimestampChannelLP-low plateauLP exit→HSLP-11 voltageClaude: flicker?Outcome
000620260420_090522dat6.4 ns0.1 ns1.016 VYES✓ FALSE ALARM
000720260420_090607dat108.0 ns3.4 ns1.016 VYES✓ FALSE ALARM
001120260420_090800datNone nsNone ns1.015 VYES✓ FALSE ALARM
001320260420_090915datNone ns0.3 ns1.016 VYES✓ FALSE ALARM
+ +

Claude Assessments

Capture 0006 [20260420_090522] — FALSE ALARM

YES
+
+The LP-low plateau of 6.4 ns is drastically below the SN65DSI83's required ≥ 50 ns minimum for SoT detection, making it virtually certain the bridge missed the start-of-transmission. This is further corroborated by the HS amplitude of only 46 mV—well below the normal 105–122 mV range and below the 50 mV "absent" threshold—indicating the HS data burst was either never properly initiated or was not recognized by the receiver. The follow-up capture shows a recovery to a 108 ns LP-low plateau and a healthy 114 mV HS amplitude, consistent with the pattern where a single corrupted frame causes a visible flicker one frame later while the link re-establishes normal operation.

Capture 0007 [20260420_090607] — FALSE ALARM

YES
+
+Although the LP-low plateau itself measures 108 ns (above the 50 ns minimum), the HS amplitude of only 21 mV is far below the normal 105–122 mV range and well under the 50 mV threshold for a valid HS signal. This means the SN65DSI83 bridge almost certainly cannot resolve the differential HS data even if SoT entry were detected. Additionally, the LP exit → HS transition of only 3 ns (spec ≥ 50 ns) indicates the LP-01/LP-00 states are effectively absent or too brief for reliable detection, compounding the problem. The combination of a virtually absent LP exit duration and critically low HS amplitude makes it highly likely the bridge missed or failed to lock onto the HS burst, producing a visible flicker event.

Capture 0011 [20260420_090800] — FALSE ALARM

YES
+
+The primary capture on the DAT0 lane shows an **absent LP-low plateau** (reported as `None`), meaning the transmitter never held LP-00/LP-01 long enough—or at all—for the SN65DSI83 to recognize a valid Start-of-Transmission preamble (≥ 50 ns required). Additionally, the HS amplitude is reported as `None`, confirming no usable HS burst was delivered in this frame. The follow-up capture corroborates the flicker scenario: although it does show an LP-low plateau of 380 ns (adequate timing), the HS amplitude is only **22 mV**—well below the 50 mV minimum detection threshold—meaning the bridge would fail to decode that burst as well. Taken together, the missing SoT preamble in the primary capture and the sub-threshold HS amplitude in the follow-up frame strongly indicate at least one (and likely two) consecutive frames were lost by the bridge, producing visible display flicker.

Capture 0013 [20260420_090915] — FALSE ALARM

YES
+
+The DAT0 lane shows an LP-low plateau of effectively 0 ns (flagged as absent/None), far below the SN65DSI83's required ≥ 50 ns minimum for SoT detection. The LP exit → HS transition time of 0 ns confirms that the LP-01/LP-00 preamble states were either entirely skipped or too brief to be resolved, meaning the bridge almost certainly missed the start-of-transmission. The follow-up capture at 090936 corroborates this: no LP-11 state, no LP→HS transition, and no HS bursts were detected, consistent with the bridge having lost synchronization and the link being in a broken/stalled state — exactly the pattern that produces visible flicker (or a blank frame) on the display.
+ + + diff --git a/reports/flicker_log.csv b/reports/flicker_log.csv index 7c651a9..a870f65 100644 --- a/reports/flicker_log.csv +++ b/reports/flicker_log.csv @@ -211,3 +211,10 @@ logged_at,capture_ts,capture_num,channel,lp_low_duration_ns,lp11_to_hs_ns,lp11_v 2026-04-17 14:42:52,20260417_144230,0355,dat,108.0,2.0,1.015 2026-04-17 14:44:36,20260417_144415,0359,dat,379.6,384.6,1.015 2026-04-17 14:48:11,20260417_144749,0368,dat,107.8,2.0,1.016 +2026-04-20 07:45:14,20260420_074452,0002,dat,107.8,3.1,1.017 +2026-04-20 07:46:15,20260420_074554,0004,dat,107.4,1.2,1.016 +2026-04-20 09:05:43,20260420_090522,0006,dat,6.4,0.1,1.016 +2026-04-20 09:06:29,20260420_090607,0007,dat,108.0,3.4,1.016 +2026-04-20 09:08:22,20260420_090800,0011,dat,,,1.015 +2026-04-20 09:08:29,20260420_090822,0011,dat,379.6,384.8,1.015 +2026-04-20 09:09:36,20260420_090915,0013,dat,,0.3,1.016 diff --git a/reports/interactive_log.csv b/reports/interactive_log.csv index 9c8d02d..3a1d735 100644 --- a/reports/interactive_log.csv +++ b/reports/interactive_log.csv @@ -141,3 +141,9 @@ logged_at,capture_ts,capture_num,claude_said_flicker,user_confirmed,lp_low_ns,re 2026-04-17 14:43:04,20260417_144230,0355,YES,NO,108.0,"YES The LP-low plateau of 108 ns meets the ≥50 ns requirement, but the LP exit-to-HS transition of only 2 ns is critically below the 50 ns spec minim" 2026-04-17 14:44:43,20260417_144415,0359,NO,NOT_ASKED,379.6,NO The LP-low plateau of 379.6 ns and the LP-11→HS transition time of 384.6 ns both comfortably exceed the SN65DSI83's 50 ns minimum requirement for 2026-04-17 14:48:47,20260417_144749,0368,YES,YES,107.8,YES The HS amplitude of 32 mV is critically low — well below the SN65DSI83's minimum differential detection threshold (typically ~70 mV single-ended +2026-04-20 07:45:30,20260420_074452,0002,YES,NO,107.8,"YES The LP-low plateau at ~108 ns exceeds the 50 ns minimum, but the critical failure here is the **LP exit → HS transition of only 3 ns**, far below" +2026-04-20 07:46:31,20260420_074554,0004,YES,NO,107.4,"YES The HS amplitude of only 32 mV (well below the 50 mV ""absent"" threshold and far from the normal 105–122 mV range) indicates the HS data burst was" +2026-04-20 09:06:06,20260420_090522,0006,YES,NO,6.4,"YES The LP-low plateau of 6.4 ns is drastically below the SN65DSI83's required ≥ 50 ns minimum for SoT detection, making it virtually certain the bri" +2026-04-20 09:06:50,20260420_090607,0007,YES,NO,108.0,"YES Although the LP-low plateau itself measures 108 ns (above the 50 ns minimum), the HS amplitude of only 21 mV is far below the normal 105–122 mV r" +2026-04-20 09:08:51,20260420_090800,0011,YES,NO,,"YES The primary capture on the DAT0 lane shows an **absent LP-low plateau** (reported as `None`), meaning the transmitter never held LP-00/LP-01 long" +2026-04-20 09:09:58,20260420_090915,0013,YES,NO,,"YES The DAT0 lane shows an LP-low plateau of effectively 0 ns (flagged as absent/None), far below the SN65DSI83's required ≥ 50 ns minimum for SoT de" diff --git a/rigol_scope.py b/rigol_scope.py index 313fb44..f618c1e 100644 --- a/rigol_scope.py +++ b/rigol_scope.py @@ -21,6 +21,14 @@ V18_TIMEBASE = 1e-6 # s/div — 1 µs/div = 10 µs total window V18_TRIG_LEVEL = 1.76 # V — falling-edge trigger on supply droop > 40 mV TRIG_TIMEOUT_S = 15.0 # s — wait this long for Rigol to capture after arming +# CH2 — SN65DSI83 IRQ pin (CMOS output, active HIGH, high-impedance when IRQ_EN=0) +# CSR 0xE0.0 IRQ_EN=0 (default): pin is high-impedance → reads ~0 V (no pull on PCB, normal) +# IRQ_EN=1, no error: driven LOW (~0 V) +# IRQ_EN=1, error asserted: driven HIGH (~1.25 V min per VOH spec) +# No pull-up required — CMOS output drives both high and low. +INT_V_SCALE = 0.2 # V/div — shows 0–~1.8 V range clearly +INT_V_OFFSET = -0.9 # V — centres display on 0.9 V midpoint + rigol: vxi11.Instrument | None = None @@ -62,19 +70,27 @@ def is_connected() -> bool: def configure(): """ - Configure Rigol for 1.8 V supply monitoring. + Configure Rigol CH1 for 1.8 V supply monitoring and CH2 for SN65DSI83 INTB pin. AUTO trigger sweep: if no droop occurs, scope still captures on timeout so we always get a supply snapshot even when the rail is healthy. """ rigol.write(":STOP") time.sleep(0.2) + # CH1 — 1.8 V supply rail rigol.write(":CHANnel1:DISPlay 1") - rigol.write(":CHANnel2:DISPlay 0") rigol.write(":CHANnel1:COUPling DC") rigol.write(":CHANnel1:PROBe 10") rigol.write(f":CHANnel1:SCALe {V18_SCALE:.3f}") rigol.write(f":CHANnel1:OFFSet {V18_OFFSET:.3f}") + + # CH2 — SN65DSI83 INTB pin (active-low open-drain, external 10 kΩ pull-up to 1.8 V required) + rigol.write(":CHANnel2:DISPlay 1") + rigol.write(":CHANnel2:COUPling DC") + rigol.write(":CHANnel2:PROBe 1") # direct probe, no attenuation + rigol.write(f":CHANnel2:SCALe {INT_V_SCALE:.3f}") + rigol.write(f":CHANnel2:OFFSet {INT_V_OFFSET:.3f}") + rigol.write(f":TIMebase:MAIN:SCALe {V18_TIMEBASE:.2E}") rigol.write(":TRIGger:MODE EDGE") rigol.write(":TRIGger:EDGe:SOURce CHANnel1") @@ -85,7 +101,7 @@ def configure(): rigol.write(":RUN") # start acquiring immediately after configure time.sleep(0.2) - print(f"[RIGOL] Configured: 1.8 V rail, {int(V18_TIMEBASE*1e6)} µs/div, " + print(f"[RIGOL] Configured: CH1=1.8 V rail, CH2=INTB pin, {int(V18_TIMEBASE*1e6)} µs/div, " f"trigger <{V18_TRIG_LEVEL} V falling (AUTO sweep, running)") @@ -117,24 +133,21 @@ def wait_captured(timeout_s: float = TRIG_TIMEOUT_S) -> bool: return False -def read_waveform_csv(path: Path) -> int: +def _read_channel_csv(channel: str, path: Path, stop_first: bool = True) -> int: """ - Read Ch1 waveform from Rigol over SCPI and write to CSV. - Sends :STOP first to ensure acquisition is complete before reading — - this is reliable regardless of trigger/status state. - + Read one Rigol channel waveform over SCPI and write to CSV. + stop_first=False skips :STOP when the scope was already stopped by a prior read. Returns the number of samples written, or 0 on error. """ try: - rigol.write(":STOP") - time.sleep(0.3) - - rigol.write(":WAVeform:SOURce CHANnel1") + if stop_first: + rigol.write(":STOP") + time.sleep(0.3) + rigol.write(f":WAVeform:SOURce {channel}") rigol.write(":WAVeform:FORMat ASC") # Rigol DS1000Z uses ASC not ASCII time.sleep(0.1) - except Exception as e: - print(f"[RIGOL] Waveform setup error: {e}") + print(f"[RIGOL] {channel} waveform setup error: {e}") return 0 try: @@ -145,7 +158,7 @@ def read_waveform_csv(path: Path) -> int: x_orig = float(preamble[5]) x_ref = float(preamble[6]) except Exception as e: - print(f"[RIGOL] Preamble error: {e}") + print(f"[RIGOL] {channel} preamble error: {e}") return 0 try: @@ -158,11 +171,11 @@ def read_waveform_csv(path: Path) -> int: vals = [float(v) for v in raw.split(",") if v.strip()] except Exception as e: - print(f"[RIGOL] Data read error: {e}") + print(f"[RIGOL] {channel} data read error: {e}") return 0 if not vals: - print("[RIGOL] No samples parsed — check scope channel and format settings") + print(f"[RIGOL] {channel}: no samples parsed — check channel and format settings") return 0 try: @@ -175,5 +188,18 @@ def read_waveform_csv(path: Path) -> int: writer.writerow([f"{t:.9f}", f"{v:.6f}"]) return len(vals) except Exception as e: - print(f"[RIGOL] CSV write error: {e}") + print(f"[RIGOL] {channel} CSV write error: {e}") return 0 + + +def read_waveform_csv(path: Path) -> int: + """Read CH1 (1.8 V supply) waveform from Rigol and write to CSV.""" + return _read_channel_csv("CHANnel1", path, stop_first=True) + + +def read_int_csv(path: Path) -> int: + """ + Read CH2 (SN65DSI83 INTB pin) waveform from Rigol and write to CSV. + Must be called after read_waveform_csv() — scope is already stopped. + """ + return _read_channel_csv("CHANnel2", path, stop_first=False)