added librevna firmware reproduce kit

Полное дерево LibreVNA v1.6.5 с доработками master/slave, эталонные битстримы
и прошивки STM32, упаковщик .vnafw. Проверено: build.sh пересобирает обе
прошивки побайтово идентично лежащим рядом .vnafw.

Мастер-битстрим (top_master.bin / extracted/master_fpga.bin) больше нигде не
существует — place&route в ISE недетерминирован, из исходников он не
восстанавливается.

Положено в multi_libreVNA_firmware/reproduce, а не в device_firmware/reproduce
как сказано в README комплекта: SHA256SUMS и build.sh ссылаются на
../combined.vnafw, а .vnafw лежат здесь.

Корневые правила .gitignore build*/build/ выбрасывали 482 файла, включая сам
build.sh и эталонные прошивки STM32 из SHA256SUMS — дописано разыгноривание.
Оставшиеся 7 файлов перебивал вложенный апстримный .gitignore (более глубокий
имеет приоритет), они добавлены force-add'ом. Промежуточные .o/.d/.su из
build-каталогов убраны, .bin/.elf/.hex/.map оставлены.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ayzen
2026-09-09 21:08:02 +03:00
co-authored by Claude Opus 5
parent 7997abe2d9
commit d2338ff10c
1241 changed files with 645541 additions and 1 deletions
@@ -0,0 +1,5 @@
/.metadata/
RemoteSystemsTempFiles
build-*
*.pyc
@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?fileVersion 4.0.0?><cproject storage_type_id="org.eclipse.cdt.core.XmlProjectDescriptionStorage">
<storageModule moduleId="org.eclipse.cdt.core.settings">
<cconfiguration id="cdt.managedbuild.config.gnu.exe.debug.2118587971">
<storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="cdt.managedbuild.config.gnu.exe.debug.2118587971" moduleId="org.eclipse.cdt.core.settings" name="Debug">
<externalSettings/>
<extensions>
<extension id="org.eclipse.cdt.core.GNU_ELF" point="org.eclipse.cdt.core.BinaryParser"/>
<extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
</extensions>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactName="${ProjName}" buildArtefactType="org.eclipse.cdt.build.core.buildArtefactType.exe" buildProperties="org.eclipse.cdt.build.core.buildArtefactType=org.eclipse.cdt.build.core.buildArtefactType.exe,org.eclipse.cdt.build.core.buildType=org.eclipse.cdt.build.core.buildType.debug" cleanCommand="rm -rf" description="" id="cdt.managedbuild.config.gnu.exe.debug.2118587971" name="Debug" optionalBuildProperties="org.eclipse.cdt.docker.launcher.containerbuild.property.selectedvolumes=,org.eclipse.cdt.docker.launcher.containerbuild.property.volumes=" parent="cdt.managedbuild.config.gnu.exe.debug">
<folderInfo id="cdt.managedbuild.config.gnu.exe.debug.2118587971." name="/" resourcePath="">
<toolChain id="cdt.managedbuild.toolchain.gnu.exe.debug.1873591644" name="Linux GCC" superClass="cdt.managedbuild.toolchain.gnu.exe.debug">
<targetPlatform id="cdt.managedbuild.target.gnu.platform.exe.debug.1811214834" name="Debug Platform" superClass="cdt.managedbuild.target.gnu.platform.exe.debug"/>
<builder buildPath="${workspace_loc:/SignalIDSamplerates}/Debug" id="cdt.managedbuild.target.gnu.builder.exe.debug.1564235544" keepEnvironmentInBuildfile="false" managedBuildOn="true" name="Gnu Make Builder" superClass="cdt.managedbuild.target.gnu.builder.exe.debug"/>
<tool id="cdt.managedbuild.tool.gnu.archiver.base.1348364703" name="GCC Archiver" superClass="cdt.managedbuild.tool.gnu.archiver.base"/>
<tool id="cdt.managedbuild.tool.gnu.cpp.compiler.exe.debug.1167532039" name="GCC C++ Compiler" superClass="cdt.managedbuild.tool.gnu.cpp.compiler.exe.debug">
<option id="gnu.cpp.compiler.exe.debug.option.optimization.level.1104651228" name="Optimization Level" superClass="gnu.cpp.compiler.exe.debug.option.optimization.level" useByScannerDiscovery="false" value="gnu.cpp.compiler.optimization.level.none" valueType="enumerated"/>
<option defaultValue="gnu.cpp.compiler.debugging.level.max" id="gnu.cpp.compiler.exe.debug.option.debugging.level.1222841339" name="Debug Level" superClass="gnu.cpp.compiler.exe.debug.option.debugging.level" useByScannerDiscovery="false" valueType="enumerated"/>
<inputType id="cdt.managedbuild.tool.gnu.cpp.compiler.input.266560721" superClass="cdt.managedbuild.tool.gnu.cpp.compiler.input"/>
</tool>
<tool id="cdt.managedbuild.tool.gnu.c.compiler.exe.debug.2042313583" name="GCC C Compiler" superClass="cdt.managedbuild.tool.gnu.c.compiler.exe.debug">
<option defaultValue="gnu.c.optimization.level.none" id="gnu.c.compiler.exe.debug.option.optimization.level.1307505016" name="Optimization Level" superClass="gnu.c.compiler.exe.debug.option.optimization.level" useByScannerDiscovery="false" valueType="enumerated"/>
<option defaultValue="gnu.c.debugging.level.max" id="gnu.c.compiler.exe.debug.option.debugging.level.1981297588" name="Debug Level" superClass="gnu.c.compiler.exe.debug.option.debugging.level" useByScannerDiscovery="false" valueType="enumerated"/>
<inputType id="cdt.managedbuild.tool.gnu.c.compiler.input.381929295" superClass="cdt.managedbuild.tool.gnu.c.compiler.input"/>
</tool>
<tool id="cdt.managedbuild.tool.gnu.c.linker.exe.debug.407773137" name="GCC C Linker" superClass="cdt.managedbuild.tool.gnu.c.linker.exe.debug"/>
<tool id="cdt.managedbuild.tool.gnu.cpp.linker.exe.debug.1838910139" name="GCC C++ Linker" superClass="cdt.managedbuild.tool.gnu.cpp.linker.exe.debug">
<inputType id="cdt.managedbuild.tool.gnu.cpp.linker.input.121550916" superClass="cdt.managedbuild.tool.gnu.cpp.linker.input">
<additionalInput kind="additionalinputdependency" paths="$(USER_OBJS)"/>
<additionalInput kind="additionalinput" paths="$(LIBS)"/>
</inputType>
</tool>
<tool id="cdt.managedbuild.tool.gnu.assembler.exe.debug.938829554" name="GCC Assembler" superClass="cdt.managedbuild.tool.gnu.assembler.exe.debug">
<inputType id="cdt.managedbuild.tool.gnu.assembler.input.1588140857" superClass="cdt.managedbuild.tool.gnu.assembler.input"/>
</tool>
</toolChain>
</folderInfo>
<sourceEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name="src"/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
<cconfiguration id="cdt.managedbuild.config.gnu.exe.release.2067076035">
<storageModule buildSystemId="org.eclipse.cdt.managedbuilder.core.configurationDataProvider" id="cdt.managedbuild.config.gnu.exe.release.2067076035" moduleId="org.eclipse.cdt.core.settings" name="Release">
<externalSettings/>
<extensions>
<extension id="org.eclipse.cdt.core.GNU_ELF" point="org.eclipse.cdt.core.BinaryParser"/>
<extension id="org.eclipse.cdt.core.GASErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GmakeErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GLDErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.CWDLocator" point="org.eclipse.cdt.core.ErrorParser"/>
<extension id="org.eclipse.cdt.core.GCCErrorParser" point="org.eclipse.cdt.core.ErrorParser"/>
</extensions>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<configuration artifactName="${ProjName}" buildArtefactType="org.eclipse.cdt.build.core.buildArtefactType.exe" buildProperties="org.eclipse.cdt.build.core.buildArtefactType=org.eclipse.cdt.build.core.buildArtefactType.exe,org.eclipse.cdt.build.core.buildType=org.eclipse.cdt.build.core.buildType.release" cleanCommand="rm -rf" description="" id="cdt.managedbuild.config.gnu.exe.release.2067076035" name="Release" optionalBuildProperties="" parent="cdt.managedbuild.config.gnu.exe.release">
<folderInfo id="cdt.managedbuild.config.gnu.exe.release.2067076035." name="/" resourcePath="">
<toolChain id="cdt.managedbuild.toolchain.gnu.exe.release.234790444" name="Linux GCC" superClass="cdt.managedbuild.toolchain.gnu.exe.release">
<targetPlatform id="cdt.managedbuild.target.gnu.platform.exe.release.1733878166" name="Debug Platform" superClass="cdt.managedbuild.target.gnu.platform.exe.release"/>
<builder buildPath="${workspace_loc:/SignalIDSamplerates}/Release" id="cdt.managedbuild.target.gnu.builder.exe.release.527109837" keepEnvironmentInBuildfile="false" managedBuildOn="true" name="Gnu Make Builder" superClass="cdt.managedbuild.target.gnu.builder.exe.release"/>
<tool id="cdt.managedbuild.tool.gnu.archiver.base.711380308" name="GCC Archiver" superClass="cdt.managedbuild.tool.gnu.archiver.base"/>
<tool id="cdt.managedbuild.tool.gnu.cpp.compiler.exe.release.405853418" name="GCC C++ Compiler" superClass="cdt.managedbuild.tool.gnu.cpp.compiler.exe.release">
<option id="gnu.cpp.compiler.exe.release.option.optimization.level.756591664" name="Optimization Level" superClass="gnu.cpp.compiler.exe.release.option.optimization.level" useByScannerDiscovery="false" value="gnu.cpp.compiler.optimization.level.most" valueType="enumerated"/>
<option defaultValue="gnu.cpp.compiler.debugging.level.none" id="gnu.cpp.compiler.exe.release.option.debugging.level.866507122" name="Debug Level" superClass="gnu.cpp.compiler.exe.release.option.debugging.level" useByScannerDiscovery="false" valueType="enumerated"/>
<inputType id="cdt.managedbuild.tool.gnu.cpp.compiler.input.388380894" superClass="cdt.managedbuild.tool.gnu.cpp.compiler.input"/>
</tool>
<tool id="cdt.managedbuild.tool.gnu.c.compiler.exe.release.1310733818" name="GCC C Compiler" superClass="cdt.managedbuild.tool.gnu.c.compiler.exe.release">
<option defaultValue="gnu.c.optimization.level.most" id="gnu.c.compiler.exe.release.option.optimization.level.779300110" name="Optimization Level" superClass="gnu.c.compiler.exe.release.option.optimization.level" useByScannerDiscovery="false" valueType="enumerated"/>
<option defaultValue="gnu.c.debugging.level.none" id="gnu.c.compiler.exe.release.option.debugging.level.1906990630" name="Debug Level" superClass="gnu.c.compiler.exe.release.option.debugging.level" useByScannerDiscovery="false" valueType="enumerated"/>
<inputType id="cdt.managedbuild.tool.gnu.c.compiler.input.1517881410" superClass="cdt.managedbuild.tool.gnu.c.compiler.input"/>
</tool>
<tool id="cdt.managedbuild.tool.gnu.c.linker.exe.release.1210134901" name="GCC C Linker" superClass="cdt.managedbuild.tool.gnu.c.linker.exe.release"/>
<tool id="cdt.managedbuild.tool.gnu.cpp.linker.exe.release.278742790" name="GCC C++ Linker" superClass="cdt.managedbuild.tool.gnu.cpp.linker.exe.release">
<inputType id="cdt.managedbuild.tool.gnu.cpp.linker.input.1368940988" superClass="cdt.managedbuild.tool.gnu.cpp.linker.input">
<additionalInput kind="additionalinputdependency" paths="$(USER_OBJS)"/>
<additionalInput kind="additionalinput" paths="$(LIBS)"/>
</inputType>
</tool>
<tool id="cdt.managedbuild.tool.gnu.assembler.exe.release.1281186340" name="GCC Assembler" superClass="cdt.managedbuild.tool.gnu.assembler.exe.release">
<inputType id="cdt.managedbuild.tool.gnu.assembler.input.902387081" superClass="cdt.managedbuild.tool.gnu.assembler.input"/>
</tool>
</toolChain>
</folderInfo>
<sourceEntries>
<entry flags="VALUE_WORKSPACE_PATH|RESOLVED" kind="sourcePath" name="src"/>
</sourceEntries>
</configuration>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.externalSettings"/>
</cconfiguration>
</storageModule>
<storageModule moduleId="cdtBuildSystem" version="4.0.0">
<project id="SignalIDSamplerates.cdt.managedbuild.target.gnu.exe.1278463665" name="Executable" projectType="cdt.managedbuild.target.gnu.exe"/>
</storageModule>
<storageModule moduleId="scannerConfiguration">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
<scannerConfigBuildInfo instanceId="cdt.managedbuild.config.gnu.exe.release.2067076035;cdt.managedbuild.config.gnu.exe.release.2067076035.;cdt.managedbuild.tool.gnu.c.compiler.exe.release.1310733818;cdt.managedbuild.tool.gnu.c.compiler.input.1517881410">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
</scannerConfigBuildInfo>
<scannerConfigBuildInfo instanceId="cdt.managedbuild.config.gnu.exe.debug.2118587971;cdt.managedbuild.config.gnu.exe.debug.2118587971.;cdt.managedbuild.tool.gnu.cpp.compiler.exe.debug.1167532039;cdt.managedbuild.tool.gnu.cpp.compiler.input.266560721">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
</scannerConfigBuildInfo>
<scannerConfigBuildInfo instanceId="cdt.managedbuild.config.gnu.exe.debug.2118587971;cdt.managedbuild.config.gnu.exe.debug.2118587971.;cdt.managedbuild.tool.gnu.c.compiler.exe.debug.2042313583;cdt.managedbuild.tool.gnu.c.compiler.input.381929295">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
</scannerConfigBuildInfo>
<scannerConfigBuildInfo instanceId="cdt.managedbuild.config.gnu.exe.release.2067076035;cdt.managedbuild.config.gnu.exe.release.2067076035.;cdt.managedbuild.tool.gnu.cpp.compiler.exe.release.405853418;cdt.managedbuild.tool.gnu.cpp.compiler.input.388380894">
<autodiscovery enabled="true" problemReportingEnabled="true" selectedProfileId=""/>
</scannerConfigBuildInfo>
</storageModule>
<storageModule moduleId="org.eclipse.cdt.core.LanguageSettingsProviders"/>
<storageModule moduleId="org.eclipse.cdt.make.core.buildtargets"/>
<storageModule moduleId="refreshScope" versionNumber="2">
<configuration configurationName="Debug">
<resource resourceType="PROJECT" workspacePath="/SignalIDSamplerates"/>
</configuration>
<configuration configurationName="Release">
<resource resourceType="PROJECT" workspacePath="/SignalIDSamplerates"/>
</configuration>
</storageModule>
</cproject>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>SignalIDSamplerates</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.genmakebuilder</name>
<triggers>clean,full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder</name>
<triggers>full,incremental,</triggers>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.cdt.core.cnature</nature>
<nature>org.eclipse.cdt.core.ccnature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.managedBuildNature</nature>
<nature>org.eclipse.cdt.managedbuilder.core.ScannerConfigNature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project>
<configuration id="cdt.managedbuild.config.gnu.exe.debug.2118587971" name="Debug">
<extension point="org.eclipse.cdt.core.LanguageSettingsProvider">
<provider copy-of="extension" id="org.eclipse.cdt.ui.UserLanguageSettingsProvider"/>
<provider-reference id="org.eclipse.cdt.core.ReferencedProjectsLanguageSettingsProvider" ref="shared-provider"/>
<provider-reference id="org.eclipse.cdt.managedbuilder.core.MBSLanguageSettingsProvider" ref="shared-provider"/>
<provider class="org.eclipse.cdt.managedbuilder.language.settings.providers.GCCBuiltinSpecsDetector" console="false" env-hash="-580598439797579494" id="org.eclipse.cdt.managedbuilder.core.GCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT GCC Built-in Compiler Settings" parameter="${COMMAND} ${FLAGS} -E -P -v -dD &quot;${INPUTS}&quot;" prefer-non-shared="true">
<language-scope id="org.eclipse.cdt.core.gcc"/>
<language-scope id="org.eclipse.cdt.core.g++"/>
</provider>
</extension>
</configuration>
<configuration id="cdt.managedbuild.config.gnu.exe.release.2067076035" name="Release">
<extension point="org.eclipse.cdt.core.LanguageSettingsProvider">
<provider copy-of="extension" id="org.eclipse.cdt.ui.UserLanguageSettingsProvider"/>
<provider-reference id="org.eclipse.cdt.core.ReferencedProjectsLanguageSettingsProvider" ref="shared-provider"/>
<provider-reference id="org.eclipse.cdt.managedbuilder.core.MBSLanguageSettingsProvider" ref="shared-provider"/>
<provider class="org.eclipse.cdt.managedbuilder.language.settings.providers.GCCBuiltinSpecsDetector" console="false" env-hash="-580598439797579494" id="org.eclipse.cdt.managedbuilder.core.GCCBuiltinSpecsDetector" keep-relative-paths="false" name="CDT GCC Built-in Compiler Settings" parameter="${COMMAND} ${FLAGS} -E -P -v -dD &quot;${INPUTS}&quot;" prefer-non-shared="true">
<language-scope id="org.eclipse.cdt.core.gcc"/>
<language-scope id="org.eclipse.cdt.core.g++"/>
</provider>
</extension>
</configuration>
</project>
@@ -0,0 +1,117 @@
//============================================================================
// Name : SignalIDSamplerates.cpp
// Author :
// Version :
// Copyright : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <iostream>
#include <vector>
#include <cstdlib>
#include <limits>
using namespace std;
constexpr int32_t freqRange = 20000000;
constexpr int32_t freqStep = 4000;
constexpr int32_t FPGA_CLK = 102400000;
constexpr int32_t IF_freq = 250000;
constexpr int32_t minAliasDistance = 100000;
constexpr uint8_t minPrescaler = 112;
constexpr uint8_t maxPrescaler = 180;
constexpr uint8_t maxPrescalers = 5;
int bestDistance;
vector<int> bestPrescalers;
int Alias(int f, int f_s) {
// move into f_s range
f %= f_s;
if (f < 0) {
f += f_s;
}
// fold at half the samplerate
if(f >= f_s / 2) {
f = f_s - f;
}
return f;
}
int DistanceToIF(double f, double f_s) {
auto alias = Alias(f, f_s);
return abs(int(alias - IF_freq));
}
int farthestDistanceForFrequency(double f, vector<int> &prescalers) {
int highestDistance = numeric_limits<int>::min();
for(auto p : prescalers) {
double f_s = (double) FPGA_CLK / p;
auto dist = DistanceToIF(f, f_s);
if(dist > highestDistance) {
highestDistance = dist;
}
}
return highestDistance;
}
void checkPrescalers(vector<int> &prescalers) {
// cout << "Checking [" << prescalers[0];
// for (auto i = prescalers.begin() + 1; i != prescalers.end(); i++) {
// cout << ',' << *i;
// }
// cout << "]... ";
int lowestDistance = numeric_limits<int>::max();
for(int freq = minAliasDistance + freqStep;freq < freqRange; freq += freqStep) {
int dist = farthestDistanceForFrequency(IF_freq + freq, prescalers);
if(dist < lowestDistance) {
lowestDistance = dist;
}
if(freq > 2*IF_freq + minAliasDistance) {
int dist = farthestDistanceForFrequency(IF_freq - freq, prescalers);
if(dist < lowestDistance) {
lowestDistance = dist;
}
}
if(lowestDistance < bestDistance) {
break;
}
}
// cout << lowestDistance << endl;
if(lowestDistance > bestDistance) {
bestDistance = lowestDistance;
bestPrescalers = prescalers;
}
}
void recursivePrescalersTest(vector<int> &prescalers, int depth) {
if(prescalers.size() >= depth) {
checkPrescalers(prescalers);
} else {
prescalers.push_back(0);
int start = minPrescaler;
if(prescalers.size() > 1) {
start = prescalers[prescalers.size() - 2] + 1;
}
for (int i = start; i <= maxPrescaler; i++) {
prescalers.back() = i;
recursivePrescalersTest(prescalers, depth);
}
prescalers.pop_back();
}
}
int main() {
vector<int> prescalers = {112};
for(int d = 2;d<=maxPrescalers;d++) {
bestDistance = 0;
recursivePrescalersTest(prescalers, d);
cout << "Depth " << d <<": best distance: " << bestDistance << " Prescalers: [" << bestPrescalers[0];
for (auto i = bestPrescalers.begin() + 1; i != bestPrescalers.end(); i++) {
cout << ',' << *i;
}
cout << "]" << endl;
}
return 0;
}
@@ -0,0 +1,60 @@
{
"Description": "Dummy ideal calibration kit",
"Manufacturer": "LibreVNA",
"Serialnumber": "1234",
"standards": [
{
"params": {
"C0": 0.0,
"C1": 0.0,
"C2": 0.0,
"C3": 0.0,
"Z0": 50.0,
"delay": 0.0,
"id": 13726173464103258082,
"loss": 0.0,
"name": "Default"
},
"type": "Open"
},
{
"params": {
"L0": 0.0,
"L1": 0.0,
"L2": 0.0,
"L3": 0.0,
"Z0": 50.0,
"delay": 0.0,
"id": 13573063103112146609,
"loss": 0.0,
"name": "Default"
},
"type": "Short"
},
{
"params": {
"Cfirst": true,
"Cparallel": 0.0,
"Lseries": 0.0,
"Z0": 50.0,
"delay": 0.0,
"id": 11987083411312060381,
"loss": 0.0,
"name": "Default",
"resistance": 50.0
},
"type": "Load"
},
{
"params": {
"Z0": 50.0,
"delay": 0.0,
"id": 2916288069679020801,
"loss": 0.0,
"name": "Default"
},
"type": "Through"
}
],
"version": "1.5.0-alpha.1-9b7f457aa"
}
@@ -0,0 +1,38 @@
import unittest
import glob, os
testmodules = [
'tests.TestUpdate', # Must go first because it updates the connected VNA to the firwmare which should be tested
'tests.TestConnect',
'tests.TestStatusRegisters',
'tests.TestMode',
'tests.TestTemperature',
'tests.TestSync',
'tests.TestVNASweep',
'tests.TestCalibration',
'tests.TestGenerator',
'tests.TestSASweep',
'tests.TestRST',
]
suite = unittest.TestSuite()
# Clean up potential error logs from previous test runs
for f in glob.glob("errorlog_*"):
os.remove(f)
for f in glob.glob("packetlog_*"):
os.remove(f)
for t in testmodules:
try:
# If the module defines a suite() function, call it to get the suite.
mod = __import__(t, globals(), locals(), ['suite'])
suitefn = getattr(mod, 'suite')
suite.addTest(suitefn())
except (ImportError, AttributeError):
# else, just load all the test cases from the module.
suite.addTest(unittest.defaultTestLoader.loadTestsFromName(t))
result = unittest.TextTestRunner(verbosity=2).run(suite)
exit(int(not result.wasSuccessful()))
@@ -0,0 +1,22 @@
0.027: [debug] Application start
0.045: [info] Listening on port 19544
0.060: [debug] Activating mode "Vector Network Analyzer"
0.068: [debug] Updated device list, found 1
0.068: [debug] Trying to connect to "208A367A4B42"
0.068: [debug] Attempting to connect to device...
0.075: [info] USB connection established
0.075: [debug] Receive thread started
0.075: [debug] Transmission started (packet type 15 ), queue at 1
0.078: [info] Connected to "208A367A4B42"
0.079: [debug] Transmission started (packet type 26 ), queue at 1
0.082: [debug] No default calibration file set for this device
0.082: [debug] Transmission started (packet type 11 ), queue at 1
0.187: [debug] Transmission started (packet type 2 ), queue at 1
0.533: [debug] Attempting to open calibration from file "LIBRECAL.cal"
0.533: [debug] Associated calibration kit expected in "LIBRECAL.calkit"
0.533: [debug] Opening calkit to file "LIBRECAL.calkit"
0.533: [debug] Parsing of calibration kit failed while opening calibration file: Unable to open file (ignore for calibration format >= 3)
0.899: [debug] Transmission started (packet type 2 ), queue at 1
1.007: [debug] Deactivated mode "Vector Network Analyzer"
1.007: [debug] Transmission started (packet type 20 ), queue at 1
1.050: [debug] Disconnected, receive thread exiting
@@ -0,0 +1,88 @@
import serial
import serial.tools.list_ports
from enum import Enum
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) or (p.vid == 0x1209 and p.pid == 0x4122):
self.ser = serial.Serial(p.device, timeout = 1)
idn = self.SCPICommand("*IDN?").split(",")
if idn[0] != "LibreCAL":
self.ser = None
continue
self.serial = idn[2]
if len(serialnum) > 0:
# serial number specified, compare
if self.serial != serialnum:
self.ser = None
continue
break
if self.ser == None:
if len(serialnum) > 0:
raise Exception("LibreCAL with serial number '"+serialnum+"' not detected")
else:
raise Exception("No LibreCAL device detected")
def getSerial(self) -> str:
return self.serial
class Standard(Enum):
NONE = 0,
OPEN = 1,
SHORT = 2,
LOAD = 3,
THROUGH = 4
def reset(self):
self.SCPICommand(":PORT 1 NONE")
self.SCPICommand(":PORT 2 NONE")
self.SCPICommand(":PORT 3 NONE")
self.SCPICommand(":PORT 4 NONE")
def setPort(self, s : Standard, port, port2 = -1):
if s == self.Standard.THROUGH and port2 == -1:
raise Exception("When setting a port to THROUGH, the destination port must also be specified")
cmd = ":PORT "+str(port)+" "+s.name
if s == self.Standard.THROUGH:
cmd += " "+str(port2)
if len(self.SCPICommand(cmd)) == 0:
return True
else:
return False
def getPort(self, port):
resp = self.SCPICommand(":PORT? "+str(port)).split(" ")[0]
try:
s = self.Standard[resp]
return s
except:
raise Exception("LibreCAL reported unknown standard '"+resp+"'")
def getTemperature(self):
return float(self.SCPICommand(":TEMP?"))
def isStable(self) -> bool:
if self.SCPICommand(":TEMP:STABLE?") == "TRUE":
return True
else:
return False
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")
if len(resp) == 0:
raise Exception("Timeout occurred in communication with LibreCAL")
if resp.strip() == "ERROR":
raise Exception("LibreCAL returned 'ERROR' for command '"+cmd+"'")
return resp.strip()
@@ -0,0 +1,75 @@
import unittest
from tests.libreVNA import libreVNA as libreVNA
import tests.definitions as defs
import subprocess
import time
import select
import os
from signal import SIGINT
class TestBase(unittest.TestCase):
def setUp(self):
f = open("log.txt", "w")
self.gui = subprocess.Popen([defs.GUI_PATH, '-p', '19544', '--reset-preferences', '--no-gui', '-platform', 'offscreen'], stdout=f, stderr=subprocess.STDOUT)
# wait for the SCPI server to become available
timeout = time.time() + 3;
fread = open("log.txt", "r")
while True:
text = fread.read()
if text and "Listening on port 19544" in text:
break
if time.time() >= timeout:
self.tearDown()
raise AssertionError("Timed out waiting for SCPI server")
self.vna = libreVNA('localhost', 19544, timeout=4)
try:
self.vna.cmd("*CLS")
except Exception as e:
self.tearDown()
raise e
if self.vna.query(":DEV:CONN?") == "Not connected":
self.tearDown()
raise AssertionError("Not connected")
def tearDown(self):
if hasattr(self._outcome, 'errors'):
# Python 3.4 - 3.10 (These two methods have no side effects)
result = self.defaultTestResult()
self._feedErrorsToResult(result, self._outcome.errors)
else:
# Python 3.11+
result = self._outcome.result
ok = all(test != self for test, text in result.errors + result.failures)
if not ok:
try:
# Try to grab the packet log and save it
packetlog = self.vna.query(":DEV:PACKETLOG?", timeout=5)
f = open("packetlog_"+time.strftime("%Y%m%d%H%M%S.vnalog"), "w")
f.write(packetlog)
f.close()
except:
pass
# make sure the GUI did not crash during the test
crashed = self.gui.poll() is not None
self.gui.send_signal(SIGINT)
try:
self.gui.wait(timeout = 3)
except subprocess.TimeoutExpired:
self.gui.kill()
if ok and not crashed:
# remove log file
os.remove("log.txt")
else:
os.rename("log.txt", "errorlog_"+time.strftime("%Y%m%d%H%M%S.txt"))
if(crashed):
raise Exception("GUI crashed")
@@ -0,0 +1,175 @@
from tests.TestBase import TestBase
from libreCAL import libreCAL
import time
import math
class TestCalibration(TestBase):
def cal_measure(self, number, timeout = 3):
self.vna.cmd(":VNA:CAL:MEAS "+str(number))
# wait for the measurement to finish
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
# are applied to the ports, the calibration result is just random data
# Set up the sweep first
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 50000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:FREQuency:START 1000000")
self.vna.cmd(":VNA:FREQuency:STOP 6000000000")
self.vna.cmd(":VNA:CAL:RESET")
# No measurements yet, activating should fail
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")
# Lets take the measurements for port 1 first
self.vna.cmd(":VNA:CAL:ADD OPEN")
self.vna.cmd(":VNA:CAL:ADD SHORT")
self.vna.cmd(":VNA:CAL:ADD LOAD")
self.vna.cmd(":VNA:CAL:PORT 0 1")
self.vna.cmd(":VNA:CAL:PORT 1 1")
self.vna.cmd(":VNA:CAL:PORT 2 1")
self.cal_measure(0)
self.cal_measure(1)
self.cal_measure(2)
# SOLT_1 should now be available
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.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")
self.vna.cmd(":VNA:CAL:ADD SHORT")
self.vna.cmd(":VNA:CAL:ADD LOAD")
self.vna.cmd(":VNA:CAL:PORT 3 2")
self.vna.cmd(":VNA:CAL:PORT 4 2")
self.vna.cmd(":VNA:CAL:PORT 5 2")
self.cal_measure(3)
self.cal_measure(4)
self.cal_measure(5)
# SOLT_1 and SOLT_2 should now be available
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.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")
self.vna.cmd(":VNA:CAL:PORT 6 1 2")
self.cal_measure(6)
# SOLT_1, SOLT_2 and SOLT_12 should now be available
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:
dB = 20*math.log10(abs(S[1]))
self.assertLessEqual(dB, dB_nominal + dB_deviation)
self.assertGreaterEqual(dB, dB_nominal - dB_deviation)
def test_SOLT_calibration(self):
# This test performs a 2-port SOLT calibration with a connected LibreCAL
# Afterwqrds, the calibration is checked with a 6dB attenuator
# Set up the sweep first
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 1000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:FREQuency:START 50000000")
self.vna.cmd(":VNA:FREQuency:STOP 6000000000")
self.vna.cmd(":VNA:CAL:RESET")
# Load calibration file (only for standard and measurement setup, no
# measurements are included)
self.assertEqual(self.vna.query(":VNA:CAL:LOAD? LIBRECAL.CAL"), "TRUE")
# Take the measurements
cal = libreCAL()
# Connections:
# LibreVNA -> LibreCAL
# 1 -> 3
# 2 -> 1
cal.reset()
cal.setPort(cal.Standard.OPEN, 1)
cal.setPort(cal.Standard.OPEN, 3)
self.cal_measure(0, 15)
self.cal_measure(3, 15)
cal.setPort(cal.Standard.SHORT, 1)
cal.setPort(cal.Standard.SHORT, 3)
self.cal_measure(1, 15)
self.cal_measure(4, 15)
cal.setPort(cal.Standard.LOAD, 1)
cal.setPort(cal.Standard.LOAD, 3)
self.cal_measure(2, 15)
self.cal_measure(5, 15)
cal.setPort(cal.Standard.THROUGH, 1, 3)
self.cal_measure(6, 15)
# activate calibration
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)
cal.setPort(cal.Standard.THROUGH, 3, 4)
# Start measurement and grab data
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")
cal.reset()
# grab trace data
S11 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S11"))
S12 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S12"))
S21 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S21"))
S22 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S22"))
# Attenuation is frequency dependent, use excessively large limits
# TODO: use smaller limits based on frequency
self.assertTrace_dB(S12, -13, 5)
self.assertTrace_dB(S21, -13, 5)
# Reflection should be below -10dB (much lower for most frequencies)
self.assertTrace_dB(S11, -100, 91)
self.assertTrace_dB(S22, -100, 91)
@@ -0,0 +1,13 @@
from tests.TestBase import TestBase
class TestConnect(TestBase):
def test_connection(self):
self.vna.cmd(":DEV:DISC")
self.assertEqual(self.vna.query(":DEV:CONN?"), "Not connected")
self.vna.cmd(":DEV:CONN")
self.assertNotEqual(self.vna.query(":DEV:CONN?"), "Not connected")
def test_list(self):
list = self.vna.query(":DEV:LIST?")
serials = list.split(',')
self.assertEqual(len(serials), 1)
@@ -0,0 +1,16 @@
from tests.TestBase import TestBase
class TestGenerator(TestBase):
def test_Generator(self):
self.vna.cmd(":DEV:MODE GEN")
self.assertEqual(self.vna.query(":DEV:MODE?"), "GEN")
self.vna.cmd(":GEN:FREQ 2000000000")
self.assertEqual(float(self.vna.query(":GEN:FREQ?")), 2000000000)
self.vna.cmd(":GEN:LVL -12.34")
self.assertEqual(float(self.vna.query(":GEN:LVL?")), -12.34)
self.assertEqual(self.vna.query(":GEN:PORT?"), "0")
self.vna.cmd(":GEN:PORT 2")
self.assertEqual(self.vna.query(":GEN:PORT?"), "2")
@@ -0,0 +1,8 @@
from tests.TestBase import TestBase
class TestMode(TestBase):
def test_Mode(self):
modes = ["VNA", "GEN", "SA"]
for m in modes:
self.vna.cmd(":DEV:MODE "+m)
self.assertEqual(self.vna.query(":DEV:MODE?"), m)
@@ -0,0 +1,245 @@
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(f"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])
@@ -0,0 +1,98 @@
from tests.TestBase import TestBase
import time
class TestSASweep(TestBase):
def waitSweepTimeout(self, timeout = 1):
self.assertEqual(self.vna.query(":SA:ACQ:FIN?"), "FALSE")
stoptime = time.time() + timeout
while self.vna.query(":SA:ACQ:FIN?") == "FALSE":
if time.time() > stoptime:
raise AssertionError("Sweep timed out")
def assertTrace_dB(self, trace, dB_nominal, dB_deviation):
for S in trace:
self.assertLessEqual(S[1], dB_nominal + dB_deviation)
self.assertGreaterEqual(S[1], dB_nominal - dB_deviation)
def test_sweep_frequency(self):
self.vna.cmd(":DEV:MODE SA")
self.vna.cmd(":SA:FREQ:CENT 2000000000")
self.vna.cmd(":SA:FREQ:SPAN 200000")
self.assertEqual(float(self.vna.query(":SA:FREQ:START?")), 1999900000)
self.assertEqual(float(self.vna.query(":SA:FREQ:STOP?")), 2000100000)
self.vna.cmd(":SA:ACQ:RBW 10000")
self.waitSweepTimeout(10)
port1 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT1"))
port2 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT2"))
self.assertEqual(port1[0][0], 1999900000)
self.assertEqual(port1[-1][0], 2000100000)
self.assertEqual(port2[0][0], 1999900000)
self.assertEqual(port2[-1][0], 2000100000)
# No signal present, signal level should be very low
self.assertTrace_dB(port1, -140, 60)
self.assertTrace_dB(port2, -140, 60)
def test_sweep_zerospan(self):
self.vna.cmd(":DEV:MODE SA")
self.vna.cmd(":SA:FREQ:CENT 2000000000")
self.vna.cmd(":SA:FREQ:SPAN 0")
self.assertEqual(float(self.vna.query(":SA:FREQ:START?")), 2000000000)
self.assertEqual(float(self.vna.query(":SA:FREQ:STOP?")), 2000000000)
self.vna.cmd(":SA:ACQ:RBW 10000")
self.waitSweepTimeout(10)
port1 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT1"))
port2 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT2"))
self.assertEqual(port1[0][0], 0.0)
self.assertGreater(port1[-1][0], 4)
self.assertLess(port1[-1][0], 8)
self.assertEqual(port2[0][0], 0.0)
self.assertGreater(port2[-1][0], 4)
self.assertLess(port2[-1][0], 8)
# No signal present, signal level should be very low
self.assertTrace_dB(port1, -140, 60)
self.assertTrace_dB(port2, -140, 60)
def test_tracking_generator(self):
self.vna.cmd(":DEV:MODE SA")
self.vna.cmd(":SA:FREQ:CENT 1930000000")
self.vna.cmd(":SA:FREQ:SPAN 200000")
self.vna.cmd(":SA:ACQ:RBW 10000")
self.vna.cmd(":SA:TRACK:PORT 1")
self.assertEqual(self.vna.query(":SA:TRACK:PORT?"), "1")
self.vna.cmd(":SA:TRACK:LVL -20")
self.vna.cmd(":SA:TRACK:OFFSET 0")
self.vna.cmd(":SA:TRACK:EN TRUE")
self.waitSweepTimeout(10)
port1 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT1"))
level = port1[0][1]
self.assertGreater(level, -30)
self.assertLess(level, -10)
# check tracking generator signal
self.assertTrace_dB(port1, level, 5)
# Enable normalization
self.vna.cmd(":SA:TRACK:NORM:LVL -10")
self.vna.cmd(":SA:TRACK:NORM:EN TRUE")
# wait for normalization to finish
self.waitSweepTimeout(10)
# trigger the next sweep
self.vna.cmd(":SA:ACQ:SINGLE TRUE")
self.waitSweepTimeout(10)
# Reported level on port1 should match normalization very closely now
port1 = self.vna.parse_SA_trace_data(self.vna.query(":SA:TRACE:DATA? PORT1"))
self.assertTrace_dB(port1, -10, 1)
@@ -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
@@ -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() + 4
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)
@@ -0,0 +1,9 @@
from tests.TestBase import TestBase
class TestConnect(TestBase):
def test_temperature(self):
res = self.vna.query(":DEV:INF:TEMP?")
self.assertEqual(len(res.split("/")), 3)
@@ -0,0 +1,18 @@
from tests.TestBase import TestBase
import time
import subprocess
class TestUpdate(TestBase):
def test_Update(self):
# first update: actually update to the firmware version we want to test
self.vna.cmd("DEV:UPDATE ../../combined.vnafw", timeout=60)
# second update: check that we still have a working firmware update with this version
self.vna.cmd("DEV:UPDATE ../../combined.vnafw", timeout=60)
reported = self.vna.query("DEV:INF:FWREVISION?")
major = subprocess.check_output("grep -oP '(?<=FW_MAJOR=)[0-9]+' ../VNA_embedded/Makefile", shell=True).strip()
minor = subprocess.check_output("grep -oP '(?<=FW_MINOR=)[0-9]+' ../VNA_embedded/Makefile", shell=True).strip()
patch = subprocess.check_output("grep -oP '(?<=FW_PATCH=)[0-9]+' ../VNA_embedded/Makefile", shell=True).strip()
expected = major.decode("utf-8") + "." + minor.decode("utf-8") + "." + patch.decode("utf-8")
self.assertEqual(reported, expected)
@@ -0,0 +1,85 @@
from tests.TestBase import TestBase
import time
class TestVNASweep(TestBase):
def waitSweepTimeout(self, timeout = 1):
self.assertEqual(self.vna.query(":VNA:ACQ:FIN?"), "FALSE")
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")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 10000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:FREQuency:START 1000000")
self.vna.cmd(":VNA:FREQuency:STOP 6000000000")
self.waitSweepTimeout(2)
S11 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S11"))
self.assertEqual(S11[0][0], 1000000)
self.assertEqual(S11[-1][0], 6000000000)
def test_sweep_zerospan(self):
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 10000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:FREQuency:START 500000000")
self.vna.cmd(":VNA:FREQuency:STOP 1500000000")
self.vna.cmd(":VNA:FREQuency:ZERO 1500000000")
self.waitSweepTimeout(2)
S11 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S11"))
self.assertEqual(S11[0][0], 0.0)
# Sweep should take about 0.125 seconds
self.assertGreater(S11[-1][0], 0.1)
self.assertLess(S11[-1][0], 0.5)
def test_sweep_power(self):
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP POWER")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 10000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:POWER:START -30")
self.vna.cmd(":VNA:POWER:STOP -10")
self.waitSweepTimeout(2)
S11 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S11"))
self.assertEqual(S11[0][0], -30)
self.assertEqual(S11[-1][0], -10)
def test_fast_single_sweeps(self):
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 50000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 501")
self.vna.cmd(":VNA:FREQuency:START 1000000")
self.vna.cmd(":VNA:FREQuency:STOP 6000000000")
for i in range(10):
# Change something irrelevant (to force reconfiguration of device)
self.vna.cmd(":VNA:FREQuency:START "+str(1000000+i))
self.waitSweepTimeout(2)
def test_segmented_sweep(self):
self.vna.cmd(":DEV:MODE VNA")
self.vna.cmd(":VNA:SWEEP FREQUENCY")
self.vna.cmd(":VNA:STIM:LVL -10")
self.vna.cmd(":VNA:ACQ:IFBW 50000")
self.vna.cmd(":VNA:ACQ:AVG 1")
self.vna.cmd(":VNA:ACQ:POINTS 10000")
self.vna.cmd(":VNA:FREQuency:START 1000000")
self.vna.cmd(":VNA:FREQuency:STOP 6000000000")
self.waitSweepTimeout(10)
S11 = self.vna.parse_VNA_trace_data(self.vna.query(":VNA:TRACE:DATA? S11"))
self.assertEqual(len(S11), 10000)
@@ -0,0 +1 @@
GUI_PATH = "../PC_Application/LibreVNA-GUI/LibreVNA-GUI"
@@ -0,0 +1 @@
../../../Documentation/UserManual/SCPI_Examples/libreVNA.py
@@ -0,0 +1,87 @@
# This file is used to ignore files which are generated
# ----------------------------------------------------------------------------
*~
*.autosave
*.a
*.core
*.moc
*.o
*.obj
*.orig
*.rej
*.so
*.so.*
*_pch.h.cpp
*_resource.rc
*.qm
.#*
*.*#
core
!core/
tags
.DS_Store
.directory
*.debug
Makefile*
*.prl
*.app
moc_*.cpp
moc_*.h
ui_*.h
qrc_*.cpp
Thumbs.db
*.res
*.rc
/.qmake.cache
/.qmake.stash
# qtcreator generated files
*.pro.user*
# xemacs temporary files
*.flc
# Vim temporary files
.*.swp
# Visual Studio generated files
*.ib_pdb_index
*.idb
*.ilk
*.pdb
*.sln
*.suo
*.vcproj
*vcproj.*.*.user
*.ncb
*.sdf
*.opensdf
*.vcxproj
*vcxproj.*
# MinGW generated files
*.Debug
*.Release
# Python byte code
*.pyc
# Binaries
# --------
*.dll
*.exe
Application
/build-*
/LibreVNA-GUI/LibreVNA-GUI
/LibreVNA-Test/LibreVNA-Test
/LibreVNA-GUI/users*appdatalocaltemp*
/LibreVNA/build
LibreVNA-GUI/build
# qmake.stash
LibreVNA-GUI/.qmake.stash
@@ -0,0 +1,3 @@
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="564e", MODE:="0666"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="4121", MODE:="0666"
SUBSYSTEMS=="usb", ATTRS{idVendor}=="1209", ATTRS{idProduct}=="4121", MODE:="0666"
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardLineEditDialog</class>
<widget class="QDialog" name="CalStandardLineEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>383</width>
<height>202</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Line Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_35">
<property name="text">
<string>Z0:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="Z0">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Delay:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="delay"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Minimum usable frequency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="minFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Maximum usable frequency:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="maxFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardLineEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardLineEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,254 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardLoadEditDialog</class>
<widget class="QDialog" name="CalStandardLoadEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>344</width>
<height>386</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Load Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="coefficients">
<property name="text">
<string>Coefficients</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="measurement">
<property name="text">
<string>Measurement file</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_29">
<property name="text">
<string>Resistance:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="resistance"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_30">
<property name="text">
<string>Z0:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="Z0">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Offset delay [ps]:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="delay"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Offset loss [GΩ/s]: </string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="loss"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_18">
<property name="text">
<string>Parallel C:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="SIUnitEdit" name="parC"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_26">
<property name="text">
<string>Series L:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="SIUnitEdit" name="serL"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Load Parameter Model</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_15">
<item>
<widget class="QRadioButton" name="L_first">
<property name="text">
<string>Series L first</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_2</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="C_first">
<property name="text">
<string>Shunt C first</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup_2</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<item>
<widget class="QLabel" name="measurementLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateFile">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update from file</string>
</property>
</widget>
</item>
<item>
<widget class="TouchstoneImport" name="touchstoneImport" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
<customwidget>
<class>TouchstoneImport</class>
<extends>QWidget</extends>
<header>CustomWidgets/touchstoneimport.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardLoadEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardLoadEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup_2"/>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardOpenEditDialog</class>
<widget class="QDialog" name="CalStandardOpenEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>358</width>
<height>342</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Open Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="coefficients">
<property name="text">
<string>Coefficients</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="measurement">
<property name="text">
<string>Measurement file</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Z0:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="Z0">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Offset delay [ps]:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="delay"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Offset loss [GΩ/s]: </string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="loss"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>C0 [10&lt;sup&gt;-15&lt;/sup&gt;F]:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="C0"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>C1 [10&lt;sup&gt;-27&lt;/sup&gt;F/Hz]:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="SIUnitEdit" name="C1"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>C2 [10&lt;sup&gt;-36&lt;/sup&gt;F/Hz&lt;sup&gt;2&lt;/sup&gt;]:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="SIUnitEdit" name="C2"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_9">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;C3 [10&lt;span style=&quot; vertical-align:super;&quot;&gt;-45&lt;/span&gt;F/Hz&lt;span style=&quot; vertical-align:super;&quot;&gt;3&lt;/span&gt;]:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="SIUnitEdit" name="C3"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<item>
<widget class="QLabel" name="measurementLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateFile">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update from file</string>
</property>
</widget>
</item>
<item>
<widget class="TouchstoneImport" name="touchstoneImport" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
<customwidget>
<class>TouchstoneImport</class>
<extends>QWidget</extends>
<header>CustomWidgets/touchstoneimport.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardOpenEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardOpenEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardReflectEditDialog</class>
<widget class="QDialog" name="CalStandardReflectEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>291</width>
<height>109</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Reflect Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Reflection type:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="type">
<item>
<property name="text">
<string>Open</string>
</property>
</item>
<item>
<property name="text">
<string>Short</string>
</property>
</item>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardReflectEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardReflectEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardShortEditDialog</class>
<widget class="QDialog" name="CalStandardShortEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>358</width>
<height>342</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Short Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="coefficients">
<property name="text">
<string>Coefficients</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="measurement">
<property name="text">
<string>Measurement file</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_15">
<property name="text">
<string>Z0:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="Z0">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Offset delay [ps]:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="delay"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Offset loss [GΩ/s]: </string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="loss"/>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;L0 [10&lt;span style=&quot; vertical-align:super;&quot;&gt;-12&lt;/span&gt;H]:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="L0"/>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_12">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;L1 [10&lt;span style=&quot; vertical-align:super;&quot;&gt;-24&lt;/span&gt;H/Hz]:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="SIUnitEdit" name="L1"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_13">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;L2 [10&lt;span style=&quot; vertical-align:super;&quot;&gt;-33&lt;/span&gt;H/Hz&lt;span style=&quot; vertical-align:super;&quot;&gt;2&lt;/span&gt;]:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="SIUnitEdit" name="L2"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="label_14">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;L3 [10&lt;span style=&quot; vertical-align:super;&quot;&gt;-42&lt;/span&gt;H/Hz&lt;span style=&quot; vertical-align:super;&quot;&gt;3&lt;/span&gt;]:&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="SIUnitEdit" name="L3"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<item>
<widget class="QLabel" name="measurementLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateFile">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update from file</string>
</property>
</widget>
</item>
<item>
<widget class="TouchstoneImport" name="touchstoneImport" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
<customwidget>
<class>TouchstoneImport</class>
<extends>QWidget</extends>
<header>CustomWidgets/touchstoneimport.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardShortEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardShortEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalStandardThroughEditDialog</class>
<widget class="QDialog" name="CalStandardThroughEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>358</width>
<height>342</height>
</rect>
</property>
<property name="windowTitle">
<string>Edit Through Standard</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="coefficients">
<property name="text">
<string>Coefficients</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="measurement">
<property name="text">
<string>Measurement file</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="page">
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_35">
<property name="text">
<string>Z0:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="Z0">
<property name="enabled">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_21">
<property name="text">
<string>Delay [ps]:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="delay"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_22">
<property name="text">
<string>Loss [GΩ/s]: </string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="loss"/>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2">
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,1">
<item>
<widget class="QLabel" name="measurementLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="updateFile">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Update from file</string>
</property>
</widget>
</item>
<item>
<widget class="TouchstoneImport" name="touchstoneImport" native="true"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
<customwidget>
<class>TouchstoneImport</class>
<extends>QWidget</extends>
<header>CustomWidgets/touchstoneimport.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalStandardThroughEditDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CalStandardThroughEditDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,128 @@
#ifndef CALDEVICE_H
#define CALDEVICE_H
#include "usbdevice.h"
#include "touchstone.h"
#include <QString>
#include <QObject>
#include <thread>
class CalDevice : public QObject
{
Q_OBJECT
public:
CalDevice(QString serial);
~CalDevice();
class Standard {
public:
enum class Type {
Open,
Short,
Load,
Through,
None
};
Standard(Type type) : type(type), throughDest(0){}
Standard(int throughDest) : type(Type::Through), throughDest(throughDest){}
Type type;
int throughDest;
};
static QString StandardToString(Standard s);
static Standard StandardFromString(QString s);
Standard getStandard(int port);
bool setStandard(int port, Standard s);
static std::vector<Standard> availableStandards();
double getTemperature();
bool stabilized();
double getHeaterPower();
QString serial();
QString getFirmware() const;
unsigned int getNumPorts() const;
QString getDateTimeUTC();
bool enterBootloader();
class CoefficientSet {
public:
QString name;
int ports;
class Coefficient {
public:
Coefficient() : t(Touchstone(1)), modified(false) {}
Touchstone t;
bool modified;
};
Coefficient *getOpen(int port);
Coefficient *getShort(int port);
Coefficient *getLoad(int port);
Coefficient *getThrough(int port1, int port2);
std::map<int, Coefficient*> opens;
std::map<int, Coefficient*> shorts;
std::map<int, Coefficient*> loads;
std::map<int, Coefficient*> throughs;
int portsToThroughIndex(int port1, int port2);
void portsFromThroughIndex(int &port1, int &port2, int index);
void createEmptyCoefficients();
bool isEmpty();
};
// Extracts the coefficients from the device. This is done with a dedicated thread.
// Do not call any other functions until the update is finished. Process can be
// monitored through the updateCoefficientsPercent and updateCoefficientsDone signals
void loadCoefficientSets(QStringList names = QStringList(), QList<int> ports = {}, bool fast=true);
void abortCoefficientLoading();
// Writes coefficient sets to the device. This will only write modified files to save
// time. This is done with a dedicated thread.
// Do not call any other functions until the update is finished. Process can be
// monitored through the updateCoefficientsPercent and updateCoefficientsDone signals
void saveCoefficientSets();
std::vector<CoefficientSet> getCoefficientSets() const;
void addCoefficientSet(QString name);
QStringList getCoefficientSetNames();
bool hasModifiedCoefficients();
bool coefficientTransferActive() { return transferActive; }
public slots:
void factoryUpdateDialog();
signals:
void updateCoefficientsPercent(int percent);
// emitted when all coefficients have been received and it is safe to call all functions again
void updateCoefficientsDone(bool success);
void disconnected();
private:
void loadCoefficientSetsThreadSlow(QStringList names, QList<int> ports);
void loadCoefficientSetsThreadFast(QStringList names, QList<int> ports);
void saveCoefficientSetsThread();
USBDevice *usb;
QString firmware;
int numPorts;
std::thread *loadThread;
bool abortLoading;
bool transferActive;
float firmware_major_minor;
std::vector<CoefficientSet> coeffSets;
};
#endif // CALDEVICE_H
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>FactoryUpdateDialog</class>
<widget class="QDialog" name="FactoryUpdateDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>635</width>
<height>448</height>
</rect>
</property>
<property name="windowTitle">
<string>Factory Coefficient Update</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Updating the factory coefficients is normally not necessary. But in case they have been lost or modified accidentally, they can be set to the original state again.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="updateServer">
<property name="text">
<string>Update coefficients from manufacturing server</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QRadioButton" name="updateFile">
<property name="text">
<string>Update from file</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLineEdit" name="file"/>
</item>
<item>
<widget class="QPushButton" name="browse">
<property name="toolTip">
<string>Select file</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPlainTextEdit" name="msg">
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progress">
<property name="value">
<number>24</number>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="startUpdate">
<property name="text">
<string>Update Factory Coefficients</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="buttonGroup"/>
</buttongroups>
</ui>
@@ -0,0 +1,688 @@
#include "librecaldialog.h"
#include "ui_librecaldialog.h"
#include "caldevice.h"
#include "usbdevice.h"
#include "CustomWidgets/informationbox.h"
#include "preferences.h"
#include <set>
#include <QFormLayout>
using namespace std;
LibreCALDialog::LibreCALDialog(Calibration *cal) :
QDialog(nullptr),
ui(new Ui::LibreCALDialog),
cal(cal),
device(nullptr),
busy(false)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
createPortAssignmentUI();
connect(this, &LibreCALDialog::portAssignmentChanged, this, &LibreCALDialog::updateCalibrationStartStatus);
connect(ui->cbDevice, &QComboBox::currentTextChanged, [=](QString text) {
if(device) {
delete device;
device = nullptr;
}
try {
device = new CalDevice(text);
} catch (exception &e) {
device = nullptr;
InformationBox::ShowError("Failed to connect", e.what(), this);
}
if(device) {
createPortAssignmentUI();
connect(device, &CalDevice::updateCoefficientsPercent, ui->progressCoeff, &QProgressBar::setValue);
connect(device, &CalDevice::updateCoefficientsDone, this, [=](bool success){
busy = false;
updateCalibrationStartStatus();
if(success) {
ui->progressCoeff->setValue(100);
ui->lCalibrationStatus->setText("Coefficients loaded.");
coeffSet = device->getCoefficientSets()[0];
if(validateCoefficients()) {
startCalibration();
} else {
enableUI();
}
} else {
ui->progressCal->setValue(0);
ui->lCalibrationStatus->setText("Failed to load coefficients");
}
}, Qt::QueuedConnection);
ui->cbCoefficients->clear();
ui->cbCoefficients->addItem("Select...");
for(auto c : device->getCoefficientSetNames()) {
ui->cbCoefficients->addItem(c);
}
ui->cbCoefficients->setEnabled(true);
// select first available coefficient set
if(ui->cbCoefficients->count() > 1) {
ui->cbCoefficients->setCurrentIndex(1);
}
} else {
ui->cbCoefficients->clear();
ui->cbCoefficients->setEnabled(false);
ui->start->setEnabled(false);
}
});
connect(ui->cbCoefficients, &QComboBox::currentTextChanged, [=](){
// no coefficient set selected
ui->progressCoeff->setValue(0);
coeffSet = CalDevice::CoefficientSet();
ui->lCalibrationStatus->setText("No coefficients loaded");
updateCalibrationStartStatus();
});
auto deviceList = USBDevice::GetDevices();
for(auto device : deviceList) {
ui->cbDevice->addItem(device);
}
connect(ui->start, &QPushButton::clicked, this, &LibreCALDialog::determineAutoPorts);
connect(this, &LibreCALDialog::autoPortComplete, this, &LibreCALDialog::loadCoefficients);
updateCalibrationStartStatus();
updateDeviceStatus();
connect(&updateTimer, &QTimer::timeout, this, &LibreCALDialog::updateDeviceStatus);
updateTimer.start(1000);
}
LibreCALDialog::~LibreCALDialog()
{
if(device) {
device->abortCoefficientLoading();
}
delete device;
delete ui;
}
bool LibreCALDialog::validatePortSelection(bool autoAllowed)
{
set<int> usedCalPorts;
QString status;
bool canStart = true;
// Check port mapping for duplicate entries (and at least one used port)
for(auto port : portAssignment) {
if(autoAllowed && port == -1) {
// auto port, to be determined later
usedCalPorts.insert(port);
// skip duplicate selection
continue;
} else if(port < 1) {
// skip unused ports
continue;
}
if(usedCalPorts.count(port)) {
status = "LibreCAL port "+QString::number(port)+" is assigned to multiple VNA ports.";
canStart = false;
break;
} else {
usedCalPorts.insert(port);
}
}
if(canStart) {
// at least one port must be used
if(usedCalPorts.size() == 0) {
status = "At least one port must be assigned.";
canStart = false;
}
}
if(!canStart) {
ui->lCalibrationStatus->setText(status);
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
return canStart;
}
bool LibreCALDialog::validateCoefficients()
{
bool canStart = true;
QString status;
double coeffMinFreq = numeric_limits<double>::max();
double coeffMaxFreq = numeric_limits<double>::lowest();
auto checkCoefficient = [&](CalDevice::CoefficientSet::Coefficient *c) -> bool {
if(!c) {
return false;
}
if(c->t.points() == 0) {
return false;
} else {
if(c->t.minFreq() < coeffMinFreq) {
coeffMinFreq = c->t.minFreq();
}
if(c->t.maxFreq() > coeffMaxFreq) {
coeffMaxFreq = c->t.maxFreq();
}
return true;
}
};
set<int> usedCalPorts;
// Check port mapping for duplicate entries (and at least one used port)
for(auto port : portAssignment) {
if(port > 0) {
usedCalPorts.insert(port);
}
}
// check if coefficients for all ports are available
for(auto i : usedCalPorts) {
// Check if OSL coefficients are there
if(!checkCoefficient(coeffSet.getOpen(i))) {
status = "Open coefficient for LibreCAL port "+QString::number(i)+" is missing.";
canStart = false;
break;
}
if(!checkCoefficient(coeffSet.getShort(i))) {
status = "Short coefficient for LibreCAL port "+QString::number(i)+" is missing.";
canStart = false;
break;
}
if(!checkCoefficient(coeffSet.getLoad(i))) {
status = "Load coefficient for LibreCAL port "+QString::number(i)+" is missing.";
canStart = false;
break;
}
for(auto j : usedCalPorts) {
if(j <= i) {
continue;
}
if(!checkCoefficient(coeffSet.getThrough(i,j))) {
status = "Through coefficient for LibreCAL port "+QString::number(i)+" to "+QString::number(j)+" is missing.";
canStart = false;
break;
}
}
}
if(!canStart) {
ui->lCalibrationStatus->setText(status);
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
return canStart;
}
void LibreCALDialog::updateCalibrationStartStatus()
{
bool canStart = true;
if(!device) {
canStart = false;
ui->lCalibrationStatus->setText("Not connected to a LibreCAL device");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
if(canStart) {
if(ui->cbCoefficients->currentIndex() == 0) {
canStart = false;
ui->lCalibrationStatus->setText("No coefficient set selected");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
}
if(canStart) {
canStart = validatePortSelection(true);
}
if(canStart) {
if(!Preferences::getInstance().Acquisition.allowUseOfUnstableLibreCALTemp && !device->stabilized()) {
canStart = false;
ui->lCalibrationStatus->setText("LibreCAL temperature unstable");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
}
ui->start->setEnabled(canStart);
if(canStart) {
ui->lCalibrationStatus->setText("Ready to start");
ui->lCalibrationStatus->setStyleSheet("");
}
}
void LibreCALDialog::updateDeviceStatus()
{
if(!device) {
ui->lDeviceStatus->setText("No LibreCAL connected");
ui->lDeviceStatus->setStyleSheet("QLabel { color : red; }");
return;
}
if(busy) {
// can't update while busy reading coefficients
return;
}
if(device->stabilized()) {
ui->lDeviceStatus->setText("LibreCAL ready for calibration");
ui->lDeviceStatus->setStyleSheet("");
} else {
ui->lDeviceStatus->setText("Heating up, please wait with calibration");
ui->lDeviceStatus->setStyleSheet("QLabel { color : orange; }");
}
updateCalibrationStartStatus();
}
void LibreCALDialog::determineAutoPorts()
{
disableUI();
ui->progressCal->setValue(0);
ui->lCalibrationStatus->setText("Autodetecting port connections...");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : green; }");
// check if any ports are set to auto
bool usesAuto = false;
for(auto port : portAssignment) {
if(port == -1) {
usesAuto = true;
break;
}
}
if(usesAuto) {
driver = DeviceDriver::getActiveDriver();
emit driver->acquireControl();
// Determine ports by setting all ports to open and then switching them to short one at a time while observing the change in the VNA measurement
for(unsigned int i=1;i<=device->getNumPorts();i++) {
device->setStandard(i, CalDevice::Standard::Type::Open);
}
autoPortMeasurements.clear();
startSweep();
} else {
// no auto ports
emit autoPortComplete();
}
}
void LibreCALDialog::loadCoefficients()
{
// validate ports again, at this point no "auto" selection is allowed anymore
if(!validatePortSelection(false)) {
enableUI();
updateCalibrationStartStatus();
} else {
// can continue with loading coefficients
// they might already be loaded from a previous calibration run, check first
if(!validateCoefficients()) {
// only load required coefficients
ui->progressCal->setValue(0);
ui->lCalibrationStatus->setText("Loading coefficients...");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : green; }");
// determine the used ports of the LibreCAL
QList<int> usedPorts;
for(auto port : portAssignment) {
if(port > 0) {
usedPorts.append(port);
}
}
busy = true;
device->loadCoefficientSets({ui->cbCoefficients->currentText()}, usedPorts);
} else {
// can proceed with calibration directly
startCalibration();
}
}
}
void LibreCALDialog::handleIncomingMeasurement(DeviceDriver::VNAMeasurement m)
{
stopSweep();
autoPortMeasurements.push_back(m);
auto nextPort = autoPortMeasurements.size();
// switch LibreCAL to next port
if(nextPort <= device->getNumPorts()) {
device->setStandard(nextPort, CalDevice::Standard::Type::Short);
}
if(nextPort > 1) {
device->setStandard(nextPort-1, CalDevice::Standard::Type::Open);
}
if(nextPort > device->getNumPorts()) {
// auto port measurements complete
// reset LibreCAL ports
for(unsigned int i=1;i<=device->getNumPorts();i++) {
device->setStandard(i, CalDevice::Standard::Type::None);
}
// evaluate results
for(unsigned int p=0;p<portAssignment.size();p++) {
auto port = portAssignment[p];
if(port != -1) {
// not set to auto, ignore
continue;
}
auto param = "S"+QString::number(p+1)+QString::number(p+1);
std::vector<std::complex<double>> measurements;
for(auto m : autoPortMeasurements) {
if(m.measurements.count(param)) {
measurements.push_back(m.measurements[param]);
}
}
if(measurements.size() != device->getNumPorts()+1) {
// not all measurements available (maybe the port has no stimulus?), set to unused
portAssignmentComboBoxes[p]->setCurrentIndex(0);
continue;
}
// got all required measurements, determine which one deviates the most from the baseline
double maxDeviation = 0.0;
int maxDevIndex = 0;
for(unsigned int i=1;i<=device->getNumPorts();i++) {
auto diff = abs(measurements[i] - measurements[0]);
if (diff > maxDeviation) {
maxDeviation = diff;
maxDevIndex = i;
}
}
constexpr double minRequiredDeviation = 0.25;
if(maxDeviation > minRequiredDeviation) {
portAssignmentComboBoxes[p]->setCurrentIndex(maxDevIndex+1);
} else {
// not enough deviation, probably unused
portAssignmentComboBoxes[p]->setCurrentIndex(0);
}
}
emit driver->releaseControl();
emit autoPortComplete();
} else {
// trigger the next measurement
startSweep();
}
}
void LibreCALDialog::startSweep()
{
// set up a sweep with a single measurement point at the start frequency
auto info = driver->getInfo();
DeviceDriver::VNASettings s = {};
s.dBmStart = info.Limits.VNA.maxdBm;
s.dBmStop = info.Limits.VNA.maxdBm;
auto freq = info.Limits.VNA.minFreq == 0 ? 100000 : info.Limits.VNA.minFreq;
s.freqStart = freq;
s.freqStop = freq;
s.IFBW = 100;
s.logSweep = false;
s.points = 1;
for(unsigned int i=1;i<=info.Limits.VNA.ports;i++) {
s.excitedPorts.push_back(i);
}
driver->setVNA(s, [=](bool){
connect(driver, &DeviceDriver::VNAmeasurementReceived, this, &LibreCALDialog::handleIncomingMeasurement, Qt::DirectConnection);
});
}
void LibreCALDialog::stopSweep()
{
disconnect(driver, &DeviceDriver::VNAmeasurementReceived, this, &LibreCALDialog::handleIncomingMeasurement);
driver->setIdle();
}
void LibreCALDialog::startCalibration()
{
disableUI();
busy = true;
ui->progressCal->setValue(0);
ui->lCalibrationStatus->setText("Creating calibration kit from coefficients...");
ui->lCalibrationStatus->setStyleSheet("");
auto& kit = cal->getKit();
kit.clearStandards();
kit.manufacturer = "LibreCAL ("+coeffSet.name+")";
kit.serialnumber = device->serial();
kit.description = "Automatically created from LibreCAL module";
// determine the used ports of the LibreCAL
QList<int> usedPorts;
for(auto port : portAssignment) {
if(port > 0) {
usedPorts.append(port);
}
}
std::sort(usedPorts.begin(), usedPorts.end());
std::map<int, CalStandard::Virtual*> openStandards;
std::map<int, CalStandard::Virtual*> shortStandards;
std::map<int, CalStandard::Virtual*> loadStandards;
std::map<int, CalStandard::Virtual*> throughStandards;
for(unsigned int idx=0;idx<usedPorts.size();idx++) {
int i=usedPorts[idx];
if(coeffSet.getOpen(i)->t.points() > 0) {
auto o = new CalStandard::Open();
o->setName("Port "+QString::number(i));
o->setMeasurement(coeffSet.getOpen(i)->t);
openStandards[i] = o;
kit.addStandard(o);
}
if(coeffSet.getShort(i)->t.points() > 0) {
auto o = new CalStandard::Short();
o->setName("Port "+QString::number(i));
o->setMeasurement(coeffSet.getShort(i)->t);
shortStandards[i] = o;
kit.addStandard(o);
}
if(coeffSet.getLoad(i)->t.points() > 0) {
auto o = new CalStandard::Load();
o->setName("Port "+QString::number(i));
o->setMeasurement(coeffSet.getLoad(i)->t);
loadStandards[i] = o;
kit.addStandard(o);
}
for(unsigned int jdx=idx+1;jdx<usedPorts.size();jdx++) {
int j=usedPorts[jdx];
auto c = coeffSet.getThrough(i,j);
if(!c) {
continue;
}
if(c->t.points() > 0) {
auto o = new CalStandard::Through();
o->setName("Port "+QString::number(i)+" to "+QString::number(j));
o->setMeasurement(c->t);
throughStandards[coeffSet.portsToThroughIndex(i, j)] = o;
kit.addStandard(o);
}
}
}
ui->lCalibrationStatus->setText("Creating calibration measurements...");
cal->reset();
auto vnaPorts = DeviceDriver::getInfo(DeviceDriver::getActiveDriver()).Limits.VNA.ports;
set<CalibrationMeasurement::Base*> openMeasurements;
set<CalibrationMeasurement::Base*> shortMeasurements;
set<CalibrationMeasurement::Base*> loadMeasurements;
vector<CalibrationMeasurement::TwoPort*> throughMeasurements;
for(unsigned int p=0;p<vnaPorts;p++) {
if(portAssignment[p] == 0) {
continue;
}
// Create SOL measurements with correct port of calkit
auto open = new CalibrationMeasurement::Open(cal);
open->setPort(p+1);
open->setStandard(openStandards[portAssignment[p]]);
openMeasurements.insert(open);
cal->measurements.push_back(open);
auto _short = new CalibrationMeasurement::Short(cal);
_short->setPort(p+1);
_short->setStandard(shortStandards[portAssignment[p]]);
shortMeasurements.insert(_short);
cal->measurements.push_back(_short);
auto load = new CalibrationMeasurement::Load(cal);
load->setPort(p+1);
load->setStandard(loadStandards[portAssignment[p]]);
loadMeasurements.insert(load);
cal->measurements.push_back(load);
for(unsigned int p2=p+1;p2<vnaPorts;p2++) {
if(portAssignment[p2] == 0) {
continue;
}
auto through = new CalibrationMeasurement::Through(cal);
through->setPort1(p+1);
through->setPort2(p2+1);
// find correct through standard
int libreCALp1 = portAssignment[p];
int libreCALp2 = portAssignment[p2];
QString forwardName = "Port "+QString::number(libreCALp1)+" to "+QString::number(libreCALp2);
QString reverseName = "Port "+QString::number(libreCALp2)+" to "+QString::number(libreCALp1);
for(auto ts : throughStandards) {
if(ts.second->getName() == forwardName) {
through->setStandard(ts.second);
through->setReverseStandard(false);
} else if(ts.second->getName() == reverseName) {
through->setStandard(ts.second);
through->setReverseStandard(true);
}
}
throughMeasurements.push_back(through);
cal->measurements.push_back(through);
}
}
ui->lCalibrationStatus->setText("Taking calibration measurements...");
measurementsTaken = 0;
auto setTerminationOnAllUsedPorts = [=](CalDevice::Standard s) {
for(auto p : portAssignment) {
if(p > 0) {
device->setStandard(p, s);
}
}
};
auto startNextCalibrationStep = [=]() {
// indicate calibration percentage
auto totalMeasurements = 3 + throughMeasurements.size();
ui->progressCal->setValue(measurementsTaken * 100 / totalMeasurements);
switch(measurementsTaken) {
case 0:
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::Open));
emit cal->startMeasurements(openMeasurements);
break;
case 1:
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::Short));
emit cal->startMeasurements(shortMeasurements);
break;
case 2:
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::Load));
emit cal->startMeasurements(loadMeasurements);
break;
default: {
// into through measurements now
unsigned int throughIndex = measurementsTaken - 3;
if(throughIndex >= throughMeasurements.size()) {
// this was the last measurement
// Try to apply the calibration
Calibration::CalType type;
type.type = Calibration::Type::SOLT;
for(unsigned int i=0;i<vnaPorts;i++) {
if(portAssignment[i] > 0) {
// this VNA port was used in the calibration
type.usedPorts.push_back(i+1);
}
}
auto res = cal->compute(type);
if(res) {
ui->progressCal->setValue(100);
ui->lCalibrationStatus->setText("Calibration activated.");
} else {
ui->progressCal->setValue(0);
ui->lCalibrationStatus->setText("Failed to activate calibration.");
ui->lCalibrationStatus->setStyleSheet("QLabel { color : red; }");
}
// sever connection to this function
disconnect(cal, &Calibration::measurementsUpdated, this, nullptr);
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::None));
enableUI();
busy = false;
break;
}
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::None));
auto m = throughMeasurements[throughIndex];
device->setStandard(portAssignment[m->getPort1()-1], CalDevice::Standard(portAssignment[m->getPort2()-1]));
emit cal->startMeasurements({m});
}
break;
}
measurementsTaken++;
};
disconnect(cal, &Calibration::measurementsUpdated, this, nullptr);
connect(cal, &Calibration::measurementsUpdated, this, startNextCalibrationStep, Qt::QueuedConnection);
connect(cal, &Calibration::measurementsAborted, this, [=](){
setTerminationOnAllUsedPorts(CalDevice::Standard(CalDevice::Standard::Type::None));
enableUI();
busy = false;
ui->lCalibrationStatus->setText("Ready to start");
});
startNextCalibrationStep();
}
void LibreCALDialog::disableUI()
{
ui->cbDevice->setEnabled(false);
ui->cbCoefficients->setEnabled(false);
ui->start->setEnabled(false);
for(auto cb : portAssignmentComboBoxes) {
cb->setEnabled(false);
}
}
void LibreCALDialog::enableUI()
{
ui->cbDevice->setEnabled(true);
ui->cbCoefficients->setEnabled(true);
ui->start->setEnabled(true);
for(auto cb : portAssignmentComboBoxes) {
cb->setEnabled(true);
}
}
void LibreCALDialog::createPortAssignmentUI()
{
auto layout = static_cast<QFormLayout*>(ui->assignmentBox->layout());
// Clear any possible previous elements
portAssignment.clear();
portAssignmentComboBoxes.clear();
while(layout->rowCount() > 1) {
layout->removeRow(1);
}
auto vnaPorts = DeviceDriver::getInfo(DeviceDriver::getActiveDriver()).Limits.VNA.ports;
portAssignment.resize(vnaPorts, 0);
auto calPorts = 0;
if(device) {
calPorts = device->getNumPorts();
}
QStringList choices = {"Unused", "Auto"};
for(int i=1;i<=calPorts;i++) {
choices.push_back("Port "+QString::number(i));
}
for(unsigned int p = 1;p<=vnaPorts;p++) {
auto label = new QLabel("Port "+QString::number(p)+":");
auto comboBox = new QComboBox();
comboBox->addItems(choices);
connect(comboBox, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
if(comboBox->currentText().startsWith("Unused")) {
portAssignment[p-1] = 0;
} else if(comboBox->currentText().startsWith("Auto")) {
portAssignment[p-1] = -1;
} else if(comboBox->currentText().startsWith("Port")) {
portAssignment[p-1] = comboBox->currentText().back().digitValue();
}
emit portAssignmentChanged();
});
comboBox->setCurrentIndex(1);
layout->addRow(label, comboBox);
portAssignmentComboBoxes.push_back(comboBox);
}
}
@@ -0,0 +1,60 @@
#ifndef LIBRECALDIALOG_H
#define LIBRECALDIALOG_H
#include "Calibration/calibration.h"
#include "caldevice.h"
#include "Device/devicedriver.h"
#include <QDialog>
#include <QTimer>
#include <QComboBox>
namespace Ui {
class LibreCALDialog;
}
class LibreCALDialog : public QDialog
{
Q_OBJECT
public:
explicit LibreCALDialog(Calibration *cal);
~LibreCALDialog();
private:
signals:
void portAssignmentChanged();
void autoPortComplete();
private slots:
bool validatePortSelection(bool autoAllowed);
bool validateCoefficients();
void updateCalibrationStartStatus();
void updateDeviceStatus();
void determineAutoPorts();
void loadCoefficients();
void startCalibration();
// auto port slots
void handleIncomingMeasurement(DeviceDriver::VNAMeasurement m);
void startSweep();
void stopSweep();
private:
void disableUI();
void enableUI();
void createPortAssignmentUI();
Ui::LibreCALDialog *ui;
Calibration *cal;
CalDevice *device;
CalDevice::CoefficientSet coeffSet;
QTimer updateTimer;
bool busy;
// 0 for unused port, -1 for auto port, otherwise the port number
std::vector<int> portAssignment;
std::vector<QComboBox*> portAssignmentComboBoxes;
std::vector<DeviceDriver::VNAMeasurement> autoPortMeasurements;
DeviceDriver *driver;
int measurementsTaken;
};
#endif // LIBRECALDIALOG_H
@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>LibreCALDialog</class>
<widget class="QDialog" name="LibreCALDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>399</width>
<height>405</height>
</rect>
</property>
<property name="windowTitle">
<string>Electronic Calibration Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Device Selection</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Device:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cbDevice"/>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lDeviceStatus">
<property name="text">
<string>DEVICE_STATUS_PLACEHOLDER</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Coefficients:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cbCoefficients"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="assignmentBox">
<property name="title">
<string>Port Assignments</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>LibreVNA</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label_4">
<property name="text">
<string>LibreCAL</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Calibration</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="start">
<property name="text">
<string>Start</string>
</property>
<property name="icon">
<iconset theme="media-playback-start">
<normaloff>.</normaloff>.</iconset>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Coefficients:</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressCoeff">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Calibration:</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressCal">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="lCalibrationStatus">
<property name="text">
<string>CALIBRATION_STATUS_PLACEHOLDER</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,290 @@
#include "usbdevice.h"
#include "CustomWidgets/informationbox.h"
#include <signal.h>
#include <QDebug>
#include <QString>
#include <QMessageBox>
#include <QDateTime>
#include <mutex>
using namespace std;
using USBID = struct {
int VID;
int PID;
};
static constexpr USBID IDs[] = {
{0x0483, 0x4122},
{0x1209, 0x4122},
};
USBDevice::USBDevice(QString serial)
{
m_handle = nullptr;
libusb_init(&m_context);
#if LIBUSB_API_VERSION >= 0x01000106
libusb_set_option(m_context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO);
#endif
SearchDevices([=](libusb_device_handle *handle, QString found_serial) -> bool {
if(serial.isEmpty() || serial == found_serial) {
// accept connection to this device
m_serial = found_serial;
m_handle = handle;
// abort device search
return false;
} else {
// not the requested device, continue search
return true;
}
}, m_context, false);
if(!m_handle) {
QString message = "No device found";
if(!serial.isEmpty()) {
// only show error message if specific device was requested
InformationBox::ShowError("Error opening device", message);
}
libusb_exit(m_context);
throw std::runtime_error(message.toStdString());
return;
}
// Found the correct device, now connect
/* claim the interface */
int ret = libusb_claim_interface(m_handle, 2);
if (ret < 0) {
libusb_close(m_handle);
/* Failed to open */
QString message = "Failed to claim interface: \"";
message.append(libusb_strerror((libusb_error) ret));
message.append("\" Maybe you are already connected to this device?");
qWarning() << message;
InformationBox::ShowError("Error opening device", message);
libusb_exit(m_context);
throw std::runtime_error(message.toStdString());
}
qInfo() << "USB connection established" << Qt::flush;
connected = true;
m_receiveThread = new std::thread(&USBDevice::USBHandleThread, this);
usbBuffer = new USBInBuffer(m_handle, LIBUSB_ENDPOINT_IN | 0x03, 65536);
connect(usbBuffer, &USBInBuffer::DataReceived, this, &USBDevice::ReceivedData, Qt::DirectConnection);
// clear any possible queued data in USB device
QString dummy;
while(receive(&dummy, 10));
}
USBDevice::~USBDevice()
{
delete usbBuffer;
connected = false;
libusb_release_interface(m_handle, 2);
libusb_close(m_handle);
m_receiveThread->join();
libusb_exit(m_context);
delete m_receiveThread;
}
bool USBDevice::Cmd(QString cmd, unsigned int timeout)
{
QString rcv;
flushReceived();
bool success = send(cmd) && receive(&rcv, timeout);
if(success && rcv == "") {
// empty response expected by commad
return true;
} else {
// failed to send/receive
emit communicationFailure();
return false;
}
}
QString USBDevice::Query(QString query, unsigned int timeout)
{
flushReceived();
if(send(query)) {
QString rcv;
if(receive(&rcv, timeout)) {
return rcv;
} else {
emit communicationFailure();
}
} else {
emit communicationFailure();
}
return QString();
}
std::set<QString> USBDevice::GetDevices()
{
std::set<QString> serials;
libusb_context *ctx;
libusb_init(&ctx);
#if LIBUSB_API_VERSION >= 0x01000106
libusb_set_option(ctx, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO);
#endif
SearchDevices([&serials](libusb_device_handle *, QString serial) -> bool {
serials.insert(serial);
return true;
}, ctx, true);
libusb_exit(ctx);
return serials;
}
void USBDevice::SearchDevices(std::function<bool (libusb_device_handle *, QString)> foundCallback, libusb_context *context, bool ignoreOpenError)
{
libusb_device **devList;
auto ndevices = libusb_get_device_list(context, &devList);
for (ssize_t idx = 0; idx < ndevices; idx++) {
int ret;
libusb_device *device = devList[idx];
libusb_device_descriptor desc = {};
ret = libusb_get_device_descriptor(device, &desc);
if (ret) {
/* some error occured */
qCritical() << "Failed to get device descriptor: "
<< libusb_strerror((libusb_error) ret);
continue;
}
bool correctID = false;
int numIDs = sizeof(IDs)/sizeof(IDs[0]);
for(int i=0;i<numIDs;i++) {
if(desc.idVendor == IDs[i].VID && desc.idProduct == IDs[i].PID) {
correctID = true;
break;
}
}
if(!correctID) {
continue;
}
/* Try to open the device */
libusb_device_handle *handle = nullptr;
ret = libusb_open(device, &handle);
if (ret) {
qDebug() << libusb_strerror((enum libusb_error) ret);
/* Failed to open */
if(!ignoreOpenError) {
QString message = "Found potential device but failed to open usb connection: \"";
message.append(libusb_strerror((libusb_error) ret));
message.append("\" On Linux this is most likely caused by a missing udev rule. "
"On Windows this most likely means that you are already connected to "
"this device (is another instance of the application already runnning?)");
qWarning() << message;
InformationBox::ShowError("Error opening device", message);
}
continue;
}
char c_product[256];
char c_serial[256];
libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber,
(unsigned char*) c_serial, sizeof(c_serial));
ret = libusb_get_string_descriptor_ascii(handle, desc.iProduct,
(unsigned char*) c_product, sizeof(c_product));
if (ret > 0) {
/* managed to read the product string */
QString product(c_product);
if (product == "LibreCAL") {
// this is a match
if(!foundCallback(handle, QString(c_serial))) {
// abort search
break;
}
}
} else {
qWarning() << "Failed to get product descriptor: "
<< libusb_strerror((libusb_error) ret);
}
libusb_close(handle);
}
libusb_free_device_list(devList, 1);
}
bool USBDevice::send(const QString &s)
{
lineBuffer.clear();
qDebug() << "USB send:" << s;
unsigned char data[s.size()+2];
memcpy(data, s.toLatin1().data(), s.size());
memcpy(&data[s.size()], "\r\n", 2);
int actual;
auto r = libusb_bulk_transfer(m_handle, LIBUSB_ENDPOINT_OUT | 0x03, data, s.size() + 2, &actual, 0);
if(r == 0 && actual == s.size() + 2) {
return true;
} else {
return false;
}
}
bool USBDevice::receive(QString *s, unsigned int timeout)
{
// check if we already have a line queued
unique_lock<mutex> lck(mtx);
while(lineBuffer.size() == 0) {
// needs to wait for an incoming line
using namespace std::chrono_literals;
if(cv.wait_for(lck, std::chrono::milliseconds(timeout)) == cv_status::timeout) {
qWarning() << "Timed out while waiting for received line";
return false;
}
}
*s = lineBuffer.takeFirst();
qDebug() << "USB recv:" << *s;
return true;
}
void USBDevice::ReceivedData()
{
uint16_t handled_len;
unique_lock<mutex> lck(mtx);
do {
handled_len = 0;
auto firstLinebreak = (uint8_t*) memchr(usbBuffer->getBuffer(), '\n', usbBuffer->getReceived());
if(firstLinebreak) {
handled_len = firstLinebreak - usbBuffer->getBuffer();
auto line = QString::fromLatin1((const char*) usbBuffer->getBuffer(), handled_len - 1);
// add received line to buffer
lineBuffer.append(line);
usbBuffer->removeBytes(handled_len + 1);
}
} while(handled_len > 0);
if(lineBuffer.size() > 0) {
cv.notify_one();
}
}
void USBDevice::flushReceived()
{
unique_lock<mutex> lck(mtx);
lineBuffer.clear();
}
void USBDevice::USBHandleThread()
{
qDebug() << "Receive thread started";
while (connected) {
libusb_handle_events(m_context);
}
qDebug() << "Disconnected, receive thread exiting";
}
QString USBDevice::serial() const
{
return m_serial;
}
@@ -0,0 +1,61 @@
#ifndef USBDEVICE_H
#define USBDEVICE_H
#include "Util/usbinbuffer.h"
#include <QtGlobal>
#ifdef Q_OS_MACOS
#include <libusb.h>
#else
#include <libusb-1.0/libusb.h>
#endif
#include <QString>
#include <set>
#include <functional>
#include <mutex>
#include <thread>
#include <QObject>
class USBDevice : public QObject
{
Q_OBJECT
public:
// connect to a CAL device. If serial is specified only connecting to this device, otherwise to the first one found
USBDevice(QString serial = QString());
~USBDevice();
bool Cmd(QString cmd, unsigned int timeout = 2000);
QString Query(QString query, unsigned int timeout = 2000);
QString serial() const;
// Returns serial numbers of all connected devices
static std::set<QString> GetDevices();
bool send(const QString &s);
bool receive(QString *s, unsigned int timeout = 2000);
void flushReceived();
signals:
void communicationFailure();
private slots:
void ReceivedData();
private:
void USBHandleThread();
bool connected;
std::thread *m_receiveThread;
static void SearchDevices(std::function<bool (libusb_device_handle *, QString)> foundCallback, libusb_context *context, bool ignoreOpenError);
libusb_device_handle *m_handle;
libusb_context *m_context;
USBInBuffer *usbBuffer;
std::mutex mtx;
std::condition_variable cv;
QStringList lineBuffer;
QString m_serial;
};
#endif // DEVICE_H
@@ -0,0 +1,186 @@
#ifndef CALIBRATION2_H
#define CALIBRATION2_H
#include "savable.h"
#include "calibrationmeasurement.h"
#include "calkit.h"
#include "Traces/trace.h"
#include "scpi.h"
#include <mutex>
class Calibration : public QObject, public Savable, public SCPINode
{
Q_OBJECT
friend class LibreCALDialog;
friend class CalibrationTests;
public:
Calibration();
enum class Type {
None,
OSL,
SOLT,
SOLTwithoutRxMatch,
ThroughNormalization,
TRL,
Last,
};
class CalType {
public:
Type type;
std::vector<unsigned int> usedPorts; // port count starts at 1
QString getReadableDescription();
QString getShortString();
static CalType fromShortString(QString s);
friend bool operator==(const CalType &lhs, const CalType &rhs);
};
static QString TypeToString(Type type);
static Type TypeFromString(QString s);
// Applies calculated calibration coefficients to measurement data
void correctMeasurement(DeviceDriver::VNAMeasurement &d);
void correctTraces(std::map<QString, Trace*> traceSet);
// Starts the calibration edit dialog, allowing the user to make/delete measurements
void edit(TraceModel *traceModel = nullptr);
Calkit& getKit();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
bool toFile(QString filename = QString());
bool fromFile(QString filename = QString());
// Returns all possible calibration types/port permutations for the currently connected device.
// If no device is connected, a two-port device is assumed
static std::vector<CalType> getAvailableCalibrations();
// Returns vector of all calibration types (without 'Last')
static std::vector<Type> getTypes();
// Checks whether all measurements for a specific calibration are available.
// If pointer to the frequency/points variables are given, the start/stop frequency and number of points the calibration will have after the calculation is stored there
bool canCompute(CalType type, double *startFreq = nullptr, double *stopFreq = nullptr, int *points = nullptr, bool *isLog = nullptr);
// Resets the calibration (deletes all measurements and calculated coefficients)
void reset();
// Returns the minimum number of ports for a given calibration type.
// E.g. the SOL(T) calibration can work with only one port, a through normalization requires at least two
static int minimumPorts(Type type);
// Adds a new measurement point (data) to all calibration measurements (m)
void addMeasurements(std::set<CalibrationMeasurement::Base*> m, const DeviceDriver::VNAMeasurement &data);
// Deletes all datapoints in the calibration measurements (m)
void clearMeasurements(std::set<CalibrationMeasurement::Base*> m);
CalType getCaltype() const;
enum class InterpolationType {
Unchanged, // Nothing has changed, settings and calibration points match
Exact, // Every frequency point in settings has an exact calibration point (but there are more calibration points outside of the sweep)
Interpolate, // Every point in the sweep can be interpolated between two calibration points
Extrapolate, // At least one point in sweep is outside of the calibration and has to be extrapolated
NoCalibration, // No calibration available
};
InterpolationType getInterpolation(double f_start, double f_stop, int npoints);
std::vector<Trace*> getErrorTermTraces();
std::vector<Trace*> getMeasurementTraces();
QString getCurrentCalibrationFile();
double getMinFreq();
double getMaxFreq();
int getNumPoints();
bool hasUnsavedChanges() const;
QString getValidDevice() const;
bool validForDevice(QString serial) const;
// query whether error terms coefficients are available. Port count starts at 1
bool hasDirectivity(unsigned int port);
bool hasReflectionTracking(unsigned int port);
bool hasSourceMatch(unsigned int port);
bool hasReceiverMatch(unsigned int sourcePort, unsigned int receivePort);
bool hasTransmissionTracking(unsigned int sourcePort, unsigned int receivePort);
bool hasIsolation(unsigned int sourcePort, unsigned int receivePort);
public slots:
// Call once all datapoints of the current span have been added
void measurementsComplete();
// Call once when a measurement is aborted before all points have been captured
void measurementsAbort();
// Attempts to calculate the calibration coefficients. If not enough measurements are available, false is returned and the currently used coefficients are not changed
bool compute(CalType type);
// Deactivates the calibration, resets the calibration coefficients. Calibration measurements are NOT deleted.
void deactivate();
signals:
// emitted when the measurement of a set of calibration measurements should be started
void startMeasurements(std::set<CalibrationMeasurement::Base*> m);
// emitted whenever a measurement is complete (triggered by calling measurementsComplete())
void measurementsUpdated();
// emitted when taking a calibration measurement is aborted
void measurementsAborted();
// emitted when calibration coefficients were calculated/updated successfully
void activated(CalType type);
// emitted when the calibrationo coefficients were reset
void deactivated();
private:
enum class DefaultMeasurements {
SOL1Port,
SOLT2Port,
SOLT3Port,
SOLT4Port,
Last
};
static QString DefaultMeasurementsToString(DefaultMeasurements dm);
void createDefaultMeasurements(DefaultMeasurements dm);
void deleteMeasurements();
static bool hasFrequencyOverlap(std::vector<CalibrationMeasurement::Base*> m, double *startFreq = nullptr, double *stopFreq = nullptr, int *points = nullptr, bool *isLog = nullptr);
// returns all measurements that match the paramaters
std::vector<CalibrationMeasurement::Base*> findMeasurements(CalibrationMeasurement::Base::Type type, int port1 = 0, int port2 = 0);
// returns the first measurement in the list that matches the parameters
CalibrationMeasurement::Base* findMeasurement(CalibrationMeasurement::Base::Type type, int port1 = 0, int port2 = 0);
CalibrationMeasurement::Base *newMeasurement(CalibrationMeasurement::Base::Type type);
class Point {
public:
double frequency;
std::vector<std::complex<double>> D; // Directivity
std::vector<std::complex<double>> R; // Reflection tracking
std::vector<std::complex<double>> S; // Source Match
std::vector<std::vector<std::complex<double>>> L; // Receiver Match
std::vector<std::vector<std::complex<double>>> T; // Transmission tracking
std::vector<std::vector<std::complex<double>>> I; // Transmission isolation
Point interpolate(const Point &to, double alpha);
};
std::vector<Point> points;
Point createInitializedPoint(double f);
Point computeOSL(double f);
Point computeSOLT(double f);
Point computeSOLTwithoutRxMatch(double f);
Point computeThroughNormalization(double f);
Point computeTRL(double f);
std::vector<CalibrationMeasurement::Base*> measurements;
Calkit kit;
CalType caltype;
QString descriptiveCalName();
QString currentCalFile;
QString validDevice;
bool unsavedChanges;
std::recursive_mutex access;
};
#endif // CALIBRATION2_H
@@ -0,0 +1,350 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalibrationDialog</class>
<widget class="QDialog" name="CalibrationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1119</width>
<height>650</height>
</rect>
</property>
<property name="windowTitle">
<string>Calibration Measurements</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Measurements</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Create default measurements for:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="createDefault"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,0">
<item>
<widget class="QTableWidget" name="table">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderHighlightSections">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="bAdd">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Add</string>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="icon">
<iconset theme="list-add" resource="../icons.qrc">
<normaloff>:/icons/add.png</normaloff>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bDelete">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Delete</string>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bMoveUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-up" resource="../icons.qrc">
<normaloff>:/icons/up.png</normaloff>:/icons/up.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bMoveDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-down" resource="../icons.qrc">
<normaloff>:/icons/down.png</normaloff>:/icons/down.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="measure">
<property name="text">
<string>Measure</string>
</property>
<property name="icon">
<iconset theme="media-playback-start" resource="../icons.qrc">
<normaloff>:/icons/play.png</normaloff>:/icons/play.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="selectMeasurement">
<property name="text">
<string>Select
Measurement</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clearMeasurement">
<property name="text">
<string>Clear</string>
</property>
<property name="icon">
<iconset theme="edit-delete" resource="../icons.qrc">
<normaloff>:/icons/trash.png</normaloff>:/icons/trash.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="eCal">
<property name="text">
<string>Electronic
Calibration</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="editCalkit">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Edit
Calibration
Kit</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>18</width>
<height>186</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Calibration</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Available calibrations:</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="calibrationList"/>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Active calibration:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="activeCalibration">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Minimum frequency:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="calMinFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Maximum frequency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="calMaxFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Points:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="calPoints">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<widget class="QPushButton" name="deactivate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Deactivate</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="activate">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Activate</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,729 @@
#include "calibrationmeasurement.h"
#include "unit.h"
#include "calibration.h"
#include "Util/util.h"
#include <QDateTime>
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QCheckBox>
using namespace std;
CalibrationMeasurement::Base::Base(Calibration *cal)
: cal(cal)
{
standard = nullptr;
timestamp = QDateTime();
}
std::vector<CalStandard::Virtual *> CalibrationMeasurement::Base::supportedStandards()
{
vector<CalStandard::Virtual*> ret;
for(auto s : cal->getKit().getStandards()) {
if(supportedStandardTypes().count(s->getType())) {
ret.push_back(s);
}
}
return ret;
}
bool CalibrationMeasurement::Base::setFirstSupportedStandard()
{
// assign first valid standard
auto supported = supportedStandards();
if(supported.size() > 0) {
setStandard(supported[0]);
return true;
}
return false;
}
bool CalibrationMeasurement::Base::setStandard(CalStandard::Virtual *standard)
{
if(standard) {
if(supportedStandardTypes().count(standard->getType())) {
// can use this standard
this->standard = standard;
connect(standard, &CalStandard::Virtual::deleted, this, [=](){
setStandard(nullptr);
});
emit standardChanged(standard);
return true;
} else {
// can't use this standard, leave unchanged
return false;
}
} else {
// nullptr passed, remove currently used standard
if(this->standard) {
disconnect(this->standard, &CalStandard::Virtual::deleted, this, nullptr);
}
this->standard = nullptr;
emit standardChanged(nullptr);
return true;
}
}
QTableWidgetItem *CalibrationMeasurement::Base::getStatisticsItem()
{
return new QTableWidgetItem(getStatistics());
}
QString CalibrationMeasurement::Base::getStatistics()
{
if(numPoints() > 0) {
QString data = QString::number(numPoints());
data.append(" points from ");
data.append(Unit::ToString(minFreq(), "Hz", " kMG"));
data.append(" to ");
data.append(Unit::ToString(maxFreq(), "Hz", " kMG"));
return data;
} else {
return "Not available";
}
}
std::vector<CalibrationMeasurement::Base::Type> CalibrationMeasurement::Base::availableTypes()
{
std::vector<Type> ret;
for(int i=0;i<(int) Type::Last;i++) {
ret.push_back((Type) i);
}
return ret;
}
QString CalibrationMeasurement::Base::TypeToString(CalibrationMeasurement::Base::Type type)
{
switch(type) {
case Type::Open: return "Open";
case Type::Short: return "Short";
case Type::Load: return "Load";
case Type::SlidingLoad: return "SlidingLoad";
case Type::Reflect: return "Reflect";
case Type::Through: return "Through";
case Type::Isolation: return "Isolation";
case Type::Line: return "Line";
case Type::Last: return "Invalid";
}
return "Invalid";
}
CalibrationMeasurement::Base::Type CalibrationMeasurement::Base::TypeFromString(QString s)
{
for(int i=0;i<(int) Type::Last;i++) {
if(TypeToString((Type) i).compare(s, Qt::CaseInsensitive) == 0) {
return (Type) i;
}
}
return Type::Last;
}
QWidget *CalibrationMeasurement::Base::createStandardWidget()
{
auto cbStandard = new QComboBox();
for(auto s : supportedStandards()) {
cbStandard->addItem(s->getDescription(), QVariant::fromValue((void*) s));
if(standard == s) {
cbStandard->setCurrentText(s->getDescription());
}
}
if(standard == 0 && cbStandard->count() > 0) {
// no standard was selected but no there is one available
setStandard((CalStandard::Virtual*) cbStandard->itemData(0, Qt::UserRole).value<void*>());
}
connect(cbStandard, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
auto s = (CalStandard::Virtual*) cbStandard->itemData(cbStandard->currentIndex(), Qt::UserRole).value<void*>();
setStandard(s);
});
connect(this, &CalibrationMeasurement::Base::standardChanged, cbStandard, [=](){
for(int i=0;i<cbStandard->count();i++) {
if((CalStandard::Virtual*) cbStandard->itemData(i, Qt::UserRole).value<void*>() == standard) {
cbStandard->setCurrentIndex(i);
}
}
});
return cbStandard;
}
nlohmann::json CalibrationMeasurement::Base::toJSON()
{
nlohmann::json j;
if(standard) {
j["standard"] = standard->getID();
}
j["timestamp"] = timestamp.toSecsSinceEpoch();
return j;
}
void CalibrationMeasurement::Base::fromJSON(nlohmann::json j)
{
if(j.contains("standard")) {
auto standards = cal->getKit().getStandards();
standard = nullptr;
unsigned long long id = j.value("standard", 0ULL);
for(auto s : standards) {
if(s->getID() == id) {
setStandard(s);
break;
}
}
}
timestamp = QDateTime::fromSecsSinceEpoch(j.value("timestamp", 0));
}
bool CalibrationMeasurement::Base::canMeasureSimultaneously(std::set<CalibrationMeasurement::Base *> measurements)
{
std::set<int> usedPorts;
for(auto m : measurements) {
std::vector<int> ports;
switch(m->getType()) {
case Type::Open:
case Type::Short:
case Type::Load:
case Type::SlidingLoad:
case Type::Reflect:
// Uses one port
ports.push_back(static_cast<OnePort*>(m)->getPort());
break;
case Type::Through:
case Type::Line:
// Uses two ports
ports.push_back(static_cast<TwoPort*>(m)->getPort1());
ports.push_back(static_cast<TwoPort*>(m)->getPort2());
break;
case Type::Isolation:
// Uses all ports, unable to measure simultaneously
if(measurements.size() > 1) {
return false;
}
break;
case Type::Last:
// invalid
return false;
}
for(auto p : ports) {
if(usedPorts.count(p)) {
// port already used for another measurement
return false;
} else {
usedPorts.insert(p);
}
}
}
// if we get here, no port collisions occurred
return true;
}
QDateTime CalibrationMeasurement::Base::getTimestamp() const
{
return timestamp;
}
CalStandard::Virtual* CalibrationMeasurement::Base::getStandard() const
{
return standard;
}
QTableWidgetItem *CalibrationMeasurement::OnePort::getStatisticsItem()
{
auto ret = Base::getStatisticsItem();
if(numPoints() > 0) {
if(!standard) {
ret->setBackground(Qt::red);
ret->setToolTip("No calibration standard assigned, unable to use this measurement");
} else if(standard->minFrequency() > points.front().frequency || standard->maxFrequency() < points.back().frequency) {
ret->setBackground(Qt::yellow);
ret->setToolTip("Usable frequency range constrained by calibration standard to "+Unit::ToString(minUsableFreq(), "Hz", " kMG", 4)+" - "+Unit::ToString(maxUsableFreq(), "Hz", " kMG", 4));
}
}
return ret;
}
double CalibrationMeasurement::OnePort::minUsableFreq()
{
if(points.size() > 0 && standard) {
return max(points.front().frequency, standard->minFrequency());
} else {
return numeric_limits<double>::max();
}
}
double CalibrationMeasurement::OnePort::maxUsableFreq()
{
if(points.size() > 0 && standard) {
return min(points.back().frequency, standard->maxFrequency());
} else {
return 0;
}
}
void CalibrationMeasurement::OnePort::clearPoints()
{
points.clear();
timestamp = QDateTime();
}
void CalibrationMeasurement::OnePort::addPoint(const DeviceDriver::VNAMeasurement &m)
{
QString measurementName = "S"+QString::number(port)+QString::number(port);
if(m.measurements.count(measurementName) > 0) {
Point p;
p.frequency = m.frequency;
p.S = m.measurements.at(measurementName);
points.push_back(p);
timestamp = QDateTime::currentDateTimeUtc();
}
}
QWidget *CalibrationMeasurement::OnePort::createSettingsWidget()
{
auto label = new QLabel("Port:");
auto cbPort = new QComboBox();
auto dev = DeviceDriver::getActiveDriver();
if(dev) {
if(port == 0) {
setPort(1);
}
for(unsigned int i=1;i<=dev->getInfo().Limits.VNA.ports;i++) {
cbPort->addItem(QString::number(i));
if(port == i) {
cbPort->setCurrentText(QString::number(i));
}
}
}
if(port > 0 && cbPort->findText(QString::number(port)) < 0) {
// set already selected port, even if device is not connected or does not have this port
cbPort->addItem(QString::number(port));
cbPort->setCurrentText(QString::number(port));
}
connect(cbPort, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
setPort(cbPort->currentText().toInt());
});
connect(this, &OnePort::portChanged, cbPort, [=](){
auto string = QString::number(port);
if(cbPort->findText(string) < 0) {
// setting does not exist yet, create (should not happen)
cbPort->addItem(string);
}
cbPort->setCurrentText(string);
});
auto ret = new QWidget();
auto layout = new QHBoxLayout;
layout->setContentsMargins(0,0,0,0);
layout->addWidget(label);
layout->addWidget(cbPort);
layout->setStretch(1, 1);
ret->setLayout(layout);
return ret;
}
nlohmann::json CalibrationMeasurement::OnePort::toJSON()
{
auto j = Base::toJSON();
j["port"] = port;
nlohmann::json jpoints;
for(auto &p : points) {
nlohmann::json jpoint;
jpoint["frequency"] = p.frequency;
jpoint["real"] = p.S.real();
jpoint["imag"] = p.S.imag();
jpoints.push_back(jpoint);
}
j["points"] = jpoints;
return j;
}
void CalibrationMeasurement::OnePort::fromJSON(nlohmann::json j)
{
clearPoints();
Base::fromJSON(j);
port = j.value("port", 0);
if(j.contains("points")) {
for(auto jpoint : j["points"]) {
Point p;
p.frequency = jpoint.value("frequency", 0.0);
p.S = complex<double>(jpoint.value("real", 0.0), jpoint.value("imag", 0.0));
points.push_back(p);
}
}
}
std::complex<double> CalibrationMeasurement::OnePort::getMeasured(double frequency)
{
if(points.size() == 0 || frequency < points.front().frequency || frequency > points.back().frequency) {
return numeric_limits<complex<double>>::quiet_NaN();
}
// frequency within points, interpolate
Point interp = Util::interpolate<Point, double>(points, frequency, [](const Point &p)->double{
return p.frequency;
});
return interp.S;
}
std::complex<double> CalibrationMeasurement::OnePort::getActual(double frequency)
{
return static_cast<CalStandard::OnePort*>(standard)->toS11(frequency);
}
int CalibrationMeasurement::OnePort::getPort() const
{
return port;
}
void CalibrationMeasurement::OnePort::setPort(unsigned int p)
{
if(port != p) {
port = p;
emit portChanged(p);
}
}
std::vector<CalibrationMeasurement::OnePort::Point> CalibrationMeasurement::OnePort::getPoints() const
{
return points;
}
QTableWidgetItem *CalibrationMeasurement::TwoPort::getStatisticsItem()
{
auto ret = Base::getStatisticsItem();
if(numPoints() > 0) {
if(!standard) {
ret->setBackground(Qt::red);
ret->setToolTip("No calibration standard assigned, unable to use this measurement");
} else if(standard->minFrequency() > points.front().frequency || standard->maxFrequency() < points.back().frequency) {
ret->setBackground(Qt::yellow);
ret->setToolTip("Usable frequency range constrained by calibration standard to "+Unit::ToString(minUsableFreq(), "Hz", " kMG", 4)+" - "+Unit::ToString(maxUsableFreq(), "Hz", " kMG", 4));
}
}
return ret;
}
double CalibrationMeasurement::TwoPort::minUsableFreq()
{
if(points.size() > 0 && standard) {
return max(points.front().frequency, standard->minFrequency());
} else {
return numeric_limits<double>::max();
}
}
double CalibrationMeasurement::TwoPort::maxUsableFreq()
{
if(points.size() > 0 && standard) {
return min(points.back().frequency, standard->maxFrequency());
} else {
return 0;
}
}
void CalibrationMeasurement::TwoPort::clearPoints()
{
points.clear();
timestamp = QDateTime();
}
void CalibrationMeasurement::TwoPort::addPoint(const DeviceDriver::VNAMeasurement &m)
{
Point p;
p.frequency = m.frequency;
p.S = m.toSparam().reduceTo({port1, port2});
points.push_back(p);
timestamp = QDateTime::currentDateTimeUtc();
}
QWidget *CalibrationMeasurement::TwoPort::createSettingsWidget()
{
auto label1 = new QLabel("From ");
auto cbPort1 = new QComboBox();
auto label2 = new QLabel(" to ");
auto cbPort2 = new QComboBox();
auto cbReverse = new QCheckBox("Reversed");
cbReverse->setToolTip("Enable this option if the calibration standard is defined with the port order swapped");
auto dev = DeviceDriver::getActiveDriver();
if(dev) {
if(port1 == 0) {
setPort1(1);
}
if(port2 == 0) {
setPort2(2);
}
for(unsigned int i=1;i<=dev->getInfo().Limits.VNA.ports;i++) {
cbPort1->addItem(QString::number(i));
cbPort2->addItem(QString::number(i));
if(port1 == i) {
cbPort1->setCurrentText(QString::number(i));
}
if(port2 == i) {
cbPort2->setCurrentText(QString::number(i));
}
}
}
if(port1 > 0 && cbPort1->findText(QString::number(port1)) < 0) {
// set already selected port, even if device is not connected or does not have this port
cbPort1->addItem(QString::number(port1));
cbPort1->setCurrentText(QString::number(port1));
}
if(port2 > 0 && cbPort2->findText(QString::number(port2)) < 0) {
// set already selected port, even if device is not connected or does not have this port
cbPort2->addItem(QString::number(port2));
cbPort2->setCurrentText(QString::number(port2));
}
cbReverse->setChecked(reverseStandard);
connect(cbPort1, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
setPort1(cbPort1->currentText().toInt());
});
connect(cbPort2, qOverload<int>(&QComboBox::currentIndexChanged), [=](){
setPort2(cbPort2->currentText().toInt());
});
connect(cbReverse, &QCheckBox::toggled, [=](){
setReverseStandard(cbReverse->isChecked());
});
connect(this, &TwoPort::port1Changed, cbPort1, [=](){
auto string = QString::number(port1);
if(cbPort1->findText(string) < 0) {
// setting does not exist yet, create (should not happen)
cbPort1->addItem(string);
}
cbPort1->setCurrentText(string);
});
connect(this, &TwoPort::port2Changed, cbPort2, [=](){
auto string = QString::number(port2);
if(cbPort2->findText(string) < 0) {
// setting does not exist yet, create (should not happen)
cbPort2->addItem(string);
}
cbPort2->setCurrentText(string);
});
connect(this, &TwoPort::reverseStandardChanged, cbReverse, [=](){
cbReverse->setChecked(reverseStandard);
});
auto ret = new QWidget();
auto layout = new QHBoxLayout;
layout->setContentsMargins(0,0,0,0);
layout->addWidget(label1);
layout->addWidget(cbPort1);
layout->addWidget(label2);
layout->addWidget(cbPort2);
layout->addWidget(cbReverse);
layout->setStretch(1, 1);
layout->setStretch(3, 1);
ret->setLayout(layout);
return ret;
}
nlohmann::json CalibrationMeasurement::TwoPort::toJSON()
{
auto j = Base::toJSON();
j["port1"] = port1;
j["port2"] = port2;
j["reverseStandard"] = reverseStandard;
nlohmann::json jpoints;
for(auto &p : points) {
nlohmann::json jpoint;
jpoint["frequency"] = p.frequency;
jpoint["Sparam"] = p.S.toJSON();
jpoints.push_back(jpoint);
}
j["points"] = jpoints;
return j;
}
void CalibrationMeasurement::TwoPort::fromJSON(nlohmann::json j)
{
clearPoints();
Base::fromJSON(j);
port1 = j.value("port1", 0);
port2 = j.value("port2", 0);
reverseStandard = j.value("reverseStandard", false);
if(j.contains("points")) {
for(auto jpoint : j["points"]) {
Point p;
p.frequency = jpoint.value("frequency", 0.0);
p.S.fromJSON(jpoint["Sparam"]);
points.push_back(p);
}
}
}
Sparam CalibrationMeasurement::TwoPort::getMeasured(double frequency)
{
if(points.size() == 0 || frequency < points.front().frequency || frequency > points.back().frequency) {
return Sparam();
}
// frequency within points, interpolate
Point interp = Util::interpolate<Point, double>(points, frequency, [](const Point &p)->double{
return p.frequency;
});
return interp.S;
}
Sparam CalibrationMeasurement::TwoPort::getActual(double frequency)
{
auto param = static_cast<CalStandard::TwoPort*>(standard)->toSparam(frequency);
if(reverseStandard) {
param.swapPorts(1, 2);
}
return param;
}
int CalibrationMeasurement::TwoPort::getPort2() const
{
return port2;
}
void CalibrationMeasurement::TwoPort::setPort1(unsigned int p)
{
if(port1 != p) {
port1 = p;
emit port1Changed(p);
}
}
void CalibrationMeasurement::TwoPort::setPort2(unsigned int p)
{
if(port2 != p) {
port2 = p;
emit port2Changed(p);
}
}
void CalibrationMeasurement::TwoPort::setReverseStandard(bool reverse)
{
if(reverseStandard != reverse) {
reverseStandard = reverse;
emit reverseStandardChanged(reverse);
}
}
std::vector<CalibrationMeasurement::TwoPort::Point> CalibrationMeasurement::TwoPort::getPoints() const
{
return points;
}
int CalibrationMeasurement::TwoPort::getPort1() const
{
return port1;
}
unsigned int CalibrationMeasurement::Isolation::numPoints()
{
return points.size();
}
void CalibrationMeasurement::Isolation::clearPoints()
{
points.clear();
timestamp = QDateTime();
}
void CalibrationMeasurement::Isolation::addPoint(const DeviceDriver::VNAMeasurement &m)
{
Point p;
p.frequency = m.frequency;
for(auto &meas : m.measurements) {
QString name = meas.first;
unsigned int rcv = name.mid(1, 1).toInt() - 1;
unsigned int src = name.mid(2, 1).toInt() - 1;
if(rcv > 8 || src > 8) {
// skip
continue;
}
if(rcv >= p.S.size()) {
p.S.resize(rcv + 1);
}
if(src >= p.S[rcv].size()) {
p.S[rcv].resize(src + 1);
}
p.S[rcv][src] = meas.second;
}
points.push_back(p);
timestamp = QDateTime::currentDateTimeUtc();
}
QWidget *CalibrationMeasurement::Isolation::createStandardWidget()
{
return new QLabel("Terminate all ports");
}
QWidget *CalibrationMeasurement::Isolation::createSettingsWidget()
{
return new QLabel("No settings available");
}
nlohmann::json CalibrationMeasurement::Isolation::toJSON()
{
auto j = Base::toJSON();
nlohmann::json jpoints;
for(auto &p : points) {
nlohmann::json jpoint;
jpoint["frequency"] = p.frequency;
nlohmann::json jdest;
for(auto dst : p.S) {
nlohmann::json jsrc;
for(auto src : dst) {
nlohmann::json jiso;
jiso["real"] = src.real();
jiso["imag"] = src.imag();
jsrc.push_back(jiso);
}
jdest.push_back(jsrc);
}
jpoint["S"] = jdest;
jpoints.push_back(jpoint);
}
j["points"] = jpoints;
return j;
}
void CalibrationMeasurement::Isolation::fromJSON(nlohmann::json j)
{
clearPoints();
Base::fromJSON(j);
if(j.contains("points")) {
for(auto jpoint : j["points"]) {
Point p;
p.frequency = jpoint.value("frequency", 0.0);
if(jpoint.contains("S")) {
for(auto jdest : jpoint["S"]) {
p.S.push_back(vector<complex<double>>());
for(auto jsrc : jdest) {
auto S = complex<double>(jsrc.value("real", 0.0), jsrc.value("imag", 0.0));
p.S.back().push_back(S);
}
}
}
points.push_back(p);
}
}
}
std::complex<double> CalibrationMeasurement::Isolation::getMeasured(double frequency, unsigned int portRcv, unsigned int portSrc)
{
if(points.size() == 0 || frequency < points.front().frequency || frequency > points.back().frequency) {
return numeric_limits<complex<double>>::quiet_NaN();
}
portRcv--;
portSrc--;
// find correct point, interpolate
Point interp = Util::interpolate<Point, double>(points, frequency, [](const Point &p)->double{
return p.frequency;
});
if(portRcv >= interp.S.size() || portSrc >= interp.S[portRcv].size()) {
return numeric_limits<complex<double>>::quiet_NaN();
} else {
return interp.S[portRcv][portSrc];
}
}
std::vector<CalibrationMeasurement::Isolation::Point> CalibrationMeasurement::Isolation::getPoints() const
{
return points;
}
QWidget *CalibrationMeasurement::SlidingLoad::createStandardWidget()
{
return new QLabel("Connect sliding load");
}
@@ -0,0 +1,367 @@
#ifndef CALIBRATIONMEASUREMENT_H
#define CALIBRATIONMEASUREMENT_H
#include "calstandard.h"
#include "Device/devicedriver.h"
#include <QDateTime>
#include <QObject>
#include <QTableWidgetItem>
class Calibration;
namespace CalibrationMeasurement {
class Base : public QObject, public Savable
{
Q_OBJECT
public:
Base(Calibration *cal);
enum class Type {
Open,
Short,
Load,
SlidingLoad,
Reflect,
Through,
Isolation,
Line,
Last,
};
std::vector<CalStandard::Virtual*> supportedStandards();
virtual bool setFirstSupportedStandard();
virtual bool setStandard(CalStandard::Virtual *standard);
virtual QTableWidgetItem *getStatisticsItem();
QString getStatistics();
virtual double minUsableFreq() = 0;
virtual double maxUsableFreq() = 0;
virtual double minFreq() = 0;
virtual double maxFreq() = 0;
virtual double getPointFreq(unsigned int p) = 0;
virtual unsigned int numPoints() = 0;
virtual bool readyForMeasurement() {return false;}
virtual bool readyForCalculation() {return false;}
static std::vector<Type> availableTypes();
static QString TypeToString(Type type);
static Type TypeFromString(QString s);
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() = 0;
virtual Type getType() = 0;
virtual void clearPoints() = 0;
virtual void addPoint(const DeviceDriver::VNAMeasurement &m) = 0;
virtual QWidget* createStandardWidget();
virtual QWidget* createSettingsWidget() = 0;
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
virtual std::vector<unsigned int> getPorts() = 0;
static bool canMeasureSimultaneously(std::set<Base *> measurements);
QDateTime getTimestamp() const;
CalStandard::Virtual *getStandard() const;
protected:
signals:
void standardChanged(CalStandard::Virtual* newStandard);
protected:
CalStandard::Virtual *standard;
QDateTime timestamp;
Calibration *cal;
};
class OnePort : public Base
{
Q_OBJECT
public:
OnePort(Calibration *cal) :
Base(cal),
port(0) {}
virtual QTableWidgetItem *getStatisticsItem() override;
virtual double minUsableFreq() override;
virtual double maxUsableFreq() override;
virtual double minFreq() override {return points.size() > 0 ? points.front().frequency : std::numeric_limits<double>::max();}
virtual double maxFreq() override {return points.size() > 0 ? points.back().frequency : 0;}
virtual double getPointFreq(unsigned int p) override { return p < points.size() ? points[p].frequency : 0;}
virtual unsigned int numPoints() override {return points.size();}
virtual bool readyForMeasurement() override {return standard != nullptr;}
virtual bool readyForCalculation() override {return standard && points.size() > 0;}
virtual void clearPoints() override;
virtual void addPoint(const DeviceDriver::VNAMeasurement &m) override;
virtual QWidget* createSettingsWidget() override;
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
virtual std::vector<unsigned int> getPorts() override {return {port};}
class Point {
public:
double frequency;
std::complex<double> S;
Point operator*(double scalar) const {
Point ret;
ret.frequency = frequency * scalar;
ret.S = S * scalar;
return ret;
}
Point operator+(const Point &p) const {
Point ret;
ret.frequency = frequency + p.frequency;
ret.S = S + p.S;
return ret;
}
};
std::vector<Point> getPoints() const;
std::complex<double> getMeasured(double frequency);
std::complex<double> getActual(double frequency);
int getPort() const;
public slots:
void setPort(unsigned int p);
protected:
signals:
void portChanged(int p);
protected:
unsigned int port; // starts at 1
std::vector<Point> points;
};
class Open : public OnePort
{
Q_OBJECT
public:
Open(Calibration *cal) :
OnePort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Open};}
virtual Type getType() override {return Type::Open;}
};
class Short : public OnePort
{
Q_OBJECT
public:
Short(Calibration *cal) :
OnePort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Short};}
virtual Type getType() override {return Type::Short;}
};
class Load : public OnePort
{
Q_OBJECT
public:
Load(Calibration *cal) :
OnePort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Load};}
virtual Type getType() override {return Type::Load;}
};
class SlidingLoad : public OnePort
{
Q_OBJECT
public:
SlidingLoad(Calibration *cal) :
OnePort(cal){setFirstSupportedStandard();}
virtual QWidget* createStandardWidget() override;
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {};}
virtual Type getType() override {return Type::SlidingLoad;}
};
class Reflect : public OnePort
{
Q_OBJECT
public:
Reflect(Calibration *cal) :
OnePort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Open,
CalStandard::Virtual::Type::Short, CalStandard::Virtual::Type::Reflect};}
virtual Type getType() override {return Type::Reflect;}
};
class TwoPort : public Base
{
Q_OBJECT
public:
TwoPort(Calibration *cal) :
Base(cal),
port1(0),
port2(0),
reverseStandard(false){}
virtual QTableWidgetItem *getStatisticsItem() override;
virtual double minUsableFreq() override;
virtual double maxUsableFreq() override;
virtual double minFreq() override {return points.size() > 0 ? points.front().frequency : std::numeric_limits<double>::max();}
virtual double maxFreq() override {return points.size() > 0 ? points.back().frequency : 0;}
virtual double getPointFreq(unsigned int p) override { return p < points.size() ? points[p].frequency : 0;}
virtual unsigned int numPoints() override {return points.size();}
virtual bool readyForMeasurement() override {return standard != nullptr;}
virtual bool readyForCalculation() override {return standard && points.size() > 0;}
virtual void clearPoints() override;
virtual void addPoint(const DeviceDriver::VNAMeasurement &m) override;
virtual QWidget* createSettingsWidget() override;
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
virtual std::vector<unsigned int> getPorts() override {return {port1, port2};}
class Point {
public:
double frequency;
Sparam S;
Point operator*(double scalar) const {
Point ret;
ret.frequency = frequency * scalar;
ret.S = S * scalar;
return ret;
}
Point operator+(const Point &p) const {
Point ret;
ret.frequency = frequency + p.frequency;
ret.S = S + p.S;
return ret;
}
};
std::vector<Point> getPoints() const;
Sparam getMeasured(double frequency);
Sparam getActual(double frequency);
int getPort1() const;
int getPort2() const;
public slots:
void setPort1(unsigned int p);
void setPort2(unsigned int p);
void setReverseStandard(bool reverse);
protected:
signals:
void port1Changed(int p);
void port2Changed(int p);
void reverseStandardChanged(bool r);
protected:
unsigned int port1, port2; // starts at 1
bool reverseStandard; // Set to true if standard is defined with ports swapped
std::vector<Point> points;
};
class Through : public TwoPort
{
Q_OBJECT
public:
Through(Calibration *cal) :
TwoPort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Through};}
virtual Type getType() override {return Type::Through;}
};
class Isolation : public Base
{
public:
Isolation(Calibration *cal) :
Base(cal){}
virtual double minUsableFreq() override {return minFreq();}
virtual double maxUsableFreq() override {return maxFreq();}
virtual double minFreq() override {return points.size() > 0 ? points.front().frequency : std::numeric_limits<double>::max();}
virtual double maxFreq() override {return points.size() > 0 ? points.back().frequency : 0;}
virtual double getPointFreq(unsigned int p) override { return p < points.size() ? points[p].frequency : 0;}
virtual unsigned int numPoints() override;
virtual bool readyForMeasurement() override {return true;}
virtual bool readyForCalculation() override {return points.size() > 0;}
virtual void clearPoints() override;
virtual void addPoint(const DeviceDriver::VNAMeasurement &m) override;
virtual QWidget* createStandardWidget() override;
virtual QWidget* createSettingsWidget() override;
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
virtual std::vector<unsigned int> getPorts() override {return {};}
class Point {
public:
double frequency;
std::vector<std::vector<std::complex<double>>> S;
Point operator*(double scalar) const {
Point ret;
ret.frequency = frequency * scalar;
for(const auto &v1 : S) {
std::vector<std::complex<double>> v;
for(const auto &s : v1) {
v.push_back(s * scalar);
}
ret.S.push_back(v);
}
return ret;
}
Point operator+(const Point &p) const {
Point ret;
ret.frequency = frequency + p.frequency;
if(S.size() != p.S.size()) {
throw std::runtime_error("Points to not have the same number of measurements");
}
for(unsigned int i=0;i<S.size();i++) {
std::vector<std::complex<double>> v;
if(S[i].size() != p.S[i].size()) {
throw std::runtime_error("Points to not have the same number of measurements");
}
for(unsigned int j=0;j<S[i].size();j++) {
v.push_back(S[i][j] + p.S[i][j]);
}
ret.S.push_back(v);
}
return ret;
}
};
std::vector<Point> getPoints() const;
std::complex<double> getMeasured(double frequency, unsigned int portRcv, unsigned int portSrc);
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {};}
virtual Type getType() override {return Type::Isolation;}
protected:
std::vector<Point> points;
};
class Line : public TwoPort
{
Q_OBJECT
public:
Line(Calibration *cal) :
TwoPort(cal){setFirstSupportedStandard();}
virtual std::set<CalStandard::Virtual::Type> supportedStandardTypes() override {return {CalStandard::Virtual::Type::Line};}
virtual Type getType() override {return Type::Line;}
};
}
#endif // CALIBRATIONMEASUREMENT_H
@@ -0,0 +1,314 @@
#include "calibrationviewdialog.h"
#include "ui_calibrationviewdialog.h"
#include <QGraphicsSimpleTextItem>
#include <QVector2D>
const QColor CalibrationViewDialog::colorNoCal = Qt::darkRed;
const QColor CalibrationViewDialog::colorHasCal = Qt::darkGreen;
CalibrationViewDialog::CalibrationViewDialog(Calibration *cal, unsigned int ports, QWidget *parent)
: QDialog(parent)
, ui(new Ui::CalibrationViewDialog)
, cal(cal)
, ports(ports)
{
setAttribute(Qt::WA_DeleteOnClose);
ui->setupUi(this);
ui->port->setMaximum(ports);
scene = new QGraphicsScene();
populateScene();
ui->view->setScene(scene);
connect(ui->port, &QSpinBox::valueChanged, this, &CalibrationViewDialog::populateScene);
}
CalibrationViewDialog::~CalibrationViewDialog()
{
delete ui;
}
void CalibrationViewDialog::populateScene()
{
scene->clear();
auto colorF = QApplication::palette().text().color();
auto colorB = QApplication::palette().base().color();
auto pen = QPen(colorF);
auto drawDot = [this](float x, float y, float size, QPen pen = QPen(), QBrush brush = QBrush()) {
scene->addEllipse(x - size/2, y - size/2, size, size, pen, brush);
};
auto drawText = [this](float x, float y, QString s, QColor color, auto alignH = Qt::AlignCenter, auto alignV = Qt::AlignCenter, float rotation = 0.0f) {
auto text = scene->addText(s);
text->setRotation(rotation);
switch(alignH) {
default:
case Qt::AlignLeft: break;
case Qt::AlignCenter:
case Qt::AlignHCenter: x -= text->boundingRect().bottomRight().x()/2; break;
case Qt::AlignRight: x -= text->boundingRect().bottomRight().x(); break;
}
switch(alignV) {
default:
case Qt::AlignTop: break;
case Qt::AlignCenter:
case Qt::AlignVCenter: y -= text->boundingRect().bottomRight().y()/2; break;
case Qt::AlignBottom: y -= text->boundingRect().bottomRight().y(); break;
}
text->setPos(QPointF(x, y));
text->setDefaultTextColor(color);
};
auto drawPath = [this, drawText](QList<QPointF> vertices, QColor color, QString label = QString(), bool arrow = false) {
// draw lines
for(unsigned int i=1;i<vertices.size();i++) {
scene->addLine(QLineF(vertices[i-1], vertices[i]), color);
if(i == vertices.size() / 2) {
// this is the middle segment, add label and arrow if required
auto midPoint = (vertices[i-1]+vertices[i])/2;
QVector2D direction = QVector2D(vertices[i] - vertices[i-1]);
direction.normalize();
auto ortho = QVector2D(-direction.y(), direction.x());
if(arrow) {
auto poly = QPolygonF({
QPointF(midPoint + direction.toPointF()*arrowLength/2),
QPointF(midPoint - direction.toPointF()*arrowLength/2 + ortho.toPointF()*arrowWidth/2),
QPointF(midPoint - direction.toPointF()*arrowLength/2 - ortho.toPointF()*arrowWidth/2)
});
scene->addPolygon(poly, color, color);
}
if(!label.isEmpty()) {
auto pos = midPoint;
if(label.startsWith("_")) {
label.remove(0, 1);
pos -= ortho.toPointF() * labelDistance;
} else {
pos += ortho.toPointF() * labelDistance;
}
auto alignH = abs(direction.x()) > abs(direction.y()) ? Qt::AlignCenter : Qt::AlignLeft;
auto alignV = abs(direction.y()) > abs(direction.x()) ? Qt::AlignCenter : Qt::AlignTop;
drawText(pos.x(), pos.y(), label, color, alignH, alignV);
}
}
}
};
auto DUTwidth = 2 * ports * pathSpacing;
auto DUTstartX = ports * pathSpacing + 2*boxSpacing;
// set the overall scene size
scene->setSceneRect(0, 0, marginLeft + DUTstartX + DUTwidth + marginRight, marginTop + ports * portHeight + marginBottom);
// create the DUT
// rectangle
scene->addRect(marginLeft + DUTstartX, marginTop, DUTwidth, ports * portHeight, pen);
drawText(marginLeft + DUTstartX + DUTwidth/2, marginTop, "DUT", colorF, Qt::AlignCenter, Qt::AlignTop);
// ports
for(unsigned int i=1;i<=ports;i++) {
// input marker
drawDot(marginLeft + DUTstartX, marginTop + i*portHeight - portHeight/2 + portForwardYOffset, portSize, colorF, colorB);
// output marker
drawDot(marginLeft + DUTstartX, marginTop + i*portHeight - portHeight/2 + portReverseYOffset, portSize, colorF, colorB);
}
// the reflection path
drawPath({
QPointF(marginLeft + DUTstartX + portSize/2, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(marginLeft + DUTstartX + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(marginLeft + DUTstartX + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(marginLeft + DUTstartX + portSize/2, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, colorF, "_S"+QString::number(ui->port->value())+QString::number(ui->port->value()), true);
// the forward transmission paths
float xstart = marginLeft + DUTstartX + pathSpacing;
for(unsigned int i=1;i<=ports;i++) {
if((int) i == ui->port->value()) {
// skip, this is the reflection path
continue;
}
drawDot(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset, junctionSize, colorF, colorF);
drawPath({
QPointF(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
QPointF(marginLeft + DUTstartX + portSize/2, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
}, colorF, QString((int) i > ui->port->value() ? "_" : "") + "S"+QString::number(i)+QString::number(ui->port->value()), true);
xstart += pathSpacing;
}
// the reverse transmission paths
bool first = true;
for(unsigned int i=1;i<=ports;i++) {
if((int) i == ui->port->value()) {
// skip, this is the reflection path
continue;
}
if(first) {
first = false;
drawDot(marginLeft + DUTstartX + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
drawPath({
QPointF(marginLeft + DUTstartX + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, colorF, "", false);
} else {
drawDot(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
}
drawPath({
QPointF(marginLeft + DUTstartX + portSize/2, marginTop + i*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + i*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, colorF, QString((int) i > ui->port->value() ? "" : "_") + "S"+QString::number(ui->port->value())+QString::number(i), true);
xstart += pathSpacing;
}
// isolation, transmission and receiver match paths
xstart = marginLeft;
for(unsigned int i=1;i<=ports;i++) {
if((int) i == ui->port->value()) {
// skip, this is the reflection path
continue;
}
// isolation
drawDot(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset, junctionSize, colorF, colorF);
drawPath({
QPointF(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
}, colorF);
drawPath({
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
}, cal->hasIsolation(ui->port->value(), i) ? colorHasCal : colorNoCal, QString((int) i > ui->port->value() ? "_" : "") + "I"+QString::number(i)+QString::number(ui->port->value()), true);
drawPath({
QPointF(xstart + pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
QPointF(marginLeft, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
}, colorF);
xstart += pathSpacing;
// transmission
drawDot(xstart, marginTop + i*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
drawPath({
QPointF(marginLeft + DUTstartX - portSize / 2, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
QPointF(marginLeft + DUTstartX - pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
}, colorF);
drawPath({
QPointF(marginLeft + DUTstartX - pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
}, cal->hasTransmissionTracking(ui->port->value(), i) ? colorHasCal : colorNoCal, "_T"+QString::number(i)+QString::number(ui->port->value()), true);
// Reveicer match
drawDot(marginLeft + DUTstartX - pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
drawPath({
QPointF(marginLeft + DUTstartX - pathSpacing, marginTop + i*portHeight - portHeight/2 + portReverseYOffset),
QPointF(marginLeft + DUTstartX - pathSpacing, marginTop + i*portHeight - portHeight/2 + portForwardYOffset),
QPointF(marginLeft + DUTstartX - portSize/2, marginTop + i*portHeight - portHeight/2 + portForwardYOffset),
}, cal->hasReceiverMatch(ui->port->value(), i) ? colorHasCal : colorNoCal, "L"+QString::number(i)+QString::number(ui->port->value()), true);
}
// reflection error box
drawDot(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset, junctionSize, colorF, colorF);
drawDot(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset, junctionSize, colorF, colorF);
drawDot(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
drawDot(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset, junctionSize, colorF, colorF);
drawDot(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset, junctionSize, colorF, colorF);
// unity paths
drawPath({
QPointF(xstart, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(marginLeft + DUTstartX - portSize/2, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
}, colorF, "", true);
drawPath({
QPointF(marginLeft + DUTstartX - portSize/2, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, colorF);
drawPath({
QPointF(marginLeft + portSize/2, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, colorF);
// directivity
drawPath({
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, cal->hasDirectivity(ui->port->value()) ? colorHasCal : colorNoCal, "_D"+QString::number(ui->port->value()), true);
// reflection tracking
drawPath({
QPointF(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart + pathSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
}, cal->hasReflectionTracking(ui->port->value()) ? colorHasCal : colorNoCal, "_R"+QString::number(ui->port->value()), true);
// source match
drawPath({
QPointF(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portReverseYOffset),
QPointF(xstart + pathSpacing + boxSpacing, marginTop + ui->port->value()*portHeight - portHeight/2 + portForwardYOffset),
}, cal->hasSourceMatch(ui->port->value()) ? colorHasCal : colorNoCal, "S"+QString::number(ui->port->value()), true);
// create the VNA ports
for(unsigned int i=1;i<=ports;i++) {
// stimulus port
if(i == (unsigned int) ui->port->value()) {
drawDot(marginLeft, marginTop + i*portHeight - portHeight/2 + portForwardYOffset, portSize, colorF, colorB);
drawText(marginLeft - portSize/2, marginTop + i*portHeight - portHeight/2 + portForwardYOffset, "a"+QString::number(i), colorF, Qt::AlignRight, Qt::AlignCenter);
}
// output marker
drawDot(marginLeft, marginTop + i*portHeight - portHeight/2 + portReverseYOffset, portSize, colorF, colorB);
drawText(marginLeft - portSize/2, marginTop + i*portHeight - portHeight/2 + portReverseYOffset, "b"+QString::number(i), colorF, Qt::AlignRight, Qt::AlignCenter);
}
// Fill the measurement correction table
ui->table->clear();
ui->table->setRowCount(ports*ports);
ui->table->setColumnCount(2);
ui->table->setHorizontalHeaderLabels({"Parameter", "Calibration Status"});
for(unsigned int i=1;i<=ports;i++) {
for(unsigned int j=1;j<=ports;j++) {
auto row = (i-1)*ports+j-1;
// add parameter
ui->table->setItem(row, 0, new QTableWidgetItem("S"+QString::number(j)+QString::number(i)));
// check the calibration status
QString status = "Uncalibrated";
if(i == j) {
// check reflection parameters
if(cal->hasSourceMatch(i) && cal->hasDirectivity(i) && cal->hasReflectionTracking(i)) {
// we are calibrated
status = "Calibrated";
// check if we have enhanced responses
QList<int> enhanced;
for(unsigned int k=1;k<=ports;k++) {
if(k==i) {
continue;
}
if(cal->hasReceiverMatch(i, k)) {
enhanced.append(k);
}
}
if(enhanced.size() == 1) {
status += " with enhanced response from port "+QString::number(enhanced[0]);
} else if(enhanced.size() > 1) {
status += " with enhanced response from ports ";
for(unsigned int k=0;k<enhanced.size();k++) {
if(k == enhanced.size() - 1) {
status += " and "+QString::number(enhanced[k]);
} else if(k > 0) {
status += ", ";
}
status += QString::number(enhanced[k]);
}
}
}
} else {
// check transmission calibration
if(cal->hasTransmissionTracking(i, j)) {
// we are calibrated
status = "Calibrated";
// check if we have isolation terms
if(cal->hasIsolation(i, j)) {
status += " with isolation measurement";
}
}
}
// add calibration status
ui->table->setItem(row, 1, new QTableWidgetItem(status));
}
}
ui->table->resizeColumnsToContents();
}
@@ -0,0 +1,51 @@
#ifndef CALIBRATIONVIEWDIALOG_H
#define CALIBRATIONVIEWDIALOG_H
#include <QDialog>
#include "calibration.h"
#include <QGraphicsScene>
namespace Ui {
class CalibrationViewDialog;
}
class CalibrationViewDialog : public QDialog
{
Q_OBJECT
public:
explicit CalibrationViewDialog(Calibration *cal, unsigned int ports, QWidget *parent = nullptr);
~CalibrationViewDialog();
private slots:
void populateScene();
private:
static constexpr int marginTop = 10;
static constexpr int marginBottom = 10;
static constexpr int marginLeft = 30;
static constexpr int marginRight = 10;
static constexpr int portHeight = 170;
static constexpr int portForwardYOffset = -50;
static constexpr int portReverseYOffset = 50;
static constexpr int boxSpacing = portReverseYOffset - portForwardYOffset;
static constexpr int portSize = 10;
static constexpr int arrowLength = 15;
static constexpr int arrowWidth = 10;
static constexpr int junctionSize = 6;
static constexpr int labelDistance = 6;
static constexpr int pathSpacing = 40;
static const QColor colorNoCal;
static const QColor colorHasCal;
Ui::CalibrationViewDialog *ui;
Calibration *cal;
unsigned int ports;
QGraphicsScene *scene;
};
#endif // CALIBRATIONVIEWDIALOG_H
@@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalibrationViewDialog</class>
<widget class="QDialog" name="CalibrationViewDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1106</width>
<height>665</height>
</rect>
</property>
<property name="windowTitle">
<string>Calibration Error Term Model</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>View error term model when stimulus is at port</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="port">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>8</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style='color: darkgreen;'&gt;Green&lt;/span&gt; error terms have been calculated from calibration measurements.&lt;/p&gt;&lt;p&gt;&lt;span style='color: darkred;'&gt;Red&lt;/span&gt; error terms are at their default values (either 1 or 0).&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QGraphicsView" name="view">
<property name="dragMode">
<enum>QGraphicsView::DragMode::ScrollHandDrag</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;D: Directivity, R: Reflection tracking, S: Source match, L: Receiver match, T: Transmission tracking, I: Isolation&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Which measurements are being corrected?</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="table">
<property name="editTriggers">
<set>QAbstractItemView::EditTrigger::NoEditTriggers</set>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SelectionMode::NoSelection</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CalibrationViewDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>374</x>
<y>509</y>
</hint>
<hint type="destinationlabel">
<x>374</x>
<y>265</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,550 @@
#include "calkit.h"
#include "calkitdialog.h"
#include "json.hpp"
#include "CustomWidgets/informationbox.h"
#include "appwindow.h"
#include "Util/app_common.h"
#include <fstream>
#include <iomanip>
#include <QMessageBox>
#include <QDebug>
#include <math.h>
using json = nlohmann::json;
using namespace std;
Calkit::Calkit()
: SCPINode("KIT"), scpi_std("STAndard")
{
// set default values
filename = "";
for(auto e : descr) {
e.var.setValue(e.def);
}
add(new SCPICommand("MANufacturer", [=](QStringList params) -> QString {
if(params.size() != 1 ) {
// no new value given
return SCPI::getResultName(SCPI::Result::Error);
}
manufacturer = params[0];
return SCPI::getResultName(SCPI::Result::Empty);
}, [=](QStringList) -> QString {
return manufacturer;
}, false));
add(new SCPICommand("SERial", [=](QStringList params) -> QString {
if(params.size() != 1 ) {
// no new value given
return SCPI::getResultName(SCPI::Result::Error);
}
serialnumber = params[0];
return SCPI::getResultName(SCPI::Result::Empty);
}, [=](QStringList) -> QString {
return serialnumber;
}, false));
add(new SCPICommand("DESCription", [=](QStringList params) -> QString {
if(params.size() != 1 ) {
// no new value given
return SCPI::getResultName(SCPI::Result::Error);
}
description = params[0];
return SCPI::getResultName(SCPI::Result::Empty);
}, [=](QStringList) -> QString {
return description;
}, false));
add(new SCPICommand("FILEname", nullptr, [=](QStringList) -> QString {
return filename;
}));
add(new SCPICommand("SAVE", [=](QStringList params) -> QString {
if(params.size() != 1 ) {
// no filename given or no calibration active
return SCPI::getResultName(SCPI::Result::Error);
}
toFile(params[0]);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr, false));
add(new SCPICommand("LOAD", nullptr, [=](QStringList params) -> QString {
if(params.size() != 1) {
// no filename given or no calibration active
return SCPI::getResultName(SCPI::Result::False);
}
if(this->fromFile(params[0])) {
return SCPI::getResultName(SCPI::Result::True);
} else {
return SCPI::getResultName(SCPI::Result::False);
}
}, false));
scpi_std.add(new SCPICommand("CLEAR", [=](QStringList params) -> QString {
Q_UNUSED(params);
setIdealDefault();
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
scpi_std.add(new SCPICommand("NUMber", nullptr, [=](QStringList params) -> QString {
Q_UNUSED(params);
return QString::number(standards.size());
}));
scpi_std.add(new SCPICommand("NEW", [=](QStringList params) -> QString {
if(params.size() != 2) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto type = CalStandard::Virtual::TypeFromString(params[0]);
if(type == CalStandard::Virtual::Type::Last) {
return SCPI::getResultName(SCPI::Result::Error);
}
auto s = CalStandard::Virtual::create(type);
if(!s) {
return SCPI::getResultName(SCPI::Result::Error);
}
s->setName(params[1]);
addStandard(s);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr, false));
scpi_std.add(new SCPICommand("DELete", [=](QStringList params) -> QString {
unsigned long long index;
if(!SCPI::paramToULongLong(params, 0, index)) {
return SCPI::getResultName(SCPI::Result::Error);
}
if(index < 1 || index > standards.size()) {
return SCPI::getResultName(SCPI::Result::Error);
}
removeStandard(standards[index-1]);
return SCPI::getResultName(SCPI::Result::Empty);
}, nullptr));
scpi_std.add(new SCPICommand("TYPE", nullptr, [=](QStringList params) -> QString {
unsigned long long index = 0;
if(!SCPI::paramToULongLong(params, 0, index)) {
return SCPI::getResultName(SCPI::Result::Error);
}
if(index < 1 || index > standards.size()) {
return SCPI::getResultName(SCPI::Result::Error);
}
return CalStandard::Virtual::TypeToString(standards[index-1]->getType()).replace(" ", "_");
}));
add(&scpi_std);
}
void Calkit::toFile(QString filename)
{
if(!filename.endsWith(".calkit")) {
filename.append(".calkit");
}
qDebug() << "Saving calkit to file" << filename;
ofstream file;
file.open(filename.toStdString());
file << setw(4) << toJSON() << endl;
file.close();
this->filename = filename;
}
static QString readLine(ifstream &file) {
string line;
getline(file, line);
return QString::fromStdString(line).simplified();
}
bool Calkit::fromFile(QString filename)
{
qDebug() << "Opening calkit from file" << filename;
ifstream file;
file.open(filename.toStdString());
if(!file.is_open()) {
qWarning() << "Unable to open file: " << filename;
return false;
}
json j;
try {
file >> j;
} catch (exception &e) {
qWarning() << "JSON parsing error: " << QString(e.what());
return false;
}
clearStandards();
this->filename = "";
if(j.contains("standards")) {
qDebug() << "new JSON format detected";
fromJSON(j);
} else {
// older format is used
struct {
using Open = struct {
double Z0, delay, loss, C0, C1, C2, C3;
QString file;
bool useMeasurements;
int Sparam;
};
Open open_m, open_f;
using Short = struct {
double Z0, delay, loss, L0, L1, L2, L3;
QString file;
bool useMeasurements;
int Sparam;
};
Short short_m, short_f;
using Load = struct {
double resistance, Z0, delay, Cparallel, Lseries;
QString file;
bool useMeasurements;
int Sparam;
};
Load load_m, load_f;
bool loadModelCFirst;
struct {
double Z0, delay, loss;
QString file;
bool useMeasurements;
int Sparam1, Sparam2;
} Through;
bool separate_male_female;
} SOLT;
struct {
struct {
double Z0;
} Through;
struct {
bool isShort;
} Reflection;
struct {
double delay, minFreq, maxFreq;
} Line;
} TRL;
if(j.contains("SOLT")) {
qDebug() << "old JSON format detected";
// calkit file uses json format, parse
Savable::parseJSON(j, descr);
const std::vector<Savable::SettingDescription> descr_deprecated = {{
{&SOLT.open_m.Z0, "SOLT.Open.Param.Z0", 50.0},
{&SOLT.open_m.delay, "SOLT.Open.Param.Delay", 0.0},
{&SOLT.open_m.loss, "SOLT.Open.Param.Loss", 0.0},
{&SOLT.open_m.C0, "SOLT.Open.Param.C0", 0.0},
{&SOLT.open_m.C1, "SOLT.Open.Param.C1", 0.0},
{&SOLT.open_m.C2, "SOLT.Open.Param.C2", 0.0},
{&SOLT.open_m.C3, "SOLT.Open.Param.C3", 0.0},
{&SOLT.open_m.useMeasurements, "SOLT.Open.Measurements.Use", false},
{&SOLT.open_m.file, "SOLT.Open.Measurements.File", ""},
{&SOLT.open_m.Sparam, "SOLT.Open.Measurements.Port", 0},
{&SOLT.open_f.Z0, "SOLT.Open.Param.Z0_Female", 50.0},
{&SOLT.open_f.delay, "SOLT.Open.Param.Delay_Female", 0.0},
{&SOLT.open_f.loss, "SOLT.Open.Param.Loss_Female", 0.0},
{&SOLT.open_f.C0, "SOLT.Open.Param.C0_Female", 0.0},
{&SOLT.open_f.C1, "SOLT.Open.Param.C1_Female", 0.0},
{&SOLT.open_f.C2, "SOLT.Open.Param.C2_Female", 0.0},
{&SOLT.open_f.C3, "SOLT.Open.Param.C3_Female", 0.0},
{&SOLT.open_f.useMeasurements, "SOLT.Open.Measurements.Use_Female", false},
{&SOLT.open_f.file, "SOLT.Open.Measurements.File_Female", ""},
{&SOLT.open_f.Sparam, "SOLT.Open.Measurements.Port_Female", 0},
{&SOLT.short_m.Z0, "SOLT.Short.Param.Z0", 50.0},
{&SOLT.short_m.delay, "SOLT.Short.Param.Delay", 0.0},
{&SOLT.short_m.loss, "SOLT.Short.Param.Loss", 0.0},
{&SOLT.short_m.L0, "SOLT.Short.Param.L0", 0.0},
{&SOLT.short_m.L1, "SOLT.Short.Param.L1", 0.0},
{&SOLT.short_m.L2, "SOLT.Short.Param.L2", 0.0},
{&SOLT.short_m.L3, "SOLT.Short.Param.L3", 0.0},
{&SOLT.short_m.useMeasurements, "SOLT.Short.Measurements.Use", false},
{&SOLT.short_m.file, "SOLT.Short.Measurements.File", ""},
{&SOLT.short_m.Sparam, "SOLT.Short.Measurements.Port", 0},
{&SOLT.short_f.Z0, "SOLT.Short.Param.Z0_Female", 50.0},
{&SOLT.short_f.delay, "SOLT.Short.Param.Delay_Female", 0.0},
{&SOLT.short_f.loss, "SOLT.Short.Param.Loss_Female", 0.0},
{&SOLT.short_f.L0, "SOLT.Short.Param.L0_Female", 0.0},
{&SOLT.short_f.L1, "SOLT.Short.Param.L1_Female", 0.0},
{&SOLT.short_f.L2, "SOLT.Short.Param.L2_Female", 0.0},
{&SOLT.short_f.L3, "SOLT.Short.Param.L3_Female", 0.0},
{&SOLT.short_f.useMeasurements, "SOLT.Short.Measurements.Use_Female", false},
{&SOLT.short_f.file, "SOLT.Short.Measurements.File_Female", ""},
{&SOLT.short_f.Sparam, "SOLT.Short.Measurements.Port_Female", 0},
{&SOLT.loadModelCFirst, "SOLT.loadModelCFirst", true},
{&SOLT.load_m.resistance, "SOLT.Load.Param.Resistance", 50.0},
{&SOLT.load_m.Z0, "SOLT.Load.Param.Z0", 50.0},
{&SOLT.load_m.delay, "SOLT.Load.Param.Delay", 0.0},
{&SOLT.load_m.Cparallel, "SOLT.Load.Param.C", 0.0},
{&SOLT.load_m.Lseries, "SOLT.Load.Param.L", 0.0},
{&SOLT.load_m.useMeasurements, "SOLT.Load.Measurements.Use", false},
{&SOLT.load_m.file, "SOLT.Load.Measurements.File", ""},
{&SOLT.load_m.Sparam, "SOLT.Load.Measurements.Port", 0},
{&SOLT.load_f.resistance, "SOLT.Load.Param.Resistance_Female", 50.0},
{&SOLT.load_f.Z0, "SOLT.Load.Param.Z0_Female", 50.0},
{&SOLT.load_f.delay, "SOLT.Load.Param.Delay_Female", 0.0},
{&SOLT.load_f.Cparallel, "SOLT.Load.Param.C_Female", 0.0},
{&SOLT.load_f.Lseries, "SOLT.Load.Param.L_Female", 0.0},
{&SOLT.load_f.useMeasurements, "SOLT.Load.Measurements.Use_Female", false},
{&SOLT.load_f.file, "SOLT.Load.Measurements.File_Female", ""},
{&SOLT.load_f.Sparam, "SOLT.Load.Measurements.Port_Female", 0},
{&SOLT.Through.Z0, "SOLT.Through.Param.Z0", 50.0},
{&SOLT.Through.delay, "SOLT.Through.Param.Delay", 0.0},
{&SOLT.Through.loss, "SOLT.Through.Param.Loss", 0.0},
{&SOLT.Through.useMeasurements, "SOLT.Through.Measurements.Use", false},
{&SOLT.Through.file, "SOLT.Through.Measurements.File", ""},
{&SOLT.Through.Sparam1, "SOLT.Through.Measurements.Port1", 0},
{&SOLT.Through.Sparam2, "SOLT.Through.Measurements.Port2", 1},
{&SOLT.separate_male_female, "SOLT.SeparateMaleFemale", false},
{&TRL.Through.Z0, "TRL.Through.Z0", 50.0},
{&TRL.Reflection.isShort, "TRL.Reflect.isShort", false},
{&TRL.Line.delay, "TRL.Line.Delay", 74.0},
{&TRL.Line.minFreq, "TRL.Line.minFreq", 751000000.0},
{&TRL.Line.maxFreq, "TRL.Line.maxFreq", 6000000000.0},
}};
Savable::parseJSON(j, descr_deprecated);
} else {
qDebug() << "Legacy format detected";
// legacy file format, return to beginning of file
file.clear();
file.seekg(0);
SOLT.open_m.useMeasurements = readLine(file).toInt();
SOLT.short_m.useMeasurements = readLine(file).toInt();
SOLT.load_m.useMeasurements = readLine(file).toInt();
SOLT.Through.useMeasurements = readLine(file).toInt();
SOLT.open_m.Z0 = readLine(file).toDouble();
SOLT.open_m.delay = readLine(file).toDouble();
SOLT.open_m.loss = readLine(file).toDouble();
SOLT.open_m.C0 = readLine(file).toDouble();
SOLT.open_m.C1 = readLine(file).toDouble();
SOLT.open_m.C2 = readLine(file).toDouble();
SOLT.open_m.C3 = readLine(file).toDouble();
SOLT.short_m.Z0 = readLine(file).toDouble();
SOLT.short_m.delay = readLine(file).toDouble();
SOLT.short_m.loss = readLine(file).toDouble();
SOLT.short_m.L0 = readLine(file).toDouble();
SOLT.short_m.L1 = readLine(file).toDouble();
SOLT.short_m.L2 = readLine(file).toDouble();
SOLT.short_m.L3 = readLine(file).toDouble();
SOLT.load_m.resistance = readLine(file).toDouble();
SOLT.Through.Z0 = readLine(file).toDouble();
SOLT.Through.delay = readLine(file).toDouble();
SOLT.Through.loss = readLine(file).toDouble();
if(SOLT.open_m.useMeasurements) {
SOLT.open_m.file = readLine(file);
SOLT.open_m.Sparam = readLine(file).toInt();
}
if(SOLT.short_m.useMeasurements) {
SOLT.short_m.file = readLine(file);
SOLT.short_m.Sparam = readLine(file).toInt();
}
if(SOLT.load_m.useMeasurements) {
SOLT.load_m.file = readLine(file);
SOLT.load_m.Sparam = readLine(file).toInt();
}
if(SOLT.Through.useMeasurements) {
SOLT.Through.file = readLine(file);
SOLT.Through.Sparam1 = readLine(file).toInt();
SOLT.Through.Sparam2 = readLine(file).toInt();
}
TRL.Through.Z0 = readLine(file).toDouble();
TRL.Reflection.isShort = readLine(file).toDouble();
TRL.Line.delay = readLine(file).toDouble();
TRL.Line.minFreq = readLine(file).toDouble();
TRL.Line.maxFreq = readLine(file).toDouble();
SOLT.separate_male_female = false;
SOLT.loadModelCFirst = false;
}
vector<QString*> filenames = {&SOLT.short_m.file, &SOLT.open_m.file, &SOLT.load_m.file, &SOLT.short_f.file, &SOLT.open_f.file, &SOLT.load_f.file, &SOLT.Through.file};
QFileInfo d(filename);
for(auto f : filenames) {
if(f->isEmpty()) {
continue;
}
if(QFileInfo(*f).isRelative()) {
auto absDir = QDir(d.dir().path() + "/" + *f);
QString buf = *f;
*f = absDir.absolutePath();
}
}
// create standards from old calkit forma
auto open_m = new CalStandard::Open(SOLT.separate_male_female ? "Default male standard" : "Default standard", SOLT.open_m.Z0, SOLT.open_m.delay, SOLT.open_m.loss, SOLT.open_m.C0, SOLT.open_m.C1, SOLT.open_m.C2, SOLT.open_m.C3);
if(SOLT.open_m.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.open_m.file.toStdString());
open_m->setMeasurement(ts, SOLT.open_m.Sparam);
}
addStandard(open_m);
if(SOLT.separate_male_female) {
auto open_f = new CalStandard::Open("Default female standard", SOLT.open_f.Z0, SOLT.open_f.delay, SOLT.open_f.loss, SOLT.open_f.C0, SOLT.open_f.C1, SOLT.open_f.C2, SOLT.open_f.C3);
if(SOLT.open_f.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.open_f.file.toStdString());
open_m->setMeasurement(ts, SOLT.open_f.Sparam);
}
addStandard(open_f);
}
auto short_m = new CalStandard::Short(SOLT.separate_male_female ? "Default male standard" : "Default standard", SOLT.short_m.Z0, SOLT.short_m.delay, SOLT.short_m.loss, SOLT.short_m.L0, SOLT.short_m.L1, SOLT.short_m.L2, SOLT.short_m.L3);
if(SOLT.short_m.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.short_m.file.toStdString());
short_m->setMeasurement(ts, SOLT.short_m.Sparam);
}
addStandard(short_m);
if(SOLT.separate_male_female) {
auto short_f = new CalStandard::Short("Default female standard", SOLT.short_f.Z0, SOLT.short_f.delay, SOLT.short_f.loss, SOLT.short_f.L0, SOLT.short_f.L1, SOLT.short_f.L2, SOLT.short_f.L3);
if(SOLT.short_f.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.short_f.file.toStdString());
short_m->setMeasurement(ts, SOLT.short_f.Sparam);
}
addStandard(short_f);
}
auto load_m = new CalStandard::Load(SOLT.separate_male_female ? "Default male standard" : "Default standard", SOLT.load_m.Z0, SOLT.load_m.delay, 0.0, SOLT.load_m.resistance, SOLT.load_m.Cparallel, SOLT.load_m.Lseries, SOLT.loadModelCFirst);
if(SOLT.load_m.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.load_m.file.toStdString());
load_m->setMeasurement(ts, SOLT.load_m.Sparam);
}
addStandard(load_m);
if(SOLT.separate_male_female) {
auto load_f = new CalStandard::Load("Default female standard", SOLT.load_m.Z0, SOLT.load_f.delay, 0.0, SOLT.load_f.resistance, SOLT.load_f.Cparallel, SOLT.load_f.Lseries, SOLT.loadModelCFirst);
if(SOLT.load_f.useMeasurements) {
auto ts = Touchstone(1);
ts.fromFile(SOLT.load_f.file.toStdString());
load_m->setMeasurement(ts, SOLT.load_f.Sparam);
}
addStandard(load_f);
}
auto through = new CalStandard::Through("Default standard", SOLT.Through.Z0, SOLT.Through.delay, SOLT.Through.loss);
if(SOLT.Through.useMeasurements) {
auto ts = Touchstone(2);
ts.fromFile(SOLT.Through.file.toStdString());
through->setMeasurement(ts, SOLT.Through.Sparam1, SOLT.Through.Sparam2);
}
addStandard(through);
InformationBox::ShowMessage("Loading calkit file", "The file \"" + filename + "\" is stored in a deprecated"
" calibration kit format. Future versions of this application might not support"
" it anymore. Please save the calibration kit to update to the new format");
}
file.close();
this->filename = filename;
updateSCPINames();
return true;
}
void Calkit::edit(std::function<void (void)> updateCal)
{
auto dialog = new CalkitDialog(*this);
if(updateCal) {
QObject::connect(dialog, &CalkitDialog::settingsChanged, [=](){
updateCal();
});
}
if(AppWindow::showGUI()) {
dialog->show();
}
}
void Calkit::clearStandards()
{
while(standards.size() > 0) {
removeStandard(standards[0]);
}
}
void Calkit::updateSCPINames()
{
// Need to remove all standards from the subnode list first, otherwise
// name changes wouldn't work due to temporarily name collisions
for(auto &s : standards) {
scpi_std.remove(s);
}
unsigned int i=1;
for(auto s : standards) {
s->changeName(QString::number(i));
scpi_std.add(s);
i++;
}
}
std::vector<CalStandard::Virtual *> Calkit::getStandards() const
{
return standards;
}
void Calkit::addStandard(CalStandard::Virtual *s)
{
// check for ID collisions. This should never happen but would mess up further
// usage of the standards. Better to catch this now and throw an error
for(auto comp : standards) {
if(comp->getID() == s->getID()) {
// collision, do not add
InformationBox::ShowError("Calibration standard ID collision", "New standard \""
+s->getName()+"\" of type \""+CalStandard::Virtual::TypeToString(s->getType())
+"\" has the same ID as already existing standard \""
+comp->getName()+"\" of type \""+CalStandard::Virtual::TypeToString(comp->getType())
+"\" (ID="+QString::number(s->getID())+"). It will be removed from the calibration kit");
return;
}
}
standards.push_back(s);
updateSCPINames();
}
void Calkit::removeStandard(CalStandard::Virtual *s)
{
standards.erase(std::remove(standards.begin(), standards.end(), s), standards.end());
delete s;
updateSCPINames();
}
nlohmann::json Calkit::toJSON()
{
json j = Savable::createJSON(descr);
nlohmann::json jstandards;
for(auto s : standards) {
nlohmann::json jstandard;
jstandard["type"] = CalStandard::Virtual::TypeToString(s->getType()).toStdString();
jstandard["params"] = s->toJSON();
jstandards.push_back(jstandard);
}
j["standards"] = jstandards;
j["version"] = qlibrevnaApp->applicationVersion().toStdString();
return j;
}
void Calkit::fromJSON(nlohmann::json j)
{
clearStandards();
filename = "";
Savable::parseJSON(j, descr);
for(auto js : j["standards"]) {
if(!js.contains("type") || !js.contains("params")) {
// missing fields
continue;
}
auto type = CalStandard::Virtual::TypeFromString(QString::fromStdString(js.value("type", "")));
if(type == CalStandard::Virtual::Type::Last) {
// failed to parse type
continue;
}
auto s = CalStandard::Virtual::create(type);
s->fromJSON(js["params"]);
addStandard(s);
}
updateSCPINames();
}
void Calkit::setIdealDefault()
{
manufacturer = "LibreVNA";
description = "Default calibration kit with ideal standards";
clearStandards();
addStandard(new CalStandard::Open("Ideal Open Standard", 50.0, 0, 0, 0, 0, 0, 0));
addStandard(new CalStandard::Short("Ideal Short Standard", 50.0, 0, 0, 0, 0, 0, 0));
addStandard(new CalStandard::Load("Ideal Load Standard", 50.0, 0, 0, 50.0, 0, 0));
addStandard(new CalStandard::Through("Ideal Through Standard", 50.0, 0, 0));
updateSCPINames();
}
@@ -0,0 +1,67 @@
#ifndef CALKIT_H
#define CALKIT_H
#include "touchstone.h"
#include "Util/qpointervariant.h"
#include "calstandard.h"
#include "savable.h"
#include "scpi.h"
#include "LibreCAL/caldevice.h"
#include <string>
#include <complex>
#include <QDir>
class Calkit : public Savable, public SCPINode
{
friend class CalkitDialog;
friend class LibreCALDialog;
public:
Calkit();
Calkit(const Calkit&) = delete;
Calkit& operator= (const Calkit&) = delete;
class SOLT {
public:
std::complex<double> Open;
std::complex<double> Short;
std::complex<double> Load;
std::complex<double> ThroughS11, ThroughS12, ThroughS21, ThroughS22;
};
class TRL {
public:
bool reflectionIsNegative;
std::complex<double> ThroughS11, ThroughS12, ThroughS21, ThroughS22;
};
void toFile(QString filename);
bool fromFile(QString filename);
void edit(std::function<void(void)> updateCal = nullptr);
std::vector<CalStandard::Virtual *> getStandards() const;
void addStandard(CalStandard::Virtual* s);
void removeStandard(CalStandard::Virtual* s);
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
void setIdealDefault();
private:
void clearStandards();
void updateSCPINames();
QString manufacturer, serialnumber, description;
QString filename;
SCPINode scpi_std;
std::vector<CalStandard::Virtual*> standards;
const std::vector<Savable::SettingDescription> descr = {{
{&manufacturer, "Manufacturer", ""},
{&serialnumber, "Serialnumber", ""},
{&description, "Description", ""},
}};
};
#endif // CALKIT_H
@@ -0,0 +1,158 @@
#include "calkitdialog.h"
#include "ui_calkitdialog.h"
#include "CustomWidgets/informationbox.h"
#include "preferences.h"
#include <QPushButton>
#include <QDebug>
#include <QFileDialog>
#include <fstream>
#include <touchstone.h>
#include <QtGlobal>
#include <QAction>
#include <QMenu>
using namespace std;
CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) :
QDialog(parent),
ui(new Ui::CalkitDialog),
kit(c)
{
ui->setupUi(this);
updateEntries();
connect(ui->bDelete, &QPushButton::clicked, [=](){
auto row = ui->list->currentRow();
if(row >= 0) {
kit.removeStandard(kit.standards[row]);
updateStandardList();
}
});
connect(ui->bMoveUp, &QPushButton::clicked, [=](){
auto row = ui->list->currentRow();
if(row >= 1) {
swap(kit.standards[row], kit.standards[row-1]);
ui->list->setCurrentRow(row-1);
updateStandardList();
kit.updateSCPINames();
}
});
connect(ui->bMoveDown, &QPushButton::clicked, [=](){
auto row = ui->list->currentRow();
if(row < ui->list->count() - 1) {
swap(kit.standards[row], kit.standards[row+1]);
ui->list->setCurrentRow(row+1);
updateStandardList();
kit.updateSCPINames();
}
});
connect(ui->list, &QListWidget::currentRowChanged, this, &CalkitDialog::updateListEditButtons);
auto addMenu = new QMenu();
for(auto t : CalStandard::Virtual::availableTypes()) {
auto action = new QAction(CalStandard::Virtual::TypeToString(t));
connect(action, &QAction::triggered, [=](){
auto newStandard = CalStandard::Virtual::create(t);
if(newStandard) {
kit.addStandard(newStandard);
updateStandardList();
// start the edit dialog of the newly created standard
kit.standards.back()->edit(bind(&CalkitDialog::updateStandardList, this));
}
});
addMenu->addAction(action);
}
ui->bAdd->setMenu(addMenu);
updateStandardList();
connect(ui->list, &QListWidget::doubleClicked, [=](const QModelIndex &index){
if(!index.isValid()) {
return;
}
kit.standards[index.row()]->edit(bind(&CalkitDialog::updateStandardList, this));
});
connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, [this]() {
parseEntries();
emit settingsChanged();
});
connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, [this]() {
parseEntries();
emit settingsChanged();
accept();
});
connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, [=](){
auto filename = QFileDialog::getOpenFileName(this, "Open calibration kit coefficients", Preferences::getInstance().UISettings.Paths.calkit, "Calibration kit files (*.calkit)", nullptr, Preferences::QFileDialogOptions());
if(filename.length() > 0) {
Preferences::getInstance().UISettings.Paths.calkit = QFileInfo(filename).path();
try {
kit.fromFile(filename);
} catch (runtime_error &e) {
InformationBox::ShowError("Error", "The calibration kit file could not be parsed (" + QString(e.what()) + ")");
qWarning() << "Parsing of calibration kit failed while opening calibration file: " << e.what();
}
updateEntries();
updateStandardList();
}
});
connect(ui->buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=](){
auto filename = QFileDialog::getSaveFileName(this, "Save calibration kit coefficients", Preferences::getInstance().UISettings.Paths.calkit, "Calibration kit files (*.calkit)", nullptr, Preferences::QFileDialogOptions());
if(filename.length() > 0) {
Preferences::getInstance().UISettings.Paths.calkit = QFileInfo(filename).path();
parseEntries();
kit.toFile(filename);
}
});
}
CalkitDialog::~CalkitDialog()
{
delete ui;
}
void CalkitDialog::updateListEditButtons()
{
ui->bDelete->setEnabled(ui->list->currentRow() >= 0);
ui->bMoveUp->setEnabled(ui->list->currentRow() >= 1);
ui->bMoveDown->setEnabled(ui->list->currentRow() >= 0 && ui->list->currentRow() < ui->list->count() - 1);
}
void CalkitDialog::parseEntries()
{
kit.manufacturer = ui->manufacturer->text();
kit.serialnumber = ui->serialnumber->text();
kit.description = ui->description->toPlainText();
}
void CalkitDialog::updateEntries()
{
ui->manufacturer->setText(kit.manufacturer);
ui->serialnumber->setText(kit.serialnumber);
ui->description->setPlainText(kit.description);
}
void CalkitDialog::updateStandardList()
{
auto row = ui->list->currentRow();
ui->list->clear();
for(auto s : kit.standards) {
ui->list->addItem(s->getDescription());
}
if(row >= 0) {
if(row < ui->list->count()) {
ui->list->setCurrentRow(row);
} else if(ui->list->count() > 0) {
ui->list->setCurrentRow(ui->list->count() - 1);
}
}
updateListEditButtons();
}
@@ -0,0 +1,36 @@
#ifndef CALKITDIALOG_H
#define CALKITDIALOG_H
#include "calkit.h"
#include <QDialog>
#include <QAbstractButton>
#include <iostream>
#include <iomanip>
namespace Ui {
class CalkitDialog;
}
class CalkitDialog : public QDialog
{
Q_OBJECT
public:
explicit CalkitDialog(Calkit &c, QWidget *parent = nullptr);
~CalkitDialog();
signals:
void settingsChanged();
private slots:
void updateListEditButtons();
private:
void parseEntries();
void updateEntries();
void updateStandardList();
Ui::CalkitDialog *ui;
Calkit &kit;
};
#endif // CALKITDIALOG_H
@@ -0,0 +1,212 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CalkitDialog</class>
<widget class="QDialog" name="CalkitDialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>648</width>
<height>530</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Calibration Kit Coefficients</string>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="sizeGripEnabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QFormLayout" name="formLayout_7">
<item row="0" column="0">
<widget class="QLabel" name="label_27">
<property name="text">
<string>Manufacturer:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="manufacturer"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_31">
<property name="text">
<string>Serial number:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="serialnumber"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<layout class="QVBoxLayout" name="verticalLayout_9">
<item>
<widget class="QLabel" name="label_28">
<property name="text">
<string>Description:</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTextEdit" name="description"/>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Standards</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="list"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="bAdd">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Add</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../icons.qrc">
<normaloff>:/icons/add.png</normaloff>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bDelete">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Delete</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bMoveUp">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move up</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-up" resource="../icons.qrc">
<normaloff>:/icons/up.png</normaloff>:/icons/up.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bMoveDown">
<property name="enabled">
<bool>false</bool>
</property>
<property name="toolTip">
<string>Move down</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="go-down" resource="../icons.qrc">
<normaloff>:/icons/down.png</normaloff>:/icons/down.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>18</width>
<height>186</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Apply|QDialogButtonBox::Ok|QDialogButtonBox::Open|QDialogButtonBox::Save</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,920 @@
#include "calstandard.h"
#include "ui_CalStandardOpenEditDialog.h"
#include "ui_CalStandardShortEditDialog.h"
#include "ui_CalStandardLoadEditDialog.h"
#include "ui_CalStandardReflectEditDialog.h"
#include "ui_CalStandardThroughEditDialog.h"
#include "ui_CalStandardLineEditDialog.h"
#include "unit.h"
#include "Util/util.h"
using namespace std;
using namespace CalStandard;
Virtual::Virtual(QString name) :
SCPINode(name),
name(name),
minFreq(std::numeric_limits<double>::lowest()),
maxFreq(std::numeric_limits<double>::max())
{
id = Util::random(numeric_limits<unsigned long long>::max());
}
Virtual *Virtual::create(Virtual::Type type)
{
switch(type) {
case Type::Open: return new Open;
case Type::Short: return new Short;
case Type::Load: return new Load;
case Type::Reflect: return new Reflect;
case Type::Through: return new Through;
case Type::Line: return new Line;
case Type::Last:
break;
}
return nullptr;
}
std::vector<Virtual::Type> Virtual::availableTypes()
{
std::vector<Type> ret;
for(int i=0;i<(int) Type::Last;i++) {
ret.push_back((Type) i);
}
return ret;
}
QString Virtual::TypeToString(Virtual::Type type)
{
switch(type) {
case Type::Open: return "Open";
case Type::Short: return "Short";
case Type::Load: return "Load";
case Type::Reflect: return "Reflect";
case Type::Through: return "Through";
case Type::Line: return "Line";
case Type::Last: return "Invalid";
}
return "Invalid";
}
Virtual::Type Virtual::TypeFromString(QString s)
{
for(int i=0;i<(int) Type::Last;i++) {
if(TypeToString((Type) i).compare(s, Qt::CaseInsensitive) == 0) {
return (Type) i;
}
}
return Type::Last;
}
QString Virtual::getDescription()
{
return TypeToString(getType())+", "+name;
}
nlohmann::json Virtual::toJSON()
{
nlohmann::json j;
j["name"] = name.toStdString();
j["id"] = id;
return j;
}
void Virtual::fromJSON(nlohmann::json j)
{
name = QString::fromStdString(j.value("name", ""));
id = j.value("id", id);
}
unsigned long long Virtual::getID()
{
return id;
}
QString Virtual::getName() const
{
return name;
}
void Virtual::setName(const QString &value)
{
name = value;
}
void Virtual::setupSCPI()
{
addStringParameter("NAME", name);
}
void OnePort::setMeasurement(const Touchstone &ts, int port)
{
if(!touchstone) {
touchstone = new Touchstone(ts);
} else {
*touchstone = ts;
}
if(touchstone->ports() > 1) {
touchstone->reduceTo1Port(port);
}
minFreq = touchstone->minFreq();
maxFreq = touchstone->maxFreq();
}
void OnePort::clearMeasurement()
{
delete touchstone;
touchstone = nullptr;
minFreq = std::numeric_limits<double>::lowest();
maxFreq = std::numeric_limits<double>::max();
}
nlohmann::json OnePort::toJSON()
{
auto j = Virtual::toJSON();
if(touchstone) {
j["touchstone"] = touchstone->toJSON();
}
return j;
}
void OnePort::fromJSON(nlohmann::json j)
{
Virtual::fromJSON(j);
if(j.contains("touchstone")) {
Touchstone ts(1);
ts.fromJSON(j["touchstone"]);
setMeasurement(ts);
}
}
void OnePort::setupSCPI()
{
add(new SCPICommand("FILE", [=](QStringList params) -> QString {
if(params.size() < 1) {
return SCPI::getResultName(SCPI::Result::Error);
}
try {
auto ts = Touchstone::fromFile(params[0].toStdString());
unsigned long long index = 0;
if(params.size() == 2) {
if(!SCPI::paramToULongLong(params, 1, index)) {
return SCPI::getResultName(SCPI::Result::Error);
}
}
setMeasurement(ts, index-1);
return SCPI::getResultName(SCPI::Result::Empty);
} catch(const std::exception& e) {
// failed to load file
return SCPI::getResultName(SCPI::Result::Error);
}
}, nullptr, false));
Virtual::setupSCPI();
}
Open::Open()
{
Z0 = 50.0;
delay = loss = C0 = C1 = C2 = C3 = 0.0;
setupSCPI();
}
std::complex<double> Open::toS11(double freq)
{
if(touchstone) {
return touchstone->interpolate(freq).S[0];
} else {
// calculate fringing capacitance for open
double Cfringing = C0 * 1e-15 + C1 * 1e-27 * freq + C2 * 1e-36 * pow(freq, 2) + C3 * 1e-45 * pow(freq, 3);
// convert to impedance
complex<double> open;
if (Cfringing == 0) {
// special case to avoid issues with infinity
open = complex<double>(1.0, 0);
} else {
auto imp_open = complex<double>(0, -1.0 / (freq * 2 * M_PI * Cfringing));
open = (imp_open - complex<double>(50.0)) / (imp_open + complex<double>(50.0));
}
return Util::addTransmissionLine(open, Z0, delay*1e-12, loss*1e9, freq);
}
}
void Open::edit(std::function<void(void)> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardOpenEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->Z0->setUnit("Ω");
ui->Z0->setPrecision(4);
ui->Z0->setValue(Z0);
ui->delay->setValue(delay);
ui->loss->setValue(loss);
ui->C0->setValue(C0);
ui->C1->setValue(C1);
ui->C2->setValue(C2);
ui->C3->setValue(C3);
auto updateMeasurementLabel = [=](){
QString label;
if(touchstone) {
label = QString::number(touchstone->points())+" points from "+Unit::ToString(touchstone->minFreq(), "Hz", " kMG")+" to "+Unit::ToString(touchstone->maxFreq(), "Hz", " kMG");
} else {
label = "No measurements stored yet";
}
ui->measurementLabel->setText(label);
};
QObject::connect(ui->coefficients, &QRadioButton::toggled, [=](bool checked) {
if(checked) {
clearMeasurement();
}
ui->stackedWidget->setCurrentIndex(checked ? 0 : 1);
});
QObject::connect(ui->measurement, &QRadioButton::toggled, [=](bool checked) {
updateMeasurementLabel();
ui->stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
QObject::connect(ui->touchstoneImport, &TouchstoneImport::statusChanged, ui->updateFile, &QPushButton::setEnabled);
ui->touchstoneImport->setPorts(1);
if(touchstone) {
ui->measurement->setChecked(true);
ui->touchstoneImport->setFile(touchstone->getFilename());
} else {
ui->coefficients->setChecked(true);
}
QObject::connect(ui->updateFile, &QPushButton::clicked, [=](){
setMeasurement(ui->touchstoneImport->getTouchstone(), ui->touchstoneImport->getPorts()[0]);
updateMeasurementLabel();
});
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
Z0 = ui->Z0->value();
delay = ui->delay->value();
loss = ui->loss->value();
C0 = ui->C0->value();
C1 = ui->C1->value();
C2 = ui->C2->value();
C3 = ui->C3->value();
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Open::toJSON()
{
auto j = OnePort::toJSON();
j["Z0"] = Z0;
j["delay"] = delay;
j["loss"] = loss;
j["C0"] = C0;
j["C1"] = C1;
j["C2"] = C2;
j["C3"] = C3;
return j;
}
void Open::fromJSON(nlohmann::json j)
{
OnePort::fromJSON(j);
Z0 = j.value("Z0", 50.0);
delay = j.value("delay", 0.0);
loss = j.value("loss", 0.0);
C0 = j.value("C0", 0.0);
C1 = j.value("C1", 0.0);
C2 = j.value("C2", 0.0);
C3 = j.value("C3", 0.0);
}
void Open::setupSCPI()
{
addDoubleParameter("Z0", Z0, true, true, [=](){clearMeasurement();});
addDoubleParameter("DELAY", delay, true, true, [=](){clearMeasurement();});
addDoubleParameter("LOSS", loss, true, true, [=](){clearMeasurement();});
addDoubleParameter("C0", C0, true, true, [=](){clearMeasurement();});
addDoubleParameter("C1", C1, true, true, [=](){clearMeasurement();});
addDoubleParameter("C2", C2, true, true, [=](){clearMeasurement();});
addDoubleParameter("C3", C3, true, true, [=](){clearMeasurement();});
OnePort::setupSCPI();
}
Short::Short()
{
Z0 = 50.0;
delay = loss = L0 = L1 = L2 = L3 = 0.0;
setupSCPI();
}
std::complex<double> Short::toS11(double freq)
{
if(touchstone) {
return touchstone->interpolate(freq).S[0];
} else {
// calculate inductance for short
double Lseries = L0 * 1e-12 + L1 * 1e-24 * freq + L2 * 1e-33 * pow(freq, 2) + L3 * 1e-42 * pow(freq, 3);
// convert to impedance
auto imp_short = complex<double>(0, freq * 2 * M_PI * Lseries);
complex<double> _short = (imp_short - complex<double>(50.0)) / (imp_short + complex<double>(50.0));
return Util::addTransmissionLine(_short, Z0, delay*1e-12, loss*1e9, freq);
}
}
void Short::edit(std::function<void(void)> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardShortEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->Z0->setUnit("Ω");
ui->Z0->setPrecision(4);
ui->Z0->setValue(Z0);
ui->delay->setValue(delay);
ui->loss->setValue(loss);
ui->L0->setValue(L0);
ui->L1->setValue(L1);
ui->L2->setValue(L2);
ui->L3->setValue(L3);
auto updateMeasurementLabel = [=](){
QString label;
if(touchstone) {
label = QString::number(touchstone->points())+" points from "+Unit::ToString(touchstone->minFreq(), "Hz", " kMG")+" to "+Unit::ToString(touchstone->maxFreq(), "Hz", " kMG");
} else {
label = "No measurements stored yet";
}
ui->measurementLabel->setText(label);
};
QObject::connect(ui->coefficients, &QRadioButton::toggled, [=](bool checked) {
if(checked) {
clearMeasurement();
}
ui->stackedWidget->setCurrentIndex(checked ? 0 : 1);
});
QObject::connect(ui->measurement, &QRadioButton::toggled, [=](bool checked) {
updateMeasurementLabel();
ui->stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
QObject::connect(ui->touchstoneImport, &TouchstoneImport::statusChanged, ui->updateFile, &QPushButton::setEnabled);
ui->touchstoneImport->setPorts(1);
if(touchstone) {
ui->measurement->setChecked(true);
ui->touchstoneImport->setFile(touchstone->getFilename());
} else {
ui->coefficients->setChecked(true);
}
QObject::connect(ui->updateFile, &QPushButton::clicked, [=](){
setMeasurement(ui->touchstoneImport->getTouchstone(), ui->touchstoneImport->getPorts()[0]);
updateMeasurementLabel();
});
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
Z0 = ui->Z0->value();
delay = ui->delay->value();
loss = ui->loss->value();
L0 = ui->L0->value();
L1 = ui->L1->value();
L2 = ui->L2->value();
L3 = ui->L3->value();
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Short::toJSON()
{
auto j = OnePort::toJSON();
j["Z0"] = Z0;
j["delay"] = delay;
j["loss"] = loss;
j["L0"] = L0;
j["L1"] = L1;
j["L2"] = L2;
j["L3"] = L3;
return j;
}
void Short::fromJSON(nlohmann::json j)
{
OnePort::fromJSON(j);
Z0 = j.value("Z0", 50.0);
delay = j.value("delay", 0.0);
loss = j.value("loss", 0.0);
L0 = j.value("L0", 0.0);
L1 = j.value("L1", 0.0);
L2 = j.value("L2", 0.0);
L3 = j.value("L3", 0.0);
}
void Short::setupSCPI()
{
addDoubleParameter("Z0", Z0, true, true, [=](){clearMeasurement();});
addDoubleParameter("DELAY", delay, true, true, [=](){clearMeasurement();});
addDoubleParameter("LOSS", loss, true, true, [=](){clearMeasurement();});
addDoubleParameter("L0", L0, true, true, [=](){clearMeasurement();});
addDoubleParameter("L1", L1, true, true, [=](){clearMeasurement();});
addDoubleParameter("L2", L2, true, true, [=](){clearMeasurement();});
addDoubleParameter("L3", L3, true, true, [=](){clearMeasurement();});
OnePort::setupSCPI();
}
Load::Load()
{
Z0 = 50.0;
resistance = 50.0;
delay = Cparallel = Lseries = 0;
Cfirst = true;
setupSCPI();
}
std::complex<double> Load::toS11(double freq)
{
if(touchstone) {
return touchstone->interpolate(freq).S[0];
} else {
auto imp_load = complex<double>(resistance, 0);
if (Cfirst) {
// C is the first parameter starting from the VNA port. But the load is modeled here starting from
// the other end, so we need to start with the inductor
imp_load += complex<double>(0, freq * 2 * M_PI * Lseries);
}
// Add parallel capacitor to impedance
if(Cparallel > 0) {
auto imp_C = complex<double>(0, -1.0 / (freq * 2 * M_PI * Cparallel));
imp_load = (imp_load * imp_C) / (imp_load + imp_C);
}
if (!Cfirst) {
// inductor not added yet, do so now
imp_load += complex<double>(0, freq * 2 * M_PI * Lseries);
}
complex<double> load = (imp_load - complex<double>(50.0)) / (imp_load + complex<double>(50.0));
return Util::addTransmissionLine(load, Z0, delay*1e-12, loss*1e9, freq);
}
}
void Load::edit(std::function<void(void)> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardLoadEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->resistance->setUnit("Ω");
ui->resistance->setPrecision(4);
ui->resistance->setValue(resistance);
ui->Z0->setUnit("Ω");
ui->Z0->setPrecision(4);
ui->Z0->setValue(Z0);
ui->delay->setValue(delay);
ui->loss->setValue(loss);
ui->parC->setUnit("F");
ui->parC->setPrefixes("pnum ");
ui->parC->setPrecision(4);
ui->parC->setValue(Cparallel);
ui->serL->setUnit("H");
ui->serL->setPrefixes("num ");
ui->serL->setPrecision(4);
ui->serL->setValue(Lseries);
if(Cfirst) {
ui->C_first->setChecked(true);
} else {
ui->L_first->setChecked(true);
}
auto updateMeasurementLabel = [=](){
QString label;
if(touchstone) {
label = QString::number(touchstone->points())+" points from "+Unit::ToString(touchstone->minFreq(), "Hz", " kMG")+" to "+Unit::ToString(touchstone->maxFreq(), "Hz", " kMG");
} else {
label = "No measurements stored yet";
}
ui->measurementLabel->setText(label);
};
QObject::connect(ui->coefficients, &QRadioButton::toggled, [=](bool checked) {
if(checked) {
clearMeasurement();
}
ui->stackedWidget->setCurrentIndex(checked ? 0 : 1);
});
QObject::connect(ui->measurement, &QRadioButton::toggled, [=](bool checked) {
updateMeasurementLabel();
ui->stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
QObject::connect(ui->touchstoneImport, &TouchstoneImport::statusChanged, ui->updateFile, &QPushButton::setEnabled);
ui->touchstoneImport->setPorts(1);
if(touchstone) {
ui->measurement->setChecked(true);
ui->touchstoneImport->setFile(touchstone->getFilename());
} else {
ui->coefficients->setChecked(true);
}
QObject::connect(ui->updateFile, &QPushButton::clicked, [=](){
setMeasurement(ui->touchstoneImport->getTouchstone(), ui->touchstoneImport->getPorts()[0]);
updateMeasurementLabel();
});
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
resistance = ui->resistance->value();
Z0 = ui->Z0->value();
delay = ui->delay->value();
loss = ui->loss->value();
Cparallel = ui->parC->value();
Lseries = ui->serL->value();
Cfirst = ui->C_first->isChecked();
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Load::toJSON()
{
auto j = OnePort::toJSON();
j["Z0"] = Z0;
j["delay"] = delay;
j["loss"] = loss;
j["resistance"] = resistance;
j["Cparallel"] = Cparallel;
j["Lseries"] = Lseries;
j["Cfirst"] = Cfirst;
return j;
}
void Load::fromJSON(nlohmann::json j)
{
OnePort::fromJSON(j);
Z0 = j.value("Z0", 50.0);
delay = j.value("delay", 0.0);
loss = j.value("loss", 0.0);
resistance = j.value("resistance", 0.0);
Cparallel = j.value("Cparallel", 0.0);
Lseries = j.value("Lseries", 0.0);
Cfirst = j.value("Cfirst", true);
}
void Load::setupSCPI()
{
addDoubleParameter("Z0", Z0, true, true, [=](){clearMeasurement();});
addDoubleParameter("DELAY", delay, true, true, [=](){clearMeasurement();});
addDoubleParameter("LOSS", loss, true, true, [=](){clearMeasurement();});
addDoubleParameter("RESistance", resistance, true, true, [=](){clearMeasurement();});
addDoubleParameter("CPARallel", Cparallel, true, true, [=](){clearMeasurement();});
addDoubleParameter("LSERies", Lseries, true, true, [=](){clearMeasurement();});
addBoolParameter("CFIRST", Cfirst, true, true, [=](){clearMeasurement();});
OnePort::setupSCPI();
}
void TwoPort::setMeasurement(const Touchstone &ts, int port1, int port2)
{
if(!touchstone) {
touchstone = new Touchstone(ts);
} else {
*touchstone = ts;
}
if(touchstone->ports() > 2) {
touchstone->reduceTo2Port(port1, port2);
}
minFreq = touchstone->minFreq();
maxFreq = touchstone->maxFreq();
}
void TwoPort::clearMeasurement()
{
delete touchstone;
touchstone = nullptr;
minFreq = std::numeric_limits<double>::lowest();
maxFreq = std::numeric_limits<double>::max();
}
nlohmann::json TwoPort::toJSON()
{
auto j = Virtual::toJSON();
if(touchstone) {
j["touchstone"] = touchstone->toJSON();
}
return j;
}
void TwoPort::fromJSON(nlohmann::json j)
{
Virtual::fromJSON(j);
if(j.contains("touchstone")) {
Touchstone ts(1);
ts.fromJSON(j["touchstone"]);
setMeasurement(ts);
}
}
void TwoPort::setupSCPI()
{
add(new SCPICommand("FILE", [=](QStringList params) -> QString {
if(params.size() != 3) {
return SCPI::getResultName(SCPI::Result::Error);
}
try {
auto ts = Touchstone::fromFile(params[0].toStdString());
unsigned long long index1, index2 = 0;
if(!SCPI::paramToULongLong(params, 1, index1)) {
return SCPI::getResultName(SCPI::Result::Error);
}
if(!SCPI::paramToULongLong(params, 2, index1)) {
return SCPI::getResultName(SCPI::Result::Error);
}
setMeasurement(ts, index1-1, index2-1);
return SCPI::getResultName(SCPI::Result::Empty);
} catch(const std::exception& e) {
// failed to load file
return SCPI::getResultName(SCPI::Result::Error);
}
}, nullptr, false));
Virtual::setupSCPI();
}
Through::Through()
{
Z0 = 50.0;
delay = 0.0;
loss = 0.0;
setupSCPI();
}
Sparam Through::toSparam(double freq)
{
if(touchstone) {
auto interp = touchstone->interpolate(freq).S;
return Sparam(interp[0], interp[1], interp[2], interp[3]);
} else {
// calculate effect of through
// nomenclature and formulas from https://loco.lab.asu.edu/loco-memos/edges_reports/report_20130807.pdf
auto w = 2.0 * M_PI * freq;
auto f_sqrt = sqrt(freq / 1e9);
auto offset_impedance = Z0; // lossless characteristic impedance
auto offset_loss = loss * 1e9;
auto offset_delay = delay * 1e-12;
auto Z_c = std::complex<double>(offset_impedance + (offset_loss / (2*w)) * f_sqrt, -(offset_loss / (2*w)) * f_sqrt);
auto gamma_l = std::complex<double>(offset_loss*offset_delay/(2*offset_impedance)*f_sqrt, w*offset_delay+offset_loss*offset_delay/(2*offset_impedance)*f_sqrt);
auto p = exp(-gamma_l); // propagation factor
auto Z_r = std::complex<double>(50.0);
auto Gamma = (Z_c - Z_r) / (Z_c + Z_r);
// S-parameter of a transmission line, see e.g. "Specifying Calibration Standards and Kits for Keysight Vector Network Analyzers"
// Keysight application note equations (6) and (7)
auto Sxx = Gamma * (1.0 - p*p) / (1.0 - p*p*Gamma*Gamma);
auto Sxy = p * (1.0 - Gamma*Gamma) / (1.0 - p*p*Gamma*Gamma);
return Sparam(Sxx, Sxy, Sxy, Sxx);
}
}
void Through::edit(std::function<void(void)> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardThroughEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->Z0->setUnit("Ω");
ui->Z0->setPrecision(4);
ui->Z0->setValue(Z0);
ui->delay->setValue(delay);
ui->loss->setValue(loss);
auto updateMeasurementLabel = [=](){
QString label;
if(touchstone) {
label = QString::number(touchstone->points())+" points from "+Unit::ToString(touchstone->minFreq(), "Hz", " kMG")+" to "+Unit::ToString(touchstone->maxFreq(), "Hz", " kMG");
} else {
label = "No measurements stored yet";
}
ui->measurementLabel->setText(label);
};
QObject::connect(ui->coefficients, &QRadioButton::toggled, [=](bool checked) {
if(checked) {
clearMeasurement();
}
ui->stackedWidget->setCurrentIndex(checked ? 0 : 1);
});
QObject::connect(ui->measurement, &QRadioButton::toggled, [=](bool checked) {
updateMeasurementLabel();
ui->stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
QObject::connect(ui->touchstoneImport, &TouchstoneImport::statusChanged, ui->updateFile, &QPushButton::setEnabled);
ui->touchstoneImport->setPorts(2);
if(touchstone) {
ui->measurement->setChecked(true);
ui->touchstoneImport->setFile(touchstone->getFilename());
} else {
ui->coefficients->setChecked(true);
}
QObject::connect(ui->updateFile, &QPushButton::clicked, [=](){
setMeasurement(ui->touchstoneImport->getTouchstone(), ui->touchstoneImport->getPorts()[0], ui->touchstoneImport->getPorts()[1]);
updateMeasurementLabel();
});
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
Z0 = ui->Z0->value();
delay = ui->delay->value();
loss = ui->loss->value();
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Through::toJSON()
{
auto j = TwoPort::toJSON();
j["Z0"] = Z0;
j["delay"] = delay;
j["loss"] = loss;
return j;
}
void Through::fromJSON(nlohmann::json j)
{
TwoPort::fromJSON(j);
Z0 = j.value("Z0", 50.0);
delay = j.value("delay", 0.0);
loss = j.value("loss", 0.0);
}
void Through::setupSCPI()
{
addDoubleParameter("Z0", Z0, true, true, [=](){clearMeasurement();});
addDoubleParameter("DELAY", delay, true, true, [=](){clearMeasurement();});
addDoubleParameter("LOSS", loss, true, true, [=](){clearMeasurement();});
TwoPort::setupSCPI();
}
Reflect::Reflect()
{
isShort = true;
setupSCPI();
}
std::complex<double> Reflect::toS11(double freq)
{
Q_UNUSED(freq)
return std::numeric_limits<complex<double>>::quiet_NaN();
}
void Reflect::edit(std::function<void ()> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardReflectEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->type->setCurrentIndex(isShort ? 1 : 0);
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
isShort = ui->type->currentIndex() == 1;
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Reflect::toJSON()
{
auto j = OnePort::toJSON();
j["isShort"] = isShort;
return j;
}
void Reflect::fromJSON(nlohmann::json j)
{
OnePort::fromJSON(j);
isShort = j.value("isShort", true);
}
bool Reflect::getIsShort() const
{
return isShort;
}
void Reflect::setupSCPI()
{
addBoolParameter("SHORT", isShort, true, true, [=](){clearMeasurement();});
OnePort::setupSCPI();
}
Line::Line()
{
Z0 = 50.0;
setDelay(0.0);
setupSCPI();
}
Sparam Line::toSparam(double freq)
{
Q_UNUSED(freq)
Sparam ret;
ret.set(1,1,numeric_limits<complex<double>>::quiet_NaN());
ret.set(1,2,numeric_limits<complex<double>>::quiet_NaN());
ret.set(2,1,numeric_limits<complex<double>>::quiet_NaN());
ret.set(2,2,numeric_limits<complex<double>>::quiet_NaN());
return ret;
}
void Line::edit(std::function<void ()> finishedCallback)
{
auto d = new QDialog;
auto ui = new Ui::CalStandardLineEditDialog;
ui->setupUi(d);
ui->name->setText(name);
ui->Z0->setUnit("Ω");
ui->Z0->setPrecision(4);
ui->Z0->setValue(Z0);
ui->delay->setValue(delay);
ui->delay->setUnit("s");
ui->delay->setPrefixes("pnum ");
ui->delay->setPrecision(4);
ui->minFreq->setUnit("Hz");
ui->minFreq->setPrecision(4);
ui->minFreq->setPrefixes(" kMG");
ui->minFreq->setValue(minFreq);
ui->maxFreq->setUnit("Hz");
ui->maxFreq->setPrecision(4);
ui->maxFreq->setPrefixes(" kMG");
ui->maxFreq->setValue(maxFreq);
QObject::connect(ui->delay, &SIUnitEdit::valueChanged, [=](double val){
ui->minFreq->setValue(1.0 / val * 20 / 360);
ui->maxFreq->setValue(1.0 / val * 160 / 360);
});
QObject::connect(d, &QDialog::accepted, [=](){
name = ui->name->text();
Z0 = ui->Z0->value();
setDelay(ui->delay->value());
if(finishedCallback) {
finishedCallback();
}
});
d->show();
}
nlohmann::json Line::toJSON()
{
auto j = TwoPort::toJSON();
j["delay"] = delay;
return j;
}
void Line::fromJSON(nlohmann::json j)
{
TwoPort::fromJSON(j);
setDelay(j.value("delay", 0.0));
}
void Line::setDelay(double delay)
{
this->delay = delay;
minFreq = 1.0 / delay * 20 / 360;
maxFreq = 1.0 / delay * 160 / 360;
}
void Line::setupSCPI()
{
addDoubleParameter("Z0", Z0, true, true, [=](){clearMeasurement();});
addDoubleParameter("DELAY", delay, true, true, [=](){clearMeasurement();});
TwoPort::setupSCPI();
}
@@ -0,0 +1,215 @@
#ifndef CALSTANDARD_H
#define CALSTANDARD_H
#include "savable.h"
#include "touchstone.h"
#include "Tools/parameters.h"
#include "scpi.h"
#include <complex>
#include <functional>
namespace CalStandard
{
class Virtual : public QObject, public Savable, public SCPINode
{
Q_OBJECT
public:
Virtual(QString name = "");
virtual ~Virtual(){emit deleted();}
enum class Type {
Open,
Short,
Load,
Reflect,
Through,
Line,
Last
};
static Virtual* create(Type type);
static std::vector<Type> availableTypes();
static QString TypeToString(Type type);
static Type TypeFromString(QString s);
virtual Type getType() = 0;
double minFrequency() {return minFreq;}
double maxFrequency() {return maxFreq;}
virtual void edit(std::function<void(void)> finishedCallback) = 0;
virtual QString getDescription();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
unsigned long long getID();
QString getName() const;
void setName(const QString &value);
signals:
void deleted();
protected:
void setupSCPI();
QString name;
double minFreq;
double maxFreq;
unsigned long long id;
};
class OnePort : public Virtual
{
public:
OnePort(QString name = "") :
Virtual(name),
touchstone(nullptr){}
virtual std::complex<double> toS11(double freq) = 0;
void setMeasurement(const Touchstone &ts, int port = 0);
void clearMeasurement();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
protected:
void setupSCPI();
Touchstone *touchstone;
};
class Calkit;
class Open : public OnePort
{
public:
Open();
Open(QString name, double Z0, double delay, double loss, double C0, double C1, double C2, double C3)
: OnePort(name), Z0(Z0), delay(delay), loss(loss), C0(C0), C1(C1), C2(C2), C3(C3){setupSCPI();}
virtual std::complex<double> toS11(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Open;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
private:
void setupSCPI();
double Z0, delay, loss, C0, C1, C2, C3;
};
class Short : public OnePort
{
public:
Short();
Short(QString name, double Z0, double delay, double loss, double L0, double L1, double L2, double L3)
: OnePort(name), Z0(Z0), delay(delay), loss(loss), L0(L0), L1(L1), L2(L2), L3(L3){setupSCPI();}
virtual std::complex<double> toS11(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Short;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
private:
void setupSCPI();
double Z0, delay, loss, L0, L1, L2, L3;
};
class Load : public OnePort
{
public:
Load();
Load(QString name, double Z0, double delay, double loss, double resistance, double Cparallel, double Lseries, bool Cfirst = true)
: OnePort(name), Z0(Z0), delay(delay), loss(loss), resistance(resistance), Cparallel(Cparallel), Lseries(Lseries), Cfirst(Cfirst){setupSCPI();}
virtual std::complex<double> toS11(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Load;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
private:
void setupSCPI();
double Z0, delay, loss, resistance, Cparallel, Lseries;
bool Cfirst;
};
class Reflect : public OnePort
{
public:
Reflect();
Reflect(QString name, bool isShort)
: OnePort(name), isShort(isShort){}
virtual std::complex<double> toS11(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Reflect;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
bool getIsShort() const;
private:
void setupSCPI();
bool isShort;
};
class TwoPort : public Virtual
{
public:
TwoPort(QString name = "") :
Virtual(name),
touchstone(nullptr){}
virtual Sparam toSparam(double freq) = 0;
void setMeasurement(const Touchstone &ts, int port1 = 0, int port2 = 1);
void clearMeasurement();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
protected:
void setupSCPI();
Touchstone *touchstone;
};
class Through : public TwoPort
{
public:
Through();
Through(QString name, double Z0, double delay, double loss)
: TwoPort(name), Z0(Z0), delay(delay), loss(loss){setupSCPI();}
virtual Sparam toSparam(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Through;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
private:
void setupSCPI();
double Z0, delay, loss;
};
class Line : public TwoPort
{
public:
Line();
Line(QString name, double Z0, double delay)
: TwoPort(name), Z0(Z0), delay(delay){setupSCPI();}
virtual Sparam toSparam(double freq) override;
virtual void edit(std::function<void(void)> finishedCallback = nullptr) override;
virtual Type getType() override {return Type::Line;}
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
private:
void setDelay(double delay);
void setupSCPI();
double Z0, delay;
};
}
#endif // CALSTANDARD_H
@@ -0,0 +1,23 @@
#include "manualcalibrationdialog.h"
#include "ui_manualcalibrationdialog.h"
#include "Traces/sparamtraceselector.h"
ManualCalibrationDialog::ManualCalibrationDialog(const TraceModel &model, Calibration *cal) :
ui(new Ui::ManualCalibrationDialog)
{
ui->setupUi(this);
auto traceSelector = new SparamTraceSelector(model, cal->getCaltype().usedPorts, true);
ui->verticalLayout->insertWidget(1, traceSelector, 1.0);
ui->buttonBox->setEnabled(false);
connect(traceSelector, &SparamTraceSelector::selectionValid, ui->buttonBox, &QDialogButtonBox::setEnabled);
connect(ui->buttonBox, &QDialogButtonBox::accepted, [=]() {
cal->correctTraces(traceSelector->getTraces());
accept();
});
}
ManualCalibrationDialog::~ManualCalibrationDialog()
{
delete ui;
}
@@ -0,0 +1,24 @@
#ifndef MANUALCALIBRATIONDIALOG_H
#define MANUALCALIBRATIONDIALOG_H
#include "calibration.h"
#include <QDialog>
namespace Ui {
class ManualCalibrationDialog;
}
class ManualCalibrationDialog : public QDialog
{
Q_OBJECT
public:
explicit ManualCalibrationDialog(const TraceModel &model, Calibration *cal);
~ManualCalibrationDialog();
private:
Ui::ManualCalibrationDialog *ui;
};
#endif // MANUALCALIBRATIONDIALOG_H
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ManualCalibrationDialog</class>
<widget class="QDialog" name="ManualCalibrationDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>597</width>
<height>331</height>
</rect>
</property>
<property name="windowTitle">
<string>Manual Calibration</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Select the traces which contain the S parameters you want to apply the calibration to:</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,36 @@
#include "colorpickerbutton.h"
#include <QColorDialog>
ColorPickerButton::ColorPickerButton(QWidget *parent)
: QPushButton(parent)
{
color = Qt::white;
connect(this, &ColorPickerButton::clicked, this, &ColorPickerButton::changeColor);
updateBackground();
}
void ColorPickerButton::setColor(const QColor &color)
{
this->color = color;
updateBackground();
}
const QColor &ColorPickerButton::getColor()
{
return color;
}
void ColorPickerButton::changeColor()
{
auto newColor = QColorDialog::getColor(color, parentWidget(), "Select color", QColorDialog::DontUseNativeDialog);
if(newColor.isValid() && newColor != color) {
setColor(newColor);
emit colorChanged(newColor);
}
}
void ColorPickerButton::updateBackground()
{
setStyleSheet("background-color:"+color.name());
}
@@ -0,0 +1,23 @@
#ifndef COLORPICKERBUTTON_H
#define COLORPICKERBUTTON_H
#include <QPushButton>
class ColorPickerButton : public QPushButton
{
Q_OBJECT
public:
ColorPickerButton(QWidget *parent = nullptr);
void setColor(const QColor& color);
const QColor& getColor();
signals:
void colorChanged(const QColor& color);
private slots:
void changeColor();
private:
void updateBackground();
QColor color;
};
#endif // COLORPICKERBUTTON_H
@@ -0,0 +1,149 @@
#include "csvimport.h"
#include "ui_csvimport.h"
#include "Traces/trace.h"
#include "preferences.h"
#include <QFileDialog>
#include <QDebug>
CSVImport::CSVImport(QWidget *parent) :
QWidget(parent),
ui(new Ui::csvimport),
status(false)
{
ui->setupUi(this);
ui->from->setPrecision(5);
ui->to->setPrecision(5);
ui->traces->setPrecision(0);
ui->points->setPrecision(0);
connect(ui->file, &QLineEdit::textChanged, this, &CSVImport::setFile);
}
CSVImport::~CSVImport()
{
delete ui;
}
bool CSVImport::getStatus()
{
return status;
}
CSV CSVImport::getCSV()
{
return csv;
}
QString CSVImport::getFilename()
{
return ui->file->text();
}
bool CSVImport::fillTrace(Trace &t)
{
try {
t.fillFromCSV(csv, ui->traceSelector->currentIndex());
return true;
} catch (const std::exception &e) {
qWarning() << QString("CSV import failed to fill trace: ") + e.what();
return false;
}
}
void CSVImport::setFile(QString filename)
{
ui->file->setText(filename);
evaluateFile();
emit filenameChanged(filename);
}
void CSVImport::selectTrace(unsigned int index)
{
ui->traceSelector->setCurrentIndex(index);
}
void CSVImport::on_browse_clicked()
{
auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", Preferences::getInstance().UISettings.Paths.data, "CSV files (*.csv)", nullptr, Preferences::QFileDialogOptions());
if (filename.length() > 0) {
Preferences::getInstance().UISettings.Paths.data = QFileInfo(filename).path();
ui->file->setText(filename);
evaluateFile();
}
}
void CSVImport::evaluateFile()
{
// store currently selected trace
auto selectedTrace = ui->traceSelector->currentText();
bool new_status = false;
ui->traces->setText("");
ui->points->setText("");
ui->from->setText("");
ui->to->setText("");
ui->status->clear();
ui->traceSelector->clear();
try {
csv = CSV::fromFile(ui->file->text());
auto domain = Trace::dataTypeFromString(csv.getHeader(0));
if (domain == Trace::DataType::Invalid) {
// just assume frequency domain
domain = Trace::DataType::Frequency;
}
ui->domain->setCurrentText(Trace::dataTypeToString(domain));
switch(domain) {
case Trace::DataType::Frequency:
ui->from->setUnit("Hz");
ui->to->setUnit("Hz");
break;
case Trace::DataType::Time:
case Trace::DataType::TimeZeroSpan:
ui->from->setUnit("s");
ui->to->setUnit("s");
break;
case Trace::DataType::Power:
ui->from->setUnit("dBm");
ui->to->setUnit("dBm");
break;
default:
break;
}
auto xvalues = csv.getColumn(0);
ui->from->setValue(xvalues.front());
ui->to->setValue(xvalues.back());
ui->points->setValue(xvalues.size());
bool okay = true;
int numTraces = 0;
while(okay) {
try {
Trace t;
auto name = t.fillFromCSV(csv, numTraces);
ui->traceSelector->addItem(name);
numTraces++;
} catch (const std::exception &e) {
// no more traces
okay = false;
}
}
ui->traces->setValue(numTraces);
// check if previously selected trace is still available
if(ui->traceSelector->findText(selectedTrace) >= 0) {
ui->traceSelector->setCurrentText(selectedTrace);
} else {
ui->traceSelector->setCurrentIndex(0);
}
new_status = true;
} catch (const std::exception &e) {
ui->status->setText(e.what());
}
if (new_status != status) {
status = new_status;
emit statusChanged(status);
}
emit filenameChanged(ui->file->text());
}
@@ -0,0 +1,45 @@
#ifndef CSVIMPORT_H
#define CSVIMPORT_H
#include "csv.h"
#include "Traces/trace.h"
#include <QWidget>
namespace Ui {
class csvimport;
}
class CSVImport : public QWidget
{
Q_OBJECT
public:
explicit CSVImport(QWidget *parent = nullptr);
~CSVImport();
bool getStatus();
CSV getCSV();
QString getFilename();
bool fillTrace(Trace &t);
signals:
void statusChanged(bool status);
void filenameChanged(QString name);
public slots:
void setFile(QString filename);
void selectTrace(unsigned int index);
private slots:
void on_browse_clicked();
private:
void evaluateFile();
Ui::csvimport *ui;
CSV csv;
bool status;
};
#endif // CSVIMPORT_H
@@ -0,0 +1,192 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>csvimport</class>
<widget class="QWidget" name="csvimport">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>232</width>
<height>256</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="file">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browse">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="status">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>239</red>
<green>41</green>
<blue>41</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>239</red>
<green>41</green>
<blue>41</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>190</red>
<green>190</green>
<blue>190</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Domain:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="domain">
<property name="enabled">
<bool>false</bool>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Traces:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="traces">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Points:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="points">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>From:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="SIUnitEdit" name="from">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>To:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="SIUnitEdit" name="to">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Trace:</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="traceSelector"/>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,121 @@
#include "informationbox.h"
#include <QCheckBox>
#include <QSettings>
#include <QDebug>
bool InformationBox::has_gui = true;
void InformationBox::ShowMessage(QString title, QString message, QString messageID, bool block, QWidget *parent)
{
if(!has_gui) {
// no gui option active, do not show any messages
return;
}
// check if the user still wants to see this message
unsigned int hash;
if(messageID.isEmpty()) {
hash = qHash(message);
} else {
hash = qHash(messageID);
}
QSettings s;
if(!s.contains(hashToSettingsKey(hash))) {
auto box = new InformationBox(title, message, QMessageBox::Information, hash, parent);
if(block) {
box->exec();
} else {
box->show();
}
}
}
void InformationBox::ShowMessageBlocking(QString title, QString message, QString messageID, QWidget *parent)
{
ShowMessage(title, message, messageID, true, parent);
}
void InformationBox::ShowError(QString title, QString message, QWidget *parent)
{
if(!has_gui) {
// no gui option active, do not show any messages
return;
}
auto box = new InformationBox(title, message, QMessageBox::Information, 0, parent);
box->show();
}
bool InformationBox::AskQuestion(QString title, QString question, bool defaultAnswer, QString messageID, QWidget *parent)
{
if(!has_gui) {
// no gui option active, do not show any messages
return defaultAnswer;
}
// check if the user still wants to see this message
unsigned int hash;
if(messageID.isEmpty()) {
hash = qHash(question);
} else {
hash = qHash(messageID);
}
QSettings s;
if(!s.contains(hashToSettingsKey(hash))) {
auto box = new InformationBox(title, question, QMessageBox::Question, hash, parent);
box->setStandardButtons(QMessageBox::Yes | QMessageBox::No);
int ret = box->exec();
if(ret == QMessageBox::Yes) {
return true;
} else {
return false;
}
} else {
// don't show this question anymore
return s.value(hashToSettingsKey(hash)).toBool();
}
}
void InformationBox::setGUI(bool enable)
{
has_gui = enable;
}
InformationBox::InformationBox(QString title, QString message, Icon icon, unsigned int hash, QWidget *parent)
: QMessageBox(parent),
hash(hash)
{
setWindowTitle(title);
setText(message);
setAttribute(Qt::WA_DeleteOnClose, true);
setModal(true);
setIcon(icon);
auto cb = new QCheckBox("Do not show this message again");
setCheckBox(cb);
if(hash == 0) {
cb->setVisible(false);
}
}
InformationBox::~InformationBox()
{
auto cb = checkBox();
if(cb->isChecked()) {
auto value = true;
auto clicked = clickedButton();
if(clicked && QMessageBox::standardButton(clicked) == StandardButton::No) {
value = false;
}
QSettings s;
s.setValue(hashToSettingsKey(hash), value);
}
}
QString InformationBox::hashToSettingsKey(unsigned int hash)
{
return QString("DoNotShowDialog/") + QString::number(hash);
}
@@ -0,0 +1,25 @@
#ifndef INFORMATIONBOX_H
#define INFORMATIONBOX_H
#include <QMessageBox>
class InformationBox : public QMessageBox
{
Q_OBJECT
public:
static void ShowMessage(QString title, QString message, QString messageID = QString(), bool block = false, QWidget *parent = nullptr);
static void ShowMessageBlocking(QString title, QString message, QString messageID = QString(), QWidget *parent = nullptr);
static void ShowError(QString title, QString message, QWidget *parent = nullptr);
// Display a dialog with yes/no buttons. Returns true if yes is clicked, false otherwise. If the user has selected to never see this message again, defaultAnswer is returned instead
static bool AskQuestion(QString title, QString question, bool defaultAnswer, QString messageID = QString(), QWidget *parent = nullptr);
static void setGUI(bool enable);
private:
InformationBox(QString title, QString message, QMessageBox::Icon icon, unsigned int hash, QWidget *parent);
~InformationBox();
static QString hashToSettingsKey(unsigned int hash);
unsigned int hash;
static bool has_gui;
};
#endif // INFORMATIONBOX_H
@@ -0,0 +1,186 @@
#include "jsonpickerdialog.h"
#include "ui_jsonpickerdialog.h"
JSONPickerDialog::JSONPickerDialog(const nlohmann::json &json, QWidget *parent) :
QDialog(parent),
ui(new Ui::JSONPickerDialog),
model(new JSONModel(json))
{
ui->setupUi(this);
ui->treeView->setModel(model);
}
JSONPickerDialog::~JSONPickerDialog()
{
delete ui;
delete model;
}
JSONModel::JSONModel(const nlohmann::json &json, QObject *parent) :
json(json)
{
Q_UNUSED(parent)
setupJsonInfo(json);
}
JSONModel::~JSONModel()
{
}
QModelIndex JSONModel::index(int row, int column, const QModelIndex &parent) const
{
if (!hasIndex(row, column, parent))
return QModelIndex();
nlohmann::json const *parentItem;
if (!parent.isValid())
parentItem = &json;
else
parentItem = static_cast<nlohmann::json*>(parent.internalPointer());
auto it = parentItem->begin();
int rb = row;
while(rb) {
it++;
rb--;
}
nlohmann::json *childItem = const_cast<nlohmann::json*>(&*it);
if (childItem)
return createIndex(row, column, childItem);
return QModelIndex();
}
QModelIndex JSONModel::parent(const QModelIndex &index) const
{
if (!index.isValid())
return QModelIndex();
nlohmann::json *childItem = static_cast<nlohmann::json*>(index.internalPointer());
if (childItem == &json) {
return QModelIndex();
}
// find the parent of this entry and its position in the list
nlohmann::json *parentItem = const_cast<nlohmann::json*>(jsonInfo.at(childItem).parent);
auto it = parentItem->begin();
int row = 0;
while(&*it != childItem) {
it++;
row++;
}
return createIndex(row, 0, parentItem);
}
int JSONModel::rowCount(const QModelIndex &parent) const
{
const nlohmann::json *parentItem;
if (parent.column() > 0) {
return 0;
}
if (!parent.isValid()) {
parentItem = &json;
} else {
parentItem = static_cast<nlohmann::json*>(parent.internalPointer());
}
if (parentItem->is_object() || parentItem->is_array()) {
return parentItem->size();
} else {
return 0;
}
}
int JSONModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent)
return 2;
}
QVariant JSONModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
nlohmann::json *item = static_cast<nlohmann::json*>(index.internalPointer());
auto info = jsonInfo.at(item);
switch(role) {
case Qt::DisplayRole:
switch(index.column()) {
case 0:
return info.name;
case 1:
if(!item->is_object() && !item->is_array()) {
return info.data;
}
}
return QVariant();
case Qt::CheckStateRole: {
if(index.column() == 0) {
return info.enabled ? Qt::Checked : Qt::Unchecked;
} else {
return QVariant();
}
}
default:
return QVariant();
}
}
QVariant JSONModel::headerData(int section, Qt::Orientation orientation, int role) const
{
Q_UNUSED(section)
Q_UNUSED(orientation)
Q_UNUSED(role)
return QVariant();
}
bool JSONModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
Q_UNUSED(value)
nlohmann::json *item = static_cast<nlohmann::json*>(index.internalPointer());
auto info = jsonInfo.at(item);
if(role == Qt::CheckStateRole)
{
info.enabled = !info.enabled;
jsonInfo[item] = info;
emit dataChanged(index, index);
return true;
}
if (role != Qt::EditRole)
return false;
return true;
}
Qt::ItemFlags JSONModel::flags(const QModelIndex &index) const
{
if (!index.isValid()) {
return Qt::NoItemFlags;
}
return Qt::ItemIsEditable | QAbstractItemModel::flags(index)|Qt::ItemIsUserCheckable;
}
void JSONModel::setupJsonInfo(const nlohmann::json &j)
{
for(auto it = j.begin();it != j.end(); it++) {
JSONInfo i;
i.parent = &j;
i.enabled = true;
if(j.is_object()) {
i.name = it.key().c_str();
} else if(j.is_array()) {
i.name = QString::number(it - j.begin() + 1);
}
if(it->is_object() || it->is_array()) {
setupJsonInfo(*it);
} else {
i.data = QString::fromStdString(it->dump());
}
jsonInfo[&*it] = i;
}
}
@@ -0,0 +1,56 @@
#ifndef JSONPICKERDIALOG_H
#define JSONPICKERDIALOG_H
#include "json.hpp"
#include <QDialog>
#include <QAbstractItemModel>
namespace Ui {
class JSONPickerDialog;
}
class JSONModel : public QAbstractItemModel
{
Q_OBJECT
public:
JSONModel(const nlohmann::json &json, QObject *parent = 0);
~JSONModel();
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &index) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
void setupJsonInfo(const nlohmann::json &j);
const nlohmann::json &json;
class JSONInfo {
public:
const nlohmann::json *parent;
QString name;
QString data;
bool enabled;
};
std::map<const nlohmann::json*, JSONInfo> jsonInfo;
};
class JSONPickerDialog : public QDialog
{
Q_OBJECT
public:
explicit JSONPickerDialog(const nlohmann::json &json, QWidget *parent = nullptr);
~JSONPickerDialog();
private:
Ui::JSONPickerDialog *ui;
JSONModel *model;
};
#endif // JSONPICKERDIALOG_H
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>JSONPickerDialog</class>
<widget class="QDialog" name="JSONPickerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTreeView" name="treeView"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>JSONPickerDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>JSONPickerDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
@@ -0,0 +1,187 @@
#include "siunitedit.h"
#include <QDoubleValidator>
#include <unit.h>
#include <QEvent>
#include <QKeyEvent>
#include <QDebug>
#include <QTimer>
#include <cmath>
SIUnitEdit::SIUnitEdit(QString unit, QString prefixes, int precision, QWidget *parent)
: QLineEdit(parent)
{
this->unit = unit;
this->prefixes = prefixes;
this->precision = precision;
setAlignment(Qt::AlignCenter);
installEventFilter(this);
connect(this, &QLineEdit::editingFinished, [this]() {
parseNewValue(1.0);
});
setValueQuiet(0);
}
SIUnitEdit::SIUnitEdit(QWidget *parent)
: SIUnitEdit("", " ", 4, parent)
{
}
void SIUnitEdit::setValue(double value)
{
if(value != _value) {
setValueQuiet(value);
emit valueChanged(value);
emit valueUpdated(this);
}
}
static char swapUpperLower(char c) {
if(isupper(c)) {
return tolower(c);
} else if(islower(c)) {
return toupper(c);
} else {
return c;
}
}
bool SIUnitEdit::eventFilter(QObject *, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
int key = static_cast<QKeyEvent *>(event)->key();
if(key == Qt::Key_Escape) {
// abort editing process and set old value
clear();
setValueQuiet(_value);
emit editingAborted();
clearFocus();
return true;
}
if(key == Qt::Key_Return || key == Qt::Key_Enter) {
// use new value without prefix
parseNewValue(1.0);
continueEditing();
return true;
}
auto mod = static_cast<QKeyEvent *>(event)->modifiers();
if (!(mod & Qt::ShiftModifier)) {
key = tolower(key);
}
if(key <= 255) {
if (prefixes.indexOf(static_cast<QChar>(key)) >= 0) {
// a valid prefix key was pressed
parseNewValue(Unit::SIPrefixToFactor(key));
continueEditing();
return true;
} else if (prefixes.indexOf(swapUpperLower(key)) >= 0) {
// no match on the pressed case but on the upper/lower case instead -> also accept this
parseNewValue(Unit::SIPrefixToFactor(swapUpperLower(key)));
continueEditing();
return true;
}
}
} else if(event->type() == QEvent::FocusOut) {
parseNewValue(1.0);
emit focusLost();
} else if(event->type() == QEvent::FocusIn) {
// online found clumsy way to select all text when clicked!?!
// just selectAll() alone does _not_ work!
QTimer::singleShot(0, this, &SIUnitEdit::continueEditing);
} else if(event->type() == QEvent::Wheel) {
if(_value == 0.0) {
// can't figure out step size with zero value
return true;
}
auto wheel = static_cast<QWheelEvent*>(event);
// most mousewheel have 15 degree increments, the reported delta is in 1/8th degree -> 120
auto increment = wheel->angleDelta().y() / 120.0;
// round toward bigger step in case of special higher resolution mousewheel
unsigned int steps = std::abs(increment > 0 ? ceil(increment) : floor(increment));
int sign = increment > 0 ? 1 : -1;
// figure out step increment
auto newVal = _value;
if(hasFocus()) {
auto cursor = cursorPosition();
if(cursor == 0) {
// cursor in front of first digit, do nothing (too big of a change, probably over/underflows)
return true;
}
// change the digit at the current cursor
int nthDigit = cursor;
// account for decimal point/leading zero/sign
if(_value < 0) {
nthDigit--;
}
auto dotPos = text().indexOf('.');
if(dotPos >= 0 && dotPos < nthDigit) {
nthDigit--;
}
if(text().startsWith("-0.") || text().startsWith("0.")) {
nthDigit--;
}
auto step_size = pow(10, floor(log10(std::abs(newVal))) - nthDigit + 1);
newVal += step_size * steps * sign;
setValue(newVal);
setText(placeholderText());
setCursorPosition(cursor);
} else {
// default to the third digit
constexpr int nthDigit = 3;
while(steps > 0) {
// do update in multiple steps because the step size could change inbetween
auto step_size = pow(10, floor(log10(std::abs(newVal))) - nthDigit + 1);
newVal += step_size * sign;
steps--;
}
setValue(newVal);
}
return true;
}
return false;
}
void SIUnitEdit::setValueQuiet(double value)
{
_value = value;
setPlaceholderText(Unit::ToString(value, unit, prefixes, precision));
if(!text().isEmpty()) {
// currently editing, update the text as well
continueEditing();
}
}
void SIUnitEdit::parseNewValue(double factor)
{
QString input = text();
if(input.isEmpty()) {
setValueQuiet(_value);
emit editingAborted();
} else {
// remove optional unit
if(input.endsWith(unit)) {
input.chop(unit.size());
}
auto lastChar = input.at(input.size()-1).toLatin1();
if(prefixes.indexOf(lastChar) >= 0) {
factor = Unit::SIPrefixToFactor(lastChar);
input.chop(1);
}
// remaining input should only contain numbers
bool conversion_ok;
auto v = input.toDouble(&conversion_ok);
clear();
if(conversion_ok) {
setValue(v * factor);
} else {
qWarning() << "SIUnit conversion failure:" << input;
}
}
}
void SIUnitEdit::continueEditing()
{
setText(placeholderText());
selectAll();
}
@@ -0,0 +1,36 @@
#ifndef SIUNITEDIT_H
#define SIUNITEDIT_H
#include <QLineEdit>
class SIUnitEdit : public QLineEdit
{
Q_OBJECT
public:
SIUnitEdit(QString unit = QString(), QString prefixes = " ", int precision = 0, QWidget *parent = nullptr);
SIUnitEdit(QWidget *parent);
void setUnit(QString unit) { this->unit = unit; setValueQuiet(_value); }
void setPrefixes(QString prefixes) { this->prefixes = prefixes; setValueQuiet(_value); }
void setPrecision(int precision) { this->precision = precision; setValueQuiet(_value); }
double value() { return _value; }
public slots:
void setValue(double value);
void setValueQuiet(double value);
signals:
void valueChanged(double newvalue);
void valueUpdated(QWidget *w);
void editingAborted();
void focusLost();
protected:
bool eventFilter(QObject *obj, QEvent *event) override;
private slots:
void continueEditing();
private:
void parseNewValue(double factor);
QString unit, prefixes;
int precision;
double _value;
};
#endif // SIUNITEDIT_H
@@ -0,0 +1,361 @@
#include "tilewidget.h"
#include "ui_tilewidget.h"
#include "Traces/tracexyplot.h"
#include "Traces/tracesmithchart.h"
#include "Traces/tracewaterfall.h"
#include "Traces/tracepolarchart.h"
#include "Traces/eyediagramplot.h"
#include <QDebug>
TileWidget::TileWidget(TraceModel &model, QWidget *parent) :
QWidget(parent),
fullScreenPlot(nullptr),
ui(new Ui::TileWidget),
splitter(0),
isSplit(false),
parent(0),
child1(0),
child2(0),
hasContent(false),
isFullScreen(false),
content(0),
model(model)
{
ui->setupUi(this);
auto layout = new QGridLayout;
layout->setContentsMargins(0,0,0,0);
auto fsLayout = new QGridLayout;
fsLayout->setContentsMargins(0,0,0,0);
ui->ContentPage->setLayout(layout);
ui->ContentFullScreenMode->setLayout(fsLayout);
ui->bClose->setVisible(false);
}
TileWidget::~TileWidget()
{
delete ui;
}
void TileWidget::clear()
{
if(hasContent) {
delete content;
content = nullptr;
hasContent = false;
}
if(isSplit) {
delete child1;
delete child2;
isSplit = false;
delete splitter;
}
}
nlohmann::json TileWidget::toJSON()
{
nlohmann::json j;
j["split"] = isSplit;
if(isSplit) {
j["orientation"] = splitter->orientation() == Qt::Horizontal ? "horizontal" : "vertical";
j["sizes"] = splitter->sizes();
j["tile1"] = child1->toJSON();
j["tile2"] = child2->toJSON();
}
if(hasContent) {
std::string plotname;
switch(content->getType()) {
case TracePlot::Type::SmithChart:
plotname = "smithchart";
break;
case TracePlot::Type::XYPlot:
plotname = "XY-plot";
break;
case TracePlot::Type::Waterfall:
plotname = "Waterfall";
break;
case TracePlot::Type::PolarChart:
plotname = "PolarChart";
break;
case TracePlot::Type::EyeDiagram:
plotname = "EyeDiagram";
break;
}
j["plot"] = plotname;
j["plotsettings"] = content->toJSON();
}
return j;
}
void TileWidget::fromJSON(nlohmann::json j)
{
// delete all childs before parsing json
clear();
bool split = j.value("split", false);
if(split) {
if(j["orientation"] == "horizontal") {
splitHorizontally();
} else {
splitVertically();
}
splitter->setSizes(j["sizes"]);
child1->fromJSON(j["tile1"]);
child2->fromJSON(j["tile2"]);
} else if(j.contains("plot")) {
// has a plot enabled
auto plotname = j["plot"];
if(plotname == "smithchart") {
content = new TraceSmithChart(model);
} else if (plotname == "XY-plot"){
content = new TraceXYPlot(model);
} else if (plotname == "Waterfall"){
content = new TraceWaterfall(model);
} else if (plotname == "PolarChart"){
content = new TracePolarChart(model);
} else if (plotname == "EyeDiagram"){
content = new EyeDiagramPlot(model);
}
if(content) {
setContent(content);
content->fromJSON(j["plotsettings"]);
}
}
}
bool TileWidget::allLimitsPassing()
{
if(isSplit) {
return child1->allLimitsPassing() && child2->allLimitsPassing();
} else if(hasContent) {
return content->getLimitPassing();
} else {
// empty tile always passes
return true;
}
}
void TileWidget::setSplitPercentage(int percentage)
{
if(!isSplit) {
return;
}
splitter->setStretchFactor(0, percentage);
splitter->setStretchFactor(1, 100 - percentage);
}
void TileWidget::splitVertically(bool moveContentToSecondChild)
{
if(isSplit) {
return;
}
isSplit = true;
splitter = new QSplitter(Qt::Vertical);
split(moveContentToSecondChild);
tryMinimize();
}
void TileWidget::splitHorizontally(bool moveContentToSecondChild)
{
if(isSplit) {
return;
}
isSplit = true;
splitter = new QSplitter(Qt::Horizontal);
split(moveContentToSecondChild);
tryMinimize();
}
void TileWidget::closeTile()
{
if(!parent) {
// Unable to close toplevel tile
return;
}
auto pTile = parent;
TileWidget *absorbedTile;
if(this == parent->child1) {
absorbedTile = parent->child2;
} else {
absorbedTile = parent->child1;
}
delete this;
if(absorbedTile->isSplit) {
pTile->isSplit = true;
pTile->child1 = absorbedTile->child1;
pTile->child2 = absorbedTile->child2;
pTile->child1->parent = pTile;
pTile->child2->parent = pTile;
pTile->ui->ContentPage->layout()->addWidget(absorbedTile->splitter);
auto oldsplitter = pTile->splitter;
pTile->splitter = absorbedTile->splitter;
delete absorbedTile;
delete oldsplitter;
} else if(absorbedTile->hasContent) {
pTile->setContent(absorbedTile->content);
delete absorbedTile;
pTile->isSplit = false;
delete pTile->splitter;
pTile->splitter = nullptr;
} else {
delete absorbedTile;
pTile->isSplit = false;
pTile->hasContent = false;
delete pTile->splitter;
pTile->ui->stack->setCurrentWidget(pTile->ui->TilePage);
}
}
void TileWidget::setPlot(TracePlot *plot)
{
if(!isSplit && !hasContent) {
setContent(plot);
}
}
void TileWidget::removePlot()
{
if(hasContent) {
content->setParentTile(nullptr);
removeContent();
}
}
TileWidget::TileWidget(TraceModel &model, TileWidget &parent)
: TileWidget(model)
{
this->parent = &parent;
ui->bClose->setVisible(true);
}
void TileWidget::split(bool moveContentToSecondChild)
{
splitter->setHandleWidth(0);
child1 = new TileWidget(model, *this);
child2 = new TileWidget(model, *this);
splitter->addWidget(child1);
splitter->addWidget(child2);
ui->ContentPage->layout()->addWidget(splitter);
if(hasContent) {
if(moveContentToSecondChild) {
child2->setContent(content);
} else {
child1->setContent(content);
}
removeContent();
}
ui->stack->setCurrentWidget(ui->ContentPage);
}
void TileWidget::setContent(TracePlot *plot)
{
content = plot;
content->setParentTile(this);
hasContent = true;
ui->ContentPage->layout()->addWidget(plot);
ui->stack->setCurrentWidget(ui->ContentPage);
connect(content, &TracePlot::deleted, this, &TileWidget::plotDeleted);
connect(content, &TracePlot::doubleClicked, this, &TileWidget::on_plotDoubleClicked);
}
void TileWidget::removeContent()
{
if(hasContent) {
disconnect(content, &TracePlot::deleted, this, &TileWidget::plotDeleted);
disconnect(content, &TracePlot::doubleClicked, this, &TileWidget::on_plotDoubleClicked);
hasContent = false;
content = nullptr;
ui->stack->setCurrentWidget(ui->TilePage);
}
}
void TileWidget::on_plotDoubleClicked()
{
setFullScreen();
}
void TileWidget::setFullScreen(void)
{
auto clickedTile = this;
if(!clickedTile->parent) {
// Toplevel tile is already in full screen
return;
}
auto rootTile = findRootTile();
rootTile->fullScreenPlot = clickedTile->content;
if (isFullScreen == false)
{
ui->ContentPage->layout()->removeWidget(clickedTile->content);
rootTile->ui->ContentFullScreenMode->layout()->addWidget(rootTile->fullScreenPlot);
rootTile->ui->stack->setCurrentWidget(rootTile->ui->ContentFullScreenMode);
isFullScreen = true;
}
else {
rootTile->ui->stack->setCurrentWidget(rootTile->ui->ContentPage);
rootTile->ui->ContentFullScreenMode->layout()->removeWidget(rootTile->fullScreenPlot);
ui->ContentPage->layout()->addWidget(clickedTile->content);
isFullScreen = false;
}
}
TileWidget* TileWidget::findRootTile(void)
{
auto node = this;
while (node->parent != nullptr) { // root tile should nullptr
node = node->parent;
}
return node;
}
void TileWidget::on_bSmithchart_clicked()
{
setContent(new TraceSmithChart(model));
}
void TileWidget::on_bXYplot_clicked()
{
auto plot = new TraceXYPlot(model);
setContent(plot);
plot->axisSetupDialog();
}
void TileWidget::plotDeleted()
{
tryMinimize();
ui->stack->setCurrentWidget(ui->TilePage);
hasContent = false;
content = nullptr;
}
void TileWidget::tryMinimize()
{
if (isFullScreen)
{
auto rootTile = findRootTile();
rootTile->ui->stack->setCurrentWidget(rootTile->ui->ContentPage);
}
}
void TileWidget::on_bWaterfall_clicked()
{
setContent(new TraceWaterfall(model));
}
void TileWidget::on_bPolarchart_clicked()
{
setContent(new TracePolarChart(model));
}
void TileWidget::on_eyeDiagram_clicked()
{
auto plot = new EyeDiagramPlot(model);
setContent(plot);
plot->axisSetupDialog();
}
@@ -0,0 +1,75 @@
#ifndef TILEWIDGET_H
#define TILEWIDGET_H
#include <QWidget>
#include "Traces/traceplot.h"
#include "Traces/tracemodel.h"
#include "savable.h"
#include <QSplitter>
namespace Ui {
class TileWidget;
}
class TileWidget : public QWidget, public Savable
{
Q_OBJECT
public:
explicit TileWidget(TraceModel &model, QWidget *parent = nullptr);
~TileWidget();
TileWidget *Child1() { return child1; }
TileWidget *Child2() { return child2; }
// closes all plots/childs, leaving only the tilewidget at the top
void clear();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
// check potential trace limits on graphs, only returns true if all traces in all graphs are within limits
bool allLimitsPassing();
void setSplitPercentage(int percentage);
public slots:
void splitVertically(bool moveContentToSecondChild = false);
void splitHorizontally(bool moveContentToSecondChild = false);
void closeTile();
void setPlot(TracePlot *plot);
void removePlot();
private slots:
void on_bSmithchart_clicked();
void on_bXYplot_clicked();
void on_plotDoubleClicked();
void plotDeleted();
void on_bWaterfall_clicked();
void on_bPolarchart_clicked();
void on_eyeDiagram_clicked();
private:
TileWidget(TraceModel &model, TileWidget &parent);
void split(bool moveContentToSecondChild = false);
void setContent(TracePlot *plot);
void removeContent();
void setChild();
TileWidget* findRootTile();
void setFullScreen();
void tryMinimize();
TracePlot *fullScreenPlot;
Ui::TileWidget *ui;
QSplitter *splitter;
bool isSplit;
TileWidget *parent;
TileWidget *child1, *child2;
bool hasContent;
bool isFullScreen;
TracePlot *content;
TraceModel &model;
};
#endif // TILEWIDGET_H
@@ -0,0 +1,288 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TileWidget</class>
<widget class="QWidget" name="TileWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>465</width>
<height>350</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QStackedWidget" name="stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="TilePage">
<layout class="QVBoxLayout" name="verticalLayout_5">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_4">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Plots</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="bSmithchart">
<property name="text">
<string>Smithchart</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bXYplot">
<property name="text">
<string>XY-plot</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bWaterfall">
<property name="text">
<string>Waterfall</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bPolarchart">
<property name="text">
<string>Polar Chart</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="eyeDiagram">
<property name="text">
<string>Eye Diagram</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Split Tile</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QPushButton" name="bSplitV">
<property name="text">
<string>Vertical</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/vertical.svg</normaloff>:/icons/vertical.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="bSplitH">
<property name="text">
<string>Horizontal</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/horizontal.svg</normaloff>:/icons/horizontal.svg</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="bClose">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Close Tile</string>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<normaloff>:/icons/close.svg</normaloff>:/icons/close.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="ContentFullScreenMode"/>
<widget class="QWidget" name="ContentPage"/>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections>
<connection>
<sender>bClose</sender>
<signal>clicked()</signal>
<receiver>TileWidget</receiver>
<slot>closeTile()</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>74</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
<connection>
<sender>bSplitH</sender>
<signal>clicked()</signal>
<receiver>TileWidget</receiver>
<slot>splitHorizontally()</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>224</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
<connection>
<sender>bSplitV</sender>
<signal>clicked()</signal>
<receiver>TileWidget</receiver>
<slot>splitVertically()</slot>
<hints>
<hint type="sourcelabel">
<x>199</x>
<y>149</y>
</hint>
<hint type="destinationlabel">
<x>199</x>
<y>149</y>
</hint>
</hints>
</connection>
</connections>
<slots>
<slot>splitVertically()</slot>
<slot>splitHorizontally()</slot>
<slot>closeTile()</slot>
</slots>
</ui>
@@ -0,0 +1,66 @@
#include "toggleswitch.h"
#include <QPainter>
#include <QMouseEvent>
ToggleSwitch::ToggleSwitch(QWidget *parent, bool state) : QAbstractButton(parent),
_height(24),
_width(128),
state(state)
{
}
QSize ToggleSwitch::sizeHint() const
{
return QSize(_width, _height);
}
void ToggleSwitch::toggle()
{
state = !state;
emit toggled(state);
}
void ToggleSwitch::setState(bool state)
{
if(this->state != state) {
this->state = state;
emit toggled(state);
}
}
void ToggleSwitch::paintEvent(QPaintEvent *)
{
QPainter p(this);
p.setPen(Qt::NoPen);
p.setBrush(Qt::black);
p.setOpacity(isEnabled() ? 0.38 : 0.12);
p.setRenderHint(QPainter::Antialiasing, true);
p.drawRoundedRect(QRect(0, 0, width(), height()), 8.0, 8.0);
p.setOpacity(1.0);
QRect rect;
QString statename;
if(state) {
p.setBrush(isEnabled() ? Qt::darkGreen : Qt::gray);
rect = QRect(width()/2, 0, width()/2, height());
statename = "ON";
} else {
p.setBrush(isEnabled() ? QColor("#AA090E") : Qt::lightGray);
rect = QRect(0, 0, width()/2, height());
statename = "OFF";
}
p.drawRoundedRect(rect, 8.0, 8.0);
QFont font = p.font();
p.setPen(Qt::SolidLine);
p.setPen(isEnabled() ? Qt::black : Qt::gray);
p.drawText(rect, Qt::AlignCenter, statename);
}
void ToggleSwitch::mouseReleaseEvent(QMouseEvent *e)
{
if(e->button() & Qt::LeftButton) {
toggle();
}
QAbstractButton::mouseReleaseEvent(e);
}
@@ -0,0 +1,28 @@
#ifndef TOGGLESWITCH_H
#define TOGGLESWITCH_H
#include <QAbstractButton>
class ToggleSwitch : public QAbstractButton
{
Q_OBJECT
public:
ToggleSwitch(QWidget* parent = nullptr, bool state = false);
QSize sizeHint() const override;
signals:
void toggled(bool newstate);
public slots:
void toggle();
void setState(bool state);
protected:
void paintEvent(QPaintEvent*) override;
void mouseReleaseEvent(QMouseEvent*) override;
private:
int _height, _width;
bool state;
};
#endif // TOGGLESWITCH_H
@@ -0,0 +1,194 @@
#include "touchstoneimport.h"
#include "ui_touchstoneimport.h"
#include "preferences.h"
#include <QFileDialog>
#include <QDebug>
#include <QtGlobal>
using namespace std;
TouchstoneImport::TouchstoneImport(QWidget *parent, int ports) :
QWidget(parent),
ui(new Ui::TouchstoneImport),
touchstone(ports),
status(false)
{
ui->setupUi(this);
ui->lowerFreq->setUnit("Hz");
ui->lowerFreq->setPrefixes(" kMG");
ui->upperFreq->setUnit("Hz");
ui->upperFreq->setPrefixes(" kMG");
connect(ui->browse, &QPushButton::clicked, this, &TouchstoneImport::evaluateFile);
ui->port1Group->setId(ui->port1_1, 0);
ui->port1Group->setId(ui->port1_2, 1);
ui->port1Group->setId(ui->port1_3, 2);
ui->port1Group->setId(ui->port1_4, 3);
ui->port2Group->setId(ui->port2_1, 0);
ui->port2Group->setId(ui->port2_2, 1);
ui->port2Group->setId(ui->port2_3, 2);
ui->port2Group->setId(ui->port2_4, 3);
// prevent selection of same port for port1 and 2
connect(ui->port1Group, qOverload<int>(&QButtonGroup::idClicked), [=](int id) {
preventCollisionWithGroup(ui->port2Group, id);
});
connect(ui->port2Group, qOverload<int>(&QButtonGroup::idClicked), [=](int id) {
preventCollisionWithGroup(ui->port1Group, id);
});
connect(ui->file, &QLineEdit::textChanged, this, &TouchstoneImport::setFile);
setPorts(ports);
}
TouchstoneImport::~TouchstoneImport()
{
delete ui;
}
bool TouchstoneImport::getStatus()
{
return status;
}
Touchstone TouchstoneImport::getTouchstone()
{
if(required_ports == 1) {
auto t1 = touchstone;
t1.reduceTo1Port(ui->port1Group->checkedId());
return t1;
} else if(required_ports == 2) {
auto t2 = touchstone;
t2.reduceTo2Port(ui->port1Group->checkedId(), ui->port2Group->checkedId());
return t2;
}
return touchstone;
}
void TouchstoneImport::setPorts(int ports)
{
required_ports = ports;
ui->port1Widget->setVisible(ports >= 1);
ui->port2Widget->setVisible(ports >= 2);
}
QString TouchstoneImport::getFilename()
{
return ui->file->text();
}
void TouchstoneImport::selectPort(int destination, int source)
{
switch(destination) {
case 0:
ui->port1Group->button(source)->setChecked(true);
preventCollisionWithGroup(ui->port2Group, source);
break;
case 1:
ui->port2Group->button(source)->setChecked(true);
preventCollisionWithGroup(ui->port1Group, source);
break;
}
}
std::vector<int> TouchstoneImport::getPorts()
{
vector<int> ret;
if(required_ports >= 1) {
ret.push_back(ui->port1Group->checkedId());
}
if(required_ports >= 2) {
ret.push_back(ui->port2Group->checkedId());
}
return ret;
}
void TouchstoneImport::setFile(QString filename)
{
ui->file->setText(filename);
evaluateFile();
emit filenameChanged(filename);
}
void TouchstoneImport::on_browse_clicked()
{
auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", Preferences::getInstance().UISettings.Paths.data, "Touchstone files (*.s1p *.s2p *.s3p *.s4p)", nullptr, Preferences::QFileDialogOptions());
if (filename.length() > 0) {
Preferences::getInstance().UISettings.Paths.data = QFileInfo(filename).path();
ui->file->setText(filename);
}
}
void TouchstoneImport::evaluateFile()
{
bool new_status = false;
ui->port1_1->setEnabled(false);
ui->port1_2->setEnabled(false);
ui->port1_3->setEnabled(false);
ui->port1_4->setEnabled(false);
if (required_ports != 1) {
ui->port2_1->setEnabled(false);
ui->port2_2->setEnabled(false);
ui->port2_3->setEnabled(false);
ui->port2_4->setEnabled(false);
}
ui->points->setText("");
ui->lowerFreq->setText("");
ui->upperFreq->setText("");
ui->status->clear();
try {
touchstone = Touchstone::fromFile(ui->file->text().toStdString());
if (required_ports > 0 && touchstone.ports() < (unsigned int) required_ports) {
throw runtime_error("Not enough ports in file");
}
ui->port1_1->setEnabled(touchstone.ports() >= 1);
ui->port1_2->setEnabled(touchstone.ports() >= 2);
ui->port1_3->setEnabled(touchstone.ports() >= 3);
ui->port1_4->setEnabled(touchstone.ports() >= 4);
if (required_ports != 1) {
ui->port2_1->setEnabled(touchstone.ports() >= 1);
ui->port2_2->setEnabled(touchstone.ports() >= 2);
ui->port2_3->setEnabled(touchstone.ports() >= 3);
ui->port2_4->setEnabled(touchstone.ports() >= 4);
}
ui->points->setText(QString::number(touchstone.points()));
ui->lowerFreq->setValue(touchstone.minFreq());
ui->upperFreq->setValue(touchstone.maxFreq());
if(ui->port1Group->checkedId() == -1 || !ui->port1Group->checkedButton()->isEnabled()) {
// no or invalid S parameter selected
ui->port1_1->setChecked(true);
}
if (required_ports != 1) {
preventCollisionWithGroup(ui->port2Group, 0);
}
new_status = true;
} catch (const exception &e) {
ui->status->setText(e.what());
}
if (new_status != status) {
status = new_status;
emit statusChanged(status);
}
emit filenameChanged(ui->file->text());
}
void TouchstoneImport::preventCollisionWithGroup(QButtonGroup *group, int id)
{
for(unsigned int i=0;i<touchstone.ports();i++) {
group->button(i)->setEnabled(true);
}
// change selection in second group and mark invalid
group->button(id)->setEnabled(false);
group->button(id)->setChecked(false);
if (group->checkedId() == -1 || group->checkedId() == id) {
for(int i=0;i<4;i++) {
if(i == id) {
continue;
}
if(group->button(i)->isEnabled()) {
group->button(i)->setChecked(true);
break;
}
}
}
}
@@ -0,0 +1,47 @@
#ifndef TOUCHSTONEIMPORT_H
#define TOUCHSTONEIMPORT_H
#include "touchstone.h"
#include <QWidget>
#include <QButtonGroup>
namespace Ui {
class TouchstoneImport;
}
class TouchstoneImport : public QWidget
{
Q_OBJECT
public:
explicit TouchstoneImport(QWidget *parent = nullptr, int ports = 0);
~TouchstoneImport();
bool getStatus();
Touchstone getTouchstone();
void setPorts(int ports);
QString getFilename();
void selectPort(int destination, int source);
std::vector<int> getPorts();
signals:
void statusChanged(bool status);
void filenameChanged(QString name);
public slots:
void setFile(QString filename);
private slots:
void on_browse_clicked();
private:
void evaluateFile();
void preventCollisionWithGroup(QButtonGroup *group, int id);
Ui::TouchstoneImport *ui;
int required_ports;
Touchstone touchstone;
bool status;
};
#endif // TOUCHSTONEIMPORT_H
@@ -0,0 +1,319 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TouchstoneImport</class>
<widget class="QWidget" name="TouchstoneImport">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>228</width>
<height>227</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="file">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="browse">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>20</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="status">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>239</red>
<green>41</green>
<blue>41</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>239</red>
<green>41</green>
<blue>41</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>190</red>
<green>190</green>
<blue>190</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="port1Widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Port 1:</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port1_1">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>1</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port1Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port1_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>2</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port1Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port1_3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>3</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port1Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port1_4">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>4</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port1Group</string>
</attribute>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="port2Widget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>6</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<layout class="QHBoxLayout" name="port2Selector">
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Port 2:</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port2_1">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>1</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port2Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port2_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>2</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port2Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port2_3">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>3</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port2Group</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="port2_4">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>4</string>
</property>
<attribute name="buttonGroup">
<string notr="true">port2Group</string>
</attribute>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
<string>Points:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="points">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_24">
<property name="text">
<string>Lower Frequency:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="SIUnitEdit" name="lowerFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_25">
<property name="text">
<string>Upper Frequency:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="SIUnitEdit" name="upperFreq">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="port1Group"/>
<buttongroup name="port2Group"/>
</buttongroups>
</ui>
@@ -0,0 +1,234 @@
#include "tracesetselector.h"
#include <QGridLayout>
#include <QLabel>
TraceSetSelector::TraceSetSelector(QWidget *parent) :
QWidget(parent),
model(nullptr)
{
points = 0;
ports = 0;
lowerFreq = upperFreq = 0;
referenceImpedance = 50.0;
freqsSet = false;
partialSelectionAllowed = false;
setLayout(new QGridLayout);
setPorts(2);
}
TraceSetSelector::~TraceSetSelector()
{
blockSignals(true);
// this will free all created widgets
setPorts(0);
blockSignals(false);
}
unsigned int TraceSetSelector::getPorts() const
{
return ports;
}
void TraceSetSelector::setPorts(unsigned int newPorts)
{
if(!model) {
return;
}
QGridLayout *layout = static_cast<QGridLayout*>(this->layout());
if(newPorts != ports) {
ports = newPorts;
// remove the previous widgets
QLayoutItem *child;
while ((child = layout->takeAt(0)) != 0) {
delete child->widget();
delete child;
}
cTraces.clear();
for(unsigned int i=0;i<ports;i++) {
cTraces.push_back(std::vector<QComboBox*>());
for(unsigned int j=0;j<ports;j++) {
auto l = new QLabel("S"+QString::number(i+1)+QString::number(j+1)+":");
auto c = new QComboBox();
cTraces[i].push_back(c);
connect(c, qOverload<int>(&QComboBox::currentIndexChanged), [=](int) {
selectionChanged(c);
});
layout->addWidget(l, i, j*2);
layout->addWidget(c, i, j*2 + 1);
}
}
}
auto availableTraces = model->getTraces();
for(unsigned int i=0;i<ports;i++) {
for(unsigned int j=0;j<ports;j++) {
auto c = cTraces[i][j];
c->blockSignals(true);
c->clear();
// create possible trace selections
c->addItem("None");
for(auto t : availableTraces) {
if(t->outputType() != Trace::DataType::Frequency) {
// can only add frequency traces
continue;
}
if(i == j && !t->isReflection()) {
// can not add through measurement at reflection port
continue;
} else if(i != j && t->isReflection()) {
// can not add reflection measurement at through port
continue;
}
c->addItem(t->name(), QVariant::fromValue<Trace*>(t));
}
c->blockSignals(false);
}
}
emit selectionChanged();
}
void TraceSetSelector::setDefaultTraces()
{
auto traces = model->getTraces();
for(unsigned int i=1;i<=ports;i++) {
for(unsigned int j=1;j<=ports;j++) {
QString defaultTraceName = "S"+QString::number(i)+QString::number(j);
for(auto t : traces) {
if(t->name() == defaultTraceName) {
setTrace(i, j, t);
break;
}
}
}
}
}
bool TraceSetSelector::setTrace(unsigned int destPort, unsigned int srcPort, Trace *t)
{
if(destPort < 1 || destPort > ports || srcPort < 1 || srcPort > ports) {
// invalid port selection
return false;
}
auto c = cTraces[destPort-1][srcPort-1];
// find required trace
for(unsigned int i=1;i<(unsigned int) c->count();i++) {
if(qvariant_cast<Trace*>(c->itemData(i)) == t) {
c->setCurrentIndex(i);
return true;
}
}
// requested trace not found, unable to set
return false;
}
Trace *TraceSetSelector::getTrace(unsigned int destPort, unsigned int srcPort)
{
if(destPort < 1 || destPort > ports || srcPort < 1 || srcPort > ports) {
// invalid port selection
return nullptr;
}
auto c = cTraces[destPort-1][srcPort-1];
if(c->currentIndex() == 0) {
// no trace selected
return nullptr;
} else {
return qvariant_cast<Trace*>(c->itemData(c->currentIndex()));
}
}
bool TraceSetSelector::selectionValid()
{
for(unsigned int i=1;i<=ports;i++) {
for(unsigned int j=1;j<=ports;j++) {
auto t = getTrace(i, j);
if(!t && !partialSelectionAllowed) {
// at least one trace is missing
return false;
}
if(t && partialSelectionAllowed) {
// at least one trace is present
return true;
}
}
}
// if we get here, either:
// - all traces are valid and partial selection is not allowed -> whole selection valid
// or:
// - all traces are invalid and partial selection is allowed -> whole selection invalid
return !partialSelectionAllowed;
}
void TraceSetSelector::selectionChanged(QComboBox *c)
{
if(c->currentIndex() != 0 && !freqsSet) {
// the first trace has been selected, extract frequency info
Trace *t = qvariant_cast<Trace*>(c->itemData(c->currentIndex()));
points = t->size();
referenceImpedance = t->getReferenceImpedance();
if(points > 0) {
lowerFreq = t->minX();
upperFreq = t->maxX();
}
freqsSet = true;
// remove all trace options with incompatible frequencies
for(auto v1 : cTraces) {
for(auto c : v1) {
for(int i=1;i<c->count();i++) {
Trace *t = qvariant_cast<Trace*>(c->itemData(i));
if(t->getReferenceImpedance() != referenceImpedance || t->size() != points || (points > 0 && (t->minX() != lowerFreq || t->maxX() != upperFreq))) {
// this trace is not available anymore
c->removeItem(i);
// decrement to check the next index in the next loop iteration
i--;
}
}
}
}
} else if(c->currentIndex() == 0 && freqsSet) {
// Check if all trace selections are set for none
for(auto v1 : cTraces) {
for(auto c : v1) {
if(c->currentIndex() != 0) {
// some trace is still selected, abort
emit selectionChanged();
return;
}
}
}
// all traces set for none
freqsSet = false;
setPorts(ports);
}
emit selectionChanged();
}
double TraceSetSelector::getReferenceImpedance() const
{
return referenceImpedance;
}
double TraceSetSelector::getUpperFreq() const
{
return upperFreq;
}
double TraceSetSelector::getLowerFreq() const
{
return lowerFreq;
}
unsigned int TraceSetSelector::getPoints() const
{
return points;
}
void TraceSetSelector::setModel(TraceModel *newModel)
{
model = newModel;
}
void TraceSetSelector::setPartialSelectionAllowed(bool newPartialSelectionAllowed)
{
partialSelectionAllowed = newPartialSelectionAllowed;
}
@@ -0,0 +1,59 @@
#ifndef TRACESETSELECTOR_H
#define TRACESETSELECTOR_H
#include "Traces/tracemodel.h"
#include <QWidget>
#include <QComboBox>
namespace Ui {
class TraceSetSelector;
}
class TraceSetSelector : public QWidget
{
Q_OBJECT
public:
explicit TraceSetSelector(QWidget *parent = nullptr);
~TraceSetSelector();
unsigned int getPorts() const;
void setPorts(unsigned int newPorts);
void setDefaultTraces();
bool setTrace(unsigned int destPort, unsigned int srcPort, Trace *t);
Trace *getTrace(unsigned int destPort, unsigned int srcPort);
bool selectionValid();
void setPartialSelectionAllowed(bool newPartialSelectionAllowed);
void setModel(TraceModel *newModel);
unsigned int getPoints() const;
double getLowerFreq() const;
double getUpperFreq() const;
double getReferenceImpedance() const;
TraceModel* getModel() const {return model;}
signals:
void selectionChanged();
private:
void selectionChanged(QComboBox *c);
Ui::TraceSetSelector *ui;
TraceModel *model;
unsigned int ports;
std::vector<std::vector<QComboBox*>> cTraces;
unsigned int points;
double lowerFreq, upperFreq;
double referenceImpedance;
bool freqsSet;
bool partialSelectionAllowed;
};
#endif // TRACESETSELECTOR_H
@@ -0,0 +1,16 @@
#include "harogicb60.h"
HarogicB60::HarogicB60()
{
validUSBIDs.clear();
validUSBIDs.append({0x367F, 0x0200, "B60"});
for(auto &s : specificSettings) {
s.name.replace("LibreVNAUSBDriver", "HarogicB60Driver");
}
}
QString HarogicB60::getDriverName()
{
return "Harogic B60";
}
@@ -0,0 +1,19 @@
#ifndef HAROGICB60_H
#define HAROGICB60_H
#include "../LibreVNA/librevnausbdriver.h"
class HarogicB60 : public LibreVNAUSBDriver
{
Q_OBJECT
public:
HarogicB60();
/**
* @brief Returns the driver name. It must be unique across all implemented drivers and is used to identify the driver
* @return driver name
*/
virtual QString getDriverName() override;
};
#endif // HAROGICB60_H
@@ -0,0 +1,87 @@
#include "compounddevice.h"
CompoundDevice::CompoundDevice()
{
name = "";
sync = LibreVNADriver::Synchronization::GUI;
}
nlohmann::json CompoundDevice::toJSON()
{
nlohmann::json j;
j["name"] = name.toStdString();
j["synchronization"] = SyncToString(sync).toStdString();
nlohmann::json jserials;
for(auto d : deviceSerials) {
jserials.push_back(d.toStdString());
}
j["devices"] = jserials;
nlohmann::json jmappings;
for(auto m : portMapping) {
nlohmann::json jmapping;
jmapping["device"] = m.device;
jmapping["port"] = m.port;
jmappings.push_back(jmapping);
}
j["mapping"] = jmappings;
return j;
}
void CompoundDevice::fromJSON(nlohmann::json j)
{
name = QString::fromStdString(j.value("name", "CompoundDevice"));
sync = SyncFromString(QString::fromStdString(j.value("synchronization", "USB")));
deviceSerials.clear();
if(j.contains("devices")) {
for(auto js : j["devices"]) {
deviceSerials.push_back(QString::fromStdString(js));
}
}
portMapping.clear();
if(j.contains("mapping")) {
for(auto jm : j["mapping"]) {
PortMapping mapping;
mapping.device = jm.value("device", 0);
mapping.port = jm.value("port", 0);
portMapping.push_back(mapping);
}
}
}
QString CompoundDevice::SyncToString(LibreVNADriver::Synchronization sync)
{
switch(sync) {
case LibreVNADriver::Synchronization::Disabled: return "Disabled";
case LibreVNADriver::Synchronization::GUI: return "GUI";
case LibreVNADriver::Synchronization::ExternalTrigger: return "Trigger";
default:
return "Invalid";
}
}
LibreVNADriver::Synchronization CompoundDevice::SyncFromString(QString s)
{
for(int i=0;i<(int) LibreVNADriver::Synchronization::Last;i++) {
if(SyncToString((LibreVNADriver::Synchronization)i) == s) {
return (LibreVNADriver::Synchronization) i;
}
}
// default to GUI
return LibreVNADriver::Synchronization::GUI;
}
QString CompoundDevice::getDesription()
{
return name + ", "+QString::number(deviceSerials.size())+" devices, "+QString::number(portMapping.size())+" ports in total";
}
unsigned int CompoundDevice::PortMapping::findActiveStage(std::vector<CompoundDevice::PortMapping> map, unsigned int device, unsigned int port)
{
for(unsigned int i=0;i<map.size();i++) {
if(map[i].device == device && map[i].port == port) {
return i;
}
}
return map.size();
}
@@ -0,0 +1,38 @@
#ifndef COMPOUNDDEVICE_H
#define COMPOUNDDEVICE_H
#include "savable.h"
#include "../librevnadriver.h"
#include <vector>
#include <QString>
class CompoundDevice : public Savable
{
friend class CompoundDeviceEditDialog;
public:
CompoundDevice();
nlohmann::json toJSON() override;
void fromJSON(nlohmann::json j) override;
class PortMapping {
public:
unsigned int device; // starts at zero
unsigned int port; // starts at zero
static unsigned int findActiveStage(std::vector<PortMapping> map, unsigned int device, unsigned int port);
};
static QString SyncToString(LibreVNADriver::Synchronization sync);
static LibreVNADriver::Synchronization SyncFromString(QString s);
QString getDesription();
QString name;
LibreVNADriver::Synchronization sync;
std::vector<QString> deviceSerials;
std::vector<PortMapping> portMapping;
};
#endif // COMPOUNDDEVICE_H
@@ -0,0 +1,554 @@
#include "compounddeviceeditdialog.h"
#include "ui_compounddeviceeditdialog.h"
#include "compounddriver.h"
#include <QPushButton>
#include <QDrag>
#include <QMimeData>
#include <QMouseEvent>
#include <QStandardItemModel>
using namespace std;
CompoundDeviceEditDialog::CompoundDeviceEditDialog(CompoundDevice *cdev, QWidget *parent) :
QDialog(parent),
ui(new Ui::CompoundDeviceEditDialog),
dropPending(false),
dragFrame(nullptr),
dropFrame(nullptr)
{
ldev = *cdev;
ui->setupUi(this);
connect(ui->name, &QLineEdit::editingFinished, [=](){
ldev.name = ui->name->text();
checkIfOkay();
});
for(int i=0;i<(int)LibreVNADriver::Synchronization::Last;i++) {
ui->sync->addItem(CompoundDevice::SyncToString((LibreVNADriver::Synchronization) i));
switch((LibreVNADriver::Synchronization) i) {
case LibreVNADriver::Synchronization::Disabled:
case LibreVNADriver::Synchronization::Reserved: {
// Disable for now
auto *model = qobject_cast<QStandardItemModel *>(ui->sync->model());
Q_ASSERT(model != nullptr);
bool disabled = true;
auto *item = model->item(i);
item->setFlags(disabled ? item->flags() & ~Qt::ItemIsEnabled
: item->flags() | Qt::ItemIsEnabled);
}
break;
default:
break;
}
}
connect(ui->sync, &QComboBox::currentTextChanged, [=](){
ldev.sync = CompoundDevice::SyncFromString(ui->sync->currentText());
updateDeviceFrames();
});
connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, [=](){
*cdev = ldev;
accept();
});
connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, this, &QDialog::reject);
connect(ui->buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=](){
ldev.saveToFileDialog("Save compound device", "Compound device file (*.cdev)", ".cdev");
});
connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, [=](){
if(ldev.openFromFileDialog("Load compound device", "Compound device file (*.cdev)")) {
setInitialGUI();
}
});
ui->status->setStyleSheet("QLabel { color : red; }");
graph = new QWidget();
ui->scrollArea->setWidget(graph);
auto layout = new QHBoxLayout();
graph->setLayout(layout);
graph->setAcceptDrops(true);
graph->setObjectName("Graph");
graph->installEventFilter(this);
ui->lLibreVNA1->installEventFilter(this);
layout->setContentsMargins(0,0,0,0);
layout->setSpacing(0);
setInitialGUI();
}
CompoundDeviceEditDialog::~CompoundDeviceEditDialog()
{
delete ui;
}
void CompoundDeviceEditDialog::checkIfOkay()
{
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false);
if(deviceFrames.size() != ldev.deviceSerials.size()) {
// probably still populating the GUI, skip check
return;
}
if(ldev.name.isEmpty()) {
ui->status->setText("Name must not be empty");
return;
}
if(ldev.deviceSerials.size() < 2) {
ui->status->setText("At least two devices are required");
return;
}
if(ldev.deviceSerials.size() > 4) {
ui->status->setText("Only up to four devices supported");
return;
}
// Check serials
for(unsigned int i=0;i<ldev.deviceSerials.size();i++) {
ldev.deviceSerials[i] = deviceFrames[i]->getSerial();
if(ldev.deviceSerials[i].isEmpty()) {
ui->status->setText("Device "+QString::number(i+1)+" has no serial number");
return;
}
}
// Check serials for duplicates
for(unsigned int i=0;i<ldev.deviceSerials.size();i++) {
for(unsigned int j=i+1;j<ldev.deviceSerials.size();j++) {
if(ldev.deviceSerials[i] == ldev.deviceSerials[j]) {
ui->status->setText("Duplicate serial number ("+ldev.deviceSerials[i]+") in devices "+QString::number(i+1)+" and "+QString::number(j+1));
return;
}
}
}
// Check port mapping
// Looking for duplicate and missing ports
bool highestPortFound = false;
unsigned int highestPort = 0;
for(unsigned int port=0;port<2*ldev.deviceSerials.size();port++) {
int num = 0;
for(unsigned int i=0;i<deviceFrames.size();i++) {
if(deviceFrames[i]->getPort1() == (int) port) {
num++;
}
if(deviceFrames[i]->getPort2() == (int) port) {
num++;
}
}
if(num > 1) {
ui->status->setText("Duplicate port "+QString::number(port+1));
return;
} else if(num == 0) {
if(port == 0) {
ui->status->setText("Port 1 must be present");
return;
}
if(!highestPortFound) {
highestPort = port;
}
highestPortFound = true;
} else if(highestPortFound) {
// port is present, but earlier port was missing
ui->status->setText("Missing port: "+QString::number(port+1)+" is selected, but port "+QString::number(port)+" is not present.");
return;
}
}
if(!highestPortFound) {
highestPort = 2*ldev.deviceSerials.size();
}
// All good, actually create the port mapping
ldev.portMapping.clear();
for(unsigned int port=0;port<highestPort;port++) {
CompoundDevice::PortMapping map;
bool found = false;
for(unsigned int i=0;i<deviceFrames.size();i++) {
if(deviceFrames[i]->getPort1() == (int) port) {
found = true;
map.device = i;
map.port = 0;
break;
}
if(deviceFrames[i]->getPort2() == (int) port) {
found = true;
map.device = i;
map.port = 1;
break;
}
}
if(!found) {
ui->status->setText("Failed to find port "+QString::number(port+1)+" (likely a bug)");
return;
}
ldev.portMapping.push_back(map);
}
ui->status->clear();
ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(true);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
}
void CompoundDeviceEditDialog::setInitialGUI()
{
ui->name->setText(ldev.name);
ui->sync->setCurrentText(CompoundDevice::SyncToString(ldev.sync));
// Removing the old frames actually modifies the state of ldev as well. Block signals to prevent this from happening
for(auto f : deviceFrames) {
f->blockSignals(true);
}
QHBoxLayout* layout = (QHBoxLayout*) graph->layout();
// remove all widgets from the layout
if (layout != NULL ) {
QLayoutItem* item;
while ((item = layout->takeAt(0)) != NULL ) {
delete item->widget();
delete item;
}
}
deviceFrames.clear();
layout->addStretch(1);
for(unsigned int i=0;i<ldev.deviceSerials.size();i++) {
auto frame = new DeviceFrame(&ldev, i);
connect(frame, &DeviceFrame::settingChanged, this, &CompoundDeviceEditDialog::checkIfOkay);
addFrame(i, frame);
}
layout->addStretch(1);
checkIfOkay();
}
DeviceFrame *CompoundDeviceEditDialog::frameAtPosition(int pos)
{
pos -= graph->layout()->itemAt(0)->geometry().width();
if(pos > 0 && pos <= (int) deviceFrames.size() * frameSize) {
return deviceFrames[pos / frameSize];
}
return nullptr;
}
unsigned int CompoundDeviceEditDialog::findInsertPosition(int xcoord)
{
xcoord -= graph->layout()->itemAt(0)->geometry().width();
// added in port 1 network
int index = (xcoord + frameSize / 2) / frameSize;
if(index < 0) {
index = 0;
} else if(index > (int) deviceFrames.size()) {
index = deviceFrames.size();
}
// add 1 (first widget is always the stretch)
return index + 1;
}
void CompoundDeviceEditDialog::addFrameAtPosition(int pos, DeviceFrame *c)
{
auto index = findInsertPosition(pos);
// add component to the deviceFrame vector
index -= 1; // first widget is fixed
if(index <= deviceFrames.size()) {
addFrame(index, c);
}
}
void CompoundDeviceEditDialog::addFrame(int index, DeviceFrame *c)
{
if(deviceFrames.size() == ldev.deviceSerials.size()) {
ldev.deviceSerials.insert(ldev.deviceSerials.begin() + index, "");
}
deviceFrames.insert(deviceFrames.begin() + index, c);
deviceFrames[index]->setPosition(index);
// remove from list when the component deletes itself
connect(c, &DeviceFrame::deleted, [=](){
removeDeviceFrame(c);
});
QHBoxLayout *l = static_cast<QHBoxLayout*>(graph->layout());
l->insertWidget(index + 1, c);
c->show();
updateDeviceFrames();
}
void CompoundDeviceEditDialog::createDragFrame(DeviceFrame *c)
{
QDrag *drag = new QDrag(this);
QMimeData *mimeData = new QMimeData;
QByteArray encodedPointer;
QDataStream stream(&encodedPointer, QIODevice::WriteOnly);
stream << quintptr(c);
mimeData->setData("compoundDeviceFrame/pointer", encodedPointer);
drag->setMimeData(mimeData);
drag->exec(Qt::MoveAction);
dragFrame = nullptr;
}
void CompoundDeviceEditDialog::updateInsertIndicator(int xcoord)
{
auto index = findInsertPosition(xcoord);
QHBoxLayout *l = static_cast<QHBoxLayout*>(graph->layout());
l->removeWidget(insertIndicator);
l->insertWidget(index, insertIndicator);
}
bool CompoundDeviceEditDialog::eventFilter(QObject *object, QEvent *event)
{
if(object->objectName() == "Graph") {
if(event->type() == QEvent::MouseButtonPress) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
dragFrame = frameAtPosition(mouseEvent->pos().x());
if(dragFrame) {
dragStartPosition = mouseEvent->pos();
return true;
}
}
return false;
} else if(event->type() == QEvent::MouseMove) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (!(mouseEvent->buttons() & Qt::LeftButton)) {
return false;
}
if (!dragFrame) {
return false;
}
if ((mouseEvent->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance()) {
return false;
}
// remove and hide component while it is being dragged
graph->layout()->removeWidget(dragFrame);
dragFrame->hide();
removeDeviceFrame(dragFrame);
graph->update();
createDragFrame(dragFrame);
return true;
} else if(event->type() == QEvent::DragEnter) {
auto dragEvent = static_cast<QDragEnterEvent*>(event);
if(dragEvent->mimeData()->hasFormat("compoundDeviceFrame/pointer")) {
dropPending = true;
auto data = dragEvent->mimeData()->data("compoundDeviceFrame/pointer");
QDataStream stream(&data, QIODevice::ReadOnly);
quintptr dropPtr;
stream >> dropPtr;
dropFrame = (DeviceFrame*) dropPtr;
dragEvent->acceptProposedAction();
insertIndicator = new QWidget();
insertIndicator->setMinimumSize(2, frameSize);
insertIndicator->setMaximumSize(2, frameSize);
insertIndicator->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
insertIndicator->setStyleSheet("background-color:red;");
updateInsertIndicator(dragEvent->position().toPoint().x());
return true;
}
} else if(event->type() == QEvent::DragMove) {
auto dragEvent = static_cast<QDragMoveEvent*>(event);
updateInsertIndicator(dragEvent->position().toPoint().x());
return true;
} else if(event->type() == QEvent::Drop) {
auto dragEvent = static_cast<QDropEvent*>(event);
delete insertIndicator;
addFrameAtPosition(dragEvent->position().toPoint().x(), dropFrame);
return true;
} else if(event->type() == QEvent::DragLeave) {
dropPending = false;
dropFrame = nullptr;
delete insertIndicator;
}
} else {
// clicked/dragged one of the components outside of the graph
if(event->type() == QEvent::MouseButtonPress) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (mouseEvent->button() == Qt::LeftButton) {
dragStartPosition = mouseEvent->pos();
if(object->objectName() == "lLibreVNA1") {
dragFrame = new DeviceFrame(&ldev, 0);
connect(dragFrame, &DeviceFrame::settingChanged, this, &CompoundDeviceEditDialog::checkIfOkay);
} else {
dragFrame = nullptr;
}
return true;
}
return false;
} else if(event->type() == QEvent::MouseMove) {
auto mouseEvent = static_cast<QMouseEvent*>(event);
if (!(mouseEvent->buttons() & Qt::LeftButton)) {
return false;
}
if (!dragFrame) {
return false;
}
if ((mouseEvent->pos() - dragStartPosition).manhattanLength()
< QApplication::startDragDistance()) {
return false;
}
createDragFrame(dragFrame);
return true;
}
}
return false;
}
void CompoundDeviceEditDialog::removeDeviceFrame(DeviceFrame *dev)
{
auto it = std::find(deviceFrames.begin(), deviceFrames.end(), dev);
if(it == deviceFrames.end()) {
// not found, shouldn't happen
return;
}
auto pos = it - deviceFrames.begin();
deviceFrames.erase(it);
ldev.deviceSerials.erase(ldev.deviceSerials.begin() + pos);
// remove all port mappings from the removed device
bool mappingFound;
do {
mappingFound = false;
for(unsigned int i=0;i<ldev.portMapping.size();i++) {
if(ldev.portMapping[i].device == pos) {
mappingFound = true;
ldev.portMapping.erase(ldev.portMapping.begin() + i);
break;
}
}
} while(mappingFound);
updateDeviceFrames();
}
void CompoundDeviceEditDialog::updateDeviceFrames()
{
int i=0;
for(auto df : deviceFrames) {
df->setPosition(i++);
}
}
DeviceFrame::DeviceFrame(CompoundDevice *dev, unsigned int position) :
position(position),
dev(dev)
{
setMinimumSize(frameSize, frameSize);
setMaximumSize(frameSize, frameSize);
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
setFocusPolicy(Qt::FocusPolicy::ClickFocus);
serial = new QComboBox();
serial->setEditable(true);
port1 = new QComboBox();
port2 = new QComboBox();
connect(serial, &QComboBox::currentTextChanged, this, &DeviceFrame::settingChanged);
connect(port1, &QComboBox::currentTextChanged, this, &DeviceFrame::settingChanged);
connect(port2, &QComboBox::currentTextChanged, this, &DeviceFrame::settingChanged);
auto layout = new QVBoxLayout;
layout->addStretch(1);
auto l1 = new QHBoxLayout;
l1->addStretch(1);
l1->addWidget(serial);
l1->addStretch(1);
layout->addLayout(l1);
layout->addStretch(1);
auto l2 = new QHBoxLayout;
l2->addWidget(port1);
l2->addStretch(1);
l2->addWidget(port2);
layout->addLayout(l2);
setLayout(layout);
update();
// Set initial state
if(position < dev->deviceSerials.size()) {
serial->setCurrentText(dev->deviceSerials[position]);
for(unsigned int i=0;i<dev->portMapping.size();i++) {
if(dev->portMapping[i].device == position) {
if(dev->portMapping[i].port == 0) {
port1->setCurrentIndex(i + 1);
} else if(dev->portMapping[i].port == 1) {
port2->setCurrentIndex(i + 1);
}
}
}
}
}
DeviceFrame::~DeviceFrame()
{
emit deleted();
}
void DeviceFrame::setPosition(int pos)
{
position = pos;
update();
}
void DeviceFrame::update()
{
auto p1 = port1->currentText();
auto p2 = port2->currentText();
auto s = serial->currentText();
port1->clear();
port2->clear();
serial->clear();
port1->addItem("Unused");
port2->addItem("Unused");
for(unsigned int i=0;i<dev->deviceSerials.size();i++) {
port1->addItem("Port "+QString::number(i*2+1));
port2->addItem("Port "+QString::number(i*2+1));
port1->addItem("Port "+QString::number(i*2+2));
port2->addItem("Port "+QString::number(i*2+2));
}
if(port1->findText(p1) >= 0) {
port1->setCurrentText(p1);
} else {
port1->setCurrentIndex(0);
}
if(port2->findText(p2) >= 0) {
port2->setCurrentText(p2);
} else {
port2->setCurrentIndex(0);
}
for(auto d : CompoundDriver::getIndividualDeviceSerials()) {
serial->addItem(d);
}
serial->setCurrentText(s);
if(dev->sync == LibreVNADriver::Synchronization::GUI) {
setStyleSheet("image: url(:/icons/compound_V1_USB.png);");
} else if(dev->sync == LibreVNADriver::Synchronization::Reserved) {
if(position == 0) {
setStyleSheet("image: url(:/icons/compound_V1_Ref_Left.png);");
} else if(position == dev->deviceSerials.size() - 1) {
setStyleSheet("image: url(:/icons/compound_V1_Ref_Right.png);");
} else {
setStyleSheet("image: url(:/icons/compound_V1_Ref_Middle.png);");
}
}
}
QString DeviceFrame::getSerial()
{
return serial->currentText();
}
int DeviceFrame::getPort1()
{
return port1->currentIndex() - 1;
}
int DeviceFrame::getPort2()
{
return port2->currentIndex() - 1;
}
@@ -0,0 +1,73 @@
#ifndef COMPOUNDDEVICEEDITDIALOG_H
#define COMPOUNDDEVICEEDITDIALOG_H
#include "compounddevice.h"
#include <QDialog>
#include <QLabel>
#include <QComboBox>
namespace Ui {
class CompoundDeviceEditDialog;
}
class DeviceFrame : public QFrame {
Q_OBJECT
public:
DeviceFrame(CompoundDevice *dev, unsigned int position);
~DeviceFrame();
void setPosition(int pos);
void update();
QString getSerial();
int getPort1();
int getPort2();
signals:
void deleted();
void settingChanged();
private:
static constexpr int frameSize = 350;
QComboBox *serial;
QComboBox *port1, *port2;
unsigned int position;
CompoundDevice *dev;
};
class CompoundDeviceEditDialog : public QDialog
{
Q_OBJECT
public:
explicit CompoundDeviceEditDialog(CompoundDevice *cdev, QWidget *parent = nullptr);
~CompoundDeviceEditDialog();
private slots:
void checkIfOkay();
private:
static constexpr int frameSize = 350;
void setInitialGUI();
DeviceFrame *frameAtPosition(int pos);
unsigned int findInsertPosition(int xcoord);
void addFrameAtPosition(int pos, DeviceFrame *c);
void addFrame(int index, DeviceFrame *c);
void createDragFrame(DeviceFrame *c);
void updateInsertIndicator(int xcoord);
bool eventFilter(QObject *object, QEvent *event) override;
void removeDeviceFrame(DeviceFrame *dev);
void updateDeviceFrames();
Ui::CompoundDeviceEditDialog *ui;
CompoundDevice ldev;
QWidget *graph, *insertIndicator;
QPoint dragStartPosition;
bool dropPending;
DeviceFrame *dragFrame;
DeviceFrame *dropFrame;
std::vector<DeviceFrame*> deviceFrames;
};
#endif // COMPOUNDDEVICEEDITDIALOG_H
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CompoundDeviceEditDialog</class>
<widget class="QDialog" name="CompoundDeviceEditDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1097</width>
<height>598</height>
</rect>
</property>
<property name="windowTitle">
<string>Compound Device Edit</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Name:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="name"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Synchronization:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="sync"/>
</item>
</layout>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Standalone Devices</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_9">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Drag/drop into compound device</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QFrame" name="frame">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>80</width>
<height>80</height>
</size>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="lLibreVNA1">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="styleSheet">
<string notr="true">border-image: url(:/icons/LibreVNAV1.svg);</string>
</property>
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<property name="text">
<string>LibreVNA V1</string>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1077</width>
<height>372</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QLabel" name="status">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Open|QDialogButtonBox::Save</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -0,0 +1,819 @@
#include "compounddriver.h"
#include "../librevnatcpdriver.h"
#include "../librevnausbdriver.h"
#include "ui_compounddriversettingswidget.h"
#include "compounddeviceeditdialog.h"
#include "preferences.h"
#include "Device/LibreVNA/devicepacketlogview.h"
#include <exception>
CompoundDriver::CompoundDriver()
{
connected = false;
isIdle = true;
triggerForwarding = false;
SApoints = 0;
drivers.push_back(new LibreVNAUSBDriver);
drivers.push_back(new LibreVNATCPDriver);
auto log = new QAction("View Packet Log");
connect(log, &QAction::triggered, this, [=](){
auto d = new DevicePacketLogView();
d->show();
});
specificActions.push_back(log);
auto &p = Preferences::getInstance();
for(auto d : drivers) {
p.load(d->driverSpecificSettings());
}
specificSettings.push_back(Savable::SettingDescription(&compoundJSONString, "compoundDriver.compoundDeviceJSON", "{}"));
specificSettings.push_back(Savable::SettingDescription(&captureRawReceiverValues, "compoundDriver.captureRawReceiverValues", false));
specificSettings.push_back(Savable::SettingDescription(&preservePhase, "compoundDriver.preservePhase", false));
}
CompoundDriver::~CompoundDriver()
{
disconnect();
for(auto d : drivers) {
delete d;
}
}
std::set<QString> CompoundDriver::GetAvailableDevices()
{
parseCompoundJSON();
std::set<QString> availableSerials;
for(auto d : drivers) {
availableSerials.merge(d->GetAvailableDevices());
}
std::set<QString> ret;
for(auto cd : configuredDevices) {
bool allAvailable = true;
for(auto s : cd->deviceSerials) {
if(availableSerials.count(s) == 0) {
allAvailable = false;
break;
}
}
if(allAvailable) {
ret.insert(cd->name);
}
}
return ret;
}
bool CompoundDriver::connectTo(QString getSerial)
{
if(connected) {
disconnect();
}
bool found = false;
for(auto cd : configuredDevices) {
if(cd->name == getSerial) {
activeDevice = *cd;
found = true;
break;
}
}
if(!found) {
qWarning() << "Attempted to connect to an unknown compound device";
return false;
}
std::vector<std::set<QString>> availableSerials;
for(auto d : drivers) {
availableSerials.push_back(d->GetAvailableDevices());
}
// attempt to connect to all individual devices
for(auto s : activeDevice.deviceSerials) {
LibreVNADriver *device = nullptr;
for(unsigned int i=0;i<availableSerials.size();i++) {
if(availableSerials[i].count(s) > 0) {
// this driver can connect to the requested device
if (i == 0) {
device = new LibreVNAUSBDriver();
break;
} else if(i == 1) {
auto tcp = new LibreVNATCPDriver();
tcp->copyDetectedDevices(*static_cast<LibreVNATCPDriver*>(drivers[i]));
device = tcp;
break;
}
}
}
if(!device) {
qWarning() << "Unable to find required serial for compound device:" <<s;
disconnect();
return false;
}
auto &p = Preferences::getInstance();
p.load(device->driverSpecificSettings());
if(!device->connectDevice(s, true)) {
qWarning() << "Unable to connect to required serial for compound device:" <<s;
delete device;
disconnect();
return false;
} else {
devices.push_back(device);
}
}
// make device connections
for(auto dev : devices) {
// Create device connections
connect(dev, &LibreVNADriver::ConnectionLost, this, &CompoundDriver::ConnectionLost, Qt::QueuedConnection);
connect(dev, &LibreVNADriver::LogLineReceived, this, [=](QString line){
emit LogLineReceived(line.prepend(dev->getSerial()+": "));
});
connect(dev, &LibreVNADriver::InfoUpdated, this, [=]() {
updatedInfo(dev);
});
connect(dev, &LibreVNADriver::passOnReceivedPacket, this, [=](const Protocol::PacketInfo& packet) {
incomingPacket(dev, packet);
});
}
connected = true;
return true;
}
void CompoundDriver::disconnect()
{
for(auto d : devices) {
QObject::disconnect(d, nullptr, this, nullptr);
d->disconnect();
delete d;
}
devices.clear();
deviceInfos.clear();
deviceStatus.clear();
compoundSABuffer.clear();
compoundVNABuffer.clear();
connected = false;
}
QString CompoundDriver::getSerial()
{
if(connected) {
return activeDevice.name;
} else {
return "";
}
}
std::set<DeviceDriver::Flag> CompoundDriver::getFlags()
{
std::set<DeviceDriver::Flag> ret;
if(lastStatus.V1.extRefInUse) {
ret.insert(Flag::ExtRef);
}
if(!lastStatus.V1.source_locked || !lastStatus.V1.LO1_locked) {
ret.insert(Flag::Unlocked);
}
if(lastStatus.V1.unlevel) {
ret.insert(Flag::Unlevel);
}
if(lastStatus.V1.ADC_overload) {
ret.insert(Flag::Overload);
}
return ret;
}
QString CompoundDriver::getStatus()
{
QString ret;
ret.append("HW Rev.");
ret.append(info.hardware_version);
ret.append(" FW "+info.firmware_version);
ret.append(" Temps: Source PLL: "+QString::number(lastStatus.V1.temp_source)+"°C LO PLL: "+QString::number(lastStatus.V1.temp_LO1)+"°C MCU: "+QString::number(lastStatus.V1.temp_MCU)+"°C");
ret.append(" Reference:");
if(lastStatus.V1.extRefInUse) {
ret.append("External");
} else {
ret.append("Internal");
if(lastStatus.V1.extRefAvailable) {
ret.append(" (External available)");
}
}
return ret;
}
QWidget *CompoundDriver::createSettingsWidget()
{
auto w = new QWidget();
auto ui = new Ui::CompoundDriverSettingsWidget;
ui->setupUi(w);
// Set initial values
ui->CaptureRawReceiverValues->setChecked(captureRawReceiverValues);
ui->PreservePhase->setChecked(preservePhase);
// make connections
connect(ui->CaptureRawReceiverValues, &QCheckBox::toggled, this, [=](){
captureRawReceiverValues = ui->CaptureRawReceiverValues->isChecked();
});
connect(ui->PreservePhase, &QCheckBox::toggled, this, [=](){
preservePhase = ui->PreservePhase->isChecked();
});
connect(ui->compoundList, &QListWidget::doubleClicked, [=](){
auto index = ui->compoundList->currentRow();
if(index >= 0 && index < (int) configuredDevices.size()) {
auto d = new CompoundDeviceEditDialog(configuredDevices[index]);
connect(d, &QDialog::accepted, [=](){
ui->compoundList->item(index)->setText(configuredDevices[index]->getDesription());
createCompoundJSON();
});
d->show();
}
});
connect(ui->compoundAdd, &QPushButton::clicked, [=](){
auto cd = new CompoundDevice;
auto d = new CompoundDeviceEditDialog(cd);
connect(d, &QDialog::accepted, [=](){
configuredDevices.push_back(cd);
ui->compoundList->addItem(cd->getDesription());
createCompoundJSON();
});
connect(d, &QDialog::rejected, [=](){
delete cd;
});
d->show();
});
connect(ui->compoundDelete, &QPushButton::clicked, [=](){
auto index = ui->compoundList->currentRow();
if(index >= 0 && index < (int) configuredDevices.size()) {
// delete the actual compound device
delete configuredDevices[index];
// delete the line in the GUI list
delete ui->compoundList->takeItem(index);
// remove compound device from list
configuredDevices.erase(configuredDevices.begin() + index);
createCompoundJSON();
}
});
for(auto cd : configuredDevices) {
ui->compoundList->addItem(cd->getDesription());
}
return w;
}
QStringList CompoundDriver::availableVNAMeasurements()
{
QStringList ret;
for(unsigned int i=1;i<=info.Limits.VNA.ports;i++) {
for(unsigned int j=1;j<=info.Limits.VNA.ports;j++) {
ret.push_back("S"+QString::number(i)+QString::number(j));
}
}
if(captureRawReceiverValues) {
for(unsigned int i=1;i<=info.Limits.VNA.ports;i++) {
for(unsigned int j=0;j<info.Limits.VNA.ports;j++) {
ret.push_back("RawPort"+QString::number(i)+"Stage"+QString::number(j));
ret.push_back("RawPort"+QString::number(i)+"Stage"+QString::number(j)+"Ref");
}
}
}
return ret;
}
bool CompoundDriver::setVNA(const DeviceDriver::VNASettings &s, std::function<void (bool)> cb)
{
if(!supports(Feature::VNA)) {
return false;
}
if(s.excitedPorts.size() == 0) {
return setIdle(cb);
}
setIdle([this](bool){
enableTriggerForwarding();
qDebug() << "Start trigger forwarding";
});
// create port->stage mapping
portStageMapping.clear();
for(unsigned int i=0;i<s.excitedPorts.size();i++) {
portStageMapping[s.excitedPorts[i]] = i;
}
zerospan = (s.freqStart == s.freqStop) && (s.dBmStart == s.dBmStop);
VNApoints = s.points;
// create vector of currently used stimulus ports
std::vector<CompoundDevice::PortMapping> activeMapping;
for(auto p : s.excitedPorts) {
activeMapping.push_back(activeDevice.portMapping[p-1]);
}
// Configure the devices
results.clear();
bool success = true;
for(unsigned int i=0;i<devices.size();i++) {
auto dev = devices[i];
dev->setSynchronization(activeDevice.sync, i == 0);
auto devSetting = s;
// indicate the number of stages
devSetting.excitedPorts = std::vector<int>(s.excitedPorts.size(), 0);
// activate the ports of this specific device at the correct stage
auto p1Stage = CompoundDevice::PortMapping::findActiveStage(activeMapping, i, 0);
if(p1Stage < s.excitedPorts.size()) {
devSetting.excitedPorts[p1Stage] = 1;
}
auto p2Stage = CompoundDevice::PortMapping::findActiveStage(activeMapping, i, 1);
if(p2Stage < s.excitedPorts.size()) {
devSetting.excitedPorts[p2Stage] = 2;
}
success &= devices[i]->setVNA(devSetting, [=](bool success){
if(cb) {
results[devices[i]] = success;
checkIfAllTransmissionsComplete(cb);
}
});
}
lastNonIdleSettings.type = Types::VNA;
lastNonIdleSettings.vna = s;
isIdle = false;
return success;
}
QStringList CompoundDriver::availableSAMeasurements()
{
QStringList ret;
for(unsigned int i=1;i<=info.Limits.SA.ports;i++) {
ret.push_back("PORT"+QString::number(i));
}
return ret;
}
bool CompoundDriver::setSA(const DeviceDriver::SASettings &s, std::function<void (bool)> cb)
{
if(!supports(Feature::SA)) {
return false;
}
zerospan = s.freqStart == s.freqStop;
setIdle([this](bool){
enableTriggerForwarding();
qDebug() << "Start trigger forwarding";
});
// Configure the devices
results.clear();
bool success = true;
for(unsigned int i=0;i<devices.size();i++) {
auto dev = devices[i];
dev->setSynchronization(activeDevice.sync, i == 0);
auto devSettings = s;
devSettings.trackingGenerator = false;
devSettings.trackingPort = 0;
if(s.trackingGenerator) {
if(activeDevice.portMapping[s.trackingPort-1].device == i) {
// tracking generator active on this device
devSettings.trackingGenerator = true;
devSettings.trackingPort = activeDevice.portMapping[s.trackingPort-1].port + 1;
}
}
success &= devices[i]->setSA(devSettings, [=](bool success){
if(cb) {
results[devices[i]] = success;
checkIfAllTransmissionsComplete(cb);
}
});
}
SApoints = devices[0]->getSApoints();
for(unsigned int i=1;i<devices.size();i++) {
if(devices[i]->getSApoints() != SApoints) {
qWarning() << "Individual devices report different number of SA points, unable to start compound SA sweep";
setIdle();
success = false;
break;
}
}
lastNonIdleSettings.type = Types::SA;
lastNonIdleSettings.sa = s;
isIdle = false;
return success;
}
QStringList CompoundDriver::availableSGPorts()
{
QStringList ret;
for(unsigned int i=1;i<info.Limits.Generator.ports;i++) {
ret.push_back("PORT"+QString::number(i));
}
return ret;
}
bool CompoundDriver::setSG(const DeviceDriver::SGSettings &s)
{
if(!supports(Feature::Generator)) {
return false;
}
// configure all devices
bool success = true;
for(unsigned int i=0;i<devices.size();i++) {
auto devSettings = s;
devSettings.port = 0;
if(s.port > 0) {
if(activeDevice.portMapping[s.port-1].device == i) {
// this device has the active port
devSettings.port = activeDevice.portMapping[s.port-1].port+1;
}
}
success &= devices[i]->setSG(devSettings);
}
lastNonIdleSettings.type = Types::SG;
lastNonIdleSettings.sg = s;
isIdle = false;
return success;
}
bool CompoundDriver::setIdle(std::function<void (bool)> cb)
{
disableTriggerForwarding();
qDebug() << "Stop trigger forwarding";
auto success = true;
results.clear();
for(auto dev : devices) {
dev->sendWithoutPayload(Protocol::PacketType::ClearTrigger);
success &= dev->setIdle([=](bool success){
if(cb) {
results[dev] = success;
checkIfAllTransmissionsComplete(cb);
}
});
}
isIdle = true;
return success;
}
QStringList CompoundDriver::availableExtRefInSettings()
{
if(!connected) {
return QStringList();
}
auto list = devices[0]->availableExtRefInSettings();
QSet<QString> set(list.begin(), list.end());
for(unsigned int i=1;i<devices.size();i++) {
list = devices[i]->availableExtRefInSettings();
set = set.intersect({list.begin(), list.end()});
}
return QStringList(set.begin(), set.end());
}
QStringList CompoundDriver::availableExtRefOutSettings()
{
if(!connected) {
return QStringList();
}
auto list = devices[0]->availableExtRefOutSettings();
QSet<QString> set(list.begin(), list.end());
for(unsigned int i=1;i<devices.size();i++) {
list = devices[i]->availableExtRefOutSettings();
set = set.intersect({list.begin(), list.end()});
}
return QStringList(set.begin(), set.end());
}
bool CompoundDriver::setExtRef(QString option_in, QString option_out)
{
auto success = true;
qDebug() << "Ref change start";
if(isIdle) {
// can immediately switch reference settings
for(auto dev : devices) {
success &= dev->setExtRef(option_in, option_out);
}
} else {
// can not switch during a sweep
// set to idle first
setIdle();
// change reference
for(auto dev : devices) {
success &= dev->setExtRef(option_in, option_out);
}
// restore last non idle state
switch(lastNonIdleSettings.type) {
case Types::VNA:
setVNA(lastNonIdleSettings.vna);
break;
case Types::SA:
setSA(lastNonIdleSettings.sa);
break;
case Types::SG:
setSG(lastNonIdleSettings.sg);
break;
}
}
qDebug() << "Ref change stop";
return success;
}
std::set<QString> CompoundDriver::getIndividualDeviceSerials()
{
std::vector<LibreVNADriver*> drivers;
drivers.push_back(new LibreVNAUSBDriver);
drivers.push_back(new LibreVNATCPDriver);
auto &p = Preferences::getInstance();
std::set<QString> ret;
for(auto d : drivers) {
p.load(d->driverSpecificSettings());
ret.merge(d->GetAvailableDevices());
}
return ret;
}
void CompoundDriver::triggerReceived(LibreVNADriver *device, bool set)
{
triggerMutex.lock();
if(activeDevice.sync == LibreVNADriver::Synchronization::GUI && triggerForwarding) {
for(unsigned int i=0;i<devices.size();i++) {
if(devices[i] == device) {
// pass on to the next device
if(i < devices.size() - 1) {
qDebug() << "Passing on trigger" << set << "from" << device->getSerial() << "to" << devices[i+1]->getSerial();
devices[i+1]->sendWithoutPayload(set ? Protocol::PacketType::SetTrigger : Protocol::PacketType::ClearTrigger);
} else {
qDebug() << "Passing on trigger" << set << "from" << device->getSerial() << "to" << devices[0]->getSerial();
devices[0]->sendWithoutPayload(set ? Protocol::PacketType::SetTrigger : Protocol::PacketType::ClearTrigger);
}
break;
}
}
}
triggerMutex.unlock();
}
void CompoundDriver::parseCompoundJSON()
{
try {
configuredDevices.clear();
if(compoundJSONString.isEmpty()) {
// empty string will fail JSON parsing. Abort now instead of running into the exception
return;
}
nlohmann::json jc = nlohmann::json::parse(compoundJSONString.toStdString());
for(auto j : jc) {
auto cd = new CompoundDevice();
cd->fromJSON(j);
configuredDevices.push_back(cd);
}
} catch(const std::exception& e){
qDebug() << "Failed to parse compound device string: " << e.what();
}
}
void CompoundDriver::createCompoundJSON()
{
if(configuredDevices.size() > 0) {
nlohmann::json j;
for(auto cd : configuredDevices) {
j.push_back(cd->toJSON());
}
compoundJSONString = QString::fromStdString(j.dump());
} else {
compoundJSONString = "[]";
}
}
void CompoundDriver::incomingPacket(LibreVNADriver *device, const Protocol::PacketInfo &p)
{
switch(p.type) {
case Protocol::PacketType::DeviceStatus:
updatedStatus(device, p.status);
break;
case Protocol::PacketType::VNADatapoint:
datapointReceivecd(device, p.VNAdatapoint);
break;
case Protocol::PacketType::SpectrumAnalyzerResult:
spectrumResultReceived(device, p.spectrumResult);
break;
default:
// nothing to do for other packet types
break;
}
}
void CompoundDriver::updatedInfo(LibreVNADriver *device)
{
deviceInfos[device] = device->getInfo();
if(deviceInfos.size() == devices.size()) {
// got infos from all devices
info = devices[0]->getInfo();
for(unsigned int i=1;i<devices.size();i++) {
info.subset(devices[i]->getInfo());
}
// overwrite number of ports (not all physical ports may be configured for this compound device)
info.Limits.VNA.ports = activeDevice.portMapping.size();
info.Limits.Generator.ports = activeDevice.portMapping.size();
info.Limits.SA.ports = activeDevice.portMapping.size();
emit InfoUpdated();
}
}
void CompoundDriver::updatedStatus(LibreVNADriver *device, const Protocol::DeviceStatus &status)
{
deviceStatus[device] = status;
if(deviceStatus.size() == devices.size()) {
// got status from all devices
for(unsigned int i=0;i<devices.size();i++) {
auto devStat = deviceStatus[devices[i]];
if(i==0) {
lastStatus = devStat;
} else {
lastStatus.V1.extRefAvailable &= devStat.V1.extRefAvailable;
lastStatus.V1.extRefInUse |= devStat.V1.extRefInUse;
lastStatus.V1.FPGA_configured &= devStat.V1.FPGA_configured;
lastStatus.V1.source_locked &= devStat.V1.source_locked;
lastStatus.V1.LO1_locked &= devStat.V1.LO1_locked;
lastStatus.V1.ADC_overload |= devStat.V1.ADC_overload;
lastStatus.V1.unlevel |= devStat.V1.unlevel;
lastStatus.V1.temp_source = std::max(lastStatus.V1.temp_source, devStat.V1.temp_source);
lastStatus.V1.temp_LO1 = std::max(lastStatus.V1.temp_LO1, devStat.V1.temp_LO1);
lastStatus.V1.temp_MCU = std::max(lastStatus.V1.temp_MCU, devStat.V1.temp_MCU);
}
}
emit StatusUpdated();
emit FlagsUpdated();
}
}
void CompoundDriver::spectrumResultReceived(LibreVNADriver *dev, Protocol::SpectrumAnalyzerResult res)
{
if(!compoundSABuffer.count(res.pointNum)) {
compoundSABuffer[res.pointNum] = std::map<LibreVNADriver*, Protocol::SpectrumAnalyzerResult>();
}
auto &buf = compoundSABuffer[res.pointNum];
buf[dev] = res;
if(buf.size() == devices.size()) {
// Got datapoints from all devices, can create merged VNA result
SAMeasurement m;
m.pointNum = res.pointNum;
if(zerospan) {
m.us = res.us;
} else {
m.frequency = res.frequency;
}
// assemble data
for(unsigned int port=0;port<activeDevice.portMapping.size();port++) {
auto device = devices[activeDevice.portMapping[port].device];
auto devicePort = activeDevice.portMapping[port].port;
QString name = "PORT"+QString::number(port+1);
if(devicePort == 0) {
m.measurements[name] = buf[device].port1;
} else {
m.measurements[name] = buf[device].port2;
}
}
emit SAmeasurementReceived(m);
// Clear this and all (incomplete) older datapoint buffers
auto it = compoundSABuffer.begin();
while(it != compoundSABuffer.end()) {
if(it->first <= res.pointNum) {
it = compoundSABuffer.erase(it);
} else {
it++;
}
}
}
}
void CompoundDriver::enableTriggerForwarding()
{
triggerMutex.lock();
for(auto d : devices) {
connect(d, &LibreVNADriver::receivedTrigger, this, &CompoundDriver::triggerReceived, Qt::UniqueConnection);
}
triggerForwarding = true;
triggerMutex.unlock();
}
void CompoundDriver::disableTriggerForwarding()
{
triggerMutex.lock();
triggerForwarding = false;
for(auto d : devices) {
QObject::disconnect(d, &LibreVNADriver::receivedTrigger, this, &CompoundDriver::triggerReceived);
}
triggerMutex.unlock();
}
void CompoundDriver::datapointReceivecd(LibreVNADriver *dev, Protocol::VNADatapoint<32> *data)
{
if(!compoundVNABuffer.count(data->pointNum)) {
compoundVNABuffer[data->pointNum] = std::map<LibreVNADriver*, Protocol::VNADatapoint<32>*>();
}
auto &buf = compoundVNABuffer[data->pointNum];
// create copy of datapoint as it will be deleted by the device driver
buf[dev] = new Protocol::VNADatapoint<32>(*data);
if(buf.size() == devices.size()) {
// Got datapoints from all devices, can create merged VNA result
VNAMeasurement m;
m.pointNum = data->pointNum;
m.Z0 = 50.0;
if(zerospan) {
m.us = data->us;
} else {
m.frequency = data->frequency;
m.dBm = (double) data->cdBm / 100;
}
// assemble data
for(auto map : portStageMapping) {
// map.first is the port (starts at one)
// map.second is the stage at which this port had the stimulus (starts at zero)
// figure out which device had the stimulus for the port...
auto stimulusDev = devices[activeDevice.portMapping[map.first-1].device];
// ...and which device port was used for the stimulus...
auto stimulusDevPort = activeDevice.portMapping[map.first-1].port;
// ...grab the reference receiver data
std::complex<double> ref = buf[stimulusDev]->getValue(map.second, stimulusDevPort, true);
// for all ports of the compound device...
for(unsigned int i=0;i<activeDevice.portMapping.size();i++) {
// ...figure out which physical device and port was used for this input...
auto inputDevice = devices[activeDevice.portMapping[i].device];
// ...and grab the data
auto inputPort = activeDevice.portMapping[i].port;
std::complex<double> input = buf[inputDevice]->getValue(map.second, inputPort, false);
if(!std::isnan(ref.real()) && !std::isnan(input.real())) {
// got both required measurements
QString name = "S"+QString::number(i+1)+QString::number(map.first);
auto S = input / ref;
if(!preservePhase && (inputDevice != stimulusDev)) {
// can't use phase information when measuring across devices
S = abs(S);
}
m.measurements[name] = S;
}
if(captureRawReceiverValues) {
QString name = "RawPort"+QString::number(inputPort+1)+"Stage"+QString::number(map.second);
m.measurements[name] = input;
name = "RawPort"+QString::number(inputPort+1)+"Stage"+QString::number(map.second)+"Ref";
m.measurements[name] = buf[inputDevice]->getValue(map.second, inputPort, true);
}
}
}
emit VNAmeasurementReceived(m);
// Clear this and all (incomplete) older datapoint buffers
int pointNum = data->pointNum;
while(compoundVNABuffer.count(pointNum)) {
auto &buf = compoundVNABuffer[pointNum];
for(auto d : buf) {
delete d.second;
}
compoundVNABuffer.erase(pointNum);
// move on to previous point
if(pointNum > 0) {
pointNum--;
} else {
pointNum = VNApoints - 1;
}
}
}
}
void CompoundDriver::checkIfAllTransmissionsComplete(std::function<void (bool)> cb)
{
if(results.size() == devices.size()) {
// got all responses
bool success = true;
for(auto res : results) {
if(res.second != true) {
success = false;
break;
}
}
if(cb) {
cb(success);
}
}
}
@@ -0,0 +1,230 @@
#ifndef COMPOUNDDRIVER_H
#define COMPOUNDDRIVER_H
#include "../../devicedriver.h"
#include "compounddevice.h"
#include <QMutex>
class CompoundDriver : public DeviceDriver
{
public:
CompoundDriver();
~CompoundDriver();
/**
* @brief Returns the driver name. It must be unique across all implemented drivers and is used to identify the driver
* @return driver name
*/
virtual QString getDriverName() override {return "LibreVNA/Compound";}
/**
* @brief Lists all available devices by their serial numbers
* @return Serial numbers of detected devices
*/
virtual std::set<QString> GetAvailableDevices() override;
protected:
/**
* @brief Connects to a device, given by its serial number
*
* @param serial Serial number of device that should be connected to
* @return true if connection successful, otherwise false
*/
virtual bool connectTo(QString getSerial) override;
/**
* @brief Disconnects from device. Has no effect if no device was connected
*/
virtual void disconnect() override;
public:
/**
* @brief Returns the serial number of the connected device
* @return Serial number of connected device (empty string if no device is connected)
*/
virtual QString getSerial() override;
/**
* @brief Returns the device information. This function will be called when a device has been connected. Its return value must be valid
* directly after returning from DeviceDriver::connectTo()
*
* Emit the InfoUpdate() signal whenever the return value of this function changes.
*
* @return Device information
*/
virtual Info getInfo() override {return info;}
/**
* @brief Returns a set of all active flags
*
* There is also a convenience function to check a specific flag, see DeviceDriver::asserted()
*
* @return Set of active flags
*/
virtual std::set<Flag> getFlags() override;
/**
* @brief Returns the device status string. It will be displayed in the status bar of the application
*
* Emit the StatusUpdated() signal whenever the return value of this function changes
*
* @return Status string
*/
virtual QString getStatus() override;
/**
* @brief Returns a widget to edit the driver specific settings.
*
* The widget is displayed in the global settings dialog and allows the user to edit the settings
* specific to this driver. The application takes ownership of the widget after returning,
* create a new widget for every call to this function. If the driver has no specific settings
* or the settings do not need to be editable by the user, return a nullptr. In this case, no
* page for this driver is created in the settings dialog
* @return newly constructed settings widget or nullptr
*/
virtual QWidget* createSettingsWidget() override;
/**
* @brief Names of available measurements.
*
* The names must be identical to the names used in the returned VNAMeasurement.
* Typically the S parameters, e.g. this function may return {"S11","S12","S21","S22"} but any other names are also allowed.
*
* @return List of available VNA measurement parameters
*/
virtual QStringList availableVNAMeasurements() override;
/**
* @brief Configures the VNA and starts a sweep
* @param s VNA settings
* @param cb Callback, must be called after the VNA has been configured
* @return true if configuration successful, false otherwise
*/
virtual bool setVNA(const VNASettings &s, std::function<void(bool)> cb = nullptr) override;
/**
* @brief Names of available measurements.
*
* The names must be identical to the names used in the returned SAMeasurement.
* Typically the port names, e.g. this function may return {"PORT1","PORT2"} but any other names are also allowed.
*
* @return List of available SA measurement parameters
*/
virtual QStringList availableSAMeasurements() override;
/**
* @brief Configures the SA and starts a sweep
* @param s SA settings
* @param cb Callback, must be called after the SA has been configured
* @return true if configuration successful, false otherwise
*/
virtual bool setSA(const SASettings &s, std::function<void(bool)> cb = nullptr) override;
/**
* @brief Returns the number of points in one spectrum analyzer sweep (as configured by the last setSA() call)
* @return Number of points in the sweep
*/
virtual unsigned int getSApoints() override {return SApoints;}
/**
* @brief Names of available generator ports.
*
* Typically the port names, e.g. this function may return {"PORT1","PORT2"} but any other names are also allowed.
*
* @return List of available SA measurement parameters
*/
virtual QStringList availableSGPorts() override;
/**
* @brief Configures the generator
* @param s Generator settings
* @return true if configuration successful, false otherwise
*/
virtual bool setSG(const SGSettings &s) override;
/**
* @brief Sets the device to idle
*
* Stops all sweeps and signal generation
*
* @param cb Callback, must be called after the device has stopped all operations
* @return true if configuration successful, false otherwise
*/
virtual bool setIdle(std::function<void(bool)> cb = nullptr) override;
/**
* @brief Returns the available options for the external reference input
* @return External reference input options
*/
virtual QStringList availableExtRefInSettings() override;
/**
* @brief Returns the available options for the external reference output
* @return External reference output options
*/
virtual QStringList availableExtRefOutSettings() override;
/**
* @brief Configures the external reference input/output
* @param option_in Reference input option (one of the options returned by availableExtRefInSettings())
* @param option_out Reference output option (one of the options returned by availableExtRefOutSettings())
* @return true if configuration successful, false otherwise
*/
virtual bool setExtRef(QString option_in, QString option_out) override;
static std::set<QString> getIndividualDeviceSerials();
private slots:
void triggerReceived(LibreVNADriver *device, bool set);
private:
void parseCompoundJSON();
void createCompoundJSON();
void incomingPacket(LibreVNADriver *device, const Protocol::PacketInfo &p);
void updatedInfo(LibreVNADriver *device);
void updatedStatus(LibreVNADriver *device, const Protocol::DeviceStatus &status);
void datapointReceivecd(LibreVNADriver *dev, Protocol::VNADatapoint<32> *data);
void spectrumResultReceived(LibreVNADriver *dev, Protocol::SpectrumAnalyzerResult res);
void enableTriggerForwarding();
void disableTriggerForwarding();
Info info;
std::map<LibreVNADriver*, Info> deviceInfos;
std::map<LibreVNADriver*, Protocol::DeviceStatus> deviceStatus;
std::map<int, std::map<LibreVNADriver*, Protocol::VNADatapoint<32>*>> compoundVNABuffer;
std::map<int, std::map<LibreVNADriver*, Protocol::SpectrumAnalyzerResult>> compoundSABuffer;
Protocol::DeviceStatus lastStatus;
// Parsed configuration of compound devices (as extracted from compoundJSONString
std::vector<CompoundDevice*> configuredDevices;
std::map<int, int> portStageMapping; // maps from excitedPort (count starts at one) to stage (count starts at zero)
// All possible drivers to interact with a LibreVNA
std::vector<LibreVNADriver*> drivers;
// Configuration of the device we are connected to
CompoundDevice activeDevice;
bool connected;
bool triggerForwarding;
QMutex triggerMutex;
std::vector<LibreVNADriver*> devices;
bool zerospan;
bool isIdle;
enum class Types{VNA, SA, SG};
struct {
VNASettings vna;
SASettings sa;
SGSettings sg;
Types type;
} lastNonIdleSettings;
unsigned int VNApoints;
unsigned int SApoints;
// Driver specific settings
bool captureRawReceiverValues;
bool preservePhase;
QString compoundJSONString;
// Buffers for storing individual device answers
std::map<LibreVNADriver*, bool> results;
void checkIfAllTransmissionsComplete(std::function<void(bool)> cb = nullptr);
};
#endif // COMPOUNDDRIVER_H
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CompoundDriverSettingsWidget</class>
<widget class="QWidget" name="CompoundDriverSettingsWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>893</width>
<height>559</height>
</rect>
</property>
<property name="windowTitle">
<string>CompoundDriverSettingsWidget</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_13">
<item>
<widget class="QListWidget" name="compoundList"/>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_19">
<item>
<widget class="QPushButton" name="compoundAdd">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-add" resource="../../../icons.qrc">
<normaloff>:/icons/add.png</normaloff>
<normalon>:/icons/add.png</normalon>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="compoundDelete">
<property name="text">
<string/>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../../../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>
<normalon>:/icons/remove.png</normalon>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_8">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_25">
<property name="title">
<string>Debug - Acquisition</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_32">
<item>
<widget class="QCheckBox" name="CaptureRawReceiverValues">
<property name="text">
<string>Capture raw receiver values</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="PreservePhase">
<property name="text">
<string>Preserve phase for measurements between devices</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources>
<include location="../../../icons.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddAmplitudePointsDialog</class>
<widget class="QDialog" name="AddAmplitudePointsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>564</width>
<height>139</height>
</rect>
</property>
<property name="windowTitle">
<string>Add points</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0,1">
<item>
<widget class="QRadioButton" name="singlePoint">
<property name="text">
<string>Single Point at</string>
</property>
<attribute name="buttonGroup">
<string notr="true">groupType</string>
</attribute>
</widget>
</item>
<item>
<widget class="SIUnitEdit" name="frequency"/>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,0,0,0,0,1">
<item>
<widget class="QRadioButton" name="array">
<property name="text">
<string>Array of </string>
</property>
<attribute name="buttonGroup">
<string notr="true">groupType</string>
</attribute>
</widget>
</item>
<item>
<widget class="QSpinBox" name="numPoints">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>64</number>
</property>
<property name="value">
<number>10</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>points from</string>
</property>
</widget>
</item>
<item>
<widget class="SIUnitEdit" name="startFreq"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>to</string>
</property>
</widget>
</item>
<item>
<widget class="SIUnitEdit" name="stopFreq"/>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="deleteExisting">
<property name="text">
<string>Delete already existing points</string>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>SIUnitEdit</class>
<extends>QLineEdit</extends>
<header>CustomWidgets/siunitedit.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="groupType"/>
</buttongroups>
</ui>
@@ -0,0 +1,696 @@
#include "amplitudecaldialog.h"
#include "ui_amplitudecaldialog.h"
#include "mode.h"
#include "unit.h"
#include "ui_addamplitudepointsdialog.h"
#include "ui_automaticamplitudedialog.h"
#include "json.hpp"
#include "Util/util.h"
#include <QDebug>
#include <QMessageBox>
#include <QFileDialog>
#include <fstream>
#include <iomanip>
#include <CustomWidgets/informationbox.h>
using namespace std;
using namespace nlohmann;
AmplitudeCalDialog::AmplitudeCalDialog(LibreVNADriver *dev, QWidget *parent) :
QDialog(parent),
ui(new Ui::AmplitudeCalDialog),
dev(dev),
model(this),
edited(false),
mode(CalibrationMode::BothPorts)
{
setAttribute(Qt::WA_DeleteOnClose);
emit dev->acquireControl();
ui->setupUi(this);
ui->view->setModel(&model);
ui->view->setColumnWidth(AmplitudeModel::ColIndexFreq, 100);
ui->view->setColumnWidth(AmplitudeModel::ColIndexCorrectionFactors, 150);
ui->view->setColumnWidth(AmplitudeModel::ColIndexPort1, 150);
ui->view->setColumnWidth(AmplitudeModel::ColIndexPort2, 150);
connect(ui->load, &QPushButton::clicked, [=](){
if(ConfirmActionIfEdited()) {
LoadFromDevice();
}
});
connect(ui->save, &QPushButton::clicked, this, &AmplitudeCalDialog::SaveToDevice);
connect(ui->saveFile, &QPushButton::clicked, [=](){
auto fileEnding = pointType() == Protocol::PacketType::SourceCalPoint ? ".srccal" : ".recvcal";
auto fileFilter = QString("Amplitude calibration files (*")+fileEnding+")";
auto filename = QFileDialog::getSaveFileName(nullptr, "Save calibration data", Preferences::getInstance().UISettings.Paths.vnacaldata, fileFilter, nullptr, Preferences::QFileDialogOptions());
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.vnacaldata = QFileInfo(filename).path();
if(!filename.endsWith(fileEnding)) {
filename.append(fileEnding);
}
json j;
for(auto p : points) {
json point;
point["Frequency"] = p.frequency;
point["Port1"] = p.correctionPort1;
point["Port2"] = p.correctionPort2;
j.push_back(point);
}
ofstream file;
file.open(filename.toStdString());
file << setw(4) << j << endl;
file.close();
edited = false;
});
connect(ui->loadFile, &QPushButton::clicked, [=](){
auto fileEnding = pointType() == Protocol::PacketType::SourceCalPoint ? ".srccal" : ".recvcal";
auto fileFilter = QString("Amplitude calibration files (*")+fileEnding+")";
auto filename = QFileDialog::getOpenFileName(nullptr, "Save calibration data", Preferences::getInstance().UISettings.Paths.vnacaldata, fileFilter, nullptr, Preferences::QFileDialogOptions());
if(filename.isEmpty()) {
// aborted selection
return;
}
Preferences::getInstance().UISettings.Paths.vnacaldata = QFileInfo(filename).path();
ifstream file;
file.open(filename.toStdString());
if(!file.is_open()) {
throw runtime_error("Unable to open file");
}
RemoveAllPoints();
json j;
file >> j;
for(auto point : j) {
if(!point.contains("Frequency") || !point.contains("Port1") || !point.contains("Port2")) {
InformationBox::ShowError("Error loading file", "Failed to parse calibration point");
return;
}
CorrectionPoint p;
p.frequency = point["Frequency"];
p.correctionPort1 = point["Port1"];
p.correctionPort2 = point["Port2"];
p.port1set = false;
p.port2set = false;
UpdateAmplitude(p);
if(!AddPoint(p)) {
InformationBox::ShowMessage("Limit reached", "Unable to add all points from file, would not fit into the device");
break;
}
}
});
connect(ui->add, &QPushButton::clicked, this, &AmplitudeCalDialog::AddPointDialog);
connect(ui->remove, &QPushButton::clicked, [=](){
unsigned int row = ui->view->currentIndex().row();
if(row < points.size()) {
RemovePoint(row);
}
});
auto selectionModel = ui->view->selectionModel();
connect(selectionModel, &QItemSelectionModel::currentChanged, [=](const QModelIndex &current, const QModelIndex&) {
if(current.isValid() && (current.column() == model.ColIndexPort1 || current.column() == model.ColIndexPort2)) {
SelectedPoint(points[current.row()].frequency, current.column() == model.ColIndexPort2);
} else {
// Invalid selection
SelectedPoint(0, false);
}
});
connect(ui->modeBoth, &QPushButton::pressed, [=](){
mode = CalibrationMode::BothPorts;
model.dataChanged(model.index(0, model.ColIndexPort1), model.index(points.size(), model.ColIndexPort2));
});
connect(ui->modePort1, &QPushButton::pressed, [=](){
mode = CalibrationMode::OnlyPort1;
model.dataChanged(model.index(0, model.ColIndexPort1), model.index(points.size(), model.ColIndexPort2));
});
connect(ui->modePort2, &QPushButton::pressed, [=](){
mode = CalibrationMode::OnlyPort2;
model.dataChanged(model.index(0, model.ColIndexPort1), model.index(points.size(), model.ColIndexPort2));
});
connect(ui->automatic, &QPushButton::clicked, this, &AmplitudeCalDialog::AutomaticMeasurementDialog);
connect(this, &AmplitudeCalDialog::SpectrumResultReceived, this, &AmplitudeCalDialog::ReceivedMeasurement);
connect(dev, &LibreVNADriver::receivedPacket, this, [=](const Protocol::PacketInfo &p){
if(p.type == Protocol::PacketType::SpectrumAnalyzerResult) {
emit SpectrumResultReceived(p.spectrumResult);
} else if(p.type == Protocol::PacketType::SourceCalPoint || p.type == Protocol::PacketType::ReceiverCalPoint) {
emit AmplitudeCorrectionPointReceived(p.amplitudePoint);
}
}, Qt::QueuedConnection);
}
AmplitudeCalDialog::~AmplitudeCalDialog()
{
emit dev->releaseControl();
delete ui;
}
void AmplitudeCalDialog::reject()
{
if(ConfirmActionIfEdited()) {
delete this;
}
}
std::vector<AmplitudeCalDialog::CorrectionPoint> AmplitudeCalDialog::getPoints() const
{
return points;
}
void AmplitudeCalDialog::setAmplitude(double amplitude, unsigned int point, bool port2)
{
if(point >= points.size()) {
return;
}
if(port2) {
points[point].amplitudePort2 = amplitude;
points[point].port2set = true;
} else {
points[point].amplitudePort1 = amplitude;
points[point].port1set = true;
}
edited = true;
AmplitudeChanged(points[point], port2);
if(mode == CalibrationMode::OnlyPort1 && !port2) {
// apply result from port 1 to port 2 as well
points[point].correctionPort2 = points[point].correctionPort1;
points[point].port2set = points[point].port1set;
points[point].amplitudePort2 = points[point].amplitudePort1;
} else if(mode == CalibrationMode::OnlyPort2 && port2) {
// apply result from port 2 to port 1 as well
points[point].correctionPort1 = points[point].correctionPort2;
points[point].port1set = points[point].port2set;
points[point].amplitudePort1 = points[point].amplitudePort2;
}
model.dataChanged(model.index(point, model.ColIndexCorrectionFactors), model.index(point, model.ColIndexPort2));
UpdateSaveButton();
}
void AmplitudeCalDialog::ReceivedPoint(Protocol::AmplitudeCorrectionPoint p)
{
qDebug() << "Received amplitude calibration point" << p.pointNum << "/" << p.totalPoints;
CorrectionPoint c;
c.frequency = p.freq * 10.0;
c.correctionPort1 = p.port1;
c.correctionPort2 = p.port2;
c.port1set = false;
c.port2set = false;
model.beginInsertRows(QModelIndex(), points.size(), points.size());
UpdateAmplitude(c);
points.push_back(c);
model.endInsertRows();
emit pointsUpdated();
if(p.pointNum == p.totalPoints - 1) {
// this was the last point
disconnect(this, &AmplitudeCalDialog::AmplitudeCorrectionPointReceived, this, nullptr);
ui->load->setEnabled(true);
}
}
void AmplitudeCalDialog::LoadFromDevice()
{
ui->load->setEnabled(false);
dev->setIdle();
RemoveAllPoints();
qDebug() << "Asking for amplitude calibration";
connect(this, &AmplitudeCalDialog::AmplitudeCorrectionPointReceived, this, &AmplitudeCalDialog::ReceivedPoint, Qt::QueuedConnection);
dev->sendWithoutPayload(requestCommand());
edited = false;
UpdateSaveButton();
}
void AmplitudeCalDialog::SaveToDevice()
{
dev->setIdle();
for(unsigned int i=0;i<points.size();i++) {
auto p = points[i];
Protocol::PacketInfo info = {};
info.type = pointType();
info.amplitudePoint.freq = p.frequency / 10.0;
info.amplitudePoint.port1 = p.correctionPort1;
info.amplitudePoint.port2 = p.correctionPort2;
info.amplitudePoint.totalPoints = points.size();
info.amplitudePoint.pointNum = i;
dev->SendPacket(info);
// qDebug() << "Sent amplitude calibration point" << i << "/" << points.size();
}
edited = false;
UpdateSaveButton();
}
void AmplitudeCalDialog::RemovePoint(unsigned int i)
{
model.beginRemoveRows(QModelIndex(), i, i);
points.erase(points.begin() + i);
model.endRemoveRows();
edited = true;
UpdateSaveButton();
}
void AmplitudeCalDialog::RemoveAllPoints()
{
model.beginResetModel();
points.clear();
model.endResetModel();
edited = true;
UpdateSaveButton();
}
bool AmplitudeCalDialog::AddPoint(AmplitudeCalDialog::CorrectionPoint &p)
{
if (points.size() >= dev->getMaxAmplitudePoints()) {
// already at limit
return false;
}
// find position at which this frequency gets inserted
auto index = upper_bound(points.begin(), points.end(), p.frequency, [](double value, const CorrectionPoint& p){
return value < p.frequency;
});
model.beginInsertRows(QModelIndex(), index - points.begin(), index - points.begin());
emit newPointCreated(p);
points.insert(index, p);
model.endInsertRows();
UpdateSaveButton();
return true;
}
bool AmplitudeCalDialog::AddPoint(double frequency)
{
CorrectionPoint newPoint;
newPoint.frequency = frequency;
newPoint.correctionPort1 = 0;
newPoint.correctionPort2 = 0;
newPoint.port1set = false;
newPoint.port2set = false;
auto success = AddPoint(newPoint);
if (success) {
edited = true;
}
return success;
}
void AmplitudeCalDialog::AddPointDialog()
{
auto d = new QDialog();
auto ui = new Ui::AddAmplitudePointsDialog();
ui->setupUi(d);
connect(d, &QDialog::finished, [=](){
delete ui;
});
ui->frequency->setUnit("Hz");
ui->frequency->setPrefixes(" kMG");
ui->startFreq->setUnit("Hz");
ui->startFreq->setPrefixes(" kMG");
ui->stopFreq->setUnit("Hz");
ui->stopFreq->setPrefixes(" kMG");
ui->frequency->setValue(1000000000.0);
ui->startFreq->setValue(dev->getInfo().Limits.SA.minFreq);
ui->stopFreq->setValue(dev->getInfo().Limits.SA.maxFreq);
connect(ui->singlePoint, &QRadioButton::toggled, [=](bool single) {
ui->stopFreq->setEnabled(!single);
ui->startFreq->setEnabled(!single);
ui->numPoints->setEnabled(!single);
ui->frequency->setEnabled(single);
});
ui->singlePoint->setChecked(true);
ui->frequency->setFocus();
connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){
// okay clicked, apply selected action
if(ui->deleteExisting->isChecked()) {
RemoveAllPoints();
}
if(ui->singlePoint->isChecked()) {
if(!AddPoint(ui->frequency->value())) {
InformationBox::ShowMessage("Limit reached", "Can't add any more points, would not fit into the device");
}
} else {
double freq_start = ui->startFreq->value();
double freq_stop = ui->stopFreq->value();
unsigned int points = ui->numPoints->value();
double freq_step = (freq_stop - freq_start) / (points - 1);
for(unsigned int i=0;i<points;i++) {
if(!AddPoint(freq_start + i * freq_step)) {
InformationBox::ShowMessage("Limit reached", "Can't add any more points, would not fit into the device");
break;
}
}
}
});
connect(d, &QDialog::rejected, ui->buttonBox, &QDialogButtonBox::rejected);
connect(ui->buttonBox, &QDialogButtonBox::rejected, [=](){
// aborted, nothing to do
delete d;
});
dev->sendWithoutPayload(requestCommand());
if(AppWindow::showGUI()) {
d->show();
}
}
void AmplitudeCalDialog::AutomaticMeasurementDialog()
{
automatic = {};
automatic.isSourceCal = pointType() == Protocol::PacketType::SourceCalPoint;
const QString ownCal = automatic.isSourceCal ? "Source" : "Receiver";
const QString otherCal = automatic.isSourceCal ? "Receiver" : "Source";
// compile info text
QString info = "It is possible to determine the " + ownCal + " Calibration if a valid " + otherCal
+ " Calibration is already present. To do so, connect a short cable with as little loss as possible"
+ " between the ports. For each point in the " + otherCal + " Calibration, the device will take"
+ " a measurement and determine the correction factors for the " + ownCal + " Calibration.";
automatic.dialog = new QDialog(this);
auto ui = new Ui::AutomaticAmplitudeDialog();
ui->setupUi(automatic.dialog);
connect(automatic.dialog, &QDialog::finished, [=](){
delete ui;
});
automatic.progress = ui->progress;
ui->explanation->setText(info);
ui->status->setText("Gathering information about "+otherCal+" Calibration...");
automatic.points.clear();
connect(automatic.dialog, &QDialog::rejected, ui->abort, &QPushButton::click);
connect(ui->abort, &QPushButton::clicked, [=](){
// aborted, clean up
disconnect(this, &AmplitudeCalDialog::SpectrumResultReceived, this, &AmplitudeCalDialog::ReceivedAutomaticMeasurementResult);
dev->setIdle();
delete ui;
delete automatic.dialog;
});
dev->setIdle();
connect(this, &AmplitudeCalDialog::AmplitudeCorrectionPointReceived, this, [this, ui, otherCal](Protocol::AmplitudeCorrectionPoint p) {
CorrectionPoint c;
c.frequency = p.freq * 10.0;
c.correctionPort1 = p.port1;
c.correctionPort2 = p.port2;
c.port1set = true;
c.port2set = true;
automatic.points.push_back(c);
ui->progress->setValue(100 * (p.pointNum+1) / p.totalPoints);
if(p.pointNum == p.totalPoints - 1) {
// this was the last point, indicate ready for measurement
ui->progress->setValue(0);
ui->status->setText(otherCal + " Calibration contains " +QString::number(p.totalPoints)+" points, ready to start measurement");
ui->start->setEnabled(true);
disconnect(this, &AmplitudeCalDialog::AmplitudeCorrectionPointReceived, this, nullptr);
qDebug() << "Received" << p.totalPoints << "points for automatic calibration";
}
}, Qt::QueuedConnection);
// request points of otherCal
// switch between source/receiver calibration
auto request = automatic.isSourceCal ? Protocol::PacketType::RequestReceiverCal : Protocol::PacketType::RequestSourceCal;
dev->sendWithoutPayload(request);
connect(ui->start, &QPushButton::clicked, [=](){
// remove any exising points in own calibration and copy points from other calibration
RemoveAllPoints();
for(auto p : automatic.points) {
AddPoint(p.frequency);
}
// intialize measurement state machine
connect(this, &AmplitudeCalDialog::SpectrumResultReceived, this, &AmplitudeCalDialog::ReceivedAutomaticMeasurementResult, Qt::QueuedConnection);
automatic.measuringPort2 = false;
automatic.measuringCount = 0;
ui->status->setText("Taking measurements...");
SetupNextAutomaticPoint(automatic.isSourceCal);
});
if(AppWindow::showGUI()) {
automatic.dialog->show();
}
}
void AmplitudeCalDialog::ReceivedMeasurement(Protocol::SpectrumAnalyzerResult res)
{
MeasurementResult m = {.port1 = Util::SparamTodB(res.port1), .port2 = Util::SparamTodB(res.port2)};
sweepMeasurements.push_back(m);
if(res.pointNum == automaticSweepPoints - 1) {
// sweep finished, find maximum for each port
double maxPort1 = std::numeric_limits<double>::lowest();
double maxPort2 = std::numeric_limits<double>::lowest();
for(auto s : sweepMeasurements) {
if(s.port1 > maxPort1) {
maxPort1 = s.port1;
}
if(s.port2 > maxPort2) {
maxPort2 = s.port2;
}
}
MeasurementResult max = {.port1 = maxPort1, .port2 = maxPort2};
if(measured.size() >= averages) {
measured.pop_front();
}
measured.push_back(max);
sweepMeasurements.clear();
}
}
bool AmplitudeCalDialog::ConfirmActionIfEdited()
{
if(edited) {
return InformationBox::AskQuestion("Confirm action", "Some points have been edited but not saved in the device yet. If you continue, all changes will be lost (unless they are already saved to a file). Do you want to continue?", true);
} else {
// not edited yet, nothing to confirm
return true;
}
}
void AmplitudeCalDialog::UpdateSaveButton()
{
bool enable = true;
if(points.size() == 0) {
// needs at least one point
enable = false;
}
for(auto p : points) {
if(!p.port1set || !p.port2set) {
// some points are not set yet
enable = false;
break;
}
}
if(!edited) {
// already saved in device, disable button
enable = false;
}
ui->save->setEnabled(enable);
}
void AmplitudeCalDialog::SetupNextAutomaticPoint(bool isSourceCal)
{
qDebug() << "Starting automatic calibration measurement for point" << automatic.measuringCount <<", measuring port2:" << automatic.measuringPort2;
auto point = automatic.points[automatic.measuringCount];
Protocol::PacketInfo p = {};
p.type = Protocol::PacketType::SpectrumAnalyzerSettings;
p.spectrumSettings.RBW = 10000;
p.spectrumSettings.UseDFT = 0;
// setup points centered around the measurement frequency:
// use a span proportional to the center frequency (this makes sure to catch the peak of the excitation signal which
// might be slightly off due to PLL divider limtis). Also make sure to use at least a span of 2 Hz (zero span not supported yet)
p.spectrumSettings.f_stop = point.frequency * 1.00001 + 1.0;
p.spectrumSettings.f_start = point.frequency / 1.00001 - 1.0;
p.spectrumSettings.pointNum = automaticSweepPoints;
p.spectrumSettings.Detector = 0;
p.spectrumSettings.SignalID = 1;
p.spectrumSettings.WindowType = 3;
p.spectrumSettings.trackingGenerator = 1;
p.spectrumSettings.trackingPower = excitationAmplitude * 100.0;
p.spectrumSettings.trackingGeneratorOffset = 0;
// For a source cal, the tracking generator has to be enabled at the measurement port (calibrating the output power)
// For a receiver cal, the tracking generator has to be enabled at the other port (calibrating received power)
if(isSourceCal) {
if(automatic.measuringPort2) {
p.spectrumSettings.trackingGeneratorPort = 1;
} else {
p.spectrumSettings.trackingGeneratorPort = 0;
}
} else {
if(automatic.measuringPort2) {
p.spectrumSettings.trackingGeneratorPort = 0;
} else {
p.spectrumSettings.trackingGeneratorPort = 1;
}
}
if(isSourceCal) {
// Calibrating the source which means the receiver is already calibrated -> use receiver corrections but no source corrections
p.spectrumSettings.applyReceiverCorrection = 1;
p.spectrumSettings.applySourceCorrection = 0;
} else {
// the other way around
p.spectrumSettings.applyReceiverCorrection = 0;
p.spectrumSettings.applySourceCorrection = 1;
}
automatic.settlingCount = averages + automaticSettling;
dev->SendPacket(p);
sweepMeasurements.clear();
sweepMeasurements.reserve(automaticSweepPoints);
}
void AmplitudeCalDialog::ReceivedAutomaticMeasurementResult(Protocol::SpectrumAnalyzerResult res)
{
if(res.pointNum != automaticSweepPoints - 1) {
// ignore everything except end of sweep
return;
}
if(automatic.settlingCount > 0) {
automatic.settlingCount--;
return;
}
if(automatic.isSourceCal) {
// grab correct measurement and convert to dbm
auto m = averageMeasurement();
double measurement = automatic.measuringPort2 ? m.port1 : m.port2;
// receiver cal already done, the measurement result is accurate and can be used to determine actual output power
setAmplitude(measurement, automatic.measuringCount, automatic.measuringPort2);
} else {
// source cal already done, the output power is accurate while the measurement might be off.
// The measurement result is already extracted by the ReceiverCalDialog
setAmplitude(excitationAmplitude, automatic.measuringCount, automatic.measuringPort2);
}
// update progress bar
automatic.progress->setValue(100 * (automatic.measuringCount * 2 + automatic.measuringPort2 + 1) / (points.size() * 2));
// advance state machine
// switch ports
automatic.measuringPort2 = !automatic.measuringPort2;
if(!automatic.measuringPort2) {
// now measuring port1, this means we have to advance to the next point
automatic.measuringCount++;
if(automatic.measuringCount >= points.size()) {
// all done, close dialog
// Closing the dialog will disconnect the connection to this function and return the device into the idle state
automatic.dialog->reject();
automatic.dialog = nullptr;
qDebug() << "Automatic source/receiver calibration complete";
return;
}
}
// Start next measurement
SetupNextAutomaticPoint(automatic.isSourceCal);
}
AmplitudeCalDialog::MeasurementResult AmplitudeCalDialog::averageMeasurement()
{
MeasurementResult ret = {};
for(auto m : measured) {
ret.port1 += m.port1;
ret.port2 += m.port2;
}
ret.port1 /= measured.size();
ret.port2 /= measured.size();
return ret;
}
AmplitudeCalDialog::CalibrationMode AmplitudeCalDialog::getMode() const
{
return mode;
}
AmplitudeModel::AmplitudeModel(AmplitudeCalDialog *c) :
QAbstractTableModel(),
c(c)
{
}
int AmplitudeModel::rowCount(const QModelIndex &) const
{
return c->getPoints().size();
}
int AmplitudeModel::columnCount(const QModelIndex &) const
{
return ColIndexLast;
}
QVariant AmplitudeModel::data(const QModelIndex &index, int role) const
{
if(role == Qt::DisplayRole && static_cast<unsigned int>(index.row()) < c->getPoints().size()) {
auto p = c->getPoints()[index.row()];
switch(index.column()) {
case ColIndexFreq:
return Unit::ToString(p.frequency, "Hz", " kMG", 6);
break;
case ColIndexCorrectionFactors:
return QString::number(p.correctionPort1) + ", " + QString::number(p.correctionPort2);
break;
case ColIndexPort1:
if (p.port1set) {
return Unit::ToString(p.amplitudePort1, "dbm", " ", 4);
} else {
return "No data";
}
break;
case ColIndexPort2:
if (p.port2set) {
return Unit::ToString(p.amplitudePort2, "dbm", " ", 4);
} else {
return "No data";
}
break;
}
}
return QVariant();
}
bool AmplitudeModel::setData(const QModelIndex &index, const QVariant &value, int)
{
if((unsigned int) index.row() >= c->getPoints().size()) {
return false;
}
switch(index.column()) {
case ColIndexPort1:
c->setAmplitude(value.toDouble(), index.row(), false);
return true;
case ColIndexPort2:
c->setAmplitude(value.toDouble(), index.row(), true);
return true;
}
return false;
}
QVariant AmplitudeModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if(orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch(section) {
case ColIndexFreq: return "Frequency"; break;
case ColIndexCorrectionFactors: return "Correction Factors"; break;
case ColIndexPort1: return "Amplitude Port 1"; break;
case ColIndexPort2: return "Amplitude Port 2"; break;
default: return QVariant(); break;
}
} else {
return QVariant();
}
}
Qt::ItemFlags AmplitudeModel::flags(const QModelIndex &index) const
{
int flags = Qt::NoItemFlags;
switch(index.column()) {
case ColIndexPort1:
if(c->getMode() != AmplitudeCalDialog::CalibrationMode::OnlyPort2) {
flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable;
}
break;
case ColIndexPort2:
if(c->getMode() != AmplitudeCalDialog::CalibrationMode::OnlyPort1) {
flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable;
}
break;
}
return (Qt::ItemFlags) flags;
}
@@ -0,0 +1,133 @@
#ifndef AMPLITUDECALDIALOG_H
#define AMPLITUDECALDIALOG_H
#include "librevnadriver.h"
#include <deque>
#include <QDialog>
#include <QAbstractTableModel>
#include <QProgressBar>
namespace Ui {
class AmplitudeCalDialog;
}
class AmplitudeCalDialog;
class AmplitudeModel : public QAbstractTableModel
{
friend class AmplitudeCalDialog;
Q_OBJECT
public:
AmplitudeModel(AmplitudeCalDialog *c);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
private:
enum {
ColIndexFreq,
ColIndexCorrectionFactors,
ColIndexPort1,
ColIndexPort2,
ColIndexLast
};
AmplitudeCalDialog *c;
};
class AmplitudeCalDialog : public QDialog
{
Q_OBJECT
public:
explicit AmplitudeCalDialog(LibreVNADriver *dev, QWidget *parent = nullptr);
~AmplitudeCalDialog();
void reject() override;
class CorrectionPoint {
public:
double frequency;
int16_t correctionPort1;
int16_t correctionPort2;
double amplitudePort1;
double amplitudePort2;
bool port1set;
bool port2set;
};
std::vector<CorrectionPoint> getPoints() const;
void setAmplitude(double amplitude, unsigned int point, bool port2);
enum class CalibrationMode {
BothPorts,
OnlyPort1,
OnlyPort2,
};
CalibrationMode getMode() const;
protected slots:
void ReceivedPoint(Protocol::AmplitudeCorrectionPoint p);
void LoadFromDevice();
void SaveToDevice();
void RemovePoint(unsigned int i);
void RemoveAllPoints();
bool AddPoint(CorrectionPoint& p);
bool AddPoint(double frequency);
void AddPointDialog();
void AutomaticMeasurementDialog();
signals:
void pointsUpdated();
void newPointCreated(CorrectionPoint& p);
void AmplitudeCorrectionPointReceived(Protocol::AmplitudeCorrectionPoint p);
void SpectrumResultReceived(Protocol::SpectrumAnalyzerResult res);
protected:
static constexpr double excitationAmplitude = -20.0;
static constexpr int automaticSweepPoints = 31;
static constexpr int averages = 3;
static constexpr int automaticSettling = 1;
void ReceivedMeasurement(Protocol::SpectrumAnalyzerResult res);
bool ConfirmActionIfEdited();
void UpdateSaveButton();
virtual Protocol::PacketType requestCommand() = 0;
virtual Protocol::PacketType pointType() = 0;
// will get called whenever a new cell is selected (frequency=0 means invalid selection)
virtual void SelectedPoint(double frequency, bool port2) = 0;
// will get called whenever the amplitude is changed. Derived class is responsible for updating correction factor
virtual void AmplitudeChanged(CorrectionPoint& point, bool port2) = 0;
// called whenver the correction factor have been retrieved from the device and the amplitudes need to be updated
virtual void UpdateAmplitude(CorrectionPoint& point) = 0;
std::vector<CorrectionPoint> points;
Ui::AmplitudeCalDialog *ui;
LibreVNADriver *dev;
AmplitudeModel model;
bool edited;
CalibrationMode mode;
void SetupNextAutomaticPoint(bool isSourceCal);
void ReceivedAutomaticMeasurementResult(Protocol::SpectrumAnalyzerResult res);
struct {
QDialog *dialog;
std::vector<CorrectionPoint> points;
bool measuringPort2; // true if port2 is being calibrated
unsigned int measuringCount; // number of calibration point
unsigned int settlingCount; // number of measurements still to ignore before taking measurement
bool isSourceCal;
QProgressBar *progress;
} automatic;
// raw (uncorrected) measurements from device.
// Used in reveicer calibration as well as in automatic calibrations
struct MeasurementResult {
double port1, port2;
};
std::vector<MeasurementResult> sweepMeasurements;
std::deque<MeasurementResult> measured;
MeasurementResult averageMeasurement();
};
#endif // SOURCECALDIALOG_H
@@ -0,0 +1,230 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AmplitudeCalDialog</class>
<widget class="QDialog" name="AmplitudeCalDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>766</width>
<height>599</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QTableView" name="view">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectItems</enum>
</property>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QPushButton" name="add">
<property name="text">
<string>Add Point(s)</string>
</property>
<property name="icon">
<iconset theme="list-add" resource="../icons.qrc">
<normaloff>:/icons/add.png</normaloff>:/icons/add.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="remove">
<property name="text">
<string>Delete Point</string>
</property>
<property name="icon">
<iconset theme="list-remove" resource="../icons.qrc">
<normaloff>:/icons/remove.png</normaloff>:/icons/remove.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="load">
<property name="text">
<string>Load from Device</string>
</property>
<property name="icon">
<iconset theme="document-open" resource="../icons.qrc">
<normaloff>:/icons/open.png</normaloff>:/icons/open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="save">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Save to Device</string>
</property>
<property name="icon">
<iconset theme="document-save" resource="../icons.qrc">
<normaloff>:/icons/save.png</normaloff>:/icons/save.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="loadFile">
<property name="text">
<string>Load from File</string>
</property>
<property name="icon">
<iconset theme="document-open" resource="../icons.qrc">
<normaloff>:/icons/open.png</normaloff>:/icons/open.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="saveFile">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Save to File</string>
</property>
<property name="icon">
<iconset theme="document-save" resource="../icons.qrc">
<normaloff>:/icons/save.png</normaloff>:/icons/save.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Calibration Mode</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="modeBoth">
<property name="text">
<string>Both Ports</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
<attribute name="buttonGroup">
<string notr="true">groupMode</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Best accuracy</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="modePort1">
<property name="text">
<string>Only Port 1</string>
</property>
<attribute name="buttonGroup">
<string notr="true">groupMode</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Use same correction for port 2</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="modePort2">
<property name="text">
<string>Only Port 2</string>
</property>
<attribute name="buttonGroup">
<string notr="true">groupMode</string>
</attribute>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="font">
<font>
<pointsize>8</pointsize>
</font>
</property>
<property name="text">
<string>Use same correction for port 1</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QPushButton" name="automatic">
<property name="text">
<string>Automatic measurement</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections/>
<buttongroups>
<buttongroup name="groupMode"/>
</buttongroups>
</ui>
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AutomaticAmplitudeDialog</class>
<widget class="QDialog" name="AutomaticAmplitudeDialog">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>575</width>
<height>215</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Automatic Measurement</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,0,0,0">
<item>
<widget class="QLabel" name="explanation">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progress">
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Status:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="status">
<property name="enabled">
<bool>true</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QPushButton" name="abort">
<property name="text">
<string>Abort</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="start">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Start Measurement</string>
</property>
<property name="icon">
<iconset theme="media-playback-start" resource="../icons.qrc">
<normaloff>:/icons/play.png</normaloff>:/icons/play.png</iconset>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../icons.qrc"/>
</resources>
<connections/>
</ui>
@@ -0,0 +1,102 @@
#include "deviceconfigurationdialogv1.h"
#include "ui_deviceconfigurationdialogv1.h"
#include <QPushButton>
#include "CustomWidgets/informationbox.h"
DeviceConfigurationDialogV1::DeviceConfigurationDialogV1(LibreVNADriver &dev, QWidget *parent) :
QDialog(parent),
ui(new Ui::DeviceConfigurationDialogV1),
dev(dev)
{
ui->setupUi(this);
setAttribute(Qt::WA_DeleteOnClose);
emit dev.acquireControl();
auto updateADCRate = [=]() {
// update ADC rate, see FPGA protocol for calculation
ui->ADCRate->setValue(102400000.0 / ui->ADCpresc->value());
};
auto updateIF2 = [=]() {
auto ADCrate = ui->ADCRate->value();
ui->IF2->setValue(ADCrate * ui->ADCphaseInc->value() / 4096);
};
connect(ui->ADCpresc, qOverload<int>(&QSpinBox::valueChanged), updateADCRate);
connect(ui->ADCpresc, qOverload<int>(&QSpinBox::valueChanged), updateIF2);
connect(ui->ADCphaseInc, qOverload<int>(&QSpinBox::valueChanged), updateIF2);
ui->IF1->setUnit("Hz");
ui->IF1->setPrefixes(" kM");
ui->IF1->setPrecision(5);
ui->ADCRate->setUnit("Hz");
ui->ADCRate->setPrefixes(" kM");
ui->ADCRate->setPrecision(5);
ui->IF2->setUnit("Hz");
ui->IF2->setPrefixes(" kM");
ui->IF2->setPrecision(5);
ui->IF1->setValue(62000000);
ui->ADCpresc->setValue(128);
ui->ADCphaseInc->setValue(1280);
updateADCRate();
updateIF2();
connect(&dev, &LibreVNADriver::receivedPacket, this, [=](const Protocol::PacketInfo &p) {
if(p.type == Protocol::PacketType::DeviceConfiguration) {
updateGUI(p.deviceConfig);
}
});
dev.sendWithoutPayload(Protocol::PacketType::RequestDeviceConfiguration);
connect(ui->buttonBox, &QDialogButtonBox::accepted, this, [=](){
updateDevice();
accept();
});
connect(ui->buttonBox, &QDialogButtonBox::rejected, this, [=](){
reject();
});
connect(ui->buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [=](){
resetDevice();
this->dev.sendWithoutPayload(Protocol::PacketType::RequestDeviceConfiguration);
});
}
DeviceConfigurationDialogV1::~DeviceConfigurationDialogV1()
{
dev.releaseControl();
delete ui;
}
void DeviceConfigurationDialogV1::updateGUI(const Protocol::DeviceConfig &c)
{
ui->IF1->setEnabled(false);
ui->IF1->setValue(c.V1.IF1);
ui->IF1->setEnabled(true);
ui->ADCpresc->setValue(c.V1.ADCprescaler);
ui->ADCphaseInc->setValue(c.V1.DFTphaseInc);
ui->PLLSettlingDelay->setValue(c.V1.PLLSettlingDelay);
}
void DeviceConfigurationDialogV1::updateDevice()
{
Protocol::PacketInfo p = {};
p.type = Protocol::PacketType::DeviceConfiguration;
p.deviceConfig.V1.IF1 = ui->IF1->value();
p.deviceConfig.V1.ADCprescaler = ui->ADCpresc->value();
p.deviceConfig.V1.DFTphaseInc = ui->ADCphaseInc->value();
p.deviceConfig.V1.PLLSettlingDelay = ui->PLLSettlingDelay->value();
dev.SendPacket(p);
}
void DeviceConfigurationDialogV1::resetDevice()
{
dev.sendWithoutPayload(Protocol::PacketType::ResetDeviceConfiguration, [=](LibreVNADriver::TransmissionResult res){
if(res != LibreVNADriver::TransmissionResult::Ack) {
InformationBox::ShowError("Error", "Failed to reset device configuration");
}
});
}
@@ -0,0 +1,29 @@
#ifndef DEVICECONFIGURATIONDIALOGV1_H
#define DEVICECONFIGURATIONDIALOGV1_H
#include <QDialog>
#include "librevnadriver.h"
namespace Ui {
class DeviceConfigurationDialogV1;
}
class DeviceConfigurationDialogV1 : public QDialog
{
Q_OBJECT
public:
explicit DeviceConfigurationDialogV1(LibreVNADriver &dev, QWidget *parent = nullptr);
~DeviceConfigurationDialogV1();
private:
void updateGUI(const Protocol::DeviceConfig &c);
void updateDevice();
void resetDevice();
Ui::DeviceConfigurationDialogV1 *ui;
LibreVNADriver &dev;
};
#endif // DEVICECONFIGURATIONDIALOGV1_H

Some files were not shown because too many files have changed in this diff Show More