diff --git a/Documentation/UserManual/ProgrammingGuide.pdf b/Documentation/UserManual/ProgrammingGuide.pdf index 97b6444..247a9ce 100644 Binary files a/Documentation/UserManual/ProgrammingGuide.pdf and b/Documentation/UserManual/ProgrammingGuide.pdf differ diff --git a/Documentation/UserManual/ProgrammingGuide.tex b/Documentation/UserManual/ProgrammingGuide.tex index 3f4b66b..7ae7061 100644 --- a/Documentation/UserManual/ProgrammingGuide.tex +++ b/Documentation/UserManual/ProgrammingGuide.tex @@ -177,44 +177,68 @@ The syntax follows the usual SCPI rules: \item All commands are case insensitive (implicitly converted to uppercase before evaluated) \item The command tree is organized in branches, separated by a colon: \begin{lstlisting} -:VNA:TRACE:LIST? +VNA:TRACE:LIST? \end{lstlisting} \item Multiple commands can be concatenated in one line using a semicolon: \begin{lstlisting} -:DEVice:CONNECT;:DEVice:INFo:FWRevision? +DEVice:CONNECT;:DEVice:INFo:FWRevision? \end{lstlisting} -\item If a command starts with a colon it is evaluated from the root branch, otherwise the last used branch is assumed: +\item If a subsequent command starts with a colon it is evaluated from the root branch, otherwise the last used branch is assumed: \begin{lstlisting} -:VNA:FREQuency:START 1000000 -STOP 2000000 #No colon, VNA:FREQuency branch was used before +VNA:FREQuency:START 1000000;STOP 2000000 #No colon, VNA:FREQuency branch was used before \end{lstlisting} \item Branches and commands can be abbreviated by using only the uppercase part of their name, the following commands are identical: \begin{lstlisting} -:DEVice:INFo:LIMits:MINFrequency? -:DEV:INF:LIM:MINF? +DEVice:INFo:LIMits:MINFrequency? +DEV:INF:LIM:MINF? \end{lstlisting} -\item Every command generates a (possibly empty) response, terminated with a newline character. +\item Every query generates a response, terminated with a newline character (exceptions exist for a few queries which return more than one line) \item Some commands require additional arguments that have to be passed after the command (separated by spaces): \begin{lstlisting} -:DEV:REF:OUT 10 +DEV:REF:OUT 10 \end{lstlisting} \item Two types of commands are available: \begin{itemize} -\item \textbf{Events} change a setting or trigger an action. They usually have an empty response (unless there was an error). +\item \textbf{Events} change a setting or trigger an action. They have no response \item \textbf{Queries} request information. They end with a question mark. \end{itemize} Some commands are both events and queries, depending on whether the question mark is present: \begin{lstlisting} -:VNA:FREQ:SPAN 50000000 # Set the span -:VNA:FREQ:SPAN? # Read the current span +VNA:FREQ:SPAN 50000000 # Set the span +VNA:FREQ:SPAN? # Read the current span \end{lstlisting} \end{itemize} \section{Commands} \subsection{General Commands} \subsubsection{*IDN} \query{Returns the identifications string}{*IDN?}{None}{LibreVNA,LibreVNA-GUI,dummy\_serial,} +\subsubsection{*RST} +\event{Resets the GUI (and any connected device) to the default state}{*RST}{None} +\subsubsection{*CLS} +\event{Clears the event status register}{*CLI}{None} +\subsubsection{*ESE} +\event{Configures the event status enable register}{*ESE}{} +\query{Returns the event status enable register}{*ESE?}{None}{} +\subsubsection{*ESR} +\query{Returns the event status register}{*ESR?}{None}{} +The bits are used according to IEEE 488: +\begin{longtable}{p{.1\textwidth} | p{.1\textwidth} | p{.4\textwidth} } +\textbf{Bitvalue} & \textbf{Name} & \textbf{Meaning}\\ +\hline +1 & OPC & Operation complete\\ +2 & RQC & Request control (not used)\\ +4 & QYE & Query error (not used)\\ +8 & DDE & Device dependent error (not used)\\ +16 & EXE & Execution error (not used)\\ +32 & CME & Command error\\ +64 & URQ & User request (not used)\\ +128 & PON & Power on (not used)\\ +\end{longtable} \subsubsection{*OPC} -\query{Returns a 1 after every previous command has been handled}{*OPC?}{None}{1} +\event{Sets the OPC bit in the event status register after all operations are complete}{*OPC}{None} +\query{Returns a 1 after every active operation has completed}{*OPC?}{None}{1} +\subsubsection{*WAI} +\event{Blocks further command parsing until all active operations are complete}{*WAI}{None} \subsubsection{*LST} \query{Lists all available commands}{*LST?}{None}{List of commands, separated by newline} \subsection{Device Commands} diff --git a/Documentation/UserManual/SCPI_Examples/libreVNA.py b/Documentation/UserManual/SCPI_Examples/libreVNA.py index 9c9bee7..c4213e1 100755 --- a/Documentation/UserManual/SCPI_Examples/libreVNA.py +++ b/Documentation/UserManual/SCPI_Examples/libreVNA.py @@ -1,13 +1,14 @@ +import re import socket from asyncio import IncompleteReadError # only import the exception class import time class SocketStreamReader: - def __init__(self, sock: socket.socket): + def __init__(self, sock: socket.socket, default_timeout=1): self._sock = sock self._sock.setblocking(0) self._recv_buffer = bytearray() - self.timeout = 1.0 + self.default_timeout = default_timeout def read(self, num_bytes: int = -1) -> bytes: raise NotImplementedError @@ -22,12 +23,14 @@ class SocketStreamReader: pos += n return bytes(buf) - def readline(self) -> bytes: - return self.readuntil(b"\n") + def readline(self, timeout=None) -> bytes: + return self.readuntil(b"\n", timeout=timeout) - def readuntil(self, separator: bytes = b"\n") -> bytes: + def readuntil(self, separator: bytes = b"\n", timeout=None) -> bytes: if len(separator) != 1: raise ValueError("Only separators of length 1 are supported.") + if timeout is None: + timeout = self.default_timeout chunk = bytearray(4096) start = 0 @@ -35,12 +38,12 @@ class SocketStreamReader: bytes_read = self._recv_into(memoryview(buf)) assert bytes_read == len(buf) - timeout = time.time() + self.timeout + time_limit = time.time() + timeout while True: idx = buf.find(separator, start) if idx != -1: break - elif time.time() > timeout: + elif time.time() > time_limit: raise Exception("Timed out waiting for response from GUI") start = len(self._recv_buffer) @@ -66,31 +69,54 @@ class SocketStreamReader: return bytes_read class libreVNA: - def __init__(self, host='localhost', port=19542): + def __init__(self, host='localhost', port=19542, + check_cmds=True, timeout=1): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: self.sock.connect((host, port)) except: raise Exception("Unable to connect to LibreVNA-GUI. Make sure it is running and the TCP server is enabled.") - self.reader = SocketStreamReader(self.sock) + self.reader = SocketStreamReader(self.sock, + default_timeout=timeout) + self.default_check_cmds = check_cmds def __del__(self): self.sock.close() - def __read_response(self): - return self.reader.readline().decode().rstrip() + def __read_response(self, timeout=None): + return self.reader.readline(timeout=timeout).decode().rstrip() - def cmd(self, cmd): + def cmd(self, cmd, check=None, timeout=None): self.sock.sendall(cmd.encode()) self.sock.send(b"\n") - resp = self.__read_response() - if len(resp) > 0: - raise Exception("Expected empty response but got "+resp) + if check or (check is None and self.default_check_cmds): + status = self.get_status(timeout=timeout) + if self.get_status() & 0x20: + raise Exception("Command Error") + if self.get_status() & 0x10: + raise Exception("Execution Error") + if self.get_status() & 0x08: + raise Exception("Device Error") + if self.get_status() & 0x04: + raise Exception("Query Error") + return status + else: + return None - def query(self, query): + def query(self, query, timeout=None): self.sock.sendall(query.encode()) self.sock.send(b"\n") - return self.__read_response() + return self.__read_response(timeout=timeout) + + def get_status(self, timeout=None): + resp = self.query("*ESR?", timeout=timeout) + if not re.match(r'^\d+$', resp): + raise Exception("Expected numeric response from *ESR? but got " + f"'{resp}'") + status = int(resp) + if status < 0 or status > 255: + raise Exception(f"*ESR? returned invalid value {status}.") + return status @staticmethod def parse_VNA_trace_data(data): diff --git a/Software/Integrationtests/Integrationtest.py b/Software/Integrationtests/Integrationtest.py index 8a8d29e..e358ed5 100644 --- a/Software/Integrationtests/Integrationtest.py +++ b/Software/Integrationtests/Integrationtest.py @@ -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() diff --git a/Software/Integrationtests/libreCAL.py b/Software/Integrationtests/libreCAL.py index 03a8927..33cf78a 100644 --- a/Software/Integrationtests/libreCAL.py +++ b/Software/Integrationtests/libreCAL.py @@ -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() \ No newline at end of file + return resp.strip() diff --git a/Software/Integrationtests/tests/TestBase.py b/Software/Integrationtests/tests/TestBase.py index 21edf90..3f5dfeb 100644 --- a/Software/Integrationtests/tests/TestBase.py +++ b/Software/Integrationtests/tests/TestBase.py @@ -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) diff --git a/Software/Integrationtests/tests/TestCalibration.py b/Software/Integrationtests/tests/TestCalibration.py index 234613f..65f82d4 100644 --- a/Software/Integrationtests/tests/TestCalibration.py +++ b/Software/Integrationtests/tests/TestCalibration.py @@ -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) - - \ No newline at end of file + diff --git a/Software/Integrationtests/tests/TestRST.py b/Software/Integrationtests/tests/TestRST.py new file mode 100644 index 0000000..c86021c --- /dev/null +++ b/Software/Integrationtests/tests/TestRST.py @@ -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]) diff --git a/Software/Integrationtests/tests/TestStatusRegisters.py b/Software/Integrationtests/tests/TestStatusRegisters.py new file mode 100644 index 0000000..c4e3ae4 --- /dev/null +++ b/Software/Integrationtests/tests/TestStatusRegisters.py @@ -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 diff --git a/Software/Integrationtests/tests/TestSync.py b/Software/Integrationtests/tests/TestSync.py new file mode 100644 index 0000000..6bd9aa6 --- /dev/null +++ b/Software/Integrationtests/tests/TestSync.py @@ -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) diff --git a/Software/Integrationtests/tests/TestVNASweep.py b/Software/Integrationtests/tests/TestVNASweep.py index bae3143..3e0a87d 100644 --- a/Software/Integrationtests/tests/TestVNASweep.py +++ b/Software/Integrationtests/tests/TestVNASweep.py @@ -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") diff --git a/Software/PC_Application/LibreVNA-GUI/Device/LibreVNA/librevnadriver.cpp b/Software/PC_Application/LibreVNA-GUI/Device/LibreVNA/librevnadriver.cpp index ee21e7e..9c49f41 100644 --- a/Software/PC_Application/LibreVNA-GUI/Device/LibreVNA/librevnadriver.cpp +++ b/Software/PC_Application/LibreVNA-GUI/Device/LibreVNA/librevnadriver.cpp @@ -546,6 +546,9 @@ bool LibreVNADriver::setExtRef(QString option_in, QString option_out) case Reference::TypeIn::None: p.reference.UseExternalRef = 0; p.reference.AutomaticSwitch = 0; + if(hardwareVersion == 0x01) { + lastStatus.V1.extRefInUse = 0; + } break; case Reference::TypeIn::Auto: p.reference.UseExternalRef = 0; @@ -554,6 +557,9 @@ bool LibreVNADriver::setExtRef(QString option_in, QString option_out) case Reference::TypeIn::External: p.reference.UseExternalRef = 1; p.reference.AutomaticSwitch = 0; + if(hardwareVersion == 0x01) { + lastStatus.V1.extRefInUse = 1; + } break; } switch(refOut) { diff --git a/Software/PC_Application/LibreVNA-GUI/SpectrumAnalyzer/spectrumanalyzer.cpp b/Software/PC_Application/LibreVNA-GUI/SpectrumAnalyzer/spectrumanalyzer.cpp index 8c0e7ae..feb0f63 100644 --- a/Software/PC_Application/LibreVNA-GUI/SpectrumAnalyzer/spectrumanalyzer.cpp +++ b/Software/PC_Application/LibreVNA-GUI/SpectrumAnalyzer/spectrumanalyzer.cpp @@ -333,6 +333,7 @@ SpectrumAnalyzer::SpectrumAnalyzer(AppWindow *window, QString name) void SpectrumAnalyzer::deactivate() { + setOperationPending(false); StoreSweepSettings(); Mode::deactivate(); } @@ -503,6 +504,9 @@ void SpectrumAnalyzer::NewDatapoint(DeviceDriver::SAMeasurement m) } auto m_avg = average.process(m); + if(average.settled()) { + setOperationPending(false); + } if(settings.freqStart == settings.freqStop) { // keep track of first point time @@ -560,6 +564,9 @@ void SpectrumAnalyzer::NewDatapoint(DeviceDriver::SAMeasurement m) void SpectrumAnalyzer::SettingsChanged() { + if(window->getDevice()) { + setOperationPending(true); + } configurationTimer.start(100); ResetLiveTraces(); } @@ -703,6 +710,7 @@ void SpectrumAnalyzer::SetAveraging(unsigned int averages) average.setAverages(averages); emit averagingChanged(averages); UpdateAverageCount(); + setOperationPending(!average.settled()); } void SpectrumAnalyzer::SetTGEnabled(bool enabled) @@ -887,6 +895,9 @@ void SpectrumAnalyzer::ConfigureDevice() void SpectrumAnalyzer::ResetLiveTraces() { + if(window->getDevice()) { + setOperationPending(true); + } average.reset(DeviceDriver::SApoints()); traceModel.clearLiveData(); UpdateAverageCount(); @@ -952,6 +963,16 @@ void SpectrumAnalyzer::SetupSCPI() }, nullptr)); auto scpi_acq = new SCPINode("ACQuisition"); SCPINode::add(scpi_acq); + scpi_acq->add(new SCPICommand("RUN", [=](QStringList) -> QString { + Run(); + return SCPI::getResultName(SCPI::Result::Empty); + }, [=](QStringList) -> QString { + return running ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); + })); + scpi_acq->add(new SCPICommand("STOP", [=](QStringList) -> QString { + Stop(); + return SCPI::getResultName(SCPI::Result::Empty); + }, nullptr)); scpi_acq->add(new SCPICommand("RBW", [=](QStringList params) -> QString { unsigned long long newval; if(!SCPI::paramToULongLong(params, 0, newval)) { diff --git a/Software/PC_Application/LibreVNA-GUI/VNA/vna.cpp b/Software/PC_Application/LibreVNA-GUI/VNA/vna.cpp index e46eca0..88c033a 100644 --- a/Software/PC_Application/LibreVNA-GUI/VNA/vna.cpp +++ b/Software/PC_Application/LibreVNA-GUI/VNA/vna.cpp @@ -692,6 +692,7 @@ QString VNA::getCalToolTip() void VNA::deactivate() { + setOperationPending(false); StoreSweepSettings(); Mode::deactivate(); } @@ -901,6 +902,9 @@ void VNA::NewDatapoint(DeviceDriver::VNAMeasurement m) } m_avg = average.process(m_avg); + if(average.settled()) { + setOperationPending(false); + } if(calMeasuring) { if(average.currentSweep() == averages) { @@ -979,6 +983,9 @@ void VNA::UpdateAverageCount() void VNA::SettingsChanged(bool resetTraces, int delay) { + if(window->getDevice()) { + setOperationPending(true); + } configurationTimer.start(delay); changingSettings = true; configurationTimerResetTraces = resetTraces; @@ -1218,6 +1225,7 @@ void VNA::SetAveraging(unsigned int averages) average.setAverages(averages); emit averagingChanged(averages); UpdateAverageCount(); + setOperationPending(!average.settled()); } void VNA::ExcitationRequired() @@ -1317,7 +1325,6 @@ void VNA::SetupSCPI() if(params.size() >= 1) { if(params[0] == "FREQUENCY") { SetSweepType(SweepType::Frequency); - ResetLiveTraces(); return SCPI::getResultName(SCPI::Result::Empty); } else if(params[0] == "POWER") { SetSweepType(SweepType::Power); @@ -1378,7 +1385,6 @@ void VNA::SetupSCPI() scpi_freq->add(new SCPICommand("FULL", [=](QStringList params) -> QString { Q_UNUSED(params) SetFullSpan(); - ResetLiveTraces(); return SCPI::getResultName(SCPI::Result::Empty); }, nullptr)); scpi_freq->add(new SCPICommand("ZERO", [=](QStringList params) -> QString { @@ -1412,6 +1418,16 @@ void VNA::SetupSCPI() })); auto scpi_acq = new SCPINode("ACQuisition"); SCPINode::add(scpi_acq); + scpi_acq->add(new SCPICommand("RUN", [=](QStringList) -> QString { + Run(); + return SCPI::getResultName(SCPI::Result::Empty); + }, [=](QStringList) -> QString { + return running ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); + })); + scpi_acq->add(new SCPICommand("STOP", [=](QStringList) -> QString { + Stop(); + return SCPI::getResultName(SCPI::Result::Empty); + }, nullptr)); scpi_acq->add(new SCPICommand("IFBW", [=](QStringList params) -> QString { unsigned long long newval; if(!SCPI::paramToULongLong(params, 0, newval)) { @@ -1449,7 +1465,7 @@ void VNA::SetupSCPI() return QString::number(average.getLevel()); })); scpi_acq->add(new SCPICommand("FINished", nullptr, [=](QStringList) -> QString { - return average.getLevel() == averages ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); + return average.settled() ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); })); scpi_acq->add(new SCPICommand("LIMit", nullptr, [=](QStringList) -> QString { return tiles->allLimitsPassing() ? "PASS" : "FAIL"; @@ -1465,16 +1481,6 @@ void VNA::SetupSCPI() }, [=](QStringList) -> QString { return singleSweep ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); })); - scpi_acq->add(new SCPICommand("RUN", [=](QStringList) -> QString { - Run(); - return SCPI::getResultName(SCPI::Result::Empty); - }, [=](QStringList) -> QString { - return running ? SCPI::getResultName(SCPI::Result::True) : SCPI::getResultName(SCPI::Result::False); - })); - scpi_acq->add(new SCPICommand("STOP", [=](QStringList) -> QString { - Stop(); - return SCPI::getResultName(SCPI::Result::Empty); - }, nullptr)); auto scpi_stim = new SCPINode("STIMulus"); SCPINode::add(scpi_stim); scpi_stim->add(new SCPICommand("LVL", [=](QStringList params) -> QString { @@ -1855,6 +1861,9 @@ void VNA::ResetLiveTraces() traceModel.clearLiveData(); UpdateAverageCount(); UpdateCalWidget(); + if(window->getDevice()) { + setOperationPending(true); + } } bool VNA::LoadCalibration(QString filename) diff --git a/Software/PC_Application/LibreVNA-GUI/appwindow.cpp b/Software/PC_Application/LibreVNA-GUI/appwindow.cpp index 7c6c6ec..5b0b367 100644 --- a/Software/PC_Application/LibreVNA-GUI/appwindow.cpp +++ b/Software/PC_Application/LibreVNA-GUI/appwindow.cpp @@ -143,11 +143,6 @@ AppWindow::AppWindow(QWidget *parent) central = new QStackedWidget; setCentralWidget(central); - auto vnaIndex = modeHandler->createMode("Vector Network Analyzer", Mode::Type::VNA); - modeHandler->createMode("Signal Generator", Mode::Type::SG); - modeHandler->createMode("Spectrum Analyzer", Mode::Type::SA); - modeHandler->setCurrentIndex(vnaIndex); - auto setModeStatusbar = [=](const QString &msg) { lModeInfo.setText(msg); }; @@ -170,10 +165,9 @@ AppWindow::AppWindow(QWidget *parent) SetupSCPI(); + SetInitialState(); + auto& pref = Preferences::getInstance(); - if(pref.Startup.UseSetupFile) { - LoadSetup(pref.Startup.SetupFile); - } // List available devices UpdateDeviceList(); if(pref.Startup.ConnectToFirstDevice && deviceList.size() > 0) { @@ -315,6 +309,23 @@ void AppWindow::closeEvent(QCloseEvent *event) QMainWindow::closeEvent(event); } +void AppWindow::SetInitialState() +{ + modeHandler->closeModes(); + + auto& pref = Preferences::getInstance(); + if(pref.Startup.UseSetupFile) { + LoadSetup(pref.Startup.SetupFile); + } else { + auto vnaIndex = modeHandler->createMode("Vector Network Analyzer", Mode::Type::VNA); + modeHandler->createMode("Signal Generator", Mode::Type::SG); + modeHandler->createMode("Spectrum Analyzer", Mode::Type::SA); + modeHandler->setCurrentIndex(vnaIndex); + } + + ResetReference(); +} + bool AppWindow::ConnectToDevice(QString serial, DeviceDriver *driver) { if(serial.isEmpty()) { @@ -477,9 +488,10 @@ void AppWindow::SetupSCPI() scpi.add(new SCPICommand("*IDN", nullptr, [=](QStringList){ return "LibreVNA,LibreVNA-GUI,dummy_serial,"+appVersion; })); - scpi.add(new SCPICommand("*OPC", nullptr, [=](QStringList){ - return "1"; - })); + scpi.add(new SCPICommand("*RST", [=](QStringList){ + SetInitialState(); + return SCPI::getResultName(SCPI::Result::Empty); + }, nullptr)); auto scpi_dev = new SCPINode("DEVice"); scpi.add(scpi_dev); scpi_dev->add(new SCPICommand("DISConnect", [=](QStringList params) -> QString { @@ -1050,6 +1062,12 @@ int AppWindow::UpdateDeviceList() return available; } +void AppWindow::ResetReference() +{ + toolbars.reference.type->setCurrentIndex(0); + toolbars.reference.outFreq->setCurrentIndex(0); +} + //void AppWindow::StartManualControl() //{ // if(!vdevice || vdevice->isCompoundDevice()) { diff --git a/Software/PC_Application/LibreVNA-GUI/appwindow.h b/Software/PC_Application/LibreVNA-GUI/appwindow.h index e47376e..911390e 100644 --- a/Software/PC_Application/LibreVNA-GUI/appwindow.h +++ b/Software/PC_Application/LibreVNA-GUI/appwindow.h @@ -58,10 +58,12 @@ public slots: protected: void closeEvent(QCloseEvent *event) override; private slots: + void SetInitialState(); bool ConnectToDevice(QString serial = QString(), DeviceDriver *driver = nullptr); void DisconnectDevice(); int UpdateDeviceList(); // void StartManualControl(); + void ResetReference(); void UpdateReferenceToolbar(); void UpdateReference(); void DeviceStatusUpdated(); diff --git a/Software/PC_Application/LibreVNA-GUI/averaging.cpp b/Software/PC_Application/LibreVNA-GUI/averaging.cpp index 341f72c..79cae68 100644 --- a/Software/PC_Application/LibreVNA-GUI/averaging.cpp +++ b/Software/PC_Application/LibreVNA-GUI/averaging.cpp @@ -84,6 +84,11 @@ unsigned int Averaging::currentSweep() } } +bool Averaging::settled() +{ + return getLevel() == averages; +} + Averaging::Mode Averaging::getMode() const { return mode; diff --git a/Software/PC_Application/LibreVNA-GUI/averaging.h b/Software/PC_Application/LibreVNA-GUI/averaging.h index ab2ad4c..e77d4cf 100644 --- a/Software/PC_Application/LibreVNA-GUI/averaging.h +++ b/Software/PC_Application/LibreVNA-GUI/averaging.h @@ -26,6 +26,8 @@ public: // Returns the number of the currently active sweep. Value is incremented whenever the the first point of the sweep is added // Returned values are in range 0 (when no data has been added yet) to averages unsigned int currentSweep(); + // Returns true if all required averages have been taken + bool settled(); Mode getMode() const; void setMode(const Mode &value); diff --git a/Software/PC_Application/LibreVNA-GUI/scpi.cpp b/Software/PC_Application/LibreVNA-GUI/scpi.cpp index 4684033..b7ad43b 100644 --- a/Software/PC_Application/LibreVNA-GUI/scpi.cpp +++ b/Software/PC_Application/LibreVNA-GUI/scpi.cpp @@ -5,7 +5,70 @@ SCPI::SCPI() : SCPINode("") { - lastNode = this; + WAIexecuting = false; + OPCsetBitScheduled = false; + OPCQueryScheduled = false; + OCAS = false; + SESR = 0x00; + ESE = 0xFF; + + add(new SCPICommand("*CLS", [=](QStringList) { + SESR = 0x00; + OCAS = false; + OPCQueryScheduled = false; + return SCPI::getResultName(SCPI::Result::Empty); + }, nullptr)); + + add(new SCPICommand("*ESE", [=](QStringList params){ + unsigned long long newval; + if(!SCPI::paramToULongLong(params, 0, newval) || newval >= 256) { + return SCPI::getResultName(SCPI::Result::Error); + } else { + ESE = newval; + return SCPI::getResultName(SCPI::Result::Empty); + } + }, [=](QStringList){ + return QString::number(ESE); + })); + + add(new SCPICommand("*ESR", nullptr, [=](QStringList){ + auto ret = QString::number(SESR); + SESR = 0x00; + return ret; + })); + + add(new SCPICommand("*OPC", [=](QStringList){ + // OPC command + if(isOperationPending()) { + OPCsetBitScheduled = true; + OCAS = true; + } else { + // operation already complete + setFlag(Flag::OPC); + } + return SCPI::getResultName(SCPI::Result::Empty); + }, [=](QStringList) -> QString { + // OPC query + if(isOperationPending()) { + // operation pending + OPCQueryScheduled = true; + OCAS = true; + return SCPI::getResultName(SCPI::Result::Empty); + } else { + // no operation, can return immediately + OCAS = false; + return "1"; + } + })); + + add(new SCPICommand("*WAI", [=](QStringList){ + // WAI command + if(isOperationPending()) { + WAIexecuting = true; + } + return SCPI::getResultName(SCPI::Result::Empty); + }, nullptr)); + add(new SCPICommand("*LST", nullptr, [=](QStringList){ QString list; createCommandList("", list); @@ -48,8 +111,14 @@ bool SCPI::paramToULongLong(QStringList params, int index, unsigned long long &d if(index >= params.size()) { return false; } - bool okay; - dest = params[index].toULongLong(&okay); + double res; + bool okay = paramToDouble(params, index, res); + if(res > std::numeric_limits::max() || res < std::numeric_limits::min()) { + okay = false; + } + if(okay) { + dest = res; + } return okay; } @@ -58,8 +127,14 @@ bool SCPI::paramToLong(QStringList params, int index, long &dest) if(index >= params.size()) { return false; } - bool okay; - dest = params[index].toLong(&okay); + double res; + bool okay = paramToDouble(params, index, res); + if(res > std::numeric_limits::max() || res < std::numeric_limits::min()) { + okay = false; + } + if(okay) { + dest = res; + } return okay; } @@ -69,10 +144,10 @@ bool SCPI::paramToBool(QStringList params, int index, bool &dest) return false; } bool okay = false; - if(params[index] == "TRUE") { + if(params[index] == "TRUE" || params[index] == "ON" || params[index] == "1") { dest = true; okay = true; - } else if(params[index] == "FALSE") { + } else if(params[index] == "FALSE" || params[index] == "OFF" || params[index] == "0") { dest = false; okay = true; } @@ -87,6 +162,12 @@ QString SCPI::getResultName(SCPI::Result r) case Result::Error: default: return "ERROR"; + case Result::CmdError: + return "CMD_ERROR"; + case Result::QueryError: + return "QUERY_ERROR"; + case Result::ExecError: + return "EXEC_ERROR"; case Result::False: return "FALSE"; case Result::True: @@ -96,20 +177,83 @@ QString SCPI::getResultName(SCPI::Result r) void SCPI::input(QString line) { - auto cmds = line.split(";"); - for(auto cmd : cmds) { - if(cmd[0] == ':' || cmd[0] == '*') { - // reset to root node - lastNode = this; + cmdQueue.append(line); + process(); +} + +void SCPI::process() +{ + while(!WAIexecuting && !cmdQueue.isEmpty()) { + auto cmd = cmdQueue.front(); + cmdQueue.pop_front(); + auto cmds = cmd.split(";"); + SCPINode *lastNode = this; + for(auto cmd : cmds) { + if(cmd.size() > 0) { + if(cmd[0] == ':' || cmd[0] == '*') { + // reset to root node + lastNode = this; + } + if(cmd[0] == ':') { + cmd.remove(0, 1); + } + auto response = lastNode->parse(cmd, lastNode); + if(response == getResultName(Result::Error)) { + setFlag(Flag::CME); + } else if(response == getResultName(Result::QueryError)) { + setFlag(Flag::CME); + } else if(response == getResultName(Result::CmdError)) { + setFlag(Flag::CME); + } else if(response == getResultName(Result::ExecError)) { + setFlag(Flag::EXE); + } else if(response == getResultName(Result::Empty)) { + // do nothing + } else { + emit output(response); + } + } } - if(cmd[0] == ':') { - cmd.remove(0, 1); - } - auto response = lastNode->parse(cmd, lastNode); - emit output(response); } } +void SCPI::someOperationCompleted() +{ + if(!isOperationPending()) { + // all operations are complete + if(OCAS) { + OCAS = false; + if(OPCsetBitScheduled) { + setFlag(Flag::OPC); + OPCsetBitScheduled = false; + } + if(OPCQueryScheduled) { + output("1"); + OPCQueryScheduled = false; + } + } + if(WAIexecuting) { + WAIexecuting = false; + // process any queued commands + process(); + } + } +} + +void SCPI::setFlag(Flag flag) +{ + SESR |= ((int) flag); +} + +void SCPI::clearFlag(Flag flag) +{ + SESR &= ~((int) flag); +} + +bool SCPI::getFlag(Flag flag) +{ + return SESR & (int) flag; +} + SCPINode::~SCPINode() { if(parent) { @@ -233,6 +377,36 @@ bool SCPINode::changeName(QString newname) return true; } +void SCPINode::setOperationPending(bool pending) +{ + if(operationPending != pending) { + operationPending = pending; + if(!operationPending) { + // operation completed, needs to perform check if all operations are complete + auto root = this; + while(root->parent) { + root = root->parent; + } + auto scpi = static_cast(root); + scpi->someOperationCompleted(); + } + } +} + +bool SCPINode::isOperationPending() +{ + if(operationPending) { + return true; + } + for(auto node : subnodes) { + if(node->isOperationPending()) { + return true; + } + } + // no node has any pending operations + return false; +} + bool SCPINode::nameCollision(QString name) { for(auto n : subnodes) { @@ -314,17 +488,25 @@ QString SCPINode::parse(QString cmd, SCPINode* &lastNode) QString SCPICommand::execute(QStringList params) { if(fn_cmd == nullptr) { - return SCPI::getResultName(SCPI::Result::Error); + return SCPI::getResultName(SCPI::Result::CmdError); } else { - return fn_cmd(params); + auto ret = fn_cmd(params); + if(ret == SCPI::getResultName(SCPI::Result::Error)) { + ret = SCPI::getResultName(SCPI::Result::CmdError); + } + return ret; } } QString SCPICommand::query(QStringList params) { if(fn_query == nullptr) { - return SCPI::getResultName(SCPI::Result::Error); + return SCPI::getResultName(SCPI::Result::QueryError); } else { - return fn_query(params); + auto ret = fn_query(params); + if(ret == SCPI::getResultName(SCPI::Result::Error)) { + ret = SCPI::getResultName(SCPI::Result::QueryError); + } + return ret; } } diff --git a/Software/PC_Application/LibreVNA-GUI/scpi.h b/Software/PC_Application/LibreVNA-GUI/scpi.h index f56f92e..85fa7df 100644 --- a/Software/PC_Application/LibreVNA-GUI/scpi.h +++ b/Software/PC_Application/LibreVNA-GUI/scpi.h @@ -31,7 +31,7 @@ class SCPINode { friend class SCPI; public: SCPINode(QString name) : - name(name), parent(nullptr){} + name(name), parent(nullptr), operationPending(false){} virtual ~SCPINode(); bool add(SCPINode *node); @@ -44,6 +44,11 @@ public: bool changeName(QString newname); +protected: + void setOperationPending(bool pending); + + bool isOperationPending(); + private: QString parse(QString cmd, SCPINode* &lastNode); bool nameCollision(QString name); @@ -52,6 +57,7 @@ private: std::vector subnodes; std::vector commands; SCPINode *parent; + bool operationPending; }; class SCPI : public QObject, public SCPINode @@ -71,19 +77,50 @@ public: enum class Result { Empty, Error, + CmdError, + QueryError, + ExecError, False, True }; static QString getResultName(SCPI::Result r); + // call whenever a subnode completes an operation + void someOperationCompleted(); + public slots: void input(QString line); + void process(); signals: void output(QString line); private: - SCPINode *lastNode; + + enum class Flag { + OPC = 0x01, // Operation complete + RQC = 0x02, // device wants to become the controller (of the bus) + QYE = 0x04, // query error + DDE = 0x08, // device-dependent error + EXE = 0x10, // execution error + CME = 0x20, // command error + URQ = 0x40, // user request + PON = 0x80, // power on + }; + + void setFlag(Flag flag); + void clearFlag(Flag flag); + bool getFlag(Flag flag); + + unsigned int SESR; + unsigned int ESE; + + bool OCAS; + bool OPCsetBitScheduled; + bool OPCQueryScheduled; + bool WAIexecuting; + + QList cmdQueue; }; #endif // SCPI_H diff --git a/Software/PC_Application/LibreVNA-GUI/tcpserver.cpp b/Software/PC_Application/LibreVNA-GUI/tcpserver.cpp index a65f830..2937def 100644 --- a/Software/PC_Application/LibreVNA-GUI/tcpserver.cpp +++ b/Software/PC_Application/LibreVNA-GUI/tcpserver.cpp @@ -12,7 +12,7 @@ TCPServer::TCPServer(int port) delete socket; socket = server.nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, [=](){ - if(socket->canReadLine()) { + while(socket->canReadLine()) { auto available = socket->bytesAvailable(); char data[available+1]; socket->readLine(data, sizeof(data));