mirror of
https://github.com/jankae/LibreVNA.git
synced 2026-01-20 23:50:20 +01:00
Merge branch 'SCPI_improvement' into HIL_actions
This commit is contained in:
commit
c5d045364c
Binary file not shown.
|
|
@ -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,<software version>}
|
||||
\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}{<enabled\_bits\_decimal>}
|
||||
\query{Returns the event status enable register}{*ESE?}{None}{<enabled\_bits\_decimal>}
|
||||
\subsubsection{*ESR}
|
||||
\query{Returns the event status register}{*ESR?}{None}{<set\_bits\_decimal>}
|
||||
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}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
244
Software/Integrationtests/tests/TestRST.py
Normal file
244
Software/Integrationtests/tests/TestRST.py
Normal 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])
|
||||
56
Software/Integrationtests/tests/TestStatusRegisters.py
Normal file
56
Software/Integrationtests/tests/TestStatusRegisters.py
Normal 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
|
||||
52
Software/Integrationtests/tests/TestSync.py
Normal file
52
Software/Integrationtests/tests/TestSync.py
Normal 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)
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ unsigned int Averaging::currentSweep()
|
|||
}
|
||||
}
|
||||
|
||||
bool Averaging::settled()
|
||||
{
|
||||
return getLevel() == averages;
|
||||
}
|
||||
|
||||
Averaging::Mode Averaging::getMode() const
|
||||
{
|
||||
return mode;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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<unsigned long long>::max() || res < std::numeric_limits<unsigned long long>::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<long>::max() || res < std::numeric_limits<long>::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<SCPI*>(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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SCPINode*> subnodes;
|
||||
std::vector<SCPICommand*> 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<QString> cmdQueue;
|
||||
};
|
||||
|
||||
#endif // SCPI_H
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Reference in a new issue