Files
MiPi_TEST/mipi_test.py

503 lines
18 KiB
Python
Raw Normal View History

2026-03-26 11:43:36 +00:00
#!/usr/bin/env python3
"""
MIPI TEST APPLICATION - MIPI_TEST.PY
- ENTRY POINT OF APPLICATION
2026-04-02 16:04:45 +01:00
VERSION: 0.3
2026-03-26 11:43:36 +00:00
AUTHOR: D. RICE 25/03/2026
© 2026 ARRIVE
"""
2026-04-09 10:29:53 +01:00
import json
2026-03-26 11:43:36 +00:00
import vxi11
import time
import sys
import requests
import threading
2026-04-02 16:04:45 +01:00
from datetime import datetime
2026-04-09 08:45:57 +01:00
from pathlib import Path
2026-04-07 15:48:40 +01:00
import ai_mgmt
2026-04-08 14:19:31 +01:00
import analyze_captures
2026-04-09 08:45:57 +01:00
import rigol_scope
2026-03-26 15:18:58 +00:00
# --- Configuration ---
2026-04-09 10:29:53 +01:00
DEVICE_BASE = "http://192.168.45.8:5000"
URL = f"{DEVICE_BASE}/display"
2026-04-07 15:48:40 +01:00
SCOPE_IP = "192.168.45.4"
PSU_IP = "192.168.45.3"
2026-04-09 11:30:22 +01:00
MGMT_INTERVAL = 3600 # seconds between management runs (3600 = 1 hour)
2026-03-26 11:43:36 +00:00
2026-04-02 16:04:45 +01:00
# --- Capture settings ---
# Pass 1 — signal quality: resolves individual bits at 140 Mbit/s (7.1 ns/bit)
SIG_SCALE = 2e-9 # 2 ns/div → 20 ns window
SIG_POINTS = 500_000 # 500 k pts → ~25 GSa/s
# Pass 2 — protocol/frame structure: shows LP↔HS transitions and burst envelope
PROTO_SCALE = 1e-6 # 1 µs/div → 10 µs window
PROTO_POINTS = 500_000 # 500 k pts → 50 MSa/s (enough to see burst structure)
2026-04-08 12:55:34 +01:00
# Pass 3 — LP state capture: widens vertical range to show LP-11 (~1.2 V)
# Channels reconfigured to 200 mV/div, offset +0.6 V → display spans 0.2 V to 1.4 V.
# Saves Ch1 (CLK+) and Ch3 (DAT0+) single-ended so LP-11/LP-00 are distinguishable.
# Trigger: falling edge on Ch1 at 0.6 V → catches LP-11 → LP-01 SoT transition.
LP_SCALE = 500e-9 # 500 ns/div → 5 µs window
LP_POINTS = 200_000 # 200 k pts → ~40 GSa/s
LP_V_SCALE = 0.2 # V/div — 8 divs = 1.6 V range
LP_V_OFFSET = 0.6 # V — center display at 0.6 V (range 0.2 V to 1.4 V)
LP_TRIG_LEVEL = 0.6 # V — midpoint of LP-11 (1.2 V) → LP-01 (0 V) fall
2026-04-02 16:08:50 +01:00
DISPLAY_SETTLE_S = 1.0 # seconds to wait after display ON before arming scope
2026-04-09 08:45:57 +01:00
DATA_DIR = Path(__file__).parent / "data"
2026-04-02 16:08:50 +01:00
2026-04-07 15:48:40 +01:00
test_running = False # controls both worker threads
resume_event = threading.Event() # cleared to pause test_worker, set to resume
capture_done = threading.Event() # set when a full dual_capture (all 4 files) completes
2026-03-26 11:43:36 +00:00
2026-03-26 15:18:58 +00:00
# --- Instrument Connection ---
2026-03-26 11:43:36 +00:00
try:
2026-04-02 15:56:20 +01:00
psu = vxi11.Instrument(PSU_IP)
scope = vxi11.Instrument(SCOPE_IP)
scope.timeout = 30 # seconds — needs to cover *RST and image saves
psu.timeout = 5
2026-03-26 11:43:36 +00:00
except Exception as e:
print(f"ERROR: CANNOT CONNECT TO INSTRUMENTS: {e}")
sys.exit(1)
2026-04-09 08:45:57 +01:00
# Rigol DS1202Z-E for 1.8 V supply monitoring (optional — test continues if unavailable)
rigol_scope.connect()
2026-04-02 15:56:20 +01:00
# ---------------------------------------------------------------------------
2026-03-26 15:18:58 +00:00
def setup_scope():
2026-04-02 15:56:20 +01:00
"""Initialises DSO80204B for MIPI DSI signals (~210 MHz)."""
print("CONFIGURING SCOPE...")
2026-03-26 15:18:58 +00:00
2026-04-02 15:56:20 +01:00
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:
2026-03-26 15:18:58 +00:00
scope.write(cmd)
2026-04-02 15:56:20 +01:00
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<n>:DISPlay ON
:FUNCtion<n>:SUBTract CHANnel<a>,CHANnel<b>
"""
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})")
2026-03-26 15:18:58 +00:00
2026-04-02 15:56:20 +01:00
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()}")
2026-03-26 15:18:58 +00:00
except Exception as e:
2026-04-02 15:56:20 +01:00
print(f" COULD NOT READ ERROR QUEUE ({e})")
2026-04-02 16:04:45 +01:00
def _set_timebase(scale, points):
"""Apply timebase scale and record length, then let the scope settle."""
scope.write(f":TIMebase:SCALe {scale:.3E}")
scope.write(f":ACQuire:POINts {points}")
time.sleep(0.3)
def _arm_and_wait(timeout=20):
"""
2026-04-02 16:17:47 +01:00
Fire a single acquisition using :DIGitize (blocking on Infiniium) and
confirm completion with *OPC?. Temporarily extends scope.timeout to
cover the full wait period.
Returns True on success, False on error/timeout.
2026-04-02 16:04:45 +01:00
"""
2026-04-02 16:17:47 +01:00
prev_timeout = scope.timeout
try:
scope.timeout = timeout + 5 # headroom over the DIGitize wait
scope.write(":DIGitize")
resp = scope.ask("*OPC?").strip()
return resp == "1"
except Exception as e:
print(f" ACQUIRE ERROR: {e}")
return False
finally:
scope.timeout = prev_timeout
2026-04-02 16:04:45 +01:00
def _save_pass(tag, iteration, ts):
"""
Save F1 (CLK diff) and F2 (DAT diff) as CSV, plus a PNG screenshot.
Files land in C:\\TEMP on the scope's local disk.
Naming: <YYYYMMDD_HHMMSS>_<tag>_<iter>_clk.csv (date-first for easy sorting/deletion)
"""
base = f"C:\\TEMP\\{ts}_{tag}_{iteration:04d}"
2026-04-02 15:56:20 +01:00
try:
2026-04-02 16:04:45 +01:00
scope.write(f':DISK:SAVE:WAVeform FUNCtion1,"{base}_clk.csv",CSV')
time.sleep(2.5)
scope.write(f':DISK:SAVE:WAVeform FUNCtion2,"{base}_dat.csv",CSV')
time.sleep(2.5)
print(f" SAVED: {base}_clk.csv {base}_dat.csv")
2026-04-02 15:56:20 +01:00
except Exception as e:
2026-04-02 16:04:45 +01:00
print(f" SAVE ERROR ({tag}): {e}")
2026-04-08 12:55:34 +01:00
def _save_pass_channels(tag, iteration, ts):
"""
Save Ch1 (CLK+) and Ch3 (DAT0+) as single-ended CSV for LP state analysis.
Single-ended is required for LP because differential (F1/F2) cannot distinguish
LP-11 (Vcm=1.2 V) from LP-00 (Vcm=0 V) both give Vdiff=0.
"""
base = f"C:\\TEMP\\{ts}_{tag}_{iteration:04d}"
try:
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)
print(f" SAVED: {base}_clk.csv {base}_dat.csv")
except Exception as e:
print(f" SAVE ERROR ({tag}): {e}")
def _configure_for_lp():
"""
Widen channel vertical scales to capture LP states and switch to a
falling-edge trigger to catch the LP-11 LP-01 SoT transition.
"""
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)
2026-04-08 14:19:31 +01:00
# Trigger on DAT0+ (Ch3) — CLK is continuous HS so it never reaches LP-11 (1.2 V).
# DAT0 has LP-11 between bursts, so Ch3 falling at 0.6 V catches LP-11 → LP-01.
scope.write(":TRIGger:EDGE:SOURce CHANnel3")
2026-04-08 12:55:34 +01:00
scope.write(":TRIGger:EDGE:SLOPe NEGative")
scope.write(f":TRIGger:EDGE:LEVel {LP_TRIG_LEVEL:.3f}")
time.sleep(0.1)
def _restore_hs_config():
"""Restore HS-mode channel scales, offsets, and trigger after LP capture."""
for ch in (1, 2, 3, 4):
scope.write(f":CHANnel{ch}:SCALe 0.1")
scope.write(f":CHANnel{ch}:OFFSet 0.0")
time.sleep(0.05)
2026-04-08 14:19:31 +01:00
scope.write(":TRIGger:EDGE:SOURce CHANnel1")
2026-04-08 12:55:34 +01:00
scope.write(":TRIGger:EDGE:SLOPe POSitive")
scope.write(f":TRIGger:EDGE:LEVel 0.05")
time.sleep(0.1)
2026-04-09 10:29:53 +01:00
def _fetch_registers(ts: str, iteration: int) -> None:
"""
GET /registers from the device Flask server and save to data/ as JSON.
Reads MIPI DSI PHY timing registers via memtool on the target.
Non-fatal a failed fetch prints a warning and returns without crashing.
"""
try:
resp = requests.get(f"{DEVICE_BASE}/registers", timeout=5)
resp.raise_for_status()
data = resp.json()
if data.get("errors"):
print(f" REGISTERS: device warnings — {data['errors']}")
DATA_DIR.mkdir(exist_ok=True)
reg_path = DATA_DIR / f"{ts}_reg_{iteration:04d}.json"
reg_path.write_text(json.dumps(data, indent=2))
n = len(data.get("registers", []))
print(f" SAVED: {reg_path.name} ({n} registers)")
except requests.exceptions.RequestException as e:
print(f" REGISTERS: fetch failed — {e}")
except Exception as e:
print(f" REGISTERS: error — {e}")
2026-04-02 16:04:45 +01:00
def dual_capture(iteration):
"""
2026-04-09 10:29:53 +01:00
Three-pass capture per test iteration. LP is captured FIRST so it catches
the SoT transition at pipeline startup the moment flicker can occur.
HS quality and frame structure passes follow once the link is stable.
Pass 1 LP / SoT startup (no settle delay fires immediately after display ON)
Pass 2 signal quality (HS differential, rise/fall)
Pass 3 frame structure (HS differential, jitter/freq)
2026-04-02 16:04:45 +01:00
"""
2026-04-07 15:48:40 +01:00
capture_done.clear()
2026-04-02 16:04:45 +01:00
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
2026-04-09 10:29:53 +01:00
print(f"CAPTURE #{iteration:04d} [{ts}]")
2026-04-02 16:04:45 +01:00
2026-04-09 10:29:53 +01:00
# ── Pass 1: LP / SoT startup transition ───────────────────────────────
# Fired immediately after display ON (test_worker has no settle delay).
# Catches the first LP-11 → LP-01 → LP-00 → HS SoT sequence, which is
# where violations causing screen flicker occur.
print(" PASS 1: LP STARTUP TRANSITION...")
2026-04-08 12:55:34 +01:00
_configure_for_lp()
_set_timebase(LP_SCALE, LP_POINTS)
2026-04-09 08:45:57 +01:00
if rigol_scope.is_connected():
2026-04-09 10:29:53 +01:00
rigol_scope.arm() # arm before Agilent so 1.8 V droop is captured
2026-04-09 08:45:57 +01:00
2026-04-08 12:55:34 +01:00
if _arm_and_wait(timeout=30):
_save_pass_channels("lp", iteration, ts)
else:
2026-04-09 10:29:53 +01:00
print(" SKIPPING LP SAVE.")
2026-04-09 08:45:57 +01:00
if rigol_scope.is_connected():
2026-04-09 09:17:42 +01:00
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)")
2026-04-09 08:45:57 +01:00
else:
2026-04-09 09:17:42 +01:00
print(" RIGOL: Waveform read failed — check connection and probe.")
2026-04-09 08:45:57 +01:00
2026-04-08 12:55:34 +01:00
_restore_hs_config()
2026-04-09 10:29:53 +01:00
# ── Pass 2: HS signal quality ──────────────────────────────────────────
# LP pass takes ~510 s total; the HS link is fully settled by now.
print(" PASS 2: SIGNAL QUALITY...")
_set_timebase(SIG_SCALE, SIG_POINTS)
if _arm_and_wait():
_save_pass("sig", iteration, ts)
else:
print(" SKIPPING SIG SAVE.")
# ── Pass 3: frame/protocol structure ──────────────────────────────────
print(" PASS 3: FRAME STRUCTURE...")
_set_timebase(PROTO_SCALE, PROTO_POINTS)
if _arm_and_wait():
_save_pass("proto", iteration, ts)
else:
print(" SKIPPING PROTO SAVE.")
# ── Fetch DSI register snapshot from device ───────────────────────────
# Display is still ON here; registers reflect the active pipeline state.
_fetch_registers(ts, iteration)
2026-04-02 16:04:45 +01:00
# ── Restore original timebase ─────────────────────────────────────────
_set_timebase(5e-9, 500_000)
scope.write(":RUN")
2026-04-07 15:48:40 +01:00
capture_done.set()
def mgmt_worker():
"""
Fires every MGMT_INTERVAL seconds while a test is running:
- Pauses test_worker via resume_event
- Runs ai_mgmt CSV scan
- Resumes test_worker
"""
while test_running:
time.sleep(MGMT_INTERVAL)
if not test_running:
break
print("\n[MGMT] WAITING FOR CURRENT CAPTURE TO COMPLETE...")
capture_done.wait()
print("[MGMT] PAUSING TEST — RUNNING MANAGEMENT TASKS...")
resume_event.clear()
try:
2026-04-07 15:58:01 +01:00
copied, failed = ai_mgmt.transfer_csv_files()
print(f"[MGMT] TRANSFERRED {copied} FILE(S) TO DATA FOLDER. {failed} FAILED.")
2026-04-08 14:19:31 +01:00
if copied > 0:
try:
2026-04-09 10:29:53 +01:00
analyze_captures.run_analysis(last=30)
2026-04-08 14:19:31 +01:00
except Exception as e:
print(f"[MGMT] ANALYSIS ERROR: {e}")
2026-04-07 15:48:40 +01:00
except Exception as e:
2026-04-08 14:19:31 +01:00
print(f"[MGMT] TRANSFER ERROR: {e}")
2026-04-07 15:48:40 +01:00
finally:
resume_event.set()
print("[MGMT] RESUMING TEST.\n")
2026-04-02 15:56:20 +01:00
2026-03-26 15:18:58 +00:00
2026-04-02 16:08:50 +01:00
def test_worker():
2026-04-02 15:56:20 +01:00
"""
Background loop:
2026-04-07 15:48:40 +01:00
- Waits if mgmt_worker has paused via resume_event
2026-04-02 16:08:50 +01:00
- Turns display ON, waits DISPLAY_SETTLE_S for it to stabilise
2026-04-02 16:04:45 +01:00
- Runs dual_capture (signal quality + frame structure)
2026-04-02 15:56:20 +01:00
- Turns display OFF for 1 second
- Repeats until test_running = False
"""
2026-03-26 11:43:36 +00:00
global test_running
2026-03-26 15:18:58 +00:00
count = 1
2026-04-02 15:56:20 +01:00
2026-03-26 11:43:36 +00:00
while test_running:
2026-04-07 15:48:40 +01:00
resume_event.wait() # block here while mgmt_worker is running
if not test_running:
break
2026-04-08 15:42:51 +01:00
try:
requests.put(URL, json={"state": "on"}, timeout=2)
except requests.exceptions.RequestException as e:
print(f" WARNING: display ON failed: {e}")
2026-04-09 10:29:53 +01:00
# No settle delay — LP pass fires immediately to catch startup SoT transition
2026-04-02 16:04:45 +01:00
dual_capture(count)
2026-03-26 15:18:58 +00:00
count += 1
2026-04-08 15:42:51 +01:00
try:
requests.put(URL, json={"state": "off"}, timeout=2)
except requests.exceptions.RequestException as e:
print(f" WARNING: display OFF failed: {e}")
2026-03-26 15:18:58 +00:00
time.sleep(1.0)
2026-03-26 11:43:36 +00:00
2026-04-02 15:56:20 +01:00
2026-03-26 11:43:36 +00:00
def main_menu():
global test_running
while True:
2026-03-26 15:18:58 +00:00
print("\n===== MIPI TEST CONTROL =====")
2026-03-26 11:43:36 +00:00
print("1. RUN IDN CHECK (PSU & SCOPE)")
2026-03-26 15:18:58 +00:00
print("2. SETUP SCOPE (RUN FIRST)")
2026-04-02 15:56:20 +01:00
print("3. CONFIGURE PSU (DEFAULT 24V / 1.5A)")
2026-03-26 15:18:58 +00:00
print("4. PSU OUTPUT ON/OFF (CH1)")
print("5. START TEST & CAPTURE")
print("6. STOP TEST")
print("7. EXIT")
2026-04-02 15:56:20 +01:00
choice = input("\nSELECT OPTION (1-7): ").strip()
2026-03-26 11:43:36 +00:00
if choice == '1':
2026-04-02 15:56:20 +01:00
print(f"PSU : {psu.ask('*IDN?').strip()}")
print(f"SCOPE: {scope.ask('*IDN?').strip()}")
2026-04-09 08:45:57 +01:00
if rigol_scope.is_connected():
print(f"RIGOL: {rigol_scope.rigol.ask('*IDN?').strip()}")
else:
print("RIGOL: NOT CONNECTED")
2026-04-02 15:56:20 +01:00
2026-03-26 11:43:36 +00:00
elif choice == '2':
2026-03-26 15:18:58 +00:00
setup_scope()
2026-04-09 08:45:57 +01:00
if rigol_scope.is_connected():
rigol_scope.configure()
2026-04-02 15:56:20 +01:00
2026-03-26 15:18:58 +00:00
elif choice == '3':
2026-03-26 11:43:36 +00:00
psu.write('CH1:VOLT 24.0')
psu.write('CH1:CURR 1.5')
2026-04-02 15:56:20 +01:00
print("PSU CONFIGURED: 24V / 1.5A")
2026-03-26 15:18:58 +00:00
elif choice == '4':
2026-04-02 15:56:20 +01:00
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'.")
2026-03-26 15:18:58 +00:00
elif choice == '5':
2026-03-26 11:43:36 +00:00
if not test_running:
2026-04-02 16:08:50 +01:00
test_running = True
2026-04-07 15:48:40 +01:00
resume_event.set()
threading.Thread(target=test_worker, daemon=True).start()
threading.Thread(target=mgmt_worker, daemon=True).start()
print(f"TEST STARTED. MANAGEMENT INTERVAL: {MGMT_INTERVAL}s.")
2026-03-26 11:43:36 +00:00
else:
print("TEST IS ALREADY RUNNING!")
2026-04-02 15:56:20 +01:00
2026-03-26 15:18:58 +00:00
elif choice == '6':
2026-03-26 11:43:36 +00:00
print("STOPPING TEST...")
test_running = False
2026-04-02 15:56:20 +01:00
elif choice == '7':
test_running = False
2026-03-26 11:43:36 +00:00
psu.close()
scope.close()
2026-04-09 08:45:57 +01:00
rigol_scope.disconnect()
2026-04-02 15:56:20 +01:00
print("INSTRUMENTS CLOSED. BYE.")
2026-03-26 11:43:36 +00:00
break
2026-04-02 15:56:20 +01:00
2026-03-26 11:43:36 +00:00
else:
2026-04-02 15:56:20 +01:00
print("INVALID ENTRY. PLEASE CHOOSE 1-7.")
2026-03-26 11:43:36 +00:00
if __name__ == "__main__":
main_menu()