From 106f03d80624548890b9cc0c60cca8ddf1c51ab8 Mon Sep 17 00:00:00 2001 From: david rice Date: Thu, 2 Apr 2026 15:56:20 +0100 Subject: [PATCH] Changes --- mipi_test.py | 252 +++++++++++++++++++++++++++++++++++---------------- 1 file changed, 174 insertions(+), 78 deletions(-) diff --git a/mipi_test.py b/mipi_test.py index 28980e5..d4958e9 100644 --- a/mipi_test.py +++ b/mipi_test.py @@ -3,7 +3,7 @@ MIPI TEST APPLICATION - MIPI_TEST.PY - ENTRY POINT OF APPLICATION -VERSION: 0.1 +VERSION: 0.2 AUTHOR: D. RICE 25/03/2026 © 2026 ARRIVE """ @@ -12,149 +12,245 @@ import time import sys import requests import threading -import os # --- Configuration --- -URL = "http://192.168.45.8:5000/display" -SCOPE_IP = "192.168.45.4" -PSU_IP = "192.168.45.3" +URL = "http://192.168.45.8:5000/display" +SCOPE_IP = "192.168.45.4" +PSU_IP = "192.168.45.3" test_running = False # Global flag to control the background thread # --- Instrument Connection --- try: - psu = vxi11.Instrument("192.168.45.3") - scope = vxi11.Instrument("192.168.45.4") - # Increase timeout for large image data transfers - scope.timeout = 5 + psu = vxi11.Instrument(PSU_IP) + scope = vxi11.Instrument(SCOPE_IP) + scope.timeout = 30 # seconds — needs to cover *RST and image saves + psu.timeout = 5 except Exception as e: print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}") sys.exit(1) +# --------------------------------------------------------------------------- + def setup_scope(): - """Initializes DSO80204B for 210MHz MIPI Signals""" - print("Configuring Scope...") - scope.write("*RST") - time.sleep(1) + """Initialises DSO80204B for MIPI DSI signals (~210 MHz).""" + print("CONFIGURING SCOPE...") - # Setup Channels (1&2=Clock, 3&4=Data) - for ch in range(1, 5): - scope.write(f":CHANnel{ch}:DISPlay ON") - scope.write(f":CHANnel{ch}:PROBe 19.2") # 910R/50R Divider - scope.write(f":CHANnel{ch}:INPut DC50") - scope.write(f":CHANnel{ch}:SCALe 0.1") # 100mV/div (200mV HS swing) - scope.write(f":CHANnel{ch}:OFFSet 0.0") + cmds = [ + # ── Reset & stop ────────────────────────────────────────────────── + "*RST", + ":RUN", + ":STOP", + + # ── Channel 1 — Clock D+ ───────────────────────────────────────── + ":CHANnel1:DISPlay ON", + ":CHANnel1:INPut DC50", + ":CHANnel1:PROBe 19.2", # 910Ω + 50Ω divider = 19.2:1 + ":CHANnel1:SCALe 0.1", # 100 mV/div + ":CHANnel1:OFFSet 0.0", + ":CHANnel1:LABel 'CLK+'", + + # ── Channel 2 — Clock D- ───────────────────────────────────────── + ":CHANnel2:DISPlay ON", + ":CHANnel2:INPut DC50", + ":CHANnel2:PROBe 19.2", + ":CHANnel2:SCALe 0.1", + ":CHANnel2:OFFSet 0.0", + ":CHANnel2:LABel 'CLK-'", + + # ── Channel 3 — Data Lane 0 D+ ─────────────────────────────────── + ":CHANnel3:DISPlay ON", + ":CHANnel3:INPut DC50", + ":CHANnel3:PROBe 19.2", + ":CHANnel3:SCALe 0.1", + ":CHANnel3:OFFSet 0.0", + ":CHANnel3:LABel 'DAT0+'", + + # ── Channel 4 — Data Lane 0 D- ─────────────────────────────────── + ":CHANnel4:DISPlay ON", + ":CHANnel4:INPut DC50", + ":CHANnel4:PROBe 19.2", + ":CHANnel4:SCALe 0.1", + ":CHANnel4:OFFSet 0.0", + ":CHANnel4:LABel 'DAT0-'", + + # ── Timebase — 5 ns/div shows ~10 cycles of 200 MHz clock ──────── + ":TIMebase:SCALe 5E-9", + ":TIMebase:POSition 0", + ":TIMebase:REFerence CENTer", + + # ── Trigger — rising edge on Ch1 (Clock D+) ────────────────────── + ":TRIGger:MODE EDGE", + ":TRIGger:EDGE:SOURce CHANnel1", + ":TRIGger:EDGE:SLOPe POSitive", + ":TRIGger:EDGE:LEVel 0.05", # 50 mV post-attenuation + ":TRIGger:SWEep NORMal", + + # ── Acquisition ─────────────────────────────────────────────────── + ":ACQuire:MODE RTIMe", + ":ACQuire:INTerpolate ON", + ":ACQuire:POINts 500000", + + # ── Display ─────────────────────────────────────────────────────── + ":DISPlay:LAYout STACKED", + + ":RUN", + ] + + for cmd in cmds: + scope.write(cmd) + time.sleep(0.05) + + print("CHANNEL SETUP COMPLETE.") + setup_math_channels() + + +def setup_math_channels(): + """ + F1 = Ch1 - Ch2 (clock differential) + F2 = Ch3 - Ch4 (lane 0 differential) + + DSO80204B firmware 05.30.0005 confirmed syntax: + :FUNCtion:DISPlay ON + :FUNCtion:SUBTract CHANnel,CHANnel + """ + print("SETTING UP MATH CHANNELS...") + + scope.write("*CLS") + time.sleep(0.2) + + math_cmds = [ + # ── F1 = Ch1 - Ch2 (clock differential) ───────────────────────── + ":FUNCtion1:DISPlay ON", + ":FUNCtion1:SUBTract CHANnel1,CHANnel2", + ":FUNCtion1:RANGe 0.8", # 0.8V range = 100mV/div (range = 8 × scale) + ":FUNCtion1:OFFSet 0.0", + + # ── F2 = Ch3 - Ch4 (lane 0 differential) ───────────────────────── + ":FUNCtion2:DISPlay ON", + ":FUNCtion2:SUBTract CHANnel3,CHANnel4", + ":FUNCtion2:RANGe 0.8", # 0.8V range = 100mV/div + ":FUNCtion2:OFFSet 0.0", + ] + + for cmd in math_cmds: + scope.write(cmd) + time.sleep(0.2) + + try: + time.sleep(1.0) + opc = scope.ask("*OPC?") + print(f" SCOPE SYNC OK (OPC={opc.strip()})") + except Exception as e: + print(f" WARNING: OPC SYNC FAILED ({e})") + + try: + err = scope.ask(":SYSTem:ERRor?") + if err.strip().startswith("0"): + print(" MATH COMMANDS ACCEPTED — NO SCPI ERRORS.") + print(" F1 = CLK DIFF (CH1-CH2), F2 = DAT DIFF (CH3-CH4)") + else: + print(f" SCPI ERROR: {err.strip()}") + except Exception as e: + print(f" COULD NOT READ ERROR QUEUE ({e})") - # Timebase & Trigger - scope.write(":TIMebase:SCALe 10e-6") # 10us/div - scope.write(":TRIGger:EDGE:SOURce CHANnel3") - scope.write(":TRIGger:EDGE:SLOPe FALLing") # Catch LP-11 to HS transition - scope.write(":TRIGger:LEVel CHANnel3, 0.15") # 150mV - scope.write(":TRIGger:SWEep SINGle") - print("Scope Configured.") def save_screenshot(iteration): + """Save a PNG screenshot to C:\\TEMP on the scope's local disk.""" try: - scope.clear() - # Use a very short, simple path. No subdirectories if possible. - # Ensure C:\TEMP exists! - filename = f"C:\\TEMP\\cap{iteration}.png" - - print(f"ATTEMPTING SIMPLEST XP SYNTAX: {filename}") - - # Try ONLY the filename and the format. - # Many XP versions hate the SCR,COL extra parameters via SCPI. - cmd = f':DISK:SAVE:IMAGe "{filename}",PNG' - - scope.write(cmd) - - # DO NOT use scope.ask() or any queries yet. - # Just sleep and check the folder manually. - time.sleep(5.0) - print("COMMAND SENT. CHECK C:\\TEMP") - + filename = f"C:\\TEMP\\cap{iteration:04d}.png" + print(f"SAVING SCREENSHOT: {filename}") + scope.write(f':DISK:SAVE:IMAGe "{filename}",PNG') + time.sleep(5.0) # wait for scope to write file — do not query during this + print("SCREENSHOT SAVED.") except Exception as e: - print(f"XP COMM ERROR: {e}") + print(f"SCREENSHOT ERROR: {e}") + def test_worker(on_time): - """ Background loop: - - Sets display ON for user-defined duration. - - Sets display OFF for exactly 1 second. - """ + """ + Background loop: + - Arms scope in single trigger mode + - Turns display ON for on_time seconds + - Saves screenshot + - Turns display OFF for 1 second + - Repeats until test_running = False + """ global test_running count = 1 - + while test_running: - # Arm scope scope.write(":SINGle") - - # Start display requests.put(URL, json={"state": "on"}, timeout=2) - - # Wait for the scope to trigger and fill memory - # We wait slightly longer than the 'on_time' to ensure capture is done - time.sleep(on_time + 0.5) - - # Pull the image + time.sleep(on_time + 0.5) # +0.5s to ensure acquisition completes save_screenshot(count) count += 1 - - # Set Display OFF requests.put(URL, json={"state": "off"}, timeout=2) - - # Fixed 1s OFF period time.sleep(1.0) + def main_menu(): global test_running - """Main Application Loop.""" while True: print("\n===== MIPI TEST CONTROL =====") print("1. RUN IDN CHECK (PSU & SCOPE)") print("2. SETUP SCOPE (RUN FIRST)") - print("3. CONFIGURE PSU (DEFAULT 24V/1.5A)") + print("3. CONFIGURE PSU (DEFAULT 24V / 1.5A)") print("4. PSU OUTPUT ON/OFF (CH1)") print("5. START TEST & CAPTURE") print("6. STOP TEST") print("7. EXIT") - - choice = input("\nSELECT OPTION (1-7): ") + + choice = input("\nSELECT OPTION (1-7): ").strip() if choice == '1': - print(f"PSU: {psu.ask('*IDN?')}") - print(f"SCOPE: {scope.ask('*IDN?')}") + print(f"PSU : {psu.ask('*IDN?').strip()}") + print(f"SCOPE: {scope.ask('*IDN?').strip()}") + elif choice == '2': setup_scope() + elif choice == '3': psu.write('CH1:VOLT 24.0') psu.write('CH1:CURR 1.5') - print("PSU CONFIGURED...") + print("PSU CONFIGURED: 24V / 1.5A") + elif choice == '4': - state = input("TYPE 'ON' OR 'OFF': ").upper() - psu.write(f'OUTP CH1,{state}') + state = input("TYPE 'ON' OR 'OFF': ").strip().upper() + if state in ('ON', 'OFF'): + psu.write(f'OUTP CH1,{state}') + print(f"PSU OUTPUT {state}.") + else: + print("INVALID — TYPE 'ON' OR 'OFF'.") + elif choice == '5': if not test_running: try: - sec = float(input("ON DURATION (sec): ")) + sec = float(input("ON DURATION (seconds): ")) test_running = True t = threading.Thread(target=test_worker, args=(sec,), daemon=True) t.start() + print(f"TEST STARTED — ON TIME: {sec}s") except ValueError: print("INVALID TIME ENTERED.") else: print("TEST IS ALREADY RUNNING!") + elif choice == '6': print("STOPPING TEST...") test_running = False - elif choice == '7': - test_running = False # Ensure thread stops + + elif choice == '7': + test_running = False psu.close() scope.close() + print("INSTRUMENTS CLOSED. BYE.") break + else: - print("INVALID ENTRY. PLEASE CHOOSE 1-6") + print("INVALID ENTRY. PLEASE CHOOSE 1-7.") + if __name__ == "__main__": - # Optional: Run auto-config on startup main_menu() \ No newline at end of file