Merge branch 'SCPI_improvement' into HIL_actions

This commit is contained in:
Jan Käberich 2024-04-22 13:21:48 +02:00
commit c5d045364c
21 changed files with 813 additions and 110 deletions

View file

@ -2,11 +2,14 @@ import unittest
testmodules = [
'tests.TestConnect',
'tests.TestStatusRegisters',
'tests.TestMode',
'tests.TestSync',
'tests.TestVNASweep',
'tests.TestCalibration',
'tests.TestGenerator',
'tests.TestSASweep',
'tests.TestRST',
]
suite = unittest.TestSuite()

View file

@ -6,12 +6,13 @@ class libreCAL:
def __init__(self, serialnum = ''):
self.ser = None
for p in serial.tools.list_ports.comports():
if p.vid == 0x0483 and p.pid == 0x4122:
if (p.vid == 0x0483 and p.pid == 0x4122) or (p.vid == 0x1209 and p.pid == 0x4122):
self.ser = serial.Serial(p.device, timeout = 1)
idn = self.SCPICommand("*IDN?").split("_")
idn = self.SCPICommand("*IDN?").split(",")
if idn[0] != "LibreCAL":
self.ser = None
continue
self.serial = idn[1]
self.serial = idn[2]
if len(serialnum) > 0:
# serial number specified, compare
if self.serial != serialnum:
@ -70,7 +71,13 @@ class libreCAL:
def getHeaterPower(self):
return float(self.SCPICommand(":HEAT:POW?"))
def getDateTimeUTC(self):
return self.SCPICommand(":DATE_TIME?")
def setDateTimeUTC(self, date_time_utc):
return self.SCPICommand(":DATE_TIME "+ date_time_utc)
def SCPICommand(self, cmd: str) -> str:
self.ser.write((cmd+"\r\n").encode())
resp = self.ser.readline().decode("ascii")
@ -78,4 +85,4 @@ class libreCAL:
raise Exception("Timeout occurred in communication with LibreCAL")
if resp.strip() == "ERROR":
raise Exception("LibreCAL returned 'ERROR' for command '"+cmd+"'")
return resp.strip()
return resp.strip()

View file

@ -25,13 +25,15 @@ class TestBase(unittest.TestCase):
self.vna = libreVNA('localhost', 19544)
try:
self.vna.cmd(":DEV:CONN")
self.vna.cmd("*CLS;:DEV:CONN")
except Exception as e:
self.tearDown()
raise e
if self.vna.query(":DEV:CONN?") == "Not connected":
self.tearDown()
raise AssertionError("Not connected")
# Tests occasionally fail without this timeout - give GUI a little bit more time to properly start
time.sleep(1)
def tearDown(self):
self.gui.send_signal(SIGINT)

View file

@ -7,11 +7,9 @@ class TestCalibration(TestBase):
def cal_measure(self, number, timeout = 3):
self.vna.cmd(":VNA:CAL:MEAS "+str(number))
# wait for the measurement to finish
stoptime = time.time() + timeout
while self.vna.query(":VNA:CAL:BUSY?") == "TRUE":
if time.time() > stoptime:
raise AssertionError("Calibration measurement timed out")
time.sleep(0.1)
assert self.vna.query(":VNA:CAL:BUSY?") == "TRUE"
self.vna.cmd("*WAI", timeout=timeout)
assert self.vna.query(":VNA:CAL:BUSY?") == "FALSE"
def test_dummy_calibration(self):
# This test just iterates through the calibration steps. As no actual standards
@ -29,7 +27,8 @@ class TestCalibration(TestBase):
self.vna.cmd(":VNA:CAL:RESET")
# No measurements yet, activating should fail
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_1"), "ERROR")
self.vna.cmd(":VNA:CAL:ACT SOLT_1", check=False)
self.assertTrue(self.vna.get_status() & 0x3C)
# Load calibration kit
self.assertEqual(self.vna.query(":VNA:CAL:KIT:LOAD? DUMMY.CALKIT"), "TRUE")
@ -47,11 +46,14 @@ class TestCalibration(TestBase):
self.cal_measure(2)
# SOLT_1 should now be available
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_1"), "")
self.vna.cmd(":VNA:CAL:ACT SOLT_1", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
# SOLT_2 and SOLT_12 should still be unavailable
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_2"), "ERROR")
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_12"), "ERROR")
self.vna.cmd(":VNA:CAL:ACT SOLT_2", check=False)
self.assertTrue(self.vna.get_status() & 0x3C)
self.vna.cmd(":VNA:CAL:ACT SOLT_12", check=False)
self.assertTrue(self.vna.get_status() & 0x3C)
# Take measurements for SOLT_2
self.vna.cmd(":VNA:CAL:ADD OPEN")
@ -66,11 +68,14 @@ class TestCalibration(TestBase):
self.cal_measure(5)
# SOLT_1 and SOLT_2 should now be available
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_1"), "")
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_2"), "")
self.vna.cmd(":VNA:CAL:ACT SOLT_1", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
self.vna.cmd(":VNA:CAL:ACT SOLT_2", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
# SOLT_12 should still be unavailable
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_12"), "ERROR")
self.vna.cmd(":VNA:CAL:ACT SOLT_12", check=False)
self.assertTrue(self.vna.get_status() & 0x3C)
# Take the final through measurement for SOLT_12
self.vna.cmd(":VNA:CAL:ADD THROUGH")
@ -79,9 +84,12 @@ class TestCalibration(TestBase):
self.cal_measure(6)
# SOLT_1, SOLT_2 and SOLT_12 should now be available
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_1"), "")
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_2"), "")
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_12"), "")
self.vna.cmd(":VNA:CAL:ACT SOLT_1", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
self.vna.cmd(":VNA:CAL:ACT SOLT_2", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
self.vna.cmd(":VNA:CAL:ACT SOLT_12", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
def assertTrace_dB(self, trace, dB_nominal, dB_deviation):
for S in trace:
@ -135,7 +143,8 @@ class TestCalibration(TestBase):
self.cal_measure(6, 15)
# activate calibration
self.assertEqual(self.vna.query(":VNA:CAL:ACT SOLT_12"), "")
self.vna.cmd(":VNA:CAL:ACT SOLT_12", check=False)
self.assertFalse(self.vna.get_status() & 0x3C)
# switch in 6dB attenuator
cal.setPort(cal.Standard.THROUGH, 1, 2)
@ -143,8 +152,9 @@ class TestCalibration(TestBase):
# Start measurement and grab data
self.vna.cmd(":VNA:ACQ:SINGLE TRUE")
while self.vna.query(":VNA:ACQ:FIN?") == "FALSE":
time.sleep(0.1)
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
self.vna.cmd("*WAI")
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
cal.reset()
@ -162,5 +172,4 @@ class TestCalibration(TestBase):
# Reflection should be below -10dB (much lower for most frequencies)
self.assertTrace_dB(S11, -100, 90)
self.assertTrace_dB(S22, -100, 90)

View file

@ -0,0 +1,244 @@
import re
from tests.TestBase import TestBase
import time
float_re = re.compile(r'^[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]\d+)?$')
int_re = re.compile(r'^\d+$')
lowerq_re = re.compile('[a-z?]')
queries = [
# Limits used to validate other parameters
("float", "DEVice:INFo:LIMits:MINFrequency?"),
("float", "DEVice:INFo:LIMits:MAXFrequency?"),
("float", "DEVice:INFo:LIMits:MINIFBW?"),
("float", "DEVice:INFo:LIMits:MAXIFBW?"),
("int", "DEVice:INFo:LIMits:MAXPoints?"),
("float", "DEVice:INFo:LIMits:MINPOWer?"),
("float", "DEVice:INFo:LIMits:MAXPOWer?"),
("float", "DEVice:INFo:LIMits:MINRBW?"),
("float", "DEVice:INFo:LIMits:MAXRBW?"),
# Settable parameters without query arguments
("str", "DEVice:MODE?"),
("str", "DEVice:REFerence:IN?"),
("str", "DEVice:REFerence:OUT?"),
("float", "GENerator:FREQuency?"),
("float", "GENerator:LVL?"),
("int", "GENerator:PORT?"),
("int", "SA:ACQuisition:AVG?"),
("str", "SA:ACQuisition:DETector?"),
("float", "SA:ACQuisition:RBW?"),
("bool", "SA:ACQuisition:RUN?"),
("bool", "SA:ACQuisition:SINGLE?"),
("str", "SA:ACQuisition:WINDow?"),
("float", "SA:FREQuency:CENTer?"),
("float", "SA:FREQuency:SPAN?"),
("float", "SA:FREQuency:START?"),
("float", "SA:FREQuency:STOP?"),
("bool", "SA:TRACKing:ENable?"),
("float", "SA:TRACKing:LVL?"),
("bool", "SA:TRACKing:NORMalize:ENable?"),
("float", "SA:TRACKing:NORMalize:LVL?"),
("float", "SA:TRACKing:OFFset?"),
("int", "SA:TRACKing:Port?"),
("int", "VNA:ACQuisition:AVG?"),
("float", "VNA:ACQuisition:IFBW?"),
("int", "VNA:ACQuisition:POINTS?"),
("bool", "VNA:ACQuisition:RUN?"),
("bool", "VNA:ACQuisition:SINGLE?"),
("str", "VNA:CALibration:ACTivate?"),
("int", "VNA:DEEMBedding:NUMber?"),
("float", "VNA:FREQuency:CENTer?"),
("float", "VNA:FREQuency:SPAN?"),
("float", "VNA:FREQuency:START?"),
("float", "VNA:FREQuency:STOP?"),
("float", "VNA:POWer:START?"),
("float", "VNA:POWer:STOP?"),
("float", "VNA:STIMulus:FREQuency?"),
("float", "VNA:STIMulus:LVL?"),
("str", "VNA:SWEEP?"),
]
class TestRST(TestBase):
def query_settings(self) -> dict:
result = dict()
for qtype, query in queries:
resp = self.vna.query(query)
if qtype == "float":
self.assertTrue(float_re.match(resp),
f"Expected float from {query}; got: '{resp}'")
value = float(resp)
elif qtype == "int":
self.assertTrue(int_re.match(resp),
f"Expected int from {query}; got: '{resp}'")
value = int(resp)
elif qtype == "bool":
self.assertTrue(resp == "TRUE" or resp == "FALSE",
f"Expected bool from {query}; got: '{resp}'")
value = True if resp == "TRUE" else False
elif qtype == "str":
value = resp
else:
assert False, "invalid type in table"
query = re.sub(lowerq_re, r'', query)
result[query] = value
return result
def validate_settings(self, settings):
# Copy limits into local vars
f_min = settings["DEV:INF:LIM:MINF"]
f_max = settings["DEV:INF:LIM:MAXF"]
ifbw_min = settings["DEV:INF:LIM:MINIFBW"]
ifbw_max = settings["DEV:INF:LIM:MAXIFBW"]
points_max = settings["DEV:INF:LIM:MAXP"]
pwr_min = settings["DEV:INF:LIM:MINPOW"]
pwr_max = settings["DEV:INF:LIM:MAXPOW"]
rbw_min = settings["DEV:INF:LIM:MINRBW"]
rbw_max = settings["DEV:INF:LIM:MAXRBW"]
# Validate select settings
self.assertEqual(settings["DEV:MODE"], "VNA")
self.assertEqual(settings["DEV:REF:IN"], "INT")
self.assertEqual(settings["DEV:REF:OUT"], "OFF") # can't source pwr
f = settings["GEN:FREQ"]
self.assertGreaterEqual(f, f_min)
self.assertLessEqual(f, f_max)
pwr = settings["GEN:LVL"]
self.assertGreaterEqual(pwr, pwr_min)
self.assertLessEqual(pwr, pwr_max)
self.assertEqual(settings["SA:ACQ:AVG"], 1)
rbw = settings["SA:ACQ:RBW"]
self.assertGreaterEqual(rbw, rbw_min)
self.assertLessEqual(rbw, rbw_max)
f_center = settings["SA:FREQ:CENT"]
f_span = settings["SA:FREQ:SPAN"]
f_start = settings["SA:FREQ:START"]
f_stop = settings["SA:FREQ:STOP"]
self.assertGreaterEqual(f_start, f_min)
self.assertLessEqual(f_start, f_stop)
self.assertLessEqual(f_stop, f_max)
f_granularity = (f_max - f_min) / points_max
self.assertTrue(abs(f_stop - f_start - f_span) < f_granularity)
self.assertTrue(abs((f_start + f_stop) / 2 - f_center) < f_granularity)
self.assertFalse(settings["SA:TRACK:EN"])
pwr = settings["SA:TRACK:LVL"]
self.assertGreaterEqual(pwr, pwr_min)
self.assertLessEqual(pwr, pwr_max)
pwr = settings["SA:TRACK:NORM:LVL"]
self.assertGreaterEqual(pwr, pwr_min)
self.assertLessEqual(pwr, pwr_max)
self.assertGreaterEqual(settings["SA:TRACK:P"], 1)
ifbw = settings["VNA:ACQ:IFBW"]
self.assertGreaterEqual(ifbw, ifbw_min)
self.assertLessEqual(ifbw, ifbw_max)
points = settings["VNA:ACQ:POINTS"]
self.assertGreaterEqual(points, 1)
self.assertLessEqual(points, points_max)
# TODO-check: In standard SCPI, the instrument does not source
# power from its ports after *RST. Automation program enables
# the output only after completing its setup.
#self.assertFalse(settings["VNA:ACQ:RUN"]) # can't source pwr
self.assertEqual(settings["VNA:DEEMB:NUM"], 0)
f_center = settings["VNA:FREQ:CENT"]
f_span = settings["VNA:FREQ:SPAN"]
f_start = settings["VNA:FREQ:START"]
f_stop = settings["VNA:FREQ:STOP"]
self.assertGreaterEqual(f_start, f_min)
self.assertLessEqual(f_start, f_stop)
self.assertLessEqual(f_stop, f_max)
self.assertTrue(abs(f_stop - f_start - f_span) < f_granularity)
self.assertTrue(abs((f_start + f_stop) / 2 - f_center) < f_granularity)
pwr_start = settings["VNA:POW:START"]
pwr_stop = settings["VNA:POW:STOP"]
self.assertGreaterEqual(pwr_start, pwr_min)
self.assertLess(pwr_start, pwr_stop)
self.assertLessEqual(pwr_stop, pwr_max)
f = settings["VNA:STIM:FREQ"]
self.assertGreaterEqual(f, f_min)
self.assertLessEqual(f, f_max)
pwr = settings["VNA:STIM:LVL"]
self.assertGreaterEqual(pwr, pwr_min)
self.assertLessEqual(pwr, pwr_max)
self.assertEqual(settings["VNA:SWEEP"], "FREQUENCY")
def test_rst_basic(self):
self.vna.cmd("*RST")
settings = self.query_settings()
self.validate_settings(settings)
def test_rst_hard(self):
self.vna.cmd("*RST")
settings1 = self.query_settings()
self.validate_settings(settings1)
# Get limits.
f_min = settings1["DEV:INF:LIM:MINF"]
f_max = settings1["DEV:INF:LIM:MAXF"]
f_1_3 = (2 * f_min + f_max) / 3
f_1_2 = (f_min + f_max) / 2
f_2_3 = (f_min + 2 * f_max) / 3
ifbw_min = settings1["DEV:INF:LIM:MINIFBW"]
ifbw_max = settings1["DEV:INF:LIM:MAXIFBW"]
ifbw_1_2 = (ifbw_min + ifbw_max) / 2
points_max = settings1["DEV:INF:LIM:MAXP"]
pwr_min = settings1["DEV:INF:LIM:MINPOW"]
pwr_max = settings1["DEV:INF:LIM:MAXPOW"]
pwr_1_3 = (2 * pwr_min + pwr_max) / 3
pwr_2_3 = (pwr_min + 2 * pwr_max) / 3
pwr_1_2 = (pwr_min + pwr_max) / 2
rbw_min = settings1["DEV:INF:LIM:MINRBW"]
rbw_max = settings1["DEV:INF:LIM:MAXRBW"]
rbw_1_2 = (rbw_max + rbw_max) / 2
# Change parameters.
self.vna.cmd("DEV:MODE SA")
self.vna.cmd("DEV:REF:IN AUTO")
self.vna.cmd("DEV:REF:OUT 10")
self.vna.cmd(f"GEN:FREQ {f_1_2}")
self.vna.cmd(f"GEN:LVL {pwr_1_2}")
self.vna.cmd("GEN:PORT 2")
self.vna.cmd("SA:ACQ:AVG 3")
self.vna.cmd("SA:ACQ:DET -PEAK")
self.vna.cmd(f"SA:ACQ:RBW {rbw_1_2}")
self.vna.cmd("SA:ACQ:SINGLE TRUE")
self.vna.cmd("SA:ACQ:WIND HANN")
self.vna.cmd(f"SA:FREQ:START {f_1_3} STOP {f_2_3}")
self.vna.cmd("SA:TRACK:EN TRUE")
self.vna.cmd(f"SA:TRACK:LVL {pwr_1_2}")
self.vna.cmd("SA:TRACK:NORM:EN TRUE")
self.vna.cmd("SA:TRACK:NORM:LVL {pwr_1_3}")
self.vna.cmd("SA:TRACK:OFF 1.0e+6;PORT 2")
self.vna.cmd("VNA:ACQ:AVG 10")
self.vna.cmd(f"VNA:ACQ:IFBW {ifbw_1_2}")
self.vna.cmd(f"VNA:ACQ:POINTS 100")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.vna.cmd(f"VNA:FREQ:START {f_1_2};STOP {f_max}")
self.vna.cmd(f"VNA:POW:START {pwr_1_3};STOP {pwr_2_3}")
self.vna.cmd(f"VNA:STIM:FREQ {f_1_3}")
self.vna.cmd(f"VNA:STIM:LVL {pwr_min}")
self.vna.cmd("VNA:SWEEP POWER")
# Reset and verify all settings revert.
self.vna.cmd("*RST")
settings2 = self.query_settings()
for key, value in settings1.items():
self.assertEqual(value, settings2[key])

View file

@ -0,0 +1,56 @@
import re
from tests.TestBase import TestBase
class TestStatusRegisters(TestBase):
def query_stb(self):
resp = self.vna.query("*STB?")
self.assertTrue(re.match(r"^\d+$", resp))
value = int(resp)
self.assertTrue(value >= 0 and value <= 255)
return value
def test_invalid_command(self):
status = self.vna.get_status()
self.assertEqual(status, 0)
self.vna.default_check_cmds = False
self.vna.cmd("INVALID:COMMAND")
status = self.vna.get_status()
self.assertEqual(status & 0x3C, 0x20)
status = self.vna.get_status()
self.assertEqual(status, 0)
def test_invalid_query(self):
status = self.vna.get_status()
self.assertEqual(status, 0)
self.vna.default_check_cmds = False
self.vna.cmd("INVALID:QUERY?") # send as cmd to avoid timeout
status = self.vna.get_status()
self.assertTrue(status & 0x20) # expect CME
status = self.vna.get_status()
self.assertEqual(status, 0)
def test_stb(self):
self.vna.default_check_cmds = False
self.vna.cmd("*SRE 0")
status = self.vna.get_status()
if status & 0x20:
self.skipTest("Skipping test: *SRE, *SRE?, *STB? not implemented")
self.vna.cmd("*RST")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.vna.cmd("*WAI")
status = self.vna.get_status()
self.assertEqual(status, 0)
self.vna.cmd("OPC") # should set OPC
self.vna.cmd(f"*ESE {0x21:d}") # mask is CME|OPC
self.assertEqual(self.query_stb() & 0x60, 0x20) # expect !MSS, ESB
self.assertEqual(self.query_stb() & 0x60, 0x20) # shouldn't clear
self.vna.cmd(f"*SRE {0x20:d}") # unmask ESB
self.assertEqual(self.query_stb() & 0x60, 0x60) # expect MSS, ESB
self.vna.cmd(f"*ESE {0x20:d}") # mask is CME only
self.assertEqual(self.query_stb() & 0x60, 0) # expect !MSS, !ESB
self.vna.cmd("INVALID:COMMAND") # should set CME
self.assertEqual(self.query_stb() & 0x60, 0x60) # expect MSS, ESB
status = self.get_status()
self.assertEqual(status, 0x21) # expect CMD|OPC, clears
self.assertEqual(self.query_stb() & 0x60, 0) # expect !MSS, !ESB

View file

@ -0,0 +1,52 @@
from tests.TestBase import TestBase
import time
class TestSync(TestBase):
def test_wai(self):
self.vna.cmd("*RST")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
self.vna.cmd("*WAI", timeout=3)
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
def test_opc_query(self):
self.vna.cmd("*RST")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
resp = self.vna.query("*OPC?", timeout=3)
self.assertEqual(resp, "1")
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
def test_opc_poll(self):
self.vna.cmd("*RST")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.vna.cmd("*OPC")
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
time_limit = time.time() + 2
while True:
status = self.vna.get_status()
if status & 0x01:
break
if time.time() >= time_limit:
raise Exception("Timeout waiting for OPC")
time.sleep(0.05)
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
self.assertEqual(self.vna.get_status(), 0)
def test_idle_waits(self):
'''
Test that *WAI and *OPC? don't hang when device is idle. Test
that *OPC query sets the OPC status bit immediately.
'''
self.vna.cmd("*RST")
self.vna.cmd("VNA:ACQ:SINGLE TRUE")
self.vna.cmd("*WAI", timeout=3)
self.assertEqual(self.vna.get_status(), 0)
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
self.vna.cmd("*WAI")
resp = self.vna.query("*OPC?")
self.assertEqual(resp, "1")
self.assertEqual(self.vna.get_status(), 0)
self.assertEqual(self.vna.cmd("*OPC"), 0x01) # should return OPC
self.assertEqual(self.vna.get_status(), 0)

View file

@ -4,11 +4,9 @@ import time
class TestVNASweep(TestBase):
def waitSweepTimeout(self, timeout = 1):
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
stoptime = time.time() + timeout
while self.vna.query(":VNA:ACQ:FIN?") == "FALSE":
if time.time() > stoptime:
raise AssertionError("Sweep timed out")
self.vna.cmd("*WAI", timeout=timeout)
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "TRUE")
def test_sweep_frequency(self):
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")