diff --git a/AssembleFirmware.py b/AssembleFirmware.py new file mode 100755 index 0000000..01f1e52 --- /dev/null +++ b/AssembleFirmware.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import os +import binascii + +FPGA_BITSTREAM = "FPGA/VNA/top.bin" +MCU_FW = "Software/VNA_embedded/Debug/VNA_embedded.bin" + +HEADER_SIZE = 24 + +f = open("test.vnafw", "wb") +f.write(bytes("VNA!", 'utf-8')) + +bitstream = open(FPGA_BITSTREAM, "rb") +firmware = open(MCU_FW, "rb") + +size_FPGA = os.path.getsize(FPGA_BITSTREAM) +size_MCU = os.path.getsize(MCU_FW) +print("Got FPGA bitstream of size "+str(size_FPGA)) +print("Got MCU firmware of size "+str(size_MCU)) + +#Create header +# Start address of FPGA bitstream +f.write((HEADER_SIZE).to_bytes(4, byteorder='little')) +# Size of FPGA bitstream +f.write(size_FPGA.to_bytes(4, byteorder='little')) +# Start address of MCU firmware +f.write((HEADER_SIZE + size_FPGA).to_bytes(4, byteorder='little')) +# Size of MCU firmware +f.write(size_MCU.to_bytes(4, byteorder='little')) + +# Calculate CRC +def CRC32_from_file(filename, initial_CRC): + buf = open(filename,'rb').read() + buf = (binascii.crc32(buf, initial_CRC) & 0xFFFFFFFF) + return buf + +print("Calculating CRC...", end="") +CRC = CRC32_from_file(FPGA_BITSTREAM, 0xFFFFFFFF) +CRC = CRC32_from_file(MCU_FW, CRC) +print(":"+hex(CRC)) +f.write(CRC.to_bytes(4, byteorder='little')) + +# Check that we used the correct header size +if f.tell() != HEADER_SIZE: + print("Incorrect header size (defined as "+str(HEADER_SIZE)+" but actual header is of size "+str(f.tell())+")") + exit(-1) + +f.write(bitstream.read()) +f.write(firmware.read()) + +if f.tell() % 256 != 0: + padding = 256 - f.tell() % 256 + f.write(bytearray(padding)) + +print("Created combined firmware file of size "+str(f.tell())) + diff --git a/FPGA/VNA/VNA.gise b/FPGA/VNA/VNA.gise index de67d02..18d9233 100644 --- a/FPGA/VNA/VNA.gise +++ b/FPGA/VNA/VNA.gise @@ -254,6 +254,8 @@ + + diff --git a/Software/PC_Application/.gitignore b/Software/PC_Application/.gitignore new file mode 100644 index 0000000..6089be0 --- /dev/null +++ b/Software/PC_Application/.gitignore @@ -0,0 +1,74 @@ +# 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 + diff --git a/Software/PC_Application/51-vna.rules b/Software/PC_Application/51-vna.rules new file mode 100644 index 0000000..6da541f --- /dev/null +++ b/Software/PC_Application/51-vna.rules @@ -0,0 +1 @@ +SUBSYSTEMS=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="564e", MODE:="0666" diff --git a/Software/PC_Application/Application b/Software/PC_Application/Application new file mode 100755 index 0000000..0faca95 Binary files /dev/null and b/Software/PC_Application/Application differ diff --git a/Software/PC_Application/Application.pro b/Software/PC_Application/Application.pro new file mode 100644 index 0000000..ebc92fb --- /dev/null +++ b/Software/PC_Application/Application.pro @@ -0,0 +1,116 @@ +HEADERS += \ + ../VNA_embedded/Application/Communication/Protocol.hpp \ + Calibration/calibration.h \ + Calibration/calibrationtracedialog.h \ + Calibration/calkit.h \ + Calibration/calkitdialog.h \ + Calibration/measurementmodel.h \ + CustomWidgets/siunitedit.h \ + CustomWidgets/tilewidget.h \ + CustomWidgets/toggleswitch.h \ + CustomWidgets/touchstoneimport.h \ + Device/device.h \ + Device/devicelog.h \ + Device/firmwareupdatedialog.h \ + Device/manualcontroldialog.h \ + Menu/menu.h \ + Menu/menuaction.h \ + Menu/menubool.h \ + Menu/menuitem.h \ + Menu/menuvalue.h \ + Tools/eseries.h \ + Tools/impedancematchdialog.h \ + Traces/bodeplotaxisdialog.h \ + Traces/markerwidget.h \ + Traces/trace.h \ + Traces/tracebodeplot.h \ + Traces/traceeditdialog.h \ + Traces/traceexportdialog.h \ + Traces/traceimportdialog.h \ + Traces/tracemarker.h \ + Traces/tracemarkermodel.h \ + Traces/tracemodel.h \ + Traces/traceplot.h \ + Traces/tracesmithchart.h \ + Traces/tracewidget.h \ + averaging.h \ + qwtplotpiecewisecurve.h \ + touchstone.h \ + unit.h \ + valueinput.h \ + vna.h + +SOURCES += \ + ../VNA_embedded/Application/Communication/Protocol.cpp \ + Calibration/calibration.cpp \ + Calibration/calibrationtracedialog.cpp \ + Calibration/calkit.cpp \ + Calibration/calkitdialog.cpp \ + Calibration/measurementmodel.cpp \ + CustomWidgets/siunitedit.cpp \ + CustomWidgets/tilewidget.cpp \ + CustomWidgets/toggleswitch.cpp \ + CustomWidgets/touchstoneimport.cpp \ + Device/device.cpp \ + Device/devicelog.cpp \ + Device/firmwareupdatedialog.cpp \ + Device/manualcontroldialog.cpp \ + Menu/menu.cpp \ + Menu/menuaction.cpp \ + Menu/menubool.cpp \ + Menu/menuitem.cpp \ + Menu/menuvalue.cpp \ + Tools/eseries.cpp \ + Tools/impedancematchdialog.cpp \ + Traces/bodeplotaxisdialog.cpp \ + Traces/markerwidget.cpp \ + Traces/trace.cpp \ + Traces/tracebodeplot.cpp \ + Traces/traceeditdialog.cpp \ + Traces/traceexportdialog.cpp \ + Traces/traceimportdialog.cpp \ + Traces/tracemarker.cpp \ + Traces/tracemarkermodel.cpp \ + Traces/tracemodel.cpp \ + Traces/traceplot.cpp \ + Traces/tracesmithchart.cpp \ + Traces/tracewidget.cpp \ + averaging.cpp \ + main.cpp \ + qwtplotpiecewisecurve.cpp \ + touchstone.cpp \ + unit.cpp \ + valueinput.cpp \ + vna.cpp + +LIBS += -lusb-1.0 +unix:INCLUDEPATH += /usr/include/qwt +unix:LIBS += -L/usr/lib/ -lqwt-qt5 +win32:INCLUDEPATH += C:\Qwt-6.1.4\include +win32:LIBS += -LC:\Qwt-6.1.4\lib -lqwt + +QT += widgets + +FORMS += \ + Calibration/calibrationtracedialog.ui \ + Calibration/calkitdialog.ui \ + CustomWidgets/tilewidget.ui \ + CustomWidgets/touchstoneimport.ui \ + Device/devicelog.ui \ + Device/firmwareupdatedialog.ui \ + Device/manualcontroldialog.ui \ + Tools/impedancematchdialog.ui \ + Traces/bodeplotaxisdialog.ui \ + Traces/markerwidget.ui \ + Traces/traceeditdialog.ui \ + Traces/traceexportdialog.ui \ + Traces/traceimportdialog.ui \ + Traces/tracewidget.ui \ + main.ui + +DISTFILES += + +RESOURCES += \ + icons.qrc + +CONFIG += c++14 diff --git a/Software/PC_Application/Calibration/calibration.cpp b/Software/PC_Application/Calibration/calibration.cpp new file mode 100644 index 0000000..2621355 --- /dev/null +++ b/Software/PC_Application/Calibration/calibration.cpp @@ -0,0 +1,666 @@ +#include "calibration.h" +#include +#include +#include +#include + +using namespace std; + +Calibration::Calibration() +{ + // Creator vectors for measurements + measurements[Measurement::Port1Open].datapoints = vector(); + measurements[Measurement::Port1Short].datapoints = vector(); + measurements[Measurement::Port1Load].datapoints = vector(); + measurements[Measurement::Port2Open].datapoints = vector(); + measurements[Measurement::Port2Short].datapoints = vector(); + measurements[Measurement::Port2Load].datapoints = vector(); + measurements[Measurement::Isolation].datapoints = vector(); + measurements[Measurement::Through].datapoints = vector(); + + type = Type::None; +} + +void Calibration::clearMeasurements() +{ + for(auto m : measurements) { + m.second.datapoints.clear(); + } +} + +void Calibration::clearMeasurement(Calibration::Measurement type) +{ + measurements[type].datapoints.clear(); + measurements[type].timestamp = QDateTime(); +} + +void Calibration::addMeasurement(Calibration::Measurement type, Protocol::Datapoint &d) +{ + measurements[type].datapoints.push_back(d); + measurements[type].timestamp = QDateTime::currentDateTime(); +} + +bool Calibration::calculationPossible(Calibration::Type type) +{ + std::vector requiredMeasurements; + switch(type) { + case Type::Port1SOL: + requiredMeasurements.push_back(Measurement::Port1Open); + requiredMeasurements.push_back(Measurement::Port1Short); + requiredMeasurements.push_back(Measurement::Port1Load); + break; + case Type::Port2SOL: + requiredMeasurements.push_back(Measurement::Port2Open); + requiredMeasurements.push_back(Measurement::Port2Short); + requiredMeasurements.push_back(Measurement::Port2Load); + break; + case Type::FullSOLT: + requiredMeasurements.push_back(Measurement::Port1Open); + requiredMeasurements.push_back(Measurement::Port1Short); + requiredMeasurements.push_back(Measurement::Port1Load); + requiredMeasurements.push_back(Measurement::Port2Open); + requiredMeasurements.push_back(Measurement::Port2Short); + requiredMeasurements.push_back(Measurement::Port2Load); + requiredMeasurements.push_back(Measurement::Through); + break; + case Type::None: + return false; + } + return SanityCheckSamples(requiredMeasurements); +} + +bool Calibration::constructErrorTerms(Calibration::Type type) +{ + if(!calculationPossible(type)) { + return false; + } + if(minFreq < kit.minFreq() || maxFreq > kit.maxFreq()) { + // Calkit does not support complete calibration range + QMessageBox::critical(nullptr, "Unable to perform calibration", "The calibration kit does not support the complete span. Please choose a different calibration kit or a narrower span."); + return false; + } + switch(type) { + case Type::Port1SOL: + constructPort1SOL(); + break; + case Type::Port2SOL: + constructPort2SOL(); + break; + case Type::FullSOLT: + construct12TermPoints(); + break; + case Type::None: + break; + } + this->type = type; + return true; +} + +void Calibration::resetErrorTerms() +{ + type = Type::None; + points.clear(); +} + +void Calibration::construct12TermPoints() +{ + std::vector requiredMeasurements; + requiredMeasurements.push_back(Measurement::Port1Open); + requiredMeasurements.push_back(Measurement::Port1Short); + requiredMeasurements.push_back(Measurement::Port1Load); + requiredMeasurements.push_back(Measurement::Port2Open); + requiredMeasurements.push_back(Measurement::Port2Short); + requiredMeasurements.push_back(Measurement::Port2Load); + requiredMeasurements.push_back(Measurement::Through); + if(!SanityCheckSamples(requiredMeasurements)) { + throw runtime_error("Missing/wrong calibration measurement"); + } + requiredMeasurements.push_back(Measurement::Isolation); + bool isolation_measured = SanityCheckSamples(requiredMeasurements); + + // If we get here the calibration measurements are all okay + points.clear(); + for(unsigned int i = 0;i(measurements[Measurement::Port1Open].datapoints[i].real_S11, measurements[Measurement::Port1Open].datapoints[i].imag_S11); + auto S11_short = complex(measurements[Measurement::Port1Short].datapoints[i].real_S11, measurements[Measurement::Port1Short].datapoints[i].imag_S11); + auto S11_load = complex(measurements[Measurement::Port1Load].datapoints[i].real_S11, measurements[Measurement::Port1Load].datapoints[i].imag_S11); + auto S22_open = complex(measurements[Measurement::Port2Open].datapoints[i].real_S22, measurements[Measurement::Port2Open].datapoints[i].imag_S22); + auto S22_short = complex(measurements[Measurement::Port2Short].datapoints[i].real_S22, measurements[Measurement::Port2Short].datapoints[i].imag_S22); + auto S22_load = complex(measurements[Measurement::Port2Load].datapoints[i].real_S22, measurements[Measurement::Port2Load].datapoints[i].imag_S22); + auto S21_isolation = complex(0,0); + auto S12_isolation = complex(0,0); + if(isolation_measured) { + S21_isolation = complex(measurements[Measurement::Isolation].datapoints[i].real_S21, measurements[Measurement::Isolation].datapoints[i].imag_S21); + S12_isolation = complex(measurements[Measurement::Isolation].datapoints[i].real_S12, measurements[Measurement::Isolation].datapoints[i].imag_S12); + } + auto S11_through = complex(measurements[Measurement::Through].datapoints[i].real_S11, measurements[Measurement::Through].datapoints[i].imag_S11); + auto S21_through = complex(measurements[Measurement::Through].datapoints[i].real_S21, measurements[Measurement::Through].datapoints[i].imag_S21); + auto S22_through = complex(measurements[Measurement::Through].datapoints[i].real_S22, measurements[Measurement::Through].datapoints[i].imag_S22); + auto S12_through = complex(measurements[Measurement::Through].datapoints[i].real_S12, measurements[Measurement::Through].datapoints[i].imag_S12); + + auto actual = kit.toReflection(p.frequency); + // Forward calibration + computeSOL(S11_short, S11_open, S11_load, p.fe00, p.fe11, p.fe10e01, actual.Open, actual.Short, actual.Load); + p.fe30 = S21_isolation; + // See page 17 of http://www2.electron.frba.utn.edu.ar/~jcecconi/Bibliografia/04%20-%20Param_S_y_VNA/Network_Analyzer_Error_Models_and_Calibration_Methods.pdf + // Formulas for S11M and S21M solved for e22 and e10e32 + auto deltaS = actual.ThroughS11*actual.ThroughS22 - actual.ThroughS21 * actual.ThroughS12; + p.fe22 = ((S11_through - p.fe00)*(1.0 - p.fe11 * actual.ThroughS11)-actual.ThroughS11*p.fe10e01) + / ((S11_through - p.fe00)*(actual.ThroughS22-p.fe11*deltaS)-deltaS*p.fe10e01); + p.fe10e32 = (S21_through - p.fe30)*(1.0 - p.fe11*actual.ThroughS11 - p.fe22*actual.ThroughS22 + p.fe11*p.fe22*deltaS) / actual.ThroughS21; + // Reverse calibration + computeSOL(S22_short, S22_open, S22_load, p.re33, p.re22, p.re23e32, actual.Open, actual.Short, actual.Load); + p.re03 = S12_isolation; + p.re11 = ((S22_through - p.re33)*(1.0 - p.re22 * actual.ThroughS22)-actual.ThroughS22*p.re23e32) + / ((S22_through - p.re33)*(actual.ThroughS11-p.re22*deltaS)-deltaS*p.re23e32); + p.re23e01 = (S12_through - p.re03)*(1.0 - p.re11*actual.ThroughS11 - p.re22*actual.ThroughS22 + p.re11*p.re22*deltaS) / actual.ThroughS12; + points.push_back(p); + } +} + +void Calibration::constructPort1SOL() +{ + std::vector requiredMeasurements; + requiredMeasurements.push_back(Measurement::Port1Open); + requiredMeasurements.push_back(Measurement::Port1Short); + requiredMeasurements.push_back(Measurement::Port1Load); + if(!SanityCheckSamples(requiredMeasurements)) { + throw runtime_error("Missing/wrong calibration measurement"); + } + + // If we get here the calibration measurements are all okay + points.clear(); + for(unsigned int i = 0;i(measurements[Measurement::Port1Open].datapoints[i].real_S11, measurements[Measurement::Port1Open].datapoints[i].imag_S11); + auto S11_short = complex(measurements[Measurement::Port1Short].datapoints[i].real_S11, measurements[Measurement::Port1Short].datapoints[i].imag_S11); + auto S11_load = complex(measurements[Measurement::Port1Load].datapoints[i].real_S11, measurements[Measurement::Port1Load].datapoints[i].imag_S11); + // OSL port1 + auto actual = kit.toReflection(p.frequency); + // See page 19 of http://www2.electron.frba.utn.edu.ar/~jcecconi/Bibliografia/04%20-%20Param_S_y_VNA/Network_Analyzer_Error_Models_and_Calibration_Methods.pdf + computeSOL(S11_short, S11_open, S11_load, p.fe00, p.fe11, p.fe10e01, actual.Open, actual.Short, actual.Load); + // All other calibration coefficients to ideal values + p.fe30 = 0.0; + p.fe22 = 0.0; + p.fe10e32 = 1.0; + p.re33 = 0.0; + p.re22 = 0.0; + p.re23e32 = 1.0; + p.re03 = 0.0; + p.re11 = 0.0; + p.re23e01 = 1.0; + points.push_back(p); + } +} + +void Calibration::constructPort2SOL() +{ + std::vector requiredMeasurements; + requiredMeasurements.push_back(Measurement::Port2Open); + requiredMeasurements.push_back(Measurement::Port2Short); + requiredMeasurements.push_back(Measurement::Port2Load); + if(!SanityCheckSamples(requiredMeasurements)) { + throw runtime_error("Missing/wrong calibration measurement"); + } + + // If we get here the calibration measurements are all okay + points.clear(); + for(unsigned int i = 0;i(measurements[Measurement::Port2Open].datapoints[i].real_S22, measurements[Measurement::Port2Open].datapoints[i].imag_S22); + auto S22_short = complex(measurements[Measurement::Port2Short].datapoints[i].real_S22, measurements[Measurement::Port2Short].datapoints[i].imag_S22); + auto S22_load = complex(measurements[Measurement::Port2Load].datapoints[i].real_S22, measurements[Measurement::Port2Load].datapoints[i].imag_S22); + // OSL port2 + auto actual = kit.toReflection(p.frequency); + // See page 19 of http://www2.electron.frba.utn.edu.ar/~jcecconi/Bibliografia/04%20-%20Param_S_y_VNA/Network_Analyzer_Error_Models_and_Calibration_Methods.pdf + computeSOL(S22_short, S22_open, S22_load, p.re33, p.re22, p.re23e32, actual.Open, actual.Short, actual.Load); + // All other calibration coefficients to ideal values + p.fe30 = 0.0; + p.fe22 = 0.0; + p.fe10e32 = 1.0; + p.fe00 = 0.0; + p.fe11 = 0.0; + p.fe10e01 = 1.0; + p.re03 = 0.0; + p.re11 = 0.0; + p.re23e01 = 1.0; + points.push_back(p); + } +} + +void Calibration::correctMeasurement(Protocol::Datapoint &d) +{ + if(type == Type::None) { + // No calibration data, do nothing + return; + } + // Convert measurements to complex variables + auto S11m = complex(d.real_S11, d.imag_S11); + auto S21m = complex(d.real_S21, d.imag_S21); + auto S22m = complex(d.real_S22, d.imag_S22); + auto S12m = complex(d.real_S12, d.imag_S12); + + // find correct entry + auto p = getCalibrationPoint(d); + + // equations from page 20 of http://www2.electron.frba.utn.edu.ar/~jcecconi/Bibliografia/04%20-%20Param_S_y_VNA/Network_Analyzer_Error_Models_and_Calibration_Methods.pdf + auto denom = (1.0 + (S11m - p.fe00) / p.fe10e01 * p.fe11) * (1.0 + (S22m - p.re33) / p.re23e32 * p.re22) + - (S21m - p.fe30) / p.fe10e32 * (S12m - p.re03) / p.re23e01 * p.fe22 * p.re11; + auto S11 = ((S11m - p.fe00) / p.fe10e01 * (1.0 + (S22m - p.re33) / p.re23e32 * p.re22) + - p.fe22 * (S21m - p.fe30) / p.fe10e32 * (S12m - p.re03) / p.re23e01) / denom; + auto S21 = ((S21m - p.fe30) / p.fe10e32 * (1.0 + (S22m - p.re33) / p.re23e32 * (p.re22 - p.fe22))) / denom; + auto S22 = ((S22m - p.re33) / p.re23e32 * (1.0 + (S11m - p.fe00) / p.fe10e01 * p.fe11) + - p.re11 * (S21m - p.fe30) / p.fe10e32 * (S12m - p.re03) / p.re23e01) / denom; + auto S12 = ((S12m - p.re03) / p.re23e01 * (1.0 + (S11m - p.fe00) / p.fe10e01 * (p.fe11 - p.re11))) / denom; + + d.real_S11 = S11.real(); + d.imag_S11 = S11.imag(); + d.real_S12 = S12.real(); + d.imag_S12 = S12.imag(); + d.real_S21 = S21.real(); + d.imag_S21 = S21.imag(); + d.real_S22 = S22.real(); + d.imag_S22 = S22.imag(); +} + +Calibration::InterpolationType Calibration::getInterpolation(Protocol::SweepSettings settings) +{ + if(!points.size()) { + return InterpolationType::NoCalibration; + } + if(settings.f_start < points.front().frequency || settings.f_stop > points.back().frequency) { + return InterpolationType::Extrapolate; + } + // Either exact or interpolation, check individual frequencies + uint32_t f_step = (settings.f_stop - settings.f_start) / (settings.points - 1); + for(uint64_t f = settings.f_start; f <= settings.f_stop; f += f_step) { + if(find_if(points.begin(), points.end(), [&f](const Point& p){ + return abs(f - p.frequency) < 100; + }) == points.end()) { + return InterpolationType::Interpolate; + } + } + // if we get here all frequency points were matched + if(points.front().frequency == settings.f_start && points.back().frequency == settings.f_stop) { + return InterpolationType::Unchanged; + } else { + return InterpolationType::Exact; + } +} + +QString Calibration::MeasurementToString(Calibration::Measurement m) +{ + switch(m) { + case Measurement::Port1Open: + return "Port 1 Open"; + case Measurement::Port1Short: + return "Port 1 Short"; + case Measurement::Port1Load: + return "Port 1 Load"; + case Measurement::Port2Open: + return "Port 2 Open"; + case Measurement::Port2Short: + return "Port 2 Short"; + case Measurement::Port2Load: + return "Port 2 Load"; + case Measurement::Through: + return "Through"; + case Measurement::Isolation: + return "Isolation"; + default: + return "Unknown"; + } +} + +QString Calibration::TypeToString(Calibration::Type t) +{ + switch(t) { + case Type::Port1SOL: return "Port 1"; break; + case Type::Port2SOL: return "Port 2"; break; + case Type::FullSOLT: return "SOLT"; break; + default: return "None"; break; + } +} + +const std::vector Calibration::Types() +{ + const std::vector ret = {Type::Port1SOL, Type::Port2SOL, Type::FullSOLT}; + return ret; +} + +const std::vector Calibration::Measurements(Calibration::Type type) +{ + switch(type) { + case Type::FullSOLT: + case Type::None: + return {Measurement::Port1Short, Measurement::Port1Open, Measurement::Port1Load, Measurement::Port2Short, Measurement::Port2Open, Measurement::Port2Load, Measurement::Through, Measurement::Isolation}; + break; + case Type::Port1SOL: + return {Measurement::Port1Short, Measurement::Port1Open, Measurement::Port1Load}; + break; + case Type::Port2SOL: + return {Measurement::Port2Short, Measurement::Port2Open, Measurement::Port2Load}; + break; + default: + return {}; + break; + } +} + +Calibration::MeasurementInfo Calibration::getMeasurementInfo(Calibration::Measurement m) +{ + MeasurementInfo info; + switch(m) { + case Measurement::Port1Short: + info.name = "Port 1 short"; + info.prerequisites = "Short standard connected to port 1, port 2 open"; + break; + case Measurement::Port1Open: + info.name = "Port 1 open"; + info.prerequisites = "Open standard connected to port 1, port 2 open"; + break; + case Measurement::Port1Load: + info.name = "Port 1 load"; + info.prerequisites = "Load standard connected to port 1, port 2 open"; + break; + case Measurement::Port2Short: + info.name = "Port 2 short"; + info.prerequisites = "Port 1 open, short standard connected to port 2"; + break; + case Measurement::Port2Open: + info.name = "Port 2 open"; + info.prerequisites = "Port 1 open, open standard connected to port 2"; + break; + case Measurement::Port2Load: + info.name = "Port 2 load"; + info.prerequisites = "Port 1 open, load standard connected to port 2"; + break; + case Measurement::Through: + info.name = "Through"; + info.prerequisites = "Port 1 connected to port 2 via through standard"; + break; + case Measurement::Isolation: + info.name = "Isolation"; + info.prerequisites = "Both ports terminated into 50 ohm"; + } + info.points = measurements[m].datapoints.size(); + if(info.points > 0) { + info.fmin = measurements[m].datapoints.front().frequency; + info.fmax = measurements[m].datapoints.back().frequency; + info.points = measurements[m].datapoints.size(); + } + info.timestamp = measurements[m].timestamp; + return info; +} + +std::vector Calibration::getErrorTermTraces() +{ + std::vector traces; + const QString traceNames[12] = {"e00", "F_e11", "e10e01", "e10e32", "F_e22", "e30", "e33", "R_e11", "e23e32", "e23e01", "R_e22", "e03"}; + constexpr bool reflection[12] = {true, true, false, false, true, false, true, true, false, false, true, false}; + for(int i=0;i<12;i++) { + auto t = new Trace(traceNames[i], Qt::red); + t->setCalibration(true); + t->setReflection(reflection[i]); + traces.push_back(t); + } + for(auto p : points) { + Trace::Data d; + d.frequency = p.frequency; + for(int i=0;i<12;i++) { + switch(i) { + case 0: d.S = p.fe00; break; + case 1: d.S = p.fe11; break; + case 2: d.S = p.fe10e01; break; + case 3: d.S = p.fe10e32; break; + case 4: d.S = p.fe22; break; + case 5: d.S = p.fe30; break; + case 6: d.S = p.re33; break; + case 7: d.S = p.re11; break; + case 8: d.S = p.re23e32; break; + case 9: d.S = p.re23e01; break; + case 10: d.S = p.re22; break; + case 11: d.S = p.re03; break; + } + traces[i]->addData(d); + } + } + return traces; +} + +bool Calibration::openFromFile(QString filename) +{ + if(filename.isEmpty()) { + filename = QFileDialog::getOpenFileName(nullptr, "Load calibration data", "", "Calibration files (*.cal)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.isEmpty()) { + // aborted selection + return false; + } + } + + // attempt to load associated calibration kit first (needs to be available when performing calibration) + auto calkit_file = filename; + auto dotPos = calkit_file.lastIndexOf('.'); + if(dotPos >= 0) { + calkit_file.truncate(dotPos); + } + calkit_file.append(".calkit"); + try { + kit = Calkit::fromFile(calkit_file.toStdString()); + } catch (runtime_error e) { + QMessageBox::warning(nullptr, "Missing calibration kit", "The calibration kit file associated with the selected calibration could not be parsed. The calibration might not be accurate. (" + QString(e.what()) + ")"); + } + + ifstream file; + file.open(filename.toStdString()); + try { + file >> *this; + } catch(runtime_error e) { + QMessageBox::warning(nullptr, "File parsing error", e.what()); + return false; + } + + return true; +} + +bool Calibration::saveToFile(QString filename) +{ + if(filename.isEmpty()) { + filename = QFileDialog::getSaveFileName(nullptr, "Save calibration data", "", "Calibration files (*.cal)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.isEmpty()) { + // aborted selection + return false; + } + } + // strip any potential file name extension and set default + auto dotPos = filename.lastIndexOf('.'); + if(dotPos >= 0) { + filename.truncate(dotPos); + } + auto calibration_file = filename; + calibration_file.append(".cal"); + ofstream file; + file.open(calibration_file.toStdString()); + file << *this; + + auto calkit_file = filename; + calkit_file.append(".calkit"); + kit.toFile(calkit_file.toStdString()); + + return true; +} + +ostream& operator<<(ostream &os, const Calibration &c) +{ + for(auto m : c.measurements) { + if(m.second.datapoints.size() > 0) { + os << c.MeasurementToString(m.first).toStdString() << endl; + os << m.second.timestamp.toSecsSinceEpoch() << endl; + os << m.second.datapoints.size() << endl; + for(auto p : m.second.datapoints) { + os << p.pointNum << " " << p.frequency << " "; + os << p.imag_S11 << " " << p.real_S11 << " " << p.imag_S21 << " " << p.real_S21 << " " << p.imag_S12 << " " << p.real_S12 << " " << p.imag_S22 << " " << p.real_S22; + os << endl; + } + } + } + os << Calibration::TypeToString(c.getType()).toStdString() << endl; + return os; +} + +istream& operator >>(istream &in, Calibration &c) +{ + std::string line; + while(getline(in, line)) { + for(auto m : Calibration::Measurements()) { + if(Calibration::MeasurementToString(m) == QString::fromStdString(line)) { + // this is the correct measurement + c.clearMeasurement(m); + uint timestamp; + in >> timestamp; + c.measurements[m].timestamp = QDateTime::fromSecsSinceEpoch(timestamp); + unsigned int points; + in >> points; + for(unsigned int i=0;i> p.pointNum >> p.frequency; + in >> p.imag_S11 >> p.real_S11 >> p.imag_S21 >> p.real_S21 >> p.imag_S12 >> p.real_S12 >> p.imag_S22 >> p.real_S22; + c.measurements[m].datapoints.push_back(p); + if(in.eof() || in.bad() || in.fail()) { + c.clearMeasurement(m); + throw runtime_error("Failed to parse measurement \"" + line + "\", aborting calibration data import."); + } + } + break; + } + } + for(auto t : Calibration::Types()) { + if(Calibration::TypeToString(t) == QString::fromStdString(line)) { + // try to apply this calibration type + if(c.calculationPossible(t)) { + c.constructErrorTerms(t); + } else { + throw runtime_error("Incomplete calibration data, the requested \"" + line + "\"-Calibration could not be performed."); + } + } + break; + } + } + return in; +} + +bool Calibration::SanityCheckSamples(std::vector &requiredMeasurements) +{ + // sanity check measurements, all need to be of the same size with the same frequencies (except for isolation which may be empty) + vector freqs; + for(auto type : requiredMeasurements) { + auto m = measurements[type]; + if(m.datapoints.size() == 0) { + // empty required measurement + return false; + } + if(freqs.size() == 0) { + // this is the first measurement, create frequency vector + for(auto p : m.datapoints) { + freqs.push_back(p.frequency); + } + } else { + // compare with already assembled frequency vector + if(m.datapoints.size() != freqs.size()) { + return false; + } + for(unsigned int i=0;i= points.back().frequency) { + // use last point even for higher frequencies + return points.back(); + } + auto p = lower_bound(points.begin(), points.end(), d.frequency, [](Point p, uint64_t freq) -> bool { + return p.frequency < freq; + }); + if(p->frequency == d.frequency) { + // Exact match, return point + return *p; + } + // need to interpolate + auto high = p; + p--; + auto low = p; + double alpha = (d.frequency - low->frequency) / (high->frequency - low->frequency); + Point ret; + ret.frequency = d.frequency; + ret.fe00 = low->fe00 * (1 - alpha) + high->fe00 * alpha; + ret.fe11 = low->fe11 * (1 - alpha) + high->fe11 * alpha; + ret.fe22 = low->fe22 * (1 - alpha) + high->fe22 * alpha; + ret.fe30 = low->fe30 * (1 - alpha) + high->fe30 * alpha; + ret.re03 = low->re03 * (1 - alpha) + high->re03 * alpha; + ret.re11 = low->re11 * (1 - alpha) + high->re11 * alpha; + ret.re22 = low->re22 * (1 - alpha) + high->re22 * alpha; + ret.re33 = low->re33 * (1 - alpha) + high->re33 * alpha; + ret.fe10e01 = low->fe10e01 * (1 - alpha) + high->fe10e01 * alpha; + ret.fe10e32 = low->fe10e32 * (1 - alpha) + high->fe10e32 * alpha; + ret.re23e01 = low->re23e01 * (1 - alpha) + high->re23e01 * alpha; + ret.re23e32 = low->re23e32 * (1 - alpha) + high->re23e32 * alpha; + return ret; +} + +void Calibration::computeSOL(std::complex s_m, std::complex o_m, std::complex l_m, + std::complex &directivity, std::complex &match, std::complex &tracking, + std::complex o_c, std::complex s_c, std::complex l_c) +{ + // equations from page 13 of http://www2.electron.frba.utn.edu.ar/~jcecconi/Bibliografia/04%20-%20Param_S_y_VNA/Network_Analyzer_Error_Models_and_Calibration_Methods.pdf + // solved while taking non ideal o/s/l standards into account + auto denom = l_c * o_c * (o_m - l_m) + l_c * s_c * (l_m - s_m) + o_c * s_c * (s_m - o_m); + directivity = (l_c * o_m * (s_m * (o_c - s_c) + l_m * s_c) - l_c * o_c * l_m * s_m + o_c * l_m * s_c * (s_m - o_m)) / denom; + match = (l_c * (o_m - s_m) + o_c * (s_m - l_m) + s_c * (l_m - o_m)) / denom; + auto delta = (l_c * l_m * (o_m - s_m) + o_c * o_m * (s_m - l_m) + s_c * s_m * (l_m - o_m)) / denom; + tracking = directivity * match - delta; +} + +std::complex Calibration::correctSOL(std::complex measured, std::complex directivity, std::complex match, std::complex tracking) +{ + return (measured - directivity) / (measured * match - directivity * match + tracking); +} + +Calkit &Calibration::getCalibrationKit() +{ + return kit; +} + +void Calibration::setCalibrationKit(const Calkit &value) +{ + kit = value; +} + +Calibration::Type Calibration::getType() const +{ + return type; +} + + diff --git a/Software/PC_Application/Calibration/calibration.h b/Software/PC_Application/Calibration/calibration.h new file mode 100644 index 0000000..d329044 --- /dev/null +++ b/Software/PC_Application/Calibration/calibration.h @@ -0,0 +1,136 @@ +#ifndef CALIBRATION_H +#define CALIBRATION_H + +#include "Device/device.h" +#include +#include +#include +#include +#include +#include "calkit.h" +#include "Traces/tracemodel.h" +#include +#include "calkit.h" + +class Calibration +{ +public: + Calibration(); + + enum class Measurement { + Port1Open, + Port1Short, + Port1Load, + Port2Open, + Port2Short, + Port2Load, + Isolation, + Through, + }; + void clearMeasurements(); + void clearMeasurement(Measurement type); + void addMeasurement(Measurement type, Protocol::Datapoint &d); + + enum class Type { + Port1SOL, + Port2SOL, + FullSOLT, + None, + }; + + + bool calculationPossible(Type type); + bool constructErrorTerms(Type type); + void resetErrorTerms(); + + void correctMeasurement(Protocol::Datapoint &d); + + 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(Protocol::SweepSettings settings); + + static QString MeasurementToString(Measurement m); + static QString TypeToString(Type t); + + class MeasurementInfo { + public: + QString name, prerequisites; + double fmin, fmax; + unsigned int points; + QDateTime timestamp; + }; + + static const std::vector Types(); + static const std::vector Measurements(Type type = Type::None); + MeasurementInfo getMeasurementInfo(Measurement m); + + friend std::ostream& operator<<(std::ostream& os, const Calibration& c); + friend std::istream& operator >> (std::istream &in, Calibration& c); + int nPoints() { + return points.size(); + } + + std::vector getErrorTermTraces(); + + bool openFromFile(QString filename = QString()); + bool saveToFile(QString filename = QString()); + Type getType() const; + + Calkit& getCalibrationKit(); + void setCalibrationKit(const Calkit &value); + +private: + void construct12TermPoints(); + void constructPort1SOL(); + void constructPort2SOL(); + bool SanityCheckSamples(std::vector &requiredMeasurements); + class Point + { + public: + double frequency; + // Forward error terms + std::complex fe00, fe11, fe10e01, fe10e32, fe22, fe30; + // Reverse error terms + std::complex re33, re11, re23e32, re23e01, re22, re03; + }; + Point getCalibrationPoint(Protocol::Datapoint &d); + /* + * Constructs directivity, match and tracking correction factors from measurements of three distinct impedances + * Normally, an open, short and load are used (with ideal reflection coefficients of 1, -1 and 0 respectively). + * The actual reflection coefficients can be passed on as optional arguments to take into account the non-ideal + * calibration kit. + */ + void computeSOL(std::complex s_m, + std::complex o_m, + std::complex l_m, + std::complex &directivity, + std::complex &match, + std::complex &tracking, + std::complex o_c = std::complex(1.0, 0), + std::complex s_c = std::complex(-1.0, 0), + std::complex l_c = std::complex(0, 0)); + std::complex correctSOL(std::complex measured, + std::complex directivity, + std::complex match, + std::complex tracking); + class MeasurementData { + public: + QDateTime timestamp; + std::vector datapoints; + }; + Type type; + + std::map measurements; + double minFreq, maxFreq; + std::vector points; + + Calkit kit; +}; + +#endif // CALIBRATION_H diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp new file mode 100644 index 0000000..014976d --- /dev/null +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -0,0 +1,73 @@ +#include "calibrationtracedialog.h" +#include "ui_calibrationtracedialog.h" +#include "measurementmodel.h" +#include + +CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, Calibration::Type type) : + QDialog(nullptr), + ui(new Ui::CalibrationTraceDialog), + cal(cal), + requestedType(type) +{ + ui->setupUi(this); + ui->bApply->setIcon(style()->standardIcon(QStyle::SP_DialogApplyButton)); + measurements = cal->Measurements(type); + if(requestedType == Calibration::Type::None) { + ui->bApply->setVisible(false); + } + model = new MeasurementModel(cal, measurements); + ui->tableView->setModel(model); + ui->tableView->setColumnWidth(0, 100); + ui->tableView->setColumnWidth(1, 350); + ui->tableView->setColumnWidth(2, 320); + ui->tableView->setColumnWidth(3, 160); + UpdateApplyButton(); +} + +CalibrationTraceDialog::~CalibrationTraceDialog() +{ + delete ui; +} + +void CalibrationTraceDialog::measurementComplete(Calibration::Measurement m) +{ + model->measurementUpdated(m); + UpdateApplyButton(); +} + +void CalibrationTraceDialog::UpdateApplyButton() +{ + ui->bApply->setEnabled(cal->calculationPossible(requestedType)); +} + +void CalibrationTraceDialog::on_bDelete_clicked() +{ + auto measurement = measurements[ui->tableView->currentIndex().row()]; + cal->clearMeasurement(measurement); + model->measurementUpdated(measurement); + UpdateApplyButton(); +} + +void CalibrationTraceDialog::on_bMeasure_clicked() +{ + auto measurement = measurements[ui->tableView->currentIndex().row()]; + emit triggerMeasurement(measurement); +} + +void CalibrationTraceDialog::on_bApply_clicked() +{ + emit applyCalibration(requestedType); + accept(); +} + +void CalibrationTraceDialog::on_bOpen_clicked() +{ + cal->openFromFile(); + UpdateApplyButton(); + emit applyCalibration(cal->getType()); +} + +void CalibrationTraceDialog::on_bSave_clicked() +{ + cal->saveToFile(); +} diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.h b/Software/PC_Application/Calibration/calibrationtracedialog.h new file mode 100644 index 0000000..b581c00 --- /dev/null +++ b/Software/PC_Application/Calibration/calibrationtracedialog.h @@ -0,0 +1,44 @@ +#ifndef CALIBRATIONTRACEDIALOG_H +#define CALIBRATIONTRACEDIALOG_H + +#include +#include "calibration.h" +#include "measurementmodel.h" + +namespace Ui { +class CalibrationTraceDialog; +} + +class CalibrationTraceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CalibrationTraceDialog(Calibration *cal, Calibration::Type type = Calibration::Type::None); + ~CalibrationTraceDialog(); + +public slots: + void measurementComplete(Calibration::Measurement m); +signals: + void triggerMeasurement(Calibration::Measurement m); + void applyCalibration(Calibration::Type type); + +private slots: + void on_bDelete_clicked(); + void on_bMeasure_clicked(); + void on_bApply_clicked(); + + void on_bOpen_clicked(); + + void on_bSave_clicked(); + +private: + void UpdateApplyButton(); + Ui::CalibrationTraceDialog *ui; + Calibration *cal; + Calibration::Type requestedType; + std::vector measurements; + MeasurementModel *model; +}; + +#endif // CALIBRATIONTRACEDIALOG_H diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.ui b/Software/PC_Application/Calibration/calibrationtracedialog.ui new file mode 100644 index 0000000..b2d9f97 --- /dev/null +++ b/Software/PC_Application/Calibration/calibrationtracedialog.ui @@ -0,0 +1,116 @@ + + + CalibrationTraceDialog + + + + 0 + 0 + 1066 + 396 + + + + Calibration Traces + + + true + + + + + + + + QAbstractItemView::SelectRows + + + true + + + false + + + true + + + false + + + false + + + + + + + + + Measure + + + + .. + + + + + + + Delete + + + + .. + + + + + + + Open + + + + + + + + + + Save + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply Calibration + + + + + + + + + + + + diff --git a/Software/PC_Application/Calibration/calkit.cpp b/Software/PC_Application/Calibration/calkit.cpp new file mode 100644 index 0000000..da483c6 --- /dev/null +++ b/Software/PC_Application/Calibration/calkit.cpp @@ -0,0 +1,290 @@ +#include "calkit.h" + +#include +#include +#include "calkitdialog.h" +#include + +using namespace std; + +Calkit::Calkit() + : ts_open(nullptr), + ts_short(nullptr), + ts_load(nullptr), + ts_through(nullptr), + ts_cached(false) +{ + open_Z0 = 50.0; + open_delay = 0.0; + open_loss = 0.0; + open_C0 = 0.0; + open_C1 = 0.0; + open_C2 = 0.0; + open_C3 = 0.0; + + short_Z0 = 50.0; + short_delay = 0.0; + short_loss = 0.0; + short_L0 = 0.0; + short_L1 = 0.0; + short_L2 = 0.0; + short_L3 = 0.0; + + load_Z0 = 50.0; + + through_Z0 = 50.0; + through_delay = 0.0; + through_loss = 0.0; + + open_measurements = false; + short_measurements = false; + load_measurements = false; + through_measurements = false; + + open_file = ""; + short_file = ""; + load_file = ""; + through_file = ""; + + open_Sparam = 0; + short_Sparam = 0; + load_Sparam = 0; + through_Sparam1 = 0; + through_Sparam2 = 1; +} + +void Calkit::toFile(std::string filename) +{ + ofstream file; + file.open(filename); + file << std::fixed << std::setprecision(12); + file << open_measurements << "\n" << short_measurements << "\n" << load_measurements << "\n" << through_measurements << "\n"; + file << open_Z0 << "\n" << open_delay << "\n" << open_loss << "\n" << open_C0 << "\n" << open_C1 << "\n" << open_C2 << "\n" << open_C3 << "\n"; + file << short_Z0 << "\n" << short_delay << "\n" << short_loss << "\n" << short_L0 << "\n" << short_L1 << "\n" << short_L2 << "\n" << short_L3 << "\n"; + file << load_Z0 << "\n"; + file << through_Z0 << "\n" << through_delay << "\n" << through_loss << "\n"; + if(open_measurements) { + file << open_file << "\n" << open_Sparam << "\n"; + } + if(short_measurements) { + file << short_file << "\n" << short_Sparam << "\n"; + } + if(load_measurements) { + file << load_file << "\n" << load_Sparam << "\n"; + } + if(through_measurements) { + file << through_file << "\n" << through_Sparam1 << "\n" << through_Sparam2 << "\n"; + } + file.close(); +} + +Calkit Calkit::fromFile(std::string filename) +{ + Calkit c; + ifstream file; + file.open(filename); + if(!file.is_open()) { + throw runtime_error("Unable to open file"); + } + file >> c.open_measurements; + file >> c.short_measurements; + file >> c.load_measurements; + file >> c.through_measurements; + file >> c.open_Z0; + file >> c.open_delay; + file >> c.open_loss; + file >> c.open_C0; + file >> c.open_C1; + file >> c.open_C2; + file >> c.open_C3; + file >> c.short_Z0; + file >> c.short_delay; + file >> c.short_loss; + file >> c.short_L0; + file >> c.short_L1; + file >> c.short_L2; + file >> c.short_L3; + file >> c.load_Z0; + file >> c.through_Z0; + file >> c.through_delay; + file >> c.through_loss; + if(c.open_measurements) { + file >> c.open_file; + file >> c.open_Sparam; + } + if(c.short_measurements) { + file >> c.short_file; + file >> c.short_Sparam; + } + if(c.load_measurements) { + file >> c.load_file; + file >> c.load_Sparam; + } + if(c.through_measurements) { + file >> c.through_file; + file >> c.through_Sparam1; + file >> c.through_Sparam2; + } + file.close(); + return c; +} + +void Calkit::edit() +{ + auto dialog = new CalkitDialog(*this); + dialog->show(); +} + +Calkit::Reflection Calkit::toReflection(double frequency) +{ + fillTouchstoneCache(); + Reflection ref; + if(load_measurements) { + ref.Load = ts_load->interpolate(frequency).S[0]; + } else { + auto imp_load = complex(load_Z0, 0); + ref.Load = (imp_load - complex(50.0)) / (imp_load + complex(50.0)); + } + + if(open_measurements) { + ref.Open = ts_open->interpolate(frequency).S[0]; + } else { + // calculate fringing capacitance for open + double Cfringing = open_C0 * 1e-15 + open_C1 * 1e-27 * frequency + open_C2 * 1e-36 * pow(frequency, 2) + open_C3 * 1e-45 * pow(frequency, 3); + // convert to impedance + if (Cfringing == 0) { + // special case to avoid issues with infinity + ref.Open = complex(1.0, 0); + } else { + auto imp_open = complex(0, -1.0 / (frequency * 2 * M_PI * Cfringing)); + ref.Open = (imp_open - complex(50.0)) / (imp_open + complex(50.0)); + } + // transform the delay into a phase shift for the given frequency + double open_phaseshift = -2 * M_PI * frequency * open_delay * 1e-12; + double open_att_db = open_loss * 1e9 * 4.3429 * open_delay * 1e-12 / open_Z0 * sqrt(frequency / 1e9); + double open_att = pow(10.0, -open_att_db / 10.0); + auto open_correction = polar(open_att, open_phaseshift); + ref.Open *= open_correction; + } + + if(short_measurements) { + ref.Short = ts_short->interpolate(frequency).S[0]; + } else { + // calculate inductance for short + double Lseries = short_L0 * 1e-12 + short_L1 * 1e-24 * frequency + short_L2 * 1e-33 * pow(frequency, 2) + short_L3 * 1e-42 * pow(frequency, 3); + // convert to impedance + auto imp_short = complex(0, frequency * 2 * M_PI * Lseries); + ref.Short = (imp_short - complex(50.0)) / (imp_short + complex(50.0)); + // transform the delay into a phase shift for the given frequency + double short_phaseshift = -2 * M_PI * frequency * short_delay * 1e-12; + double short_att_db = short_loss * 1e9 * 4.3429 * short_delay * 1e-12 / short_Z0 * sqrt(frequency / 1e9);; + double short_att = pow(10.0, -short_att_db / 10.0); + auto short_correction = polar(short_att, short_phaseshift); + ref.Short *= short_correction; + } + + if(through_measurements) { + auto interp = ts_through->interpolate(frequency); + ref.ThroughS11 = interp.S[0]; + ref.ThroughS12 = interp.S[1]; + ref.ThroughS21 = interp.S[2]; + ref.ThroughS22 = interp.S[3]; + } else { + // calculate effect of through + double through_phaseshift = -2 * M_PI * frequency * through_delay * 1e-12; + double through_att_db = through_loss * 1e9 * 4.3429 * through_delay * 1e-12 / through_Z0 * sqrt(frequency / 1e9);; + double through_att = pow(10.0, -through_att_db / 10.0); + ref.ThroughS12 = polar(through_att, through_phaseshift); + // Assume symmetric and perfectly matched through for other parameters + ref.ThroughS21 = ref.ThroughS12; + ref.ThroughS11 = 0.0; + ref.ThroughS22 = 0.0; + } + + return ref; +} + +double Calkit::minFreq() +{ + fillTouchstoneCache(); + double min = std::numeric_limits::min(); + array ts_list = {ts_open, ts_short, ts_load, ts_through}; + // find the highest minimum frequency in all measurement files + for(auto ts : ts_list) { + if(!ts) { + // this calibration standard is defined by coefficients, no minimum frequency + continue; + } + if(ts->minFreq() > min) { + min = ts->minFreq(); + } + } + return min; +} + +double Calkit::maxFreq() +{ + fillTouchstoneCache(); + double max = std::numeric_limits::max(); + array ts_list = {ts_open, ts_short, ts_load, ts_through}; + // find the highest minimum frequency in all measurement files + for(auto ts : ts_list) { + if(!ts) { + // this calibration standard is defined by coefficients, no minimum frequency + continue; + } + if(ts->maxFreq() < max) { + max = ts->maxFreq(); + } + } + return max; +} + +void Calkit::clearTouchstoneCache() +{ + if(ts_open) { + delete ts_open; + ts_open = nullptr; + } + if(ts_short) { + delete ts_short; + ts_short = nullptr; + } + if(ts_load) { + delete ts_load; + ts_load = nullptr; + } + if(ts_through) { + delete ts_through; + ts_through = nullptr; + } + ts_cached = false; +} + +void Calkit::fillTouchstoneCache() +{ + if(ts_cached) { + return; + } + if(open_measurements) { + ts_open = new Touchstone(1); + *ts_open = Touchstone::fromFile(open_file); + ts_open->reduceTo1Port(open_Sparam); + } + if(short_measurements) { + ts_short = new Touchstone(1); + *ts_short = Touchstone::fromFile(short_file); + ts_open->reduceTo1Port(short_Sparam); + } + if(load_measurements) { + ts_load = new Touchstone(1); + *ts_load = Touchstone::fromFile(load_file); + ts_open->reduceTo1Port(load_Sparam); + } + if(through_measurements) { + ts_through = new Touchstone(2); + *ts_through = Touchstone::fromFile(through_file); + ts_through->reduceTo2Port(through_Sparam1, through_Sparam2); + } + ts_cached = true; +} diff --git a/Software/PC_Application/Calibration/calkit.h b/Software/PC_Application/Calibration/calkit.h new file mode 100644 index 0000000..81f03f7 --- /dev/null +++ b/Software/PC_Application/Calibration/calkit.h @@ -0,0 +1,50 @@ +#ifndef CALKIT_H +#define CALKIT_H + +#include +#include +#include "touchstone.h" + +class Calkit +{ + friend class CalkitDialog; +public: + Calkit(); + + class Reflection { + public: + std::complex Open; + std::complex Short; + std::complex Load; + std::complex ThroughS11, ThroughS12, ThroughS21, ThroughS22; + }; + + void toFile(std::string filename); + static Calkit fromFile(std::string filename); + void edit(); + Reflection toReflection(double frequency); + double minFreq(); + double maxFreq(); +private: + double open_Z0, open_delay, open_loss, open_C0, open_C1, open_C2, open_C3; + double short_Z0, short_delay, short_loss, short_L0, short_L1, short_L2, short_L3; + double load_Z0; + double through_Z0, through_delay, through_loss; + + // coefficients/measurement file switch + bool open_measurements; + bool short_measurements; + bool load_measurements; + bool through_measurements; + + std::string open_file, short_file, load_file, through_file; + int open_Sparam, short_Sparam, load_Sparam, through_Sparam1, through_Sparam2; + + Touchstone *ts_open, *ts_short, *ts_load, *ts_through; + bool ts_cached; + + void clearTouchstoneCache(); + void fillTouchstoneCache(); +}; + +#endif // CALKIT_H diff --git a/Software/PC_Application/Calibration/calkitdialog.cpp b/Software/PC_Application/Calibration/calkitdialog.cpp new file mode 100644 index 0000000..db49740 --- /dev/null +++ b/Software/PC_Application/Calibration/calkitdialog.cpp @@ -0,0 +1,219 @@ +#include "calkitdialog.h" +#include "ui_calkitdialog.h" +#include + +#include +#include +#include +#include +#include + +using namespace std; + +CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : + QDialog(parent), + ui(new Ui::CalkitDialog), + open_ok(true), + short_ok(true), + load_ok(true), + through_ok(true), + editKit(c) +{ + ui->setupUi(this); + + ui->OpenType->setId(ui->open_coefficients, 0); + ui->OpenType->setId(ui->open_measurement, 1); + + ui->ShortType->setId(ui->short_coefficients, 0); + ui->ShortType->setId(ui->short_measurement, 1); + + ui->LoadType->setId(ui->load_coefficients, 0); + ui->LoadType->setId(ui->load_measurement, 1); + + ui->ThroughType->setId(ui->through_coefficients, 0); + ui->ThroughType->setId(ui->through_measurement, 1); + + ui->open_touchstone->setPorts(1); + ui->short_touchstone->setPorts(1); + ui->load_touchstone->setPorts(1); + ui->through_touchstone->setPorts(2); + + editKit.clearTouchstoneCache(); + ownKit = editKit; + updateEntries(); + + auto UpdateStatus = [=]() { + bool ok = true; + if(ui->open_measurement->isChecked() && !ui->open_touchstone->getStatus()) { + ok = false; + } + if(ui->short_measurement->isChecked() && !ui->short_touchstone->getStatus()) { + ok = false; + } + if(ui->load_measurement->isChecked() && !ui->load_touchstone->getStatus()) { + ok = false; + } + if(ui->through_measurement->isChecked() && !ui->through_touchstone->getStatus()) { + ok = false; + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ok); + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(ok); + }; + + connect(ui->open_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->short_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->load_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->through_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); + + connect(ui->OpenType, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + connect(ui->ShortType, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + connect(ui->LoadType, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + connect(ui->ThroughType, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + + connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked, [this]() { + parseEntries(); + editKit = ownKit; + delete this; + }); + connect(ui->buttonBox->button(QDialogButtonBox::Cancel), &QPushButton::clicked, [this]() { + delete this; + }); + connect(ui->buttonBox->button(QDialogButtonBox::Open), &QPushButton::clicked, [=](){ + auto filename = QFileDialog::getOpenFileName(this, "Open calibration kit coefficients", "", "Calibration kit files (*.calkit)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.length() > 0) { + ownKit = Calkit::fromFile(filename.toStdString()); + updateEntries(); + } + }); + + connect(ui->buttonBox->button(QDialogButtonBox::Save), &QPushButton::clicked, [=](){ + auto filename = QFileDialog::getSaveFileName(this, "Save calibration kit coefficients", "", "Calibration kit files (*.calkit)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.length() > 0) { + parseEntries(); + ownKit.toFile(filename.toStdString()); + } + }); +} + +CalkitDialog::~CalkitDialog() +{ + delete ui; +} + +void CalkitDialog::parseEntries() +{ + + // type + ownKit.open_measurements = ui->open_measurement->isChecked(); + ownKit.short_measurements = ui->short_measurement->isChecked(); + ownKit.load_measurements = ui->load_measurement->isChecked(); + ownKit.through_measurements = ui->through_measurement->isChecked(); + + // coefficients + ownKit.open_Z0 = ui->open_Z0->text().toDouble(); + ownKit.open_delay = ui->open_delay->text().toDouble(); + ownKit.open_loss = ui->open_loss->text().toDouble(); + ownKit.open_C0 = ui->open_C0->text().toDouble(); + ownKit.open_C1 = ui->open_C1->text().toDouble(); + ownKit.open_C2 = ui->open_C2->text().toDouble(); + ownKit.open_C3 = ui->open_C3->text().toDouble(); + + ownKit.short_Z0 = ui->short_Z0->text().toDouble(); + ownKit.short_delay = ui->short_delay->text().toDouble(); + ownKit.short_loss = ui->short_loss->text().toDouble(); + ownKit.short_L0 = ui->short_L0->text().toDouble(); + ownKit.short_L1 = ui->short_L1->text().toDouble(); + ownKit.short_L2 = ui->short_L2->text().toDouble(); + ownKit.short_L3 = ui->short_L3->text().toDouble(); + + ownKit.load_Z0 = ui->load_Z0->text().toDouble(); + + ownKit.through_Z0 = ui->through_Z0->text().toDouble(); + ownKit.through_delay = ui->through_delay->text().toDouble(); + ownKit.through_loss = ui->through_loss->text().toDouble(); + + // file + ownKit.open_file = ui->open_touchstone->getFilename().toStdString(); + ownKit.short_file = ui->short_touchstone->getFilename().toStdString(); + ownKit.load_file = ui->load_touchstone->getFilename().toStdString(); + ownKit.through_file = ui->through_touchstone->getFilename().toStdString(); + + ownKit.open_Sparam = ui->open_touchstone->getPorts()[0]; + ownKit.short_Sparam = ui->short_touchstone->getPorts()[0]; + ownKit.load_Sparam = ui->load_touchstone->getPorts()[0]; + ownKit.through_Sparam1 = ui->through_touchstone->getPorts()[0]; + ownKit.through_Sparam2 = ui->through_touchstone->getPorts()[1]; +} + +void CalkitDialog::updateEntries() +{ + // Coefficients + ui->open_Z0->setText(QString::number(ownKit.open_Z0)); + ui->open_delay->setText(QString::number(ownKit.open_delay)); + ui->open_loss->setText(QString::number(ownKit.open_loss)); + ui->open_C0->setText(QString::number(ownKit.open_C0)); + ui->open_C1->setText(QString::number(ownKit.open_C1)); + ui->open_C2->setText(QString::number(ownKit.open_C2)); + ui->open_C3->setText(QString::number(ownKit.open_C3)); + + ui->short_Z0->setText(QString::number(ownKit.short_Z0)); + ui->short_delay->setText(QString::number(ownKit.short_delay)); + ui->short_loss->setText(QString::number(ownKit.short_loss)); + ui->short_L0->setText(QString::number(ownKit.short_L0)); + ui->short_L1->setText(QString::number(ownKit.short_L1)); + ui->short_L2->setText(QString::number(ownKit.short_L2)); + ui->short_L3->setText(QString::number(ownKit.short_L3)); + + ui->load_Z0->setText(QString::number(ownKit.load_Z0)); + + ui->through_Z0->setText(QString::number(ownKit.through_Z0)); + ui->through_delay->setText(QString::number(ownKit.through_delay)); + ui->through_loss->setText(QString::number(ownKit.through_loss)); + + // Measurements + ui->open_touchstone->setFile(QString::fromStdString(ownKit.open_file)); + ui->open_touchstone->selectPort(0, ownKit.open_Sparam); + + ui->short_touchstone->setFile(QString::fromStdString(ownKit.short_file)); + ui->short_touchstone->selectPort(0, ownKit.short_Sparam); + + ui->load_touchstone->setFile(QString::fromStdString(ownKit.load_file)); + ui->load_touchstone->selectPort(0, ownKit.load_Sparam); + + ui->through_touchstone->setFile(QString::fromStdString(ownKit.through_file)); + ui->through_touchstone->selectPort(0, ownKit.through_Sparam1); + ui->through_touchstone->selectPort(1, ownKit.through_Sparam2); + + // Type + if (ownKit.open_measurements) { + ui->open_measurement->click(); + } else { + ui->open_coefficients->click(); + } + + if (ownKit.short_measurements) { + ui->short_measurement->click(); + } else { + ui->short_coefficients->click(); + } + + if (ownKit.load_measurements) { + ui->load_measurement->click(); + } else { + ui->load_coefficients->click(); + } + + if (ownKit.through_measurements) { + ui->through_measurement->click(); + } else { + ui->through_coefficients->click(); + } +} diff --git a/Software/PC_Application/Calibration/calkitdialog.h b/Software/PC_Application/Calibration/calkitdialog.h new file mode 100644 index 0000000..fc1b701 --- /dev/null +++ b/Software/PC_Application/Calibration/calkitdialog.h @@ -0,0 +1,33 @@ +#ifndef CALKITDIALOG_H +#define CALKITDIALOG_H + +#include +#include +#include +#include +#include "calkit.h" + +namespace Ui { +class CalkitDialog; +} + +class CalkitDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CalkitDialog(Calkit &c, QWidget *parent = nullptr); + ~CalkitDialog(); + +private: + void parseEntries(); + void updateEntries(); + Ui::CalkitDialog *ui; + + bool open_ok, short_ok, load_ok, through_ok; + + Calkit ownKit; + Calkit &editKit; +}; + +#endif // CALKITDIALOG_H diff --git a/Software/PC_Application/Calibration/calkitdialog.ui b/Software/PC_Application/Calibration/calkitdialog.ui new file mode 100644 index 0000000..c09e4c7 --- /dev/null +++ b/Software/PC_Application/Calibration/calkitdialog.ui @@ -0,0 +1,601 @@ + + + CalkitDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 1121 + 345 + + + + + 0 + 0 + + + + Calibration Kit Coefficients + + + false + + + false + + + + + + + + + + + 16 + + + + Open + + + Qt::AlignCenter + + + + + + + + + Coefficients + + + OpenType + + + + + + + Measurement file + + + OpenType + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + C0 [10<sup>-15</sup>F]: + + + + + + + + + + C1 [10<sup>-27</sup>F/Hz]: + + + + + + + + + + C2 [10<sup>-36</sup>F/Hz<sup>2</sup>]: + + + + + + + + + + C0 [10<sup>-45</sup>F/Hz<sup>3</sup>]: + + + + + + + + + + + + + Z0 [Ω]: + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + 16 + + + + Short + + + Qt::AlignCenter + + + + + + + + + Coefficients + + + ShortType + + + + + + + Measurement file + + + ShortType + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + <html><head/><body><p>L0 [10<span style=" vertical-align:super;">-12</span>F]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L1 [10<span style=" vertical-align:super;">-24</span>F/Hz]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L2 [10<span style=" vertical-align:super;">-33</span>F/Hz<span style=" vertical-align:super;">2</span>]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L3 [10<span style=" vertical-align:super;">-42</span>F/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> + + + + + + + + + + + + + Z0 [Ω]: + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + 16 + + + + Load + + + Qt::AlignCenter + + + + + + + + + Coefficients + + + LoadType + + + + + + + Measurement file + + + LoadType + + + + + + + + + 0 + + + + + + + + + Z0 [Ω]: + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + 16 + + + + Through + + + Qt::AlignCenter + + + + + + + + + Coefficients + + + ThroughType + + + + + + + Measurement file + + + ThroughType + + + + + + + + + 0 + + + + + + + + + Z0 [Ω]: + + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::Open|QDialogButtonBox::Save + + + false + + + + + + + + TouchstoneImport + QWidget +
CustomWidgets/touchstoneimport.h
+ 1 +
+
+ + open_Z0 + open_delay + open_loss + open_C0 + open_C1 + open_C2 + + + + + OpenType + buttonClicked(int) + open_stack + setCurrentIndex(int) + + + -1 + -1 + + + 141 + 187 + + + + + ShortType + buttonClicked(int) + short_stack + setCurrentIndex(int) + + + -1 + -1 + + + 420 + 187 + + + + + LoadType + buttonClicked(int) + load_stack + setCurrentIndex(int) + + + -1 + -1 + + + 699 + 187 + + + + + ThroughType + buttonClicked(int) + through_stack + setCurrentIndex(int) + + + -1 + -1 + + + 978 + 187 + + + + + + + + + + +
diff --git a/Software/PC_Application/Calibration/measurementmodel.cpp b/Software/PC_Application/Calibration/measurementmodel.cpp new file mode 100644 index 0000000..de31317 --- /dev/null +++ b/Software/PC_Application/Calibration/measurementmodel.cpp @@ -0,0 +1,88 @@ +#include "measurementmodel.h" +#include "../unit.h" +#include + +MeasurementModel::MeasurementModel(Calibration *cal, std::vector measurements) : + QAbstractTableModel(), + cal(cal), + measurements(measurements) +{ + +} + +int MeasurementModel::rowCount(const QModelIndex &) const +{ + return measurements.size(); +} + +int MeasurementModel::columnCount(const QModelIndex &) const +{ + return ColIndexLast; +} + +QVariant MeasurementModel::data(const QModelIndex &index, int role) const +{ + auto info = cal->getMeasurementInfo(measurements[index.row()]); + if(role == Qt::DisplayRole) { + switch(index.column()) { + case ColIndexName: + return info.name; + break; + case ColIndexDescription: + return info.prerequisites; + break; + case ColIndexData: + if(info.points > 0) { + QString data = QString::number(info.points); + data.append(" points from "); + data.append(Unit::ToString(info.fmin, "Hz", " kMG")); + data.append(" to "); + data.append(Unit::ToString(info.fmax, "Hz", " kMG")); + return data; + } else { + return "Not available"; + } + break; + case ColIndexDate: + return info.timestamp.toString("dd.MM.yyyy hh:mm:ss"); + break; + } + } else if(role == Qt::SizeHintRole) { + switch(index.column()) { + case ColIndexName: return 200; break; + case ColIndexDescription: return 500; break; + case ColIndexData: return 300; break; + case ColIndexDate: return 300; break; + case ColIndexStatusSymbol: return 150; break; + default: return QVariant(); break; + } + } + + return QVariant(); +} + +QVariant MeasurementModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch(section) { + case ColIndexName: return "Type"; break; + case ColIndexDescription: return "Prerequisites"; break; + case ColIndexData: return "Statistics"; break; + case ColIndexDate: return "Timestamp"; break; + case ColIndexStatusSymbol: return "Status"; break; + default: return QVariant(); break; + } + } else { + return QVariant(); + } +} + +void MeasurementModel::measurementUpdated(Calibration::Measurement m) +{ + // find correct index in vector + auto it = std::find(measurements.begin(), measurements.end(), m); + if(it != measurements.end()) { + int row = it - measurements.begin(); + emit dataChanged(index(row, 0), index(row, ColIndexLast - 1)); + } +} diff --git a/Software/PC_Application/Calibration/measurementmodel.h b/Software/PC_Application/Calibration/measurementmodel.h new file mode 100644 index 0000000..42a0a18 --- /dev/null +++ b/Software/PC_Application/Calibration/measurementmodel.h @@ -0,0 +1,36 @@ +#ifndef MEASUREMENTMODEL_H +#define MEASUREMENTMODEL_H + +#include +#include +#include +#include "calibration.h" + +class MeasurementModel : public QAbstractTableModel +{ + Q_OBJECT +public: + MeasurementModel(Calibration *cal, std::vector measurements); + + 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; + +public slots: + void measurementUpdated(Calibration::Measurement m); + +private: + enum { + ColIndexName, + ColIndexDescription, + ColIndexData, + ColIndexDate, + ColIndexStatusSymbol, + ColIndexLast + }; + Calibration *cal; + std::vector measurements; +}; + +#endif // MEASUREMENTMODEL_H diff --git a/Software/PC_Application/CustomWidgets/siunitedit.cpp b/Software/PC_Application/CustomWidgets/siunitedit.cpp new file mode 100644 index 0000000..2dfc32a --- /dev/null +++ b/Software/PC_Application/CustomWidgets/siunitedit.cpp @@ -0,0 +1,79 @@ +#include "siunitedit.h" + +#include +#include +#include +#include + +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); + setValidator(new QDoubleValidator); + connect(this, &QLineEdit::editingFinished, [this]() { + parseNewValue(1.0); + }); +} + +SIUnitEdit::SIUnitEdit(QWidget *parent) + : SIUnitEdit("", " ", 4, parent) +{ + +} + +void SIUnitEdit::setValue(double value) +{ + setValueQuiet(value); + emit valueChanged(value); +} + +bool SIUnitEdit::eventFilter(QObject *, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + int key = static_cast(event)->key(); + if(key == Qt::Key_Escape) { + // abort editing process and set old value + setValueQuiet(_value); + return true; + } + if(key == Qt::Key_Return) { + // use new value without prefix + parseNewValue(1.0); + return true; + } + auto mod = static_cast(event)->modifiers(); + if (!(mod & Qt::ShiftModifier)) { + key = tolower(key); + } + if(key <= 255 && prefixes.indexOf(key) >= 0) { + // a valid prefix key was pressed + parseNewValue(Unit::SIPrefixToFactor(key)); + return true; + } + } else if(event->type() == QEvent::FocusOut) { + if(!text().isEmpty()) { + parseNewValue(1.0); + } else { + setValueQuiet(_value); + } + } + return false; +} + +void SIUnitEdit::setValueQuiet(double value) +{ + _value = value; + clear(); + setPlaceholderText(Unit::ToString(value, unit, prefixes, precision)); + clearFocus(); +} + +void SIUnitEdit::parseNewValue(double factor) +{ + double v = text().toDouble() * factor; + setValue(v); +} diff --git a/Software/PC_Application/CustomWidgets/siunitedit.h b/Software/PC_Application/CustomWidgets/siunitedit.h new file mode 100644 index 0000000..2b4e62f --- /dev/null +++ b/Software/PC_Application/CustomWidgets/siunitedit.h @@ -0,0 +1,31 @@ +#ifndef SIUNITEDIT_H +#define SIUNITEDIT_H + +#include + +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); +protected: + bool eventFilter(QObject *obj, QEvent *event) override; +private: + void parseNewValue(double factor); + QString unit, prefixes; + int precision; + double _value; +}; + +#endif // SIUNITEDIT_H diff --git a/Software/PC_Application/CustomWidgets/tilewidget.cpp b/Software/PC_Application/CustomWidgets/tilewidget.cpp new file mode 100644 index 0000000..51e3ba4 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/tilewidget.cpp @@ -0,0 +1,141 @@ +#include "tilewidget.h" +#include "ui_tilewidget.h" +#include +#include "Traces/tracebodeplot.h" +#include "Traces/tracesmithchart.h" + +TileWidget::TileWidget(TraceModel &model, QWidget *parent) : + QWidget(parent), + ui(new Ui::TileWidget), + splitter(0), + isSplit(false), + parent(0), + child1(0), + child2(0), + hasContent(false), + content(0), + model(model) +{ + ui->setupUi(this); + auto layout = new QGridLayout; + layout->setContentsMargins(0,0,0,0); + ui->ContentPage->setLayout(layout); + ui->bClose->setVisible(false); +} + +TileWidget::~TileWidget() +{ + delete ui; +} + +void TileWidget::splitVertically() +{ + if(isSplit) { + return; + } + isSplit = true; + splitter = new QSplitter(Qt::Vertical); + split(); +} + +void TileWidget::splitHorizontally() +{ + if(isSplit) { + return; + } + isSplit = true; + splitter = new QSplitter(Qt::Horizontal); + split(); +} + +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); + } +} + +TileWidget::TileWidget(TraceModel &model, TileWidget &parent) + : TileWidget(model) +{ + this->parent = &parent; + ui->bClose->setVisible(true); +} + +void TileWidget::split() +{ + splitter->setHandleWidth(0); + child1 = new TileWidget(model, *this); + child2 = new TileWidget(model, *this); + splitter->addWidget(child1); + splitter->addWidget(child2); + ui->ContentPage->layout()->addWidget(splitter); + ui->stack->setCurrentWidget(ui->ContentPage); +} + +void TileWidget::setContent(TracePlot *plot) +{ + content = plot; + hasContent = true; + ui->ContentPage->layout()->addWidget(plot); + ui->stack->setCurrentWidget(ui->ContentPage); + connect(content, &TracePlot::deleted, this, &TileWidget::traceDeleted); +} + +void TileWidget::on_bSmithchart_clicked() +{ + setContent(new TraceSmithChart(model)); +} + +void TileWidget::on_bBodeplot_clicked() +{ + setContent(new TraceBodePlot(model)); +} + +void TileWidget::traceDeleted(TracePlot *) +{ + ui->stack->setCurrentWidget(ui->TilePage); + hasContent = false; + content = nullptr; +} diff --git a/Software/PC_Application/CustomWidgets/tilewidget.h b/Software/PC_Application/CustomWidgets/tilewidget.h new file mode 100644 index 0000000..a896e3b --- /dev/null +++ b/Software/PC_Application/CustomWidgets/tilewidget.h @@ -0,0 +1,49 @@ +#ifndef TILEWIDGET_H +#define TILEWIDGET_H + +#include +#include "Traces/traceplot.h" +#include +#include "Traces/tracemodel.h" + +namespace Ui { +class TileWidget; +} + +class TileWidget : public QWidget +{ + Q_OBJECT + +public: + explicit TileWidget(TraceModel &model, QWidget *parent = nullptr); + ~TileWidget(); + + TileWidget *Child1() { return child1; }; + TileWidget *Child2() { return child2; }; +public slots: + void splitVertically(); + void splitHorizontally(); + void closeTile(); + void setPlot(TracePlot *plot); + +private slots: + void on_bSmithchart_clicked(); + void on_bBodeplot_clicked(); + void traceDeleted(TracePlot *t); + +private: + TileWidget(TraceModel &model, TileWidget &parent); + void split(); + void setContent(TracePlot *plot); + void setChild(); + Ui::TileWidget *ui; + QSplitter *splitter; + bool isSplit; + TileWidget *parent; + TileWidget *child1, *child2; + bool hasContent; + TracePlot *content; + TraceModel &model; +}; + +#endif // TILEWIDGET_H diff --git a/Software/PC_Application/CustomWidgets/tilewidget.ui b/Software/PC_Application/CustomWidgets/tilewidget.ui new file mode 100644 index 0000000..ce2f51c --- /dev/null +++ b/Software/PC_Application/CustomWidgets/tilewidget.ui @@ -0,0 +1,266 @@ + + + TileWidget + + + + 0 + 0 + 465 + 350 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Plots + + + + + + Smithchart + + + + + + + Bodeplot + + + + + + + + + + Split Tile + + + + + + Vertical + + + + :/icons/vertical.svg:/icons/vertical.svg + + + + + + + Horizontal + + + + :/icons/horizontal.svg:/icons/horizontal.svg + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Close Tile + + + + :/icons/close.svg:/icons/close.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + bClose + clicked() + TileWidget + closeTile() + + + 199 + 74 + + + 199 + 149 + + + + + bSplitH + clicked() + TileWidget + splitHorizontally() + + + 199 + 224 + + + 199 + 149 + + + + + bSplitV + clicked() + TileWidget + splitVertically() + + + 199 + 149 + + + 199 + 149 + + + + + + splitVertically() + splitHorizontally() + closeTile() + + diff --git a/Software/PC_Application/CustomWidgets/toggleswitch.cpp b/Software/PC_Application/CustomWidgets/toggleswitch.cpp new file mode 100644 index 0000000..d524c04 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/toggleswitch.cpp @@ -0,0 +1,66 @@ +#include "toggleswitch.h" + +#include +#include + +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); +} diff --git a/Software/PC_Application/CustomWidgets/toggleswitch.h b/Software/PC_Application/CustomWidgets/toggleswitch.h new file mode 100644 index 0000000..685f018 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/toggleswitch.h @@ -0,0 +1,28 @@ +#ifndef TOGGLESWITCH_H +#define TOGGLESWITCH_H + +#include + +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 diff --git a/Software/PC_Application/CustomWidgets/touchstoneimport.cpp b/Software/PC_Application/CustomWidgets/touchstoneimport.cpp new file mode 100644 index 0000000..c9d35b9 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/touchstoneimport.cpp @@ -0,0 +1,185 @@ +#include "touchstoneimport.h" +#include "ui_touchstoneimport.h" +#include +#include +#include + +using namespace std; + +TouchstoneImport::TouchstoneImport(QWidget *parent, int ports) : + QWidget(parent), + ui(new Ui::TouchstoneImport), + touchstone(ports), + status(false) +{ + ui->setupUi(this); + 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(&QButtonGroup::buttonClicked), [=](int id) { + preventCollisionWithGroup(ui->port2Group, id); + }); + connect(ui->port2Group, qOverload(&QButtonGroup::buttonClicked), [=](int id) { + preventCollisionWithGroup(ui->port1Group, id); + }); + 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 TouchstoneImport::getPorts() +{ + vector 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(); +} + +void TouchstoneImport::on_browse_clicked() +{ + auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", "", "Touchstone files (*.s1p *.s2p *.s3p *.s4p)", nullptr, QFileDialog::DontUseNativeDialog); + if (filename.length() > 0) { + ui->file->setText(filename); + evaluateFile(); + } +} + +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->setText(QString::number(touchstone.minFreq())); + ui->upperFreq->setText(QString::number(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;ibutton(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; + } + } + } +} diff --git a/Software/PC_Application/CustomWidgets/touchstoneimport.h b/Software/PC_Application/CustomWidgets/touchstoneimport.h new file mode 100644 index 0000000..9b0bfa9 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/touchstoneimport.h @@ -0,0 +1,46 @@ +#ifndef TOUCHSTONEIMPORT_H +#define TOUCHSTONEIMPORT_H + +#include +#include "touchstone.h" +#include + +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 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 diff --git a/Software/PC_Application/CustomWidgets/touchstoneimport.ui b/Software/PC_Application/CustomWidgets/touchstoneimport.ui new file mode 100644 index 0000000..c8331a2 --- /dev/null +++ b/Software/PC_Application/CustomWidgets/touchstoneimport.ui @@ -0,0 +1,312 @@ + + + TouchstoneImport + + + + 0 + 0 + 228 + 227 + + + + Form + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + 20 + 16777215 + + + + ... + + + + + + + + + + + + + + 239 + 41 + 41 + + + + + + + + + 239 + 41 + 41 + + + + + + + + + 190 + 190 + 190 + + + + + + + + + + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Port 1: + + + + + + + false + + + 1 + + + port1Group + + + + + + + false + + + 2 + + + port1Group + + + + + + + false + + + 3 + + + port1Group + + + + + + + false + + + 4 + + + port1Group + + + + + + + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Port 2: + + + + + + + false + + + 1 + + + port2Group + + + + + + + false + + + 2 + + + port2Group + + + + + + + false + + + 3 + + + port2Group + + + + + + + false + + + 4 + + + port2Group + + + + + + + + + + + + + + Points: + + + + + + + false + + + + + + + Lower Frequency: + + + + + + + false + + + + + + + Upper Frequency: + + + + + + + false + + + + + + + + + + + + + + diff --git a/Software/PC_Application/Device/device.cpp b/Software/PC_Application/Device/device.cpp new file mode 100644 index 0000000..f360fcd --- /dev/null +++ b/Software/PC_Application/Device/device.cpp @@ -0,0 +1,408 @@ +#include "device.h" + +#include +#include +#include +#include +#include + +using namespace std; + +Device::Device(QString serial) +{ + qDebug() << "Starting device connection..."; + + m_handle = nullptr; + libusb_init(&m_context); + + 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); + + if(!m_handle) { + QString message = "No device found"; + auto msg = new QMessageBox(QMessageBox::Icon::Warning, "Error opening device", message); + msg->exec(); + libusb_exit(m_context); + throw std::runtime_error(message.toStdString()); + return; + } + + // Found the correct device, now connect + /* claim the interfaces */ + for (int if_num = 0; if_num < 1; if_num++) { + int ret = libusb_claim_interface(m_handle, if_num); + 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; + auto msg = new QMessageBox(QMessageBox::Icon::Warning, "Error opening device", message); + msg->exec(); + libusb_exit(m_context); + throw std::runtime_error(message.toStdString()); + } + } + qInfo() << "USB connection established" << flush; + m_connected = true; + m_receiveThread = new std::thread(&Device::USBHandleThread, this); + dataBuffer = new USBInBuffer(m_handle, EP_Data_In_Addr, 2048); + logBuffer = new USBInBuffer(m_handle, EP_Log_In_Addr, 2048); + connect(dataBuffer, &USBInBuffer::DataReceived, this, &Device::ReceivedData, Qt::DirectConnection); + connect(dataBuffer, &USBInBuffer::TransferError, this, &Device::ConnectionLost); + connect(logBuffer, &USBInBuffer::DataReceived, this, &Device::ReceivedLog, Qt::DirectConnection); +} + +Device::~Device() +{ + if(m_connected) { + delete dataBuffer; + delete logBuffer; + m_connected = false; + for (int if_num = 0; if_num < 1; if_num++) { + int ret = libusb_release_interface(m_handle, if_num); + if (ret < 0) { + qCritical() << "Error releasing interface" << libusb_error_name(ret); + } + } + libusb_close(m_handle); + m_receiveThread->join(); + libusb_exit(m_context); + } +} + +bool Device::Configure(Protocol::SweepSettings settings) +{ + if(m_connected) { + unsigned char buffer[128]; + Protocol::PacketInfo p; + p.type = Protocol::PacketType::SweepSettings; + p.settings = settings; + unsigned int length = Protocol::EncodePacket(p, buffer, sizeof(buffer)); + if(!length) { + qCritical() << "Failed to encode packet"; + return false; + } + int actual_length; + auto ret = libusb_bulk_transfer(m_handle, EP_Data_Out_Addr, buffer, length, &actual_length, 0); + if(ret < 0) { + qCritical() << "Error sending data: " + << libusb_strerror((libusb_error) ret); + return false; + } + return true; + } else { + return false; + } +} + +bool Device::SetManual(Protocol::ManualControl manual) +{ + if(m_connected) { + unsigned char buffer[128]; + Protocol::PacketInfo p; + p.type = Protocol::PacketType::ManualControl; + p.manual = manual; + unsigned int length = Protocol::EncodePacket(p, buffer, sizeof(buffer)); + if(!length) { + qCritical() << "Failed to encode packet"; + return false; + } + int actual_length; + auto ret = libusb_bulk_transfer(m_handle, EP_Data_Out_Addr, buffer, length, &actual_length, 0); + if(ret < 0) { + qCritical() << "Error sending data: " + << libusb_strerror((libusb_error) ret); + return false; + } + return true; + } else { + return false; + } +} + +bool Device::SendFirmwareChunk(Protocol::FirmwarePacket &fw) +{ + if(m_connected) { + unsigned char buffer[Protocol::FirmwareChunkSize + 4 + 8]; + Protocol::PacketInfo p; + p.type = Protocol::PacketType::FirmwarePacket; + p.firmware = fw; + unsigned int length = Protocol::EncodePacket(p, buffer, sizeof(buffer)); + if(!length) { + qCritical() << "Failed to encode packet"; + return false; + } + int actual_length; + auto ret = libusb_bulk_transfer(m_handle, EP_Data_Out_Addr, buffer, length, &actual_length, 0); + if(ret < 0) { + qCritical() << "Error sending data: " + << libusb_strerror((libusb_error) ret); + return false; + } + return true; + } else { + return false; + } +} + +std::vector Device::GetDevices() +{ + std::vector serials; + + libusb_context *ctx; + libusb_init(&ctx); + + SearchDevices([&serials](libusb_device_handle *, QString serial) -> bool { + serials.push_back(serial); + return true; + }, ctx); + + libusb_exit(ctx); + + return serials; +} + +void Device::USBHandleThread() +{ + qInfo() << "Receive thread started" << flush; + while (m_connected) { + libusb_handle_events(m_context); + } + qDebug() << "Disconnected, receive thread exiting"; +} + +void Device::SearchDevices(std::function foundCallback, libusb_context *context) +{ + 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; + } + + if (desc.idVendor != VID || desc.idProduct != PID) { + /* Not the correct IDs */ + continue; + } + + /* Try to open the device */ + libusb_device_handle *handle = nullptr; + ret = libusb_open(device, &handle); + if (ret) { + /* Failed to open */ + 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 it could be a missing driver. Try installing the WinUSB driver using Zadig (https://zadig.akeo.ie/)"); + qWarning() << message; + auto msg = new QMessageBox(QMessageBox::Icon::Warning, "Error opening device", message); + msg->exec(); + 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); + qDebug() << "Opened device: " << product; + if (product == "VNA") { + // 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); +} + +Protocol::DeviceInfo Device::getLastInfo() const +{ + return lastInfo; +} + +QString Device::getLastDeviceInfoString() +{ + QString ret; + if(!lastInfoValid) { + ret.append("No device information available yet"); + } else { + ret.append("HW Rev."); + ret.append(lastInfo.HW_Revision); + ret.append(" FW "+QString::number(lastInfo.FW_major)+"."+QString::number(lastInfo.FW_minor).rightJustified(2, '0')); + ret.append(" Temps: "+QString::number(lastInfo.temperatures.source)+"°C/"+QString::number(lastInfo.temperatures.LO1)+"°C/"+QString::number(lastInfo.temperatures.MCU)+"°C"); + ret.append(" Reference:"); + if(lastInfo.extRefInUse) { + ret.append("External"); + } else { + ret.append("Internal"); + if(lastInfo.extRefAvailable) { + ret.append(" (External available)"); + } + } + } + return ret; +} + +void Device::ReceivedData() +{ + Protocol::PacketInfo packet; + uint16_t handled_len; + do { + handled_len = Protocol::DecodeBuffer(dataBuffer->getBuffer(), dataBuffer->getReceived(), &packet); + dataBuffer->removeBytes(handled_len); + if(packet.type == Protocol::PacketType::Datapoint) { + emit DatapointReceived(packet.datapoint); + } else if(packet.type == Protocol::PacketType::Status) { + qDebug() << "Got status"; + emit ManualStatusReceived(packet.status); + } else if(packet.type == Protocol::PacketType::DeviceInfo) { + lastInfo = packet.info; + lastInfoValid = true; + emit DeviceInfoUpdated(); + } else if(packet.type == Protocol::PacketType::Ack) { + emit AckReceived(); + } + } while (handled_len > 0); +} + +void Device::ReceivedLog() +{ + uint16_t handled_len; + do { + handled_len = 0; + auto firstLinebreak = (uint8_t*) memchr(logBuffer->getBuffer(), '\n', logBuffer->getReceived()); + if(firstLinebreak) { + handled_len = firstLinebreak - logBuffer->getBuffer(); + auto line = QString::fromLatin1((const char*) logBuffer->getBuffer(), handled_len - 1); + emit LogLineReceived(line); + logBuffer->removeBytes(handled_len + 1); + } + } while(handled_len > 0); +} + +QString Device::serial() const +{ + return m_serial; +} + +USBInBuffer::USBInBuffer(libusb_device_handle *handle, unsigned char endpoint, int buffer_size) : + buffer_size(buffer_size), + received_size(0), + inCallback(false) +{ + buffer = new unsigned char[buffer_size]; + transfer = libusb_alloc_transfer(0); + libusb_fill_bulk_transfer(transfer, handle, endpoint, buffer, 64, CallbackTrampoline, this, 100); + libusb_submit_transfer(transfer); +} + +USBInBuffer::~USBInBuffer() +{ + if(transfer) { + qDebug() << "Start cancellation"; + libusb_cancel_transfer(transfer); + // wait for cancellation to complete + mutex mtx; + unique_lock lck(mtx); + cv.wait(lck); + qDebug() << "Cancellation complete"; + } + delete buffer; +} + +void USBInBuffer::removeBytes(int handled_bytes) +{ + if(!inCallback) { + throw runtime_error("Removing of bytes is only allowed from within receive callback"); + } + if(handled_bytes >= received_size) { + received_size = 0; + } else { + // not removing all bytes, have to move remaining data to the beginning of the buffer + memmove(buffer, &buffer[handled_bytes], received_size - handled_bytes); + received_size -= handled_bytes; + } +} + +int USBInBuffer::getReceived() const +{ + return received_size; +} + +void USBInBuffer::Callback(libusb_transfer *transfer) +{ + switch(transfer->status) { + case LIBUSB_TRANSFER_COMPLETED: + received_size += transfer->actual_length; + inCallback = true; + emit DataReceived(); + inCallback = false; + break; + case LIBUSB_TRANSFER_ERROR: + case LIBUSB_TRANSFER_NO_DEVICE: + case LIBUSB_TRANSFER_OVERFLOW: + case LIBUSB_TRANSFER_STALL: + qCritical() << "LIBUSB_TRANSFER_ERROR"; + libusb_free_transfer(transfer); + this->transfer = nullptr; + emit TransferError(); + return; + break; + case LIBUSB_TRANSFER_TIMED_OUT: + // nothing to do + break; + case LIBUSB_TRANSFER_CANCELLED: + // destructor called, do not resubmit + libusb_free_transfer(transfer); + this->transfer = nullptr; + cv.notify_all(); + return; + break; + } + // Resubmit the transfer + transfer->buffer = &buffer[received_size]; + libusb_submit_transfer(transfer); +} + +void USBInBuffer::CallbackTrampoline(libusb_transfer *transfer) +{ + auto usb = (USBInBuffer*) transfer->user_data; + usb->Callback(transfer); +} + +uint8_t *USBInBuffer::getBuffer() const +{ + return buffer; +} + diff --git a/Software/PC_Application/Device/device.h b/Software/PC_Application/Device/device.h new file mode 100644 index 0000000..2037cfe --- /dev/null +++ b/Software/PC_Application/Device/device.h @@ -0,0 +1,93 @@ +#ifndef DEVICE_H +#define DEVICE_H + +#include "../VNA_embedded/Application/Communication/Protocol.hpp" +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(Protocol::Datapoint); +Q_DECLARE_METATYPE(Protocol::ManualStatus); +Q_DECLARE_METATYPE(Protocol::DeviceInfo); + +class USBInBuffer : public QObject { + Q_OBJECT; +public: + USBInBuffer(libusb_device_handle *handle, unsigned char endpoint, int buffer_size); + ~USBInBuffer(); + + void removeBytes(int handled_bytes); + int getReceived() const; + uint8_t *getBuffer() const; + +signals: + void DataReceived(); + void TransferError(); + +private: + void Callback(libusb_transfer *transfer); + static void LIBUSB_CALL CallbackTrampoline(libusb_transfer *transfer); + libusb_transfer *transfer; + unsigned char *buffer; + int buffer_size; + int received_size; + bool inCallback; + std::condition_variable cv; +}; + + +class Device : public QObject +{ + Q_OBJECT +public: + // connect to a VNA device. If serial is specified only connecting to this device, otherwise to the first one found + Device(QString serial = QString()); + ~Device(); + bool Configure(Protocol::SweepSettings settings); + bool SetManual(Protocol::ManualControl manual); + bool SendFirmwareChunk(Protocol::FirmwarePacket &fw); + // Returns serial numbers of all connected devices + static std::vector GetDevices(); + QString serial() const; + Protocol::DeviceInfo getLastInfo() const; + QString getLastDeviceInfoString(); + +signals: + void DatapointReceived(Protocol::Datapoint); + void ManualStatusReceived(Protocol::ManualStatus); + void DeviceInfoUpdated(); + void ConnectionLost(); + void AckReceived(); + void LogLineReceived(QString line); +private slots: + void ReceivedData(); + void ReceivedLog(); + +private: + static constexpr int VID = 0x0483; + static constexpr int PID = 0x564e; + static constexpr int EP_Data_Out_Addr = 0x01; + static constexpr int EP_Data_In_Addr = 0x81; + static constexpr int EP_Log_In_Addr = 0x82; + + void USBHandleThread(); + // foundCallback is called for every device that is found. If it returns true the search continues, otherwise it is aborted. + // When the search is aborted the last found device is still opened + static void SearchDevices(std::function foundCallback, libusb_context *context); + + libusb_device_handle *m_handle; + libusb_context *m_context; + USBInBuffer *dataBuffer; + USBInBuffer *logBuffer; + + + QString m_serial; + bool m_connected; + std::thread *m_receiveThread; + Protocol::DeviceInfo lastInfo; + bool lastInfoValid; +}; + +#endif // DEVICE_H diff --git a/Software/PC_Application/Device/devicelog.cpp b/Software/PC_Application/Device/devicelog.cpp new file mode 100644 index 0000000..e952756 --- /dev/null +++ b/Software/PC_Application/Device/devicelog.cpp @@ -0,0 +1,61 @@ +#include "devicelog.h" +#include "ui_devicelog.h" +#include +#include +#include + +using namespace std; + +DeviceLog::DeviceLog(QWidget *parent) : + QWidget(parent), + ui(new Ui::DeviceLog) +{ + ui->setupUi(this); + connect(ui->bClear, &QPushButton::clicked, this, &DeviceLog::clear); +} + +DeviceLog::~DeviceLog() +{ + delete ui; +} + +void DeviceLog::addLine(QString line) +{ + // Set color depending on log level + QColor color = Qt::black; + if(line.contains(",CRT]")) { + color = Qt::red; + } else if(line.contains(",ERR]")) { + color = QColor("orange"); + } else if(line.contains(",WRN]")) { + color = Qt::darkYellow; + } else if(line.contains(",DBG")) { + color = Qt::gray; + } + QTextCharFormat tf; + tf = ui->text->currentCharFormat(); + tf.setForeground(QBrush(color)); + ui->text->setCurrentCharFormat(tf); + ui->text->appendPlainText(line); + if(ui->cbAutoscroll->isChecked()) { + QScrollBar *sb = ui->text->verticalScrollBar(); + sb->setValue(sb->maximum()); + } +} + +void DeviceLog::clear() +{ + ui->text->clear(); +} + +void DeviceLog::on_bToFile_clicked() +{ + auto filename = QFileDialog::getSaveFileName(this, "Select file for device log", "", "", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.length() > 0) { + // create file + ofstream file; + file.open(filename.toStdString()); + file << ui->text->toPlainText().toStdString(); + file.close(); + } +} diff --git a/Software/PC_Application/Device/devicelog.h b/Software/PC_Application/Device/devicelog.h new file mode 100644 index 0000000..fba689f --- /dev/null +++ b/Software/PC_Application/Device/devicelog.h @@ -0,0 +1,29 @@ +#ifndef DEVICELOG_H +#define DEVICELOG_H + +#include + +namespace Ui { +class DeviceLog; +} + +class DeviceLog : public QWidget +{ + Q_OBJECT + +public: + explicit DeviceLog(QWidget *parent = nullptr); + ~DeviceLog(); + +public slots: + void addLine(QString line); + void clear(); + +private slots: + void on_bToFile_clicked(); + +private: + Ui::DeviceLog *ui; +}; + +#endif // DEVICELOG_H diff --git a/Software/PC_Application/Device/devicelog.ui b/Software/PC_Application/Device/devicelog.ui new file mode 100644 index 0000000..e0997b0 --- /dev/null +++ b/Software/PC_Application/Device/devicelog.ui @@ -0,0 +1,72 @@ + + + DeviceLog + + + + 0 + 0 + 810 + 211 + + + + Form + + + + + + true + + + + + + + + + To File + + + + + + + + + + Clear + + + + + + + + + + Autoscroll + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/Software/PC_Application/Device/firmwareupdatedialog.cpp b/Software/PC_Application/Device/firmwareupdatedialog.cpp new file mode 100644 index 0000000..1dbbbb6 --- /dev/null +++ b/Software/PC_Application/Device/firmwareupdatedialog.cpp @@ -0,0 +1,127 @@ +#include "firmwareupdatedialog.h" +#include "ui_firmwareupdatedialog.h" +#include + +FirmwareUpdateDialog::FirmwareUpdateDialog(Device &dev, QWidget *parent) : + QDialog(parent), + ui(new Ui::FirmwareUpdateDialog), + dev(dev), + file(), + timer(), + state(State::Idle) +{ + ui->setupUi(this); + ui->bFile->setIcon(this->style()->standardPixmap(QStyle::SP_FileDialogStart)); + ui->bStart->setIcon(this->style()->standardPixmap(QStyle::SP_MediaPlay)); + timer.setSingleShot(true); + connect(&timer, &QTimer::timeout, [=](){ + abortWithError("Response timed out"); + }); +} + +FirmwareUpdateDialog::~FirmwareUpdateDialog() +{ + delete ui; +} + +void FirmwareUpdateDialog::on_bFile_clicked() +{ + ui->bStart->setEnabled(false); + auto filename = QFileDialog::getOpenFileName(nullptr, "Open firmware file", "", "Firmware file (*.vnafw)", nullptr, QFileDialog::DontUseNativeDialog); + if (filename.length() > 0) { + ui->lFile->setText(filename); + if(file) { + delete file; + } + file = new QFile(filename); + ui->bStart->setEnabled(true); + } +} + +void FirmwareUpdateDialog::on_bStart_clicked() +{ + ui->status->clear(); + ui->bStart->setEnabled(false); + if(!file->isOpen()) { + if(!file->open(QIODevice::ReadOnly)) { + abortWithError("Unable to open file"); + return; + } + } + file->seek(0); + addStatus("Evaluating file..."); + if(file->size() % Protocol::FirmwareChunkSize != 0) { + abortWithError("Invalid file size"); + return; + } + char header[24]; + file->read(header, sizeof(header)); + if(strncmp(header, "VNA!", 4)) { + abortWithError("Invalid magic header constant"); + return; + } + state = State::ErasingFLASH; + addStatus("Erasing device memory..."); + // TODO issue write command + timer.start(10000); +} + +void FirmwareUpdateDialog::addStatus(QString line) +{ + ui->status->appendPlainText(line); +} + +void FirmwareUpdateDialog::abortWithError(QString error) +{ + QTextCharFormat tf; + tf = ui->status->currentCharFormat(); + tf.setForeground(QBrush(Qt::red)); + ui->status->setCurrentCharFormat(tf); + ui->status->appendPlainText(error); + tf.setForeground(QBrush(Qt::black)); + ui->status->setCurrentCharFormat(tf); + ui->bStart->setEnabled(true); + state = State::Idle; +} + +void FirmwareUpdateDialog::receivedAck() +{ + switch(state) { + case State::Idle: + // no firmware update in progress, ignore + break; + case State::ErasingFLASH: + // FLASH erased, begin transferring firmware + state = State::TransferringData; + transferredBytes = 0; + addStatus("Transferring firmware..."); + sendNextFirmwareChunk(); + timer.start(1000); + break; + case State::TransferringData: + transferredBytes += Protocol::FirmwareChunkSize; + ui->progress->setValue(100 * transferredBytes / file->size()); + if(transferredBytes >= file->size()) { + // complete file transferred + addStatus("Triggering device update..."); + state = State::TriggeringUpdate; + // TODO trigger update + timer.start(5000); + } + sendNextFirmwareChunk(); + timer.start(1000); + break; + case State::TriggeringUpdate: + addStatus("Rebooting device..."); + // TODO listen for detected device + state = State::Idle; + break; + } +} + +void FirmwareUpdateDialog::sendNextFirmwareChunk() +{ + Protocol::FirmwarePacket fw; + fw.address = transferredBytes; + file->read((char*) &fw.data, Protocol::FirmwareChunkSize); +} diff --git a/Software/PC_Application/Device/firmwareupdatedialog.h b/Software/PC_Application/Device/firmwareupdatedialog.h new file mode 100644 index 0000000..9016a85 --- /dev/null +++ b/Software/PC_Application/Device/firmwareupdatedialog.h @@ -0,0 +1,46 @@ +#ifndef FIRMWAREUPDATEDIALOG_H +#define FIRMWAREUPDATEDIALOG_H + +#include +#include "device.h" +#include +#include + +namespace Ui { +class FirmwareUpdateDialog; +} + +class FirmwareUpdateDialog : public QDialog +{ + Q_OBJECT + +public: + explicit FirmwareUpdateDialog(Device &dev, QWidget *parent = nullptr); + ~FirmwareUpdateDialog(); + +private slots: + void on_bFile_clicked(); + + void on_bStart_clicked(); + +private: + void addStatus(QString line); + void abortWithError(QString error); + void receivedAck(); + void sendNextFirmwareChunk(); + Ui::FirmwareUpdateDialog *ui; + Device &dev; + QFile *file; + QTimer timer; + + enum class State { + Idle, + ErasingFLASH, + TransferringData, + TriggeringUpdate, + }; + State state; + unsigned int transferredBytes; +}; + +#endif // FIRMWAREUPDATEDIALOG_H diff --git a/Software/PC_Application/Device/firmwareupdatedialog.ui b/Software/PC_Application/Device/firmwareupdatedialog.ui new file mode 100644 index 0000000..1ae910d --- /dev/null +++ b/Software/PC_Application/Device/firmwareupdatedialog.ui @@ -0,0 +1,105 @@ + + + FirmwareUpdateDialog + + + + 0 + 0 + 520 + 327 + + + + Firmware Update + + + + + + + + File: + + + + + + + true + + + + + + + + 0 + 0 + + + + + 30 + 16777215 + + + + + + + true + + + + + + + + + true + + + true + + + + + + + 0 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Start + + + + + + + + + + diff --git a/Software/PC_Application/Device/manualcontroldialog.cpp b/Software/PC_Application/Device/manualcontroldialog.cpp new file mode 100644 index 0000000..3b1eca2 --- /dev/null +++ b/Software/PC_Application/Device/manualcontroldialog.cpp @@ -0,0 +1,254 @@ +#include "manualcontroldialog.h" +#include "ui_manualcontroldialog.h" +#include +#include +#include +#include + +using namespace std; + +ManualControlDialog::ManualControlDialog(Device &dev, QWidget *parent) : + QDialog(parent), + ui(new Ui::ManualControlDialog), + dev(dev) +{ + ui->setupUi(this); + + ui->SourceLowFrequency->setUnit("Hz"); + ui->SourceLowFrequency->setPrefixes(" kM"); + ui->SourceLowFrequency->setPrecision(6); + ui->SourceLowFrequency->setValueQuiet(1000000); + + ui->SourceHighFrequency->setUnit("Hz"); + ui->SourceHighFrequency->setPrefixes(" kMG"); + ui->SourceHighFrequency->setPrecision(6); + ui->SourceHighFrequency->setValueQuiet(1000000000); + + ui->IF1->setUnit("Hz"); + ui->IF1->setPrefixes(" kM"); + ui->IF1->setPrecision(6); + + ui->LO1Frequency->setUnit("Hz"); + ui->LO1Frequency->setPrefixes(" kMG"); + ui->LO1Frequency->setPrecision(6); + + ui->IF2->setUnit("Hz"); + ui->IF2->setPrefixes(" kM"); + ui->IF2->setPrecision(6); + + ui->LO2Frequency->setUnit("Hz"); + ui->LO2Frequency->setPrefixes(" kM"); + ui->LO2Frequency->setPrecision(6); + + auto UpdateLO1 = [=]() { + double sourceFreq; + if (ui->SwitchLowband->isChecked()) { + sourceFreq = ui->SourceLowFrequency->value(); + } else { + sourceFreq = ui->SourceHighFrequency->value(); + } + if (ui->LO1FreqType->currentIndex() == 0) { + // fixed IF mode + ui->LO1Frequency->setValueQuiet(sourceFreq + ui->IF1->value()); + } else { + // Manual Frequency mode + ui->IF1->setValueQuiet(ui->LO1Frequency->value() - sourceFreq); + } + }; + auto UpdateLO2 = [=]() { + double IF1 = ui->IF1->value(); + if (ui->LO2FreqType->currentIndex() == 0) { + // fixed IF mode + ui->LO2Frequency->setValueQuiet(IF1 + ui->IF2->value()); + } else { + // Manual Frequency mode + ui->IF2->setValueQuiet(ui->LO2Frequency->value() - IF1); + } + }; + + connect(ui->IF1, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO1(); + UpdateLO2(); + }); + connect(ui->LO1Frequency, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO1(); + UpdateLO2(); + }); + connect(ui->IF2, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO2(); + }); + connect(ui->LO2Frequency, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO2(); + }); + connect(ui->SourceSwitchGroup, qOverload(&QButtonGroup::buttonToggled), [=](int, bool) { + UpdateLO1(); + UpdateLO2(); + }); + connect(ui->SourceLowFrequency, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO1(); + UpdateLO2(); + }); + connect(ui->SourceHighFrequency, &SIUnitEdit::valueChanged, [=](double) { + UpdateLO1(); + UpdateLO2(); + }); + + ui->IF1->setValue(60000000); + ui->IF2->setValue(250000); + + // LO1/LO2 mode switch connections + connect(ui->LO1FreqType, qOverload(&QComboBox::activated), [=](int index) { + switch(index) { + case 0: + ui->LO1Frequency->setEnabled(false); + ui->IF1->setEnabled(true); + break; + case 1: + ui->LO1Frequency->setEnabled(true); + ui->IF1->setEnabled(false); + break; + } + }); + connect(ui->LO2FreqType, qOverload(&QComboBox::activated), [=](int index) { + switch(index) { + case 0: + ui->LO2Frequency->setEnabled(false); + ui->IF2->setEnabled(true); + break; + case 1: + ui->LO2Frequency->setEnabled(true); + ui->IF2->setEnabled(false); + break; + } + }); + + // Readonly widgets + auto MakeReadOnly = [](QWidget* w) { + w->setAttribute(Qt::WA_TransparentForMouseEvents); + w->setFocusPolicy(Qt::NoFocus); + }; + MakeReadOnly(ui->SourceLocked); + MakeReadOnly(ui->LO1locked); + MakeReadOnly(ui->port1min); + MakeReadOnly(ui->port1max); + MakeReadOnly(ui->port1mag); + MakeReadOnly(ui->port1phase); + MakeReadOnly(ui->port1referenced); + MakeReadOnly(ui->port2min); + MakeReadOnly(ui->port2max); + MakeReadOnly(ui->port2mag); + MakeReadOnly(ui->port2phase); + MakeReadOnly(ui->port2referenced); + MakeReadOnly(ui->refmin); + MakeReadOnly(ui->refmax); + MakeReadOnly(ui->refmag); + MakeReadOnly(ui->refphase); + + qRegisterMetaType("Status"); + connect(&dev, &Device::ManualStatusReceived, this, &ManualControlDialog::NewStatus); + + connect(ui->SourceCE, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->SourceRFEN, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->LO1CE, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->LO1RFEN, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->SourceLowEnable, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->AmplifierEnable, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->LO2EN, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->Port1Enable, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->Port2Enable, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + connect(ui->RefEnable, &QCheckBox::toggled, [=](bool) { UpdateDevice(); }); + + connect(ui->SourceHighPower, qOverload(&QComboBox::activated), [=](int) { UpdateDevice(); }); + connect(ui->SourceLowpass, qOverload(&QComboBox::activated), [=](int) { UpdateDevice(); }); + connect(ui->SourceLowPower, qOverload(&QComboBox::activated), [=](int) { UpdateDevice(); }); + + connect(ui->SourceHighFrequency, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + connect(ui->SourceLowFrequency, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + connect(ui->LO1Frequency, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + connect(ui->IF1, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + connect(ui->LO2Frequency, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + connect(ui->IF2, &SIUnitEdit::valueChanged, [=](double) { UpdateDevice(); }); + + connect(ui->PortSwitchGroup, qOverload(&QButtonGroup::buttonToggled), [=](int, bool) { UpdateDevice(); }); + connect(ui->SourceSwitchGroup, qOverload(&QButtonGroup::buttonToggled), [=](int, bool) { UpdateDevice(); }); + + connect(ui->Attenuator, qOverload(&QDoubleSpinBox::valueChanged), [=](double) { UpdateDevice(); }); + connect(ui->Samples, qOverload(&QSpinBox::valueChanged), [=](double) { UpdateDevice(); }); + + UpdateDevice(); +} + +ManualControlDialog::~ManualControlDialog() +{ + delete ui; +} + +void ManualControlDialog::NewStatus(Protocol::ManualStatus status) +{ + // ADC values + ui->port1min->setText(QString::number(status.port1min)); + ui->port1max->setText(QString::number(status.port1max)); + auto port1 = complex(status.port1real, status.port1imag); + ui->port1mag->setText(QString::number(abs(port1))); + ui->port1phase->setText(QString::number(arg(port1)*180/M_PI)); + + ui->port2min->setText(QString::number(status.port2min)); + ui->port2max->setText(QString::number(status.port2max)); + auto port2 = complex(status.port2real, status.port2imag); + ui->port2mag->setText(QString::number(abs(port2))); + ui->port2phase->setText(QString::number(arg(port2)*180/M_PI)); + + ui->refmin->setText(QString::number(status.refmin)); + ui->refmax->setText(QString::number(status.refmax)); + auto ref = complex(status.refreal, status.refimag); + ui->refmag->setText(QString::number(abs(ref))); + ui->refphase->setText(QString::number(arg(ref)*180/M_PI)); + + auto port1referenced = port1 / ref; + auto port2referenced = port2 / ref; + auto port1db = 20*log10(abs(port1referenced)); + auto port2db = 20*log10(abs(port2referenced)); + + ui->port1referenced->setText(QString::number(port1db, 'f', 1) + "db@" + QString::number(arg(port1referenced)*180/M_PI, 'f', 0) + "°"); + ui->port2referenced->setText(QString::number(port2db, 'f', 1) + "db@" + QString::number(arg(port2referenced)*180/M_PI, 'f', 0) + "°"); + + // PLL state + ui->SourceLocked->setChecked(status.source_locked); + ui->LO1locked->setChecked(status.LO_locked); +} + +void ManualControlDialog::UpdateDevice() +{ + Protocol::ManualControl m; + // Source highband + m.SourceHighCE = ui->SourceCE->isChecked(); + m.SourceHighRFEN = ui->SourceRFEN->isChecked(); + m.SourceHighPower = ui->SourceHighPower->currentIndex(); + m.SourceHighFrequency = ui->SourceHighFrequency->value(); + m.SourceHighLowpass = ui->SourceLowpass->currentIndex(); + // Source lowband + m.SourceLowEN = ui->SourceLowEnable->isChecked(); + m.SourceLowPower = ui->SourceLowPower->currentIndex(); + m.SourceLowFrequency = ui->SourceLowFrequency->value(); + // Source signal path + m.SourceHighband = ui->SwitchHighband->isChecked(); + m.AmplifierEN = ui->AmplifierEnable->isChecked(); + m.PortSwitch = ui->Port2Switch->isChecked(); + m.attenuator = -ui->Attenuator->value() / 0.25; + // LO1 + m.LO1CE = ui->LO1CE->isChecked(); + m.LO1RFEN = ui->LO1RFEN->isChecked(); + m.LO1Frequency = ui->LO1Frequency->value(); + // LO2 + m.LO2EN = ui->LO2EN->isChecked(); + m.LO2Frequency = ui->LO2Frequency->value(); + // Acquisition + m.Port1EN = ui->Port1Enable->isChecked(); + m.Port2EN = ui->Port2Enable->isChecked(); + m.RefEN = ui->RefEnable->isChecked(); + m.Samples = ui->Samples->value(); + + qDebug() << "Updating manual control state"; + + dev.SetManual(m); +} diff --git a/Software/PC_Application/Device/manualcontroldialog.h b/Software/PC_Application/Device/manualcontroldialog.h new file mode 100644 index 0000000..ae756a2 --- /dev/null +++ b/Software/PC_Application/Device/manualcontroldialog.h @@ -0,0 +1,28 @@ +#ifndef MANUALCONTROLDIALOG_H +#define MANUALCONTROLDIALOG_H + +#include +#include "device.h" + +namespace Ui { +class ManualControlDialog; +} + +class ManualControlDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ManualControlDialog(Device &dev, QWidget *parent = nullptr); + ~ManualControlDialog(); + +public slots: + void NewStatus(Protocol::ManualStatus status); + +private: + void UpdateDevice(); + Ui::ManualControlDialog *ui; + Device &dev; +}; + +#endif // MANUALCONTROLDIALOG_H diff --git a/Software/PC_Application/Device/manualcontroldialog.ui b/Software/PC_Application/Device/manualcontroldialog.ui new file mode 100644 index 0000000..c3b0a27 --- /dev/null +++ b/Software/PC_Application/Device/manualcontroldialog.ui @@ -0,0 +1,751 @@ + + + ManualControlDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 1023 + 628 + + + + Manual System Control + + + + + + + + Signal Generation + + + + + + + + + + Highband Source + + + + + + Chip Enable + + + + + + + RF Enable + + + + + + + true + + + Locked + + + + + + + + + Power: + + + + + + + + -4dbm + + + + + -1dbm + + + + + +2dbm + + + + + +5dbm + + + + + + + + Frequency: + + + + + + + + + + Lowpass: + + + + + + + + 947MHz + + + + + 1880MHz + + + + + 3500MHz + + + + + None + + + + + + + + + + + + + + + Lowband Source + + + + + + + + Enable + + + + + + + + + Power: + + + + + + + + 2mA + + + + + 4mA + + + + + 6mA + + + + + 8mA + + + + + + + + Frequency: + + + + + + + + + + + + + + + + + Source Switch + + + + + + Lowband + + + SourceSwitchGroup + + + + + + + Highband + + + true + + + SourceSwitchGroup + + + + + + + + + + + + + + + + Attenuator + + + + + + db + + + -31.750000000000000 + + + 0.000000000000000 + + + 0.250000000000000 + + + + + + + + + + Amplifier + + + + + + Enable + + + false + + + + + + + + + + Port Switch + + + + + + Port 1 + + + true + + + PortSwitchGroup + + + + + + + Port 2 + + + PortSwitchGroup + + + + + + + + + + + + + + + + + Signal Analysis + + + + + + + + LO1 + + + + + + Chip Enable + + + + + + + RF Enable + + + + + + + true + + + Locked + + + + + + + + + Freq. Type: + + + + + + + + IF1 + + + + + Absolute + + + + + + + + Frequency: + + + + + + + false + + + + + + + IF1: + + + + + + + + + + + + + + + LO2 + + + + + + Enable + + + + + + + + + Freq. Type: + + + + + + + + IF2 + + + + + Absolute + + + + + + + + Frequency: + + + + + + + false + + + + + + + IF2: + + + + + + + + + + + + + + + Aquisition + + + + + + Port 1 Enable + + + + + + + Port 2 Enable + + + + + + + Reference Enable + + + + + + + + + Samples: + + + + + + + 128 + + + 131072 + + + 128 + + + 131072 + + + + + + + + + + + + + + + + + + + Measurements + + + + + + Port 1 + + + + + + + + ADC min: + + + + + + + + + + ADC max: + + + + + + + + + + Magnitude: + + + + + + + + + + Phase: + + + + + + + + + + Referenced: + + + + + + + + + + + + + + + Port 2 + + + + + + + + ADC min: + + + + + + + + + + ADC max: + + + + + + + + + + Magnitude: + + + + + + + + + + Phase: + + + + + + + + + + Referenced: + + + + + + + + + + + + + + + Reference + + + + + + + + ADC min: + + + + + + + + + + ADC max: + + + + + + + + + + Magnitude: + + + + + + + + + + Phase: + + + + + + + + + + + + + + + + + + + SIUnitEdit + QLineEdit +
CustomWidgets/siunitedit.h
+
+
+ + + + + + +
diff --git a/Software/PC_Application/Menu/menu.cpp b/Software/PC_Application/Menu/menu.cpp new file mode 100644 index 0000000..818c847 --- /dev/null +++ b/Software/PC_Application/Menu/menu.cpp @@ -0,0 +1,84 @@ +#include "menu.h" +#include +#include "menuaction.h" +#include + +using namespace std; + +Menu::Menu(QStackedLayout &layout, QString name) + : name(name), + m_containingLayout(layout) +{ + m_layout = new QVBoxLayout; + setLayout(m_layout); + m_widgetCount = 0; + setFixedSize(180, 800); + parent = nullptr; + layout.addWidget(this); + if(name.length() > 0) { + auto back = new MenuAction(name, MenuAction::ArrowType::Left); + back->setStyleSheet("background-color:lightblue;"); + connect(back, &MenuAction::triggered, this, &Menu::leave); + addItem(back); + } +} + +void Menu::addItem(MenuItem *i) +{ + if(m_widgetCount >= maxWidgets) { + throw runtime_error("Menu already at maximum capacity"); + } + m_layout->addWidget(i, 1); + items.push_back(i); + m_widgetCount++; +} + +void Menu::addMenu(Menu *m) +{ + auto menuLabel = new MenuAction(m->name, MenuAction::ArrowType::Right); + submenus.push_back(SubmenuEntry(menuLabel, m, m_widgetCount)); + connect(menuLabel, &MenuAction::triggered, [=]() { + m->m_containingLayout.setCurrentWidget(m); + }); + addItem(menuLabel); + m->parent = this; +} + +void Menu::finalize() +{ + m_layout->addStretch(maxWidgets - m_widgetCount); +} + +void Menu::keyPressEvent(QKeyEvent *event) +{ + // check if softkey pressed + int index = -1; + switch(event->key()) { + case Qt::Key_F1: index = 0; break; + case Qt::Key_F2: index = 1; break; + case Qt::Key_F3: index = 2; break; + case Qt::Key_F4: index = 3; break; + case Qt::Key_F5: index = 4; break; + case Qt::Key_F6: index = 5; break; + case Qt::Key_F7: index = 6; break; + case Qt::Key_F8: index = 7; break; + } + if(index >= 0) { + auto w = m_layout->itemAt(index); + w->widget()->setFocus(); + items[index]->userSelected(); + event->accept(); + } else if(event->key() == Qt::Key_Escape) { + leave(); + event->accept(); + } +} + +void Menu::leave() +{ + if(parent) { + // got a parent menu + parent->m_containingLayout.setCurrentWidget(parent); + } +} + diff --git a/Software/PC_Application/Menu/menu.h b/Software/PC_Application/Menu/menu.h new file mode 100644 index 0000000..0da8423 --- /dev/null +++ b/Software/PC_Application/Menu/menu.h @@ -0,0 +1,41 @@ +#ifndef MENU_H +#define MENU_H + +#include +#include +#include "menuitem.h" +#include "menuaction.h" + +class Menu : public QWidget +{ + Q_OBJECT +public: + Menu(QStackedLayout &layout, QString name = QString()); + void addItem(MenuItem *i); + void addMenu(Menu *m); + void finalize(); +signals: + +protected: + void keyPressEvent(QKeyEvent *event) override; +private: + void leave(); + class SubmenuEntry { + public: + SubmenuEntry(MenuAction *label, Menu *menu, int index) : + label(label), menu(menu), index(index){}; + MenuAction *label; + Menu *menu; + int index; + }; + static constexpr int maxWidgets = 12; + QVBoxLayout *m_layout; + Menu *parent; + const QString name; + QStackedLayout &m_containingLayout; + std::vector submenus; + std::vector items; + int m_widgetCount; +}; + +#endif // MENU_H diff --git a/Software/PC_Application/Menu/menuaction.cpp b/Software/PC_Application/Menu/menuaction.cpp new file mode 100644 index 0000000..9a9887d --- /dev/null +++ b/Software/PC_Application/Menu/menuaction.cpp @@ -0,0 +1,65 @@ +#include "menuaction.h" + +#include +#include +#include + +MenuAction::MenuAction(const QString &l, MenuAction::ArrowType arrow) +{ + subline = nullptr; + auto label = new QLabel(l, this); + label->setAlignment(Qt::AlignCenter); + auto labelLayout = new QHBoxLayout(); + if(arrow == ArrowType::Left) { + auto lIcon = new QLabel(this); + lIcon->setPixmap(style()->standardIcon(QStyle::SP_ArrowLeft).pixmap(16)); + lIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + labelLayout->addWidget(lIcon); + } + labelLayout->addWidget(label); + if(arrow == ArrowType::Right) { + auto lIcon = new QLabel(this); + lIcon->setPixmap(style()->standardIcon(QStyle::SP_ArrowRight).pixmap(16)); + lIcon->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); + labelLayout->addWidget(lIcon); + } + layout.addLayout(labelLayout); + setLayout(&layout); +} + +void MenuAction::AddSubline(const QString &l) +{ + if(!subline) { + subline = new QLabel(this); + subline->setAlignment(Qt::AlignCenter); + layout.addWidget(subline); + } + QFont f( "Arial", 8); + subline->setFont( f); + subline->setText(l); +} + +void MenuAction::RemoveSubline() +{ + if(subline) { + layout.removeWidget(subline); + delete subline; + subline = nullptr; + } +} + +void MenuAction::userSelected() +{ + emit triggered(); +} + +void MenuAction::mouseReleaseEvent(QMouseEvent *me) +{ + setFrameStyle(QFrame::Raised | QFrame::Panel); + MenuItem::mouseReleaseEvent(me); +} + +void MenuAction::mousePressEvent(QMouseEvent *) +{ + setFrameStyle(QFrame::Sunken | QFrame::Panel); +} diff --git a/Software/PC_Application/Menu/menuaction.h b/Software/PC_Application/Menu/menuaction.h new file mode 100644 index 0000000..6527ebd --- /dev/null +++ b/Software/PC_Application/Menu/menuaction.h @@ -0,0 +1,33 @@ +#ifndef MENULABEL_H +#define MENULABEL_H + +#include "menuitem.h" +#include +#include + +class MenuAction : public MenuItem +{ + Q_OBJECT +public: + enum class ArrowType { + None, + Left, + Right, + }; + + MenuAction(const QString &l, ArrowType arrow = ArrowType::None); + void AddSubline(const QString &l); + void RemoveSubline(); +signals: + void triggered(); +public slots: + void userSelected() override; +private: + QVBoxLayout layout; + QLabel *subline; +protected: + void mouseReleaseEvent(QMouseEvent *me) override; + void mousePressEvent(QMouseEvent *me) override; +}; + +#endif // MENULABEL_H diff --git a/Software/PC_Application/Menu/menubool.cpp b/Software/PC_Application/Menu/menubool.cpp new file mode 100644 index 0000000..71b85ce --- /dev/null +++ b/Software/PC_Application/Menu/menubool.cpp @@ -0,0 +1,36 @@ +#include "menubool.h" + +#include + +MenuBool::MenuBool(QString name, bool defaultValue) +{ + auto label = new QLabel(name, this); + label->setAlignment(Qt::AlignCenter); + layout.addWidget(label); + sw = new ToggleSwitch(this, defaultValue); + layout.addWidget(sw); + setLayout(&layout); + connect(sw, &ToggleSwitch::toggled, this, &MenuBool::valueChanged); + sw->setFocusPolicy(Qt::NoFocus); +} + +void MenuBool::setValue(bool value) +{ + sw->setState(value); +} + +void MenuBool::userSelected() +{ + sw->toggle(); +} + +void MenuBool::mouseReleaseEvent(QMouseEvent *me) +{ + setFrameStyle(QFrame::Raised | QFrame::Panel); + MenuItem::mouseReleaseEvent(me); +} + +void MenuBool::mousePressEvent(QMouseEvent *) +{ + setFrameStyle(QFrame::Sunken | QFrame::Panel); +} diff --git a/Software/PC_Application/Menu/menubool.h b/Software/PC_Application/Menu/menubool.h new file mode 100644 index 0000000..544d6cc --- /dev/null +++ b/Software/PC_Application/Menu/menubool.h @@ -0,0 +1,28 @@ +#ifndef MENUBOOL_H +#define MENUBOOL_H + +#include +#include "menuitem.h" +#include +#include "CustomWidgets/toggleswitch.h" + +class MenuBool : public MenuItem +{ + Q_OBJECT +public: + MenuBool(QString name, bool defaultValue = false); + +signals: + void valueChanged(bool value); +public slots: + void setValue(bool value); + void userSelected() override; +protected: + void mouseReleaseEvent(QMouseEvent *me) override; + void mousePressEvent(QMouseEvent *me) override; +private: + QVBoxLayout layout; + ToggleSwitch *sw; +}; + +#endif // MENUBOOL_H diff --git a/Software/PC_Application/Menu/menuitem.cpp b/Software/PC_Application/Menu/menuitem.cpp new file mode 100644 index 0000000..6ab76a6 --- /dev/null +++ b/Software/PC_Application/Menu/menuitem.cpp @@ -0,0 +1,17 @@ +#include "menuitem.h" + +#include + +MenuItem::MenuItem() : QFrame() +{ + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + //setStyleSheet("*:focus {background: lightblue}"); + setFocusPolicy(Qt::StrongFocus); + setFrameStyle(QFrame::Raised | QFrame::Panel); +} + +void MenuItem::mouseReleaseEvent(QMouseEvent *me) +{ + userSelected(); + me->accept(); +} diff --git a/Software/PC_Application/Menu/menuitem.h b/Software/PC_Application/Menu/menuitem.h new file mode 100644 index 0000000..4fe62b8 --- /dev/null +++ b/Software/PC_Application/Menu/menuitem.h @@ -0,0 +1,20 @@ +#ifndef MENUITEM_H +#define MENUITEM_H + +#include + +class MenuItem : public QFrame +{ + Q_OBJECT +public: + MenuItem(); + +public slots: + virtual void userSelected(){}; + +protected: + void mouseReleaseEvent(QMouseEvent *me) override; + +}; + +#endif // MENUITEM_H diff --git a/Software/PC_Application/Menu/menuvalue.cpp b/Software/PC_Application/Menu/menuvalue.cpp new file mode 100644 index 0000000..c6f52dd --- /dev/null +++ b/Software/PC_Application/Menu/menuvalue.cpp @@ -0,0 +1,46 @@ +#include "menuvalue.h" + +#include +#include +#include +#include +#include "valueinput.h" +#include +#include "unit.h" +#include + +using namespace std; + +MenuValue::MenuValue(QString name, double defaultValue, QString unit, QString prefixes, int precision) + : name(name) +{ + if(prefixes.indexOf(' ') < 0) { + throw runtime_error("Prefix string must contain space"); + } + auto layout = new QVBoxLayout; + auto label = new QLabel(name, this); + label->setAlignment(Qt::AlignCenter); + layout->addWidget(label); + lvalue = new SIUnitEdit(unit, prefixes, precision); + // pass on signal + connect(lvalue, &SIUnitEdit::valueChanged, this, &MenuValue::valueChanged); + layout->addWidget(lvalue); + setValue(defaultValue); + setLayout(layout); +} + +void MenuValue::setValue(double value) +{ + lvalue->setValue(value); +} + +void MenuValue::setValueQuiet(double value) +{ + lvalue->setValueQuiet(value); +} + +void MenuValue::userSelected() +{ + lvalue->setFocus(); + //startInputDialog(); +} diff --git a/Software/PC_Application/Menu/menuvalue.h b/Software/PC_Application/Menu/menuvalue.h new file mode 100644 index 0000000..dcb629f --- /dev/null +++ b/Software/PC_Application/Menu/menuvalue.h @@ -0,0 +1,26 @@ +#ifndef MENUVALUE_H +#define MENUVALUE_H + +#include "menuitem.h" +#include +#include + +class MenuValue : public MenuItem +{ + Q_OBJECT +public: + MenuValue(QString name, double defaultValue = 0.0, QString unit = QString(), QString prefixes = " ", int precision = 0); + +signals: + void valueChanged(double value); +public slots: + void setValue(double value); + // same as setValue, except that no valueChanged signal is emitted + void setValueQuiet(double value); + void userSelected() override; +private: + SIUnitEdit *lvalue; + const QString name; +}; + +#endif // MENUVALUE_H diff --git a/Software/PC_Application/Tools/eseries.cpp b/Software/PC_Application/Tools/eseries.cpp new file mode 100644 index 0000000..fe5aefe --- /dev/null +++ b/Software/PC_Application/Tools/eseries.cpp @@ -0,0 +1,60 @@ +#include "eseries.h" +#include +#include + +static const std::vector E6 = { + 1.0, 1.5, 2.2, 3.3, 4.7, 6.8 +}; +static const std::vector E12 = { + 1.0, 1.2, 1.5, 1.8, 2.2, 2.7, 3.3, 3.9, 4.7, 5.6, 6.8, 8.2 +}; +static const std::vector E24 = { + 1.0, 1.1, 1.2, 1.3, 1.5, 1.6, 1.8, 2.0, 2.2, 2.4, 2.7, 3.0, 3.3, 3.6, 3.9, 4.3, 4.7, 5.1, 5.6, 6.2, 6.8, 7.5, 8.2, 9.1 +}; +static const std::vector E48 = { + 1.00, 1.05, 1.10, 1.15, 1.21, 1.27, 1.33, 1.40, 1.47, 1.54, 1.62, 1.69, 1.78, 1.87, 1.96, 2.05, 2.15, 2.26, 2.37, 2.49, 2.61, 2.74, 2.87, 3.01, 3.16, 3.32, 3.48, 3.65, 3.83, 4.02, 4.22, 4.42, 4.64, 4.87, 5.11, 5.36, 5.62, 5.90, 6.19, 6.49, 6.81, 7.15, 7.50, 7.87, 8.25, 8.66, 9.09, 9.53 +}; +static const std::vector E96 = { + 1.00, 1.02, 1.05, 1.07, 1.10, 1.13, 1.15, 1.18, 1.21, 1.24, 1.27, 1.30, 1.33, 1.37, 1.40, 1.43, 1.47, 1.50, 1.54, 1.58, 1.62, 1.65, 1.69, 1.74, 1.78, 1.82, 1.87, 1.91, 1.96, 2.00, 2.05, 2.10, 2.15, 2.21, 2.26, 2.32, 2.37, 2.43, 2.49, 2.55, 2.61, 2.67, 2.74, 2.80, 2.87, 2.94, 3.01, 3.09, 3.16, 3.24, 3.32, 3.40, 3.48, 3.57, 3.65, 3.74, 3.83, 3.92, 4.02, 4.12, 4.22, 4.32, 4.42, 4.53, 4.64, 4.75, 4.87, 4.99, 5.11, 5.23, 5.36, 5.49, 5.62, 5.76, 5.90, 6.04, 6.19, 6.34, 6.49, 6.65, 6.81, 6.98, 7.15, 7.32, 7.50, 7.68, 7.87, 8.06, 8.25, 8.45, 8.66, 8.87, 9.09, 9.31, 9.53, 9.76 +}; + +double ESeries::ToESeries(double value, ESeries::Series s, ESeries::Type t) +{ + if(s == Series::Ideal) { + // nothing to do + return value; + } + // bring value into [1.0, 10.0) interval + int shift10 = floor(log10(value)); + value *= pow(10.0, -shift10); + std::vector series; + switch(s) { + case Series::E96: series = E96; break; + case Series::E48: series = E48; break; + case Series::E24: series = E24; break; + case Series::E12: series = E12; break; + case Series::E6: series = E6; break; + case Series::Ideal: /* already handled */ break; + } + unsigned int index = 1; + while(index < 96 && series[index] <= value) { + index++; + } + auto lower = series[index - 1]; + double higher = 10.0; + if(index < series.size()) { + higher = series[index]; + } + double approximation; + switch(t) { + case Type::Lower: approximation = lower; break; + case Type::Higher: approximation = higher; break; + case Type::BestMatch: + if(fabs(value - lower) < fabs(value - higher)) { + approximation = lower; + } else { + approximation = higher; + } + } + return approximation * pow(10, shift10); +} diff --git a/Software/PC_Application/Tools/eseries.h b/Software/PC_Application/Tools/eseries.h new file mode 100644 index 0000000..506e5c1 --- /dev/null +++ b/Software/PC_Application/Tools/eseries.h @@ -0,0 +1,25 @@ +#ifndef ESERIES_H +#define ESERIES_H + + +class ESeries +{ +public: + enum class Series { + Ideal = 0, + E6 = 1, + E12 = 2, + E24 = 3, + E48 = 4, + E96 = 5, + }; + enum class Type { + BestMatch = 0, + Lower = 1, + Higher = 2, + }; + + static double ToESeries(double value, Series s, Type t = Type::BestMatch); +}; + +#endif // ESERIES_H diff --git a/Software/PC_Application/Tools/impedancematchdialog.cpp b/Software/PC_Application/Tools/impedancematchdialog.cpp new file mode 100644 index 0000000..5c0d597 --- /dev/null +++ b/Software/PC_Application/Tools/impedancematchdialog.cpp @@ -0,0 +1,275 @@ +#include "impedancematchdialog.h" +#include "ui_impedancematchdialog.h" +#include "Tools/eseries.h" + +using namespace std; + +constexpr double ImpedanceMatchDialog::Z0; + +ImpedanceMatchDialog::ImpedanceMatchDialog(TraceMarkerModel &model, TraceMarker *marker, QWidget *parent) : + QDialog(parent), + ui(new Ui::ImpedanceMatchDialog) +{ + ui->setupUi(this); + + // set SI units and prefixes + ui->zReal->setUnit("Ohm"); + ui->zImag->setUnit("Ohm"); + ui->zFreq->setUnit("Hz"); + ui->zFreq->setPrefixes(" kMG"); + + ui->mImag->setUnit("Ohm"); + ui->mReal->setUnit("Ohm"); + ui->mLoss->setUnit("db"); + + ui->lValue->setUnit("H"); + ui->lValue->setPrefixes("pnum "); + ui->cValue->setUnit("F"); + ui->cValue->setPrefixes("pnum "); + + connect(ui->zFreq, &SIUnitEdit::valueChanged, this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->zImag, &SIUnitEdit::valueChanged, this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->zReal, &SIUnitEdit::valueChanged, this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->zGroup, qOverload(&QButtonGroup::buttonClicked), this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->cMatchType, qOverload(&QComboBox::currentIndexChanged), this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->lGroup, qOverload(&QButtonGroup::buttonClicked), this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->cGroup, qOverload(&QButtonGroup::buttonClicked), this, &ImpedanceMatchDialog::calculateMatch); + connect(ui->zGroup, qOverload(&QButtonGroup::buttonClicked), this, &ImpedanceMatchDialog::calculateMatch); + + // populate marker options + auto markers = model.getMarker(); + for(auto m : markers) { + if(!m->trace()->isReflection()) { + // matching only possible for reflections + continue; + } + ui->cSource->addItem("From Marker "+QString::number(m->getNumber()), QVariant::fromValue(m)); + if(m == marker) { + // select the last index, e.i. the just created marker + ui->cSource->setCurrentIndex(ui->cSource->count()-1); + } + } +} + +ImpedanceMatchDialog::~ImpedanceMatchDialog() +{ + delete ui; +} + +void ImpedanceMatchDialog::on_cSource_currentIndexChanged(int index) +{ + ui->rbSeries->setEnabled(index == 0); + ui->rbParallel->setEnabled(index == 0); + ui->zReal->setEnabled(index == 0); + ui->zImag->setEnabled(index == 0); + ui->zFreq->setEnabled(index == 0); + if(index > 0) { + auto m = qvariant_cast(ui->cSource->itemData(index)); + ui->rbSeries->setChecked(true); + auto data = m->getData(); + auto reflection = Z0 * (1.0 + data) / (1.0 - data); + ui->zReal->setValue(reflection.real()); + ui->zImag->setValue(reflection.imag()); + ui->zFreq->setValue(m->getFrequency()); + } +} + +void ImpedanceMatchDialog::calculateMatch() +{ + try { + double freq = ui->zFreq->value(); + complex Z; + if(ui->rbSeries->isChecked()) { + Z.real(ui->zReal->value()); + Z.imag(ui->zImag->value()); + } else { + auto real = complex(ui->zReal->value(), 0.0); + auto imag = complex(0.0, ui->zImag->value()); + // calculate parallel impedance + Z = real * imag / (real + imag); + } + bool seriesC = ui->cMatchType->currentIndex() == 0 ? true : false; + // equations taken from http://www.ittc.ku.edu/~jstiles/723/handouts/section_5_1_Matching_with_Lumped_Elements_package.pdf + double B, X; + if(Z.real() > Z0) { + B = sqrt(Z.real()/Z0)*sqrt(norm(Z)-Z0*Z.real()); + if (seriesC) { + B = -B; + } + B += Z.imag(); + B /= norm(Z); + X = 1/B + Z.imag()*Z0/Z.real()-Z0/(B*Z.real()); + } else { + B = sqrt((Z0-Z.real())/Z.real())/Z0; + X = sqrt(Z.real()*(Z0-Z.real())); + if (seriesC) { + B = -B; + X = -X; + } + X -= Z.imag(); + } + // convert X and B to inductor and capacitor + bool twoCs = false; + bool twoLs = false; + double L, C, C2, L2; + if(X >= 0) { + L = X/(2*M_PI*freq); + if(B > 0) { + C = B/(2*M_PI*freq); + } else { + L2 = X/(2*M_PI*freq); + L = -1/(B*2*M_PI*freq); + twoLs = true; + } + } else { + C = -1/(X*2*M_PI*freq); + if(B < 0) { + L = -1/(B*2*M_PI*freq); + } else { + C2 = B/(2*M_PI*freq); + twoCs = true; + } + } + + ESeries::Series Lseries; + if(ui->lE6->isChecked()) { + Lseries = ESeries::Series::E6; + } else if(ui->lE12->isChecked()) { + Lseries = ESeries::Series::E12; + } else if(ui->lE24->isChecked()) { + Lseries = ESeries::Series::E24; + } else if(ui->lE48->isChecked()) { + Lseries = ESeries::Series::E48; + } else if(ui->lE96->isChecked()) { + Lseries = ESeries::Series::E96; + } else { + Lseries = ESeries::Series::Ideal; + } + ESeries::Series Cseries; + if(ui->cE6->isChecked()) { + Cseries = ESeries::Series::E6; + } else if(ui->cE12->isChecked()) { + Cseries = ESeries::Series::E12; + } else if(ui->cE24->isChecked()) { + Cseries = ESeries::Series::E24; + } else if(ui->cE48->isChecked()) { + Cseries = ESeries::Series::E48; + } else if(ui->cE96->isChecked()) { + Cseries = ESeries::Series::E96; + } else { + Cseries = ESeries::Series::Ideal; + } + + L = ESeries::ToESeries(L, Lseries); + C = ESeries::ToESeries(C, Cseries); + L2 = ESeries::ToESeries(L2, Lseries); + C2 = ESeries::ToESeries(C2, Cseries); + + if(twoCs) { + for(auto b : ui->lGroup->buttons()) { + b->setEnabled(false); + } + for(auto b : ui->cGroup->buttons()) { + b->setEnabled(true); + } + ui->lL->setText("C1:"); + ui->lC->setText("C2:"); + ui->lValue->setUnit("F"); + ui->cValue->setUnit("F"); + ui->lValue->setValue(C2); + ui->cValue->setValue(C); + } else if(twoLs) { + for(auto b : ui->cGroup->buttons()) { + b->setEnabled(false); + } + for(auto b : ui->lGroup->buttons()) { + b->setEnabled(true); + } + ui->lC->setText("L2:"); + ui->lL->setText("L1:"); + ui->cValue->setUnit("H"); + ui->lValue->setUnit("H"); + ui->cValue->setValue(L2); + ui->lValue->setValue(L); + } else { + for(auto b : ui->cGroup->buttons()) { + b->setEnabled(true); + } + for(auto b : ui->lGroup->buttons()) { + b->setEnabled(true); + } + ui->lC->setText("C:"); + ui->lL->setText("L:"); + ui->lValue->setUnit("H"); + ui->cValue->setUnit("F"); + ui->lValue->setValue(L); + ui->cValue->setValue(C); + } + // calculate actual matched impedance + complex Zmatched; + complex Zp, Zs; + if(seriesC) { + if(twoLs) { + Zs = complex(0, 2*M_PI*freq*L2); + Zp = complex(0, 2*M_PI*freq*L); + } else if(twoCs) { + Zs = complex(0, -1/(2*M_PI*freq*C2)); + Zp = complex(0, -1/(2*M_PI*freq*C)); + } else { + Zs = complex(0, -1/(2*M_PI*freq*C)); + Zp = complex(0, 2*M_PI*freq*L); + } + } else { + if(twoCs) { + Zs = complex(0, -1/(2*M_PI*freq*C)); + Zp = complex(0, -1/(2*M_PI*freq*C2)); + } else if(twoLs){ + Zs = complex(0, 2*M_PI*freq*L); + Zp = complex(0, 2*M_PI*freq*L2); + } else { + Zs = complex(0, 2*M_PI*freq*L); + Zp = complex(0, -1/(2*M_PI*freq*C)); + } + } + if(Z.real() > Z0) { + Zmatched = Z*Zp/(Z+Zp) + Zs; + } else { + Zmatched = Zp*(Z+Zs)/(Zp+Z+Zs); + } + ui->mReal->setValue(Zmatched.real()); + ui->mImag->setValue(Zmatched.imag()); + double reflection = abs((Zmatched-Z0)/(Zmatched+Z0)); + auto loss = 20.0*log10(reflection); + ui->mLoss->setValue(loss); + + // set correct image + if(Z.real() > Z0) { + if(X >= 0 && B >= 0) { + ui->Image->setPixmap(QPixmap(":/icons/sLpC_small.png")); + } else if(X < 0 && B < 0) { + ui->Image->setPixmap(QPixmap(":/icons/sCpL_small.png")); + } else if(X >= 0 && B < 0) { + ui->Image->setPixmap(QPixmap(":/icons/sCpC_small.png")); // TODO check + } else { + ui->Image->setPixmap(QPixmap(":/icons/sLpL_small.png")); // TODO check + } + } else { + if(X >= 0 && B >= 0) { + ui->Image->setPixmap(QPixmap(":/icons/pCsL_small.png")); + } else if(X < 0 && B < 0) { + ui->Image->setPixmap(QPixmap(":/icons/pLsC_small.png")); + } else if(X >= 0 && B < 0) { + ui->Image->setPixmap(QPixmap(":/icons/pLsL_small.png")); + } else { + ui->Image->setPixmap(QPixmap(":/icons/pCsC_small.png")); + } + } + } catch (exception e){ + // something went wrong, probably caused by (intermediate) invalid input, such as f=0Hz + ui->lValue->setValue(nan("")); + ui->cValue->setValue(nan("")); + ui->mReal->setValue(nan("")); + ui->mImag->setValue(nan("")); + ui->mLoss->setValue(nan("")); + } +} diff --git a/Software/PC_Application/Tools/impedancematchdialog.h b/Software/PC_Application/Tools/impedancematchdialog.h new file mode 100644 index 0000000..1c520cb --- /dev/null +++ b/Software/PC_Application/Tools/impedancematchdialog.h @@ -0,0 +1,28 @@ +#ifndef IMPEDANCEMATCHDIALOG_H +#define IMPEDANCEMATCHDIALOG_H + +#include +#include "Traces/tracemarkermodel.h" + +namespace Ui { +class ImpedanceMatchDialog; +} + +class ImpedanceMatchDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ImpedanceMatchDialog(TraceMarkerModel &model, TraceMarker *marker = nullptr, QWidget *parent = nullptr); + ~ImpedanceMatchDialog(); + +private slots: + void on_cSource_currentIndexChanged(int index); + void calculateMatch(); + +private: + static constexpr double Z0 = 50.0; + Ui::ImpedanceMatchDialog *ui; +}; + +#endif // IMPEDANCEMATCHDIALOG_H diff --git a/Software/PC_Application/Tools/impedancematchdialog.ui b/Software/PC_Application/Tools/impedancematchdialog.ui new file mode 100644 index 0000000..ed564ed --- /dev/null +++ b/Software/PC_Application/Tools/impedancematchdialog.ui @@ -0,0 +1,457 @@ + + + ImpedanceMatchDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 664 + 497 + + + + Impedance Matching + + + + + + + + + + Unmatched Impedance Z + + + + + + + Custom + + + + + + + + + + Series + + + true + + + zGroup + + + + + + + Parallel + + + zGroup + + + + + + + + + + + Real: + + + + + + + + + + Imag: + + + + + + + + + + Frequency: + + + + + + + + + + + + + + + Matched Impedance + + + + + + Real: + + + + + + + true + + + true + + + + + + + Imag: + + + + + + + true + + + true + + + + + + + Return loss: + + + + + + + true + + + true + + + + + + + + + + + + Settings + + + + + + + Series C - Parallel L + + + + + Parallel C - Series L + + + + + + + + + + + + Inductor + + + + + + Ideal + + + true + + + lGroup + + + + + + + E24 + + + lGroup + + + + + + + E6 + + + lGroup + + + + + + + E48 + + + lGroup + + + + + + + E12 + + + lGroup + + + + + + + E96 + + + lGroup + + + + + + + + + + + + L: + + + + + + + true + + + true + + + + + + + + + + + + + Capacitor + + + + + + E6 + + + cGroup + + + + + + + E12 + + + cGroup + + + + + + + Ideal + + + true + + + cGroup + + + + + + + E24 + + + cGroup + + + + + + + E48 + + + cGroup + + + + + + + E96 + + + cGroup + + + + + + + + + + + + C: + + + + + + + true + + + true + + + + + + + + + + + + + + + + false + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + SIUnitEdit + QLineEdit +
CustomWidgets/siunitedit.h
+
+
+ + + + buttonBox + accepted() + ImpedanceMatchDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ImpedanceMatchDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + + + + +
diff --git a/Software/PC_Application/Traces/bodeplotaxisdialog.cpp b/Software/PC_Application/Traces/bodeplotaxisdialog.cpp new file mode 100644 index 0000000..9e8f218 --- /dev/null +++ b/Software/PC_Application/Traces/bodeplotaxisdialog.cpp @@ -0,0 +1,118 @@ +#include "bodeplotaxisdialog.h" +#include "ui_bodeplotaxisdialog.h" + +BodeplotAxisDialog::BodeplotAxisDialog(TraceBodePlot *plot) : + QDialog(nullptr), + ui(new Ui::BodeplotAxisDialog), + plot(plot) +{ + ui->setupUi(this); + + // Setup GUI connections + connect(ui->Y1type, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + ui->Y1log->setEnabled(index != 0); + ui->Y1linear->setEnabled(index != 0); + ui->Y1auto->setEnabled(index != 0); + bool autoRange = ui->Y1auto->isChecked(); + ui->Y1min->setEnabled(index != 0 && !autoRange); + ui->Y1max->setEnabled(index != 0 && !autoRange); + ui->Y1divs->setEnabled(index != 0 && !autoRange); + auto type = (TraceBodePlot::YAxisType) index; + QString unit; + switch(type) { + case TraceBodePlot::YAxisType::Magnitude: unit = "db"; break; + case TraceBodePlot::YAxisType::Phase: unit = "°"; break; + case TraceBodePlot::YAxisType::VSWR: unit = ""; break; + default: unit = ""; break; + } + ui->Y1min->setUnit(unit); + ui->Y1max->setUnit(unit); + ui->Y1divs->setUnit(unit); + }); + connect(ui->Y1auto, &QCheckBox::toggled, [this](bool checked) { + ui->Y1min->setEnabled(!checked); + ui->Y1max->setEnabled(!checked); + ui->Y1divs->setEnabled(!checked); + }); + + connect(ui->Y2type, qOverload(&QComboBox::currentIndexChanged), [this](int index) { + ui->Y2log->setEnabled(index != 0); + ui->Y2linear->setEnabled(index != 0); + ui->Y2auto->setEnabled(index != 0); + bool autoRange = ui->Y2auto->isChecked(); + ui->Y2min->setEnabled(index != 0 && !autoRange); + ui->Y2max->setEnabled(index != 0 && !autoRange); + ui->Y2divs->setEnabled(index != 0 && !autoRange); + auto type = (TraceBodePlot::YAxisType) index; + QString unit; + switch(type) { + case TraceBodePlot::YAxisType::Magnitude: unit = "db"; break; + case TraceBodePlot::YAxisType::Phase: unit = "°"; break; + case TraceBodePlot::YAxisType::VSWR: unit = ""; break; + default: unit = ""; break; + } + ui->Y2min->setUnit(unit); + ui->Y2max->setUnit(unit); + ui->Y2divs->setUnit(unit); + }); + connect(ui->Y2auto, &QCheckBox::toggled, [this](bool checked) { + ui->Y2min->setEnabled(!checked); + ui->Y2max->setEnabled(!checked); + ui->Y2divs->setEnabled(!checked); + }); + + connect(ui->Xauto, &QCheckBox::toggled, [this](bool checked) { + ui->Xmin->setEnabled(!checked); + ui->Xmax->setEnabled(!checked); + ui->Xdivs->setEnabled(!checked); + }); + + ui->Xmin->setUnit("Hz"); + ui->Xmax->setUnit("Hz"); + ui->Xdivs->setUnit("Hz"); + ui->Xmin->setPrefixes(" kMG"); + ui->Xmax->setPrefixes(" kMG"); + ui->Xdivs->setPrefixes(" kMG"); + + // Fill initial values + // assume same order in YAxisType enum as in ComboBox items + ui->Y1type->setCurrentIndex((int) plot->YAxis[0].type); + if(plot->YAxis[0].log) { + ui->Y1log->setChecked(true); + } else { + ui->Y1linear->setChecked(true); + } + ui->Y1auto->setChecked(plot->YAxis[0].autorange); + ui->Y1min->setValueQuiet(plot->YAxis[0].rangeMin); + ui->Y1max->setValueQuiet(plot->YAxis[0].rangeMax); + ui->Y1divs->setValueQuiet(plot->YAxis[0].rangeDiv); + + ui->Y2type->setCurrentIndex((int) plot->YAxis[1].type); + if(plot->YAxis[1].log) { + ui->Y2log->setChecked(true); + } else { + ui->Y2linear->setChecked(true); + } + ui->Y2auto->setChecked(plot->YAxis[1].autorange); + ui->Y2min->setValueQuiet(plot->YAxis[1].rangeMin); + ui->Y2max->setValueQuiet(plot->YAxis[1].rangeMax); + ui->Y2divs->setValueQuiet(plot->YAxis[1].rangeDiv); + + ui->Xauto->setChecked(plot->XAxis.autorange); + ui->Xmin->setValueQuiet(plot->XAxis.rangeMin); + ui->Xmax->setValueQuiet(plot->XAxis.rangeMax); + ui->Xdivs->setValueQuiet(plot->XAxis.rangeDiv); +} + +BodeplotAxisDialog::~BodeplotAxisDialog() +{ + delete ui; +} + +void BodeplotAxisDialog::on_buttonBox_accepted() +{ + // set plot values to the ones selected in the dialog + plot->setYAxis(0, (TraceBodePlot::YAxisType) ui->Y1type->currentIndex(), ui->Y1log->isChecked(), ui->Y1auto->isChecked(), ui->Y1min->value(), ui->Y1max->value(), ui->Y1divs->value()); + plot->setYAxis(1, (TraceBodePlot::YAxisType) ui->Y2type->currentIndex(), ui->Y2log->isChecked(), ui->Y2auto->isChecked(), ui->Y2min->value(), ui->Y2max->value(), ui->Y2divs->value()); + plot->setXAxis(ui->Xauto->isChecked(), ui->Xmin->value(), ui->Xmax->value(), ui->Xdivs->value()); +} diff --git a/Software/PC_Application/Traces/bodeplotaxisdialog.h b/Software/PC_Application/Traces/bodeplotaxisdialog.h new file mode 100644 index 0000000..c0ad49d --- /dev/null +++ b/Software/PC_Application/Traces/bodeplotaxisdialog.h @@ -0,0 +1,27 @@ +#ifndef BODEPLOTAXISDIALOG_H +#define BODEPLOTAXISDIALOG_H + +#include +#include "tracebodeplot.h" + +namespace Ui { +class BodeplotAxisDialog; +} + +class BodeplotAxisDialog : public QDialog +{ + Q_OBJECT + +public: + explicit BodeplotAxisDialog(TraceBodePlot *plot); + ~BodeplotAxisDialog(); + +private slots: + void on_buttonBox_accepted(); + +private: + Ui::BodeplotAxisDialog *ui; + TraceBodePlot *plot; +}; + +#endif // BODEPLOTAXISDIALOG_H diff --git a/Software/PC_Application/Traces/bodeplotaxisdialog.ui b/Software/PC_Application/Traces/bodeplotaxisdialog.ui new file mode 100644 index 0000000..122aa82 --- /dev/null +++ b/Software/PC_Application/Traces/bodeplotaxisdialog.ui @@ -0,0 +1,462 @@ + + + BodeplotAxisDialog + + + + 0 + 0 + 715 + 282 + + + + Axis Setup + + + true + + + + + + + + + + + 15 + + + + Primary Y axis + + + Qt::AlignCenter + + + + + + + + + Type: + + + + + + + + Disabled + + + + + Magnitude + + + + + Phase + + + + + VSWR + + + + + + + + + + Qt::Horizontal + + + + + + + + + Linear + + + Y1group + + + + + + + Log + + + Y1group + + + + + + + + + Qt::Horizontal + + + + + + + + + Range: + + + + + + + Auto + + + + + + + Maximum: + + + + + + + + + + Minimum: + + + + + + + + + + Divisions: + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + 15 + + + + Secondary Y axis + + + Qt::AlignCenter + + + + + + + + + Type: + + + + + + + + Disabled + + + + + Magnitude + + + + + Phase + + + + + VSWR + + + + + + + + + + Qt::Horizontal + + + + + + + + + Linear + + + Y2group + + + + + + + Log + + + Y2group + + + + + + + + + Qt::Horizontal + + + + + + + + + Range: + + + + + + + Auto + + + + + + + Maximum: + + + + + + + + + + Minimum: + + + + + + + + + + Divisions: + + + + + + + + + + + + + + Qt::Vertical + + + + + + + + + + 0 + 0 + + + + + 15 + + + + X axis + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Range: + + + + + + + Auto + + + + + + + Maximum: + + + + + + + + + + Minimum: + + + + + + + + + + Divisions: + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + SIUnitEdit + QLineEdit +
CustomWidgets/siunitedit.h
+
+
+ + + + buttonBox + accepted() + BodeplotAxisDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + BodeplotAxisDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + + + + +
diff --git a/Software/PC_Application/Traces/markerwidget.cpp b/Software/PC_Application/Traces/markerwidget.cpp new file mode 100644 index 0000000..d9aef8a --- /dev/null +++ b/Software/PC_Application/Traces/markerwidget.cpp @@ -0,0 +1,41 @@ +#include "markerwidget.h" +#include "ui_markerwidget.h" + +MarkerWidget::MarkerWidget(TraceMarkerModel &model, QWidget *parent) : + QWidget(parent), + ui(new Ui::MarkerWidget), + model(model) +{ + ui->setupUi(this); + ui->tableView->setModel(&model); + ui->tableView->setItemDelegateForColumn(1, new TraceChooserDelegate); + + connect(&model.getModel(), &TraceModel::traceAdded, this, &MarkerWidget::updatePersistentEditors); + connect(&model.getModel(), &TraceModel::traceRemoved, this, &MarkerWidget::updatePersistentEditors); +} + +MarkerWidget::~MarkerWidget() +{ + delete ui; +} + +void MarkerWidget::on_bDelete_clicked() +{ + model.removeMarker(ui->tableView->currentIndex().row()); +} + +void MarkerWidget::on_bAdd_clicked() +{ + auto marker = model.createDefaultMarker(); + model.addMarker(marker); + updatePersistentEditors(); +} + +void MarkerWidget::updatePersistentEditors(Trace *) +{ + for(int i=0;itableView->closePersistentEditor(index); + ui->tableView->openPersistentEditor(index); + } +} diff --git a/Software/PC_Application/Traces/markerwidget.h b/Software/PC_Application/Traces/markerwidget.h new file mode 100644 index 0000000..70bf782 --- /dev/null +++ b/Software/PC_Application/Traces/markerwidget.h @@ -0,0 +1,29 @@ +#ifndef MARKERWIDGET_H +#define MARKERWIDGET_H + +#include +#include "tracemarkermodel.h" + +namespace Ui { +class MarkerWidget; +} + +class MarkerWidget : public QWidget +{ + Q_OBJECT + +public: + explicit MarkerWidget(TraceMarkerModel &model, QWidget *parent = nullptr); + ~MarkerWidget(); + +private slots: + void on_bDelete_clicked(); + void on_bAdd_clicked(); + void updatePersistentEditors(Trace *dummy = nullptr); + +private: + Ui::MarkerWidget *ui; + TraceMarkerModel &model; +}; + +#endif // MARKERWIDGET_H diff --git a/Software/PC_Application/Traces/markerwidget.ui b/Software/PC_Application/Traces/markerwidget.ui new file mode 100644 index 0000000..8199b80 --- /dev/null +++ b/Software/PC_Application/Traces/markerwidget.ui @@ -0,0 +1,89 @@ + + + MarkerWidget + + + + 0 + 0 + 729 + 195 + + + + Form + + + + + + QAbstractItemView::SelectRows + + + true + + + false + + + + + + + + + + 0 + 0 + + + + Add + + + + + + + + + + + + + + 0 + 0 + + + + Delete + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + diff --git a/Software/PC_Application/Traces/trace.cpp b/Software/PC_Application/Traces/trace.cpp new file mode 100644 index 0000000..a1d12fd --- /dev/null +++ b/Software/PC_Application/Traces/trace.cpp @@ -0,0 +1,238 @@ +#include "trace.h" + +using namespace std; + +Trace::Trace(QString name, QColor color) + : _name(name), + _color(color), + _liveType(LivedataType::Overwrite), + reflection(true), + visible(true), + paused(false), + touchstone(false), + calibration(false) +{ + +} + +Trace::~Trace() +{ + emit deleted(this); +} + +void Trace::clear() { + if(paused) { + return; + } + _data.clear(); + emit cleared(this); + emit dataChanged(); +} + +void Trace::addData(Trace::Data d) { + // add or replace data in vector while keeping it sorted with increasing frequency + auto lower = lower_bound(_data.begin(), _data.end(), d, [](const Data &lhs, const Data &rhs) -> bool { + return lhs.frequency < rhs.frequency; + }); + if(lower == _data.end()) { + // highest frequency yet, add to vector + _data.push_back(d); + } else if(lower->frequency == d.frequency) { + switch(_liveType) { + case LivedataType::Overwrite: + // replace this data element + *lower = d; + break; + case LivedataType::MaxHold: + // replace this data element + if(abs(d.S) > abs(lower->S)) { + *lower = d; + } + break; + case LivedataType::MinHold: + // replace this data element + if(abs(d.S) < abs(lower->S)) { + *lower = d; + } + break; + } + + } else { + // insert at this position + _data.insert(lower, d); + } + emit dataAdded(this, d); + emit dataChanged(); +} + +void Trace::setName(QString name) { + _name = name; + emit nameChanged(); +} + +void Trace::fillFromTouchstone(Touchstone &t, unsigned int parameter, QString filename) +{ + if(parameter >= t.ports()*t.ports()) { + throw runtime_error("Parameter for touchstone out of range"); + } + clear(); + setTouchstoneParameter(parameter); + setTouchstoneFilename(filename); + for(unsigned int i=0;i Trace::getMarkers() const +{ + return markers; +} + +void Trace::setVisible(bool visible) +{ + if(visible != this->visible) { + this->visible = visible; + emit visibilityChanged(this); + } +} + +bool Trace::isVisible() +{ + return visible; +} + +void Trace::pause() +{ + paused = true; +} + +void Trace::resume() +{ + paused = false; +} + +bool Trace::isPaused() +{ + return paused; +} + +bool Trace::isTouchstone() +{ + return touchstone; +} + +bool Trace::isCalibration() +{ + return calibration; +} + +bool Trace::isLive() +{ + return !isCalibration() && !isTouchstone() && !isPaused(); +} + +bool Trace::isReflection() +{ + return reflection; +} + +QString Trace::getTouchstoneFilename() const +{ + return touchstoneFilename; +} + +void Trace::setTouchstoneFilename(const QString &value) +{ + touchstoneFilename = value; +} + +unsigned int Trace::getTouchstoneParameter() const +{ + return touchstoneParameter; +} + +std::complex Trace::getData(double frequency) +{ + if(_data.size() == 0 || frequency < minFreq() || frequency > maxFreq()) { + return std::numeric_limits>::quiet_NaN(); + } + + return sample(index(frequency)).S; +} + +int Trace::index(double frequency) +{ + auto lower = lower_bound(_data.begin(), _data.end(), frequency, [](const Data &lhs, const double freq) -> bool { + return lhs.frequency < freq; + }); + return lower - _data.begin(); +} + +void Trace::setTouchstoneParameter(int value) +{ + touchstoneParameter = value; +} diff --git a/Software/PC_Application/Traces/trace.h b/Software/PC_Application/Traces/trace.h new file mode 100644 index 0000000..daa9b4a --- /dev/null +++ b/Software/PC_Application/Traces/trace.h @@ -0,0 +1,105 @@ +#ifndef TRACE_H +#define TRACE_H + +#include +#include +#include +#include +#include +#include "touchstone.h" + +class TraceMarker; + +class Trace : public QObject +{ + Q_OBJECT +public: + + class Data { + public: + double frequency; + std::complex S; + }; + + Trace(QString name = QString(), QColor color = Qt::darkYellow); + ~Trace(); + + enum class LivedataType { + Overwrite, + MaxHold, + MinHold, + }; + enum class LiveParameter { + S11, + S12, + S21, + S22, + }; + + void clear(); + void addData(Data d); + void setName(QString name); + void fillFromTouchstone(Touchstone &t, unsigned int parameter, QString filename = QString()); + void fromLivedata(LivedataType type, LiveParameter param); + QString name() { return _name; }; + QColor color() { return _color; }; + bool isVisible(); + void pause(); + void resume(); + bool isPaused(); + bool isTouchstone(); + bool isCalibration(); + bool isLive(); + bool isReflection(); + LiveParameter liveParameter() { return _liveParam; } + LivedataType liveType() { return _liveType; } + unsigned int size() { return _data.size(); } + double minFreq() { return _data.front().frequency; }; + double maxFreq() { return _data.back().frequency; }; + Data sample(unsigned int index) { return _data.at(index); } + QString getTouchstoneFilename() const; + unsigned int getTouchstoneParameter() const; + std::complex getData(double frequency); + int index(double frequency); + std::set getMarkers() const; + void setCalibration(bool value); + void setReflection(bool value); + +public slots: + void setTouchstoneParameter(int value); + void setTouchstoneFilename(const QString &value); + void setVisible(bool visible); + void setColor(QColor color); + void addMarker(TraceMarker *m); + void removeMarker(TraceMarker *m); + +private: +signals: + void cleared(Trace *t); + void typeChanged(Trace *t); + void dataAdded(Trace *t, Data d); + void deleted(Trace *t); + void visibilityChanged(Trace *t); + void dataChanged(); + void nameChanged(); + void colorChanged(Trace *t); + void markerAdded(TraceMarker *m); + void markerRemoved(TraceMarker *m); + +private: + std::vector _data; + QString _name; + QColor _color; + LivedataType _liveType; + LiveParameter _liveParam; + bool reflection; + bool visible; + bool paused; + bool touchstone; + bool calibration; + QString touchstoneFilename; + unsigned int touchstoneParameter; + std::set markers; +}; + +#endif // TRACE_H diff --git a/Software/PC_Application/Traces/tracebodeplot.cpp b/Software/PC_Application/Traces/tracebodeplot.cpp new file mode 100644 index 0000000..857162f --- /dev/null +++ b/Software/PC_Application/Traces/tracebodeplot.cpp @@ -0,0 +1,489 @@ +#include "tracebodeplot.h" +#include +#include +#include "qwtplotpiecewisecurve.h" +#include "qwt_series_data.h" +#include "trace.h" +#include +#include +#include +#include +#include +#include "tracemarker.h" +#include +#include +#include +#include "bodeplotaxisdialog.h" + +using namespace std; + +static double AxisTransformation(TraceBodePlot::YAxisType type, complex data) { + switch(type) { + case TraceBodePlot::YAxisType::Magnitude: return 20*log10(abs(data)); break; + case TraceBodePlot::YAxisType::Phase: return arg(data) * 180.0 / M_PI; break; + case TraceBodePlot::YAxisType::VSWR: + if(abs(data) < 1.0) { + return (1+abs(data)) / (1-abs(data)); + } + break; + default: break; + } + return numeric_limits::quiet_NaN(); +} + +template class QwtTraceSeries : public QwtSeriesData { +public: + QwtTraceSeries(Trace &t) + : QwtSeriesData(), + t(t){}; + size_t size() const override { + return t.size(); + } + QPointF sample(size_t i) const override { + Trace::Data d = t.sample(i); + QPointF p; + p.setX(d.frequency); + p.setY(AxisTransformation(E, d.S)); + return p; + } + QRectF boundingRect() const override { + return qwtBoundingRect(*this); + } + +private: + Trace &t; +}; + +// Derived plotpicker, exposing transformation functions +class BodeplotPicker : public QwtPlotPicker { +public: + BodeplotPicker(int xAxis, int yAxis, RubberBand rubberBand, DisplayMode trackerMode, QWidget *w) + : QwtPlotPicker(xAxis, yAxis, rubberBand, trackerMode, w) {}; + QPoint plotToPixel(const QPointF &pos) { + return transform(pos); + } + QPointF pixelToPlot(const QPoint &pos) { + return invTransform(pos); + } +}; + +TraceBodePlot::TraceBodePlot(TraceModel &model, QWidget *parent) + : TracePlot(parent), + selectedMarker(nullptr) +{ + plot = new QwtPlot(this); + plot->setCanvasBackground(Background); + auto pal = plot->palette(); + pal.setColor(QPalette::Window, Background); + pal.setColor(QPalette::WindowText, Border); + pal.setColor(QPalette::Text, Border); + auto canvas = new QwtPlotCanvas(plot); + canvas->setFrameStyle(QFrame::Plain); + plot->setCanvas(canvas); + plot->setPalette(pal); + plot->setAutoFillBackground(true); + + auto selectPicker = new BodeplotPicker(plot->xBottom, plot->yLeft, QwtPicker::NoRubberBand, QwtPicker::ActiveOnly, plot->canvas()); + selectPicker->setStateMachine(new QwtPickerClickPointMachine); + + auto drawPicker = new BodeplotPicker(plot->xBottom, plot->yLeft, QwtPicker::NoRubberBand, QwtPicker::ActiveOnly, plot->canvas()); + drawPicker->setStateMachine(new QwtPickerDragPointMachine); + drawPicker->setTrackerPen(QPen(Qt::white)); + + // Marker selection + connect(selectPicker, qOverload(&QwtPlotPicker::selected), [=](const QPointF pos) { + auto clickPoint = drawPicker->plotToPixel(pos); + unsigned int closestDistance = numeric_limits::max(); + TraceMarker *closestMarker = nullptr; + for(auto m : markers) { + auto markerPoint = drawPicker->plotToPixel(m.second->value()); + auto yDiff = abs(markerPoint.y() - clickPoint.y()); + auto xDiff = abs(markerPoint.x() - clickPoint.x()); + unsigned int distance = xDiff * xDiff + yDiff * yDiff; + if(distance < closestDistance) { + closestDistance = distance; + closestMarker = m.first; + } + } + if(closestDistance <= 400) { + selectedMarker = closestMarker; + selectedCurve = curves[0][selectedMarker->trace()].curve; + } else { + selectedMarker = nullptr; + selectedCurve = nullptr; + } + }); + // Marker movement + connect(drawPicker, qOverload(&QwtPlotPicker::moved), [=](const QPointF pos) { + if(!selectedMarker || !selectedCurve) { + return; + } +// int index = selectedCurve->closestPoint(pos.toPoint()); +// qDebug() << index; +// if(index < 0) { +// // unable to find closest point +// return; +// } +// selectedMarker->setFrequency(selectedCurve->sample(index).x()); + selectedMarker->setFrequency(pos.x()); + }); + + QwtPlotGrid *grid = new QwtPlotGrid(); + grid->setMajorPen(QPen(Divisions, 1.0, Qt::DotLine)); + grid->attach(plot); + auto layout = new QGridLayout; + layout->addWidget(plot); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + plot->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); +// plot->plotLayout()->setAlignCanvasToScales(true); + initializeTraceInfo(model); + setAutoFillBackground(true); + + // Setup default axis + setYAxis(0, YAxisType::Magnitude, false, false, -120, 20, 10); + setYAxis(1, YAxisType::Phase, false, false, -180, 180, 30); + // enable autoscaling and set for full span (no information about actual span available yet) + setXAxis(0, 6000000000); + setXAxis(true, 0, 6000000000, 600000000); +} + +TraceBodePlot::~TraceBodePlot() +{ + for(int axis = 0;axis < 2;axis++) { + for(auto pd : curves[axis]) { + delete pd.second.curve; + } + } +} + +void TraceBodePlot::setXAxis(double min, double max) +{ + sweep_fmin = min; + sweep_fmax = max; + updateXAxis(); +} + +void TraceBodePlot::setYAxis(int axis, TraceBodePlot::YAxisType type, bool log, bool autorange, double min, double max, double div) +{ + if(YAxis[axis].type != type) { + YAxis[axis].type = type; + // remove traces that are active but not supported with the new axis type + bool erased = false; + do { + erased = false; + for(auto t : tracesAxis[axis]) { + if(!supported(t, type)) { + enableTraceAxis(t, axis, false); + erased = true; + break; + } + } + } while(erased); + + for(auto t : tracesAxis[axis]) { + // supported but needs an adjusted QwtSeriesData + auto td = curves[axis][t]; + td.data = createQwtSeriesData(*t, axis); + // call to setSamples deletes old QwtSeriesData + td.curve->setSamples(td.data); + if(axis == 0) { + // update marker data + auto marker = t->getMarkers(); + for(auto m : marker) { + markerDataChanged(m); + } + } + } + } + YAxis[axis].log = log; + YAxis[axis].autorange = autorange; + YAxis[axis].rangeMin = min; + YAxis[axis].rangeMax = max; + YAxis[axis].rangeDiv = div; + // enable/disable y axis + auto qwtaxis = axis == 0 ? QwtPlot::yLeft : QwtPlot::yRight; + plot->enableAxis(qwtaxis, type != YAxisType::Disabled); + if(autorange) { + plot->setAxisAutoScale(qwtaxis, true); + } else { + plot->setAxisScale(qwtaxis, min, max, div); + } + updateContextMenu(); + replot(); +} + +void TraceBodePlot::setXAxis(bool autorange, double min, double max, double div) +{ + XAxis.autorange = autorange; + XAxis.rangeMin = min; + XAxis.rangeMax = max; + XAxis.rangeDiv = div; + updateXAxis(); +} + +void TraceBodePlot::enableTrace(Trace *t, bool enabled) +{ + for(int axis = 0;axis < 2;axis++) { + if(supported(t, YAxis[axis].type)) { + enableTraceAxis(t, axis, enabled); + } + } +} + +void TraceBodePlot::updateContextMenu() +{ + contextmenu->clear(); +// for(int axis = 0;axis < 2;axis++) { +// QMenu *axisMenu; +// if(axis == 0) { +// axisMenu = contextmenu->addMenu("Primary Axis"); +// } else { +// axisMenu = contextmenu->addMenu("Secondary Axis"); +// } +// auto group = new QActionGroup(this); +// for(int i=0;i<(int) YAxisType::Last;i++) { +// auto action = new QAction(AxisTypeToName((YAxisType) i)); +// action->setCheckable(true); +// group->addAction(action); +// if(YAxis[axis].type == (YAxisType) i) { +// action->setChecked(true); +// } +// connect(action, &QAction::triggered, [=](bool active) { +// if(active) { +// setYAxisType(axis, (YAxisType) i); +// } +// }); +// } +// axisMenu->addActions(group->actions()); +// } + auto setup = new QAction("Axis setup..."); + connect(setup, &QAction::triggered, [this]() { + auto setup = new BodeplotAxisDialog(this); + setup->show(); + }); + contextmenu->addAction(setup); + for(int axis = 0;axis < 2;axis++) { + if(YAxis[axis].type == YAxisType::Disabled) { + continue; + } + if(axis == 0) { + contextmenu->addSection("Primary Traces"); + } else { + contextmenu->addSection("Secondary Traces"); + } + for(auto t : traces) { + // Skip traces that are not applicable for the selected axis type + if(!supported(t.first, YAxis[axis].type)) { + continue; + } + + auto action = new QAction(t.first->name()); + action->setCheckable(true); + if(tracesAxis[axis].find(t.first) != tracesAxis[axis].end()) { + action->setChecked(true); + } + connect(action, &QAction::toggled, [=](bool active) { + enableTraceAxis(t.first, axis, active); + }); + contextmenu->addAction(action); + } + } + contextmenu->addSeparator(); + auto close = new QAction("Close"); + contextmenu->addAction(close); + connect(close, &QAction::triggered, [=]() { + markedForDeletion = true; + }); +} + +bool TraceBodePlot::supported(Trace *) +{ + // potentially possible to add every kind of trace (depends on axis) + return true; +} + +void TraceBodePlot::replot() +{ + plot->replot(); +} + +QString TraceBodePlot::AxisTypeToName(TraceBodePlot::YAxisType type) +{ + switch(type) { + case YAxisType::Disabled: return "Disabled"; break; + case YAxisType::Magnitude: return "Magnitude"; break; + case YAxisType::Phase: return "Phase"; break; + case YAxisType::VSWR: return "VSWR"; break; + default: return "Unknown"; break; + } +} + +void TraceBodePlot::enableTraceAxis(Trace *t, int axis, bool enabled) +{ + bool alreadyEnabled = tracesAxis[axis].find(t) != tracesAxis[axis].end(); + if(alreadyEnabled != enabled) { + if(enabled) { + tracesAxis[axis].insert(t); + CurveData cd; + cd.data = createQwtSeriesData(*t, axis); + cd.curve = new QwtPlotPiecewiseCurve(); + cd.curve->attach(plot); + cd.curve->setYAxis(axis == 0 ? QwtPlot::yLeft : QwtPlot::yRight); + cd.curve->setSamples(cd.data); + curves[axis][t] = cd; + // connect signals + connect(t, &Trace::dataChanged, this, &TraceBodePlot::triggerReplot); + connect(t, &Trace::colorChanged, this, &TraceBodePlot::traceColorChanged); + connect(t, &Trace::visibilityChanged, this, &TraceBodePlot::traceColorChanged); + connect(t, &Trace::visibilityChanged, this, &TraceBodePlot::triggerReplot); + if(axis == 0) { + connect(t, &Trace::markerAdded, this, &TraceBodePlot::markerAdded); + connect(t, &Trace::markerRemoved, this, &TraceBodePlot::markerRemoved); + auto tracemarkers = t->getMarkers(); + for(auto m : tracemarkers) { + markerAdded(m); + } + } + traceColorChanged(t); + } else { + tracesAxis[axis].erase(t); + // clean up and delete + if(curves[axis].find(t) != curves[axis].end()) { + if(curves[axis][t].curve) { + delete curves[axis][t].curve; + } + curves[axis].erase(t); + } + int otherAxis = axis == 0 ? 1 : 0; + if(curves[otherAxis].find(t) == curves[otherAxis].end()) { + // this trace is not used anymore, disconnect from notifications + disconnect(t, &Trace::dataChanged, this, &TraceBodePlot::triggerReplot); + disconnect(t, &Trace::colorChanged, this, &TraceBodePlot::traceColorChanged); + disconnect(t, &Trace::visibilityChanged, this, &TraceBodePlot::traceColorChanged); + disconnect(t, &Trace::visibilityChanged, this, &TraceBodePlot::triggerReplot); + } + if(axis == 0) { + disconnect(t, &Trace::markerAdded, this, &TraceBodePlot::markerAdded); + disconnect(t, &Trace::markerRemoved, this, &TraceBodePlot::markerRemoved); + auto tracemarkers = t->getMarkers(); + for(auto m : tracemarkers) { + markerRemoved(m); + } + } + } + + updateContextMenu(); + replot(); + } +} + +bool TraceBodePlot::supported(Trace *t, TraceBodePlot::YAxisType type) +{ + switch(type) { + case YAxisType::Disabled: + return false; + case YAxisType::VSWR: + if(!t->isReflection()) { + return false; + } + break; + default: + break; + } + return true; +} + +void TraceBodePlot::updateXAxis() +{ + if(XAxis.autorange) { + QList tickList; + for(double tick = sweep_fmin;tick <= sweep_fmax;tick+= (sweep_fmax-sweep_fmin)/10) { + tickList.append(tick); + } + QwtScaleDiv scalediv(sweep_fmin, sweep_fmax, QList(), QList(), tickList); + plot->setAxisScaleDiv(QwtPlot::xBottom, scalediv); + } else { + plot->setAxisScale(QwtPlot::xBottom, XAxis.rangeMin, XAxis.rangeMax, XAxis.rangeDiv); + } + triggerReplot(); +} + +QwtSeriesData *TraceBodePlot::createQwtSeriesData(Trace &t, int axis) +{ + switch(YAxis[axis].type) { + case YAxisType::Magnitude: + return new QwtTraceSeries(t); + case YAxisType::Phase: + return new QwtTraceSeries(t); + case YAxisType::VSWR: + return new QwtTraceSeries(t); + default: + return nullptr; + } +} + +void TraceBodePlot::traceColorChanged(Trace *t) +{ + for(int axis = 0;axis < 2;axis++) { + if(curves[axis].find(t) != curves[axis].end()) { + // trace active, change the pen color + if(t->isVisible()) { + if(axis == 0) { + curves[axis][t].curve->setPen(t->color()); + } else { + curves[axis][t].curve->setPen(t->color(), 1.0, Qt::DashLine); + } + for(auto m : t->getMarkers()) { + if(markers.count(m)) { + markers[m]->attach(plot); + } + } + } else { + curves[axis][t].curve->setPen(t->color(), 0.0, Qt::NoPen); + for(auto m : t->getMarkers()) { + if(markers.count(m)) { + markers[m]->detach(); + } + } + } + } + } +} + +void TraceBodePlot::markerAdded(TraceMarker *m) +{ + if(markers.count(m)) { + return; + } + QwtSymbol *sym=new QwtSymbol; + sym->setPixmap(m->getSymbol()); + sym->setPinPoint(QPointF(m->getSymbol().width()/2, m->getSymbol().height())); + auto qwtMarker = new QwtPlotMarker; + qwtMarker->setSymbol(sym); + connect(m, &TraceMarker::dataChanged, this, &TraceBodePlot::markerDataChanged); + markers[m] = qwtMarker; + markerDataChanged(m); + qwtMarker->attach(plot); + triggerReplot(); +} + +void TraceBodePlot::markerRemoved(TraceMarker *m) +{ + disconnect(m, &TraceMarker::dataChanged, this, &TraceBodePlot::markerDataChanged); + if(markers.count(m)) { + markers[m]->detach(); + delete markers[m]; + markers.erase(m); + } + triggerReplot(); +} + +void TraceBodePlot::markerDataChanged(TraceMarker *m) +{ + auto qwtMarker = markers[m]; + qwtMarker->setXValue(m->getFrequency()); + qwtMarker->setYValue(AxisTransformation(YAxis[0].type, m->getData())); + triggerReplot(); +} + diff --git a/Software/PC_Application/Traces/tracebodeplot.h b/Software/PC_Application/Traces/tracebodeplot.h new file mode 100644 index 0000000..64be9e5 --- /dev/null +++ b/Software/PC_Application/Traces/tracebodeplot.h @@ -0,0 +1,76 @@ +#ifndef TRACEBODEPLOT_H +#define TRACEBODEPLOT_H + +#include "traceplot.h" +#include +#include +#include +#include +#include + +class TraceBodePlot : public TracePlot +{ + friend class BodeplotAxisDialog; + Q_OBJECT +public: + TraceBodePlot(TraceModel &model, QWidget *parent = nullptr); + ~TraceBodePlot(); + + enum class YAxisType { + Disabled = 0, + Magnitude = 1, + Phase = 2, + VSWR = 3, + Last, + }; + + virtual void setXAxis(double min, double max) override; + void setYAxis(int axis, YAxisType type, bool log, bool autorange, double min, double max, double div); + void setXAxis(bool autorange, double min, double max, double div); + void enableTrace(Trace *t, bool enabled) override; + +protected: + virtual void updateContextMenu(); + virtual bool supported(Trace *t); + void replot() override; + +private slots: + void traceColorChanged(Trace *t); + void markerAdded(TraceMarker *m) override; + void markerRemoved(TraceMarker *m) override; + void markerDataChanged(TraceMarker *m); +private: + QString AxisTypeToName(YAxisType type); + void enableTraceAxis(Trace *t, int axis, bool enabled); + bool supported(Trace *t, YAxisType type); + void updateXAxis(); + QwtSeriesData *createQwtSeriesData(Trace &t, int axis); + + std::set tracesAxis[2]; + + class Axis { + public: + YAxisType type; + bool log; + bool autorange; + double rangeMin; + double rangeMax; + double rangeDiv; + }; + Axis YAxis[2]; + Axis XAxis; + double sweep_fmin, sweep_fmax; + + using CurveData = struct { + QwtPlotCurve *curve; + QwtSeriesData *data; + }; + + std::map curves[2]; + std::map markers; + QwtPlot *plot; + TraceMarker *selectedMarker; + QwtPlotCurve *selectedCurve; +}; + +#endif // TRACEBODEPLOT_H diff --git a/Software/PC_Application/Traces/traceeditdialog.cpp b/Software/PC_Application/Traces/traceeditdialog.cpp new file mode 100644 index 0000000..6c45ceb --- /dev/null +++ b/Software/PC_Application/Traces/traceeditdialog.cpp @@ -0,0 +1,123 @@ +#include "traceeditdialog.h" +#include "ui_traceeditdialog.h" +#include +#include + +TraceEditDialog::TraceEditDialog(Trace &t, QWidget *parent) : + QDialog(parent), + ui(new Ui::TraceEditDialog), + trace(t) +{ + ui->setupUi(this); + ui->name->setText(t.name()); + setColor(trace.color()); + + ui->GSource->setId(ui->bLive, 0); + ui->GSource->setId(ui->bFile, 1); + + if(t.isCalibration()) { + // prevent editing imported calibration traces + ui->bLive->setEnabled(false); + ui->bFile->setEnabled(false); + ui->CLiveType->setEnabled(false); + ui->CLiveParam->setEnabled(false); + } + + if(t.isTouchstone()) { + ui->bFile->click(); + ui->touchstoneImport->setFile(t.getTouchstoneFilename()); + } + + auto updateFileStatus = [this]() { + // remove all options from paramater combo box + while(ui->CParameter->count() > 0) { + ui->CParameter->removeItem(0); + } + if (ui->bFile->isChecked() && !ui->touchstoneImport->getStatus()) { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + } else { + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true); + auto touchstone = ui->touchstoneImport->getTouchstone(); + for(unsigned int i=0;iCParameter->addItem(name); + } + } + if(trace.getTouchstoneParameter() < touchstone.ports()*touchstone.ports()) { + ui->CParameter->setCurrentIndex(trace.getTouchstoneParameter()); + } else { + ui->CParameter->setCurrentIndex(0); + } + } + }; + + switch(t.liveType()) { + case Trace::LivedataType::Overwrite: ui->CLiveType->setCurrentIndex(0); break; + case Trace::LivedataType::MaxHold: ui->CLiveType->setCurrentIndex(1); break; + case Trace::LivedataType::MinHold: ui->CLiveType->setCurrentIndex(2); break; + } + + switch(t.liveParameter()) { + case Trace::LiveParameter::S11: ui->CLiveParam->setCurrentIndex(0); break; + case Trace::LiveParameter::S12: ui->CLiveParam->setCurrentIndex(1); break; + case Trace::LiveParameter::S21: ui->CLiveParam->setCurrentIndex(2); break; + case Trace::LiveParameter::S22: ui->CLiveParam->setCurrentIndex(3); break; + } + + connect(ui->GSource, qOverload(&QButtonGroup::buttonClicked), updateFileStatus); + connect(ui->touchstoneImport, &TouchstoneImport::statusChanged, updateFileStatus); + connect(ui->touchstoneImport, &TouchstoneImport::filenameChanged, updateFileStatus); + + updateFileStatus(); +} + +TraceEditDialog::~TraceEditDialog() +{ + delete ui; +} + +void TraceEditDialog::on_color_clicked() +{ + auto color = QColorDialog::getColor(trace.color(), this, "Select color", QColorDialog::DontUseNativeDialog); + setColor(color); +} + + +void TraceEditDialog::on_buttonBox_accepted() +{ + trace.setName(ui->name->text()); + if(!trace.isCalibration()) { + // only apply changes if it is not a calibration trace + if (ui->bFile->isChecked()) { + auto t = ui->touchstoneImport->getTouchstone(); + trace.fillFromTouchstone(t, ui->CParameter->currentIndex(), ui->touchstoneImport->getFilename()); + } else { + Trace::LivedataType type; + Trace::LiveParameter param; + switch(ui->CLiveType->currentIndex()) { + case 0: type = Trace::LivedataType::Overwrite; break; + case 1: type = Trace::LivedataType::MaxHold; break; + case 2: type = Trace::LivedataType::MinHold; break; + } + switch(ui->CLiveParam->currentIndex()) { + case 0: param = Trace::LiveParameter::S11; break; + case 1: param = Trace::LiveParameter::S12; break; + case 2: param = Trace::LiveParameter::S21; break; + case 3: param = Trace::LiveParameter::S22; break; + } + trace.fromLivedata(type, param); + } + } + delete this; +} + +void TraceEditDialog::setColor(QColor c) +{ + QPalette pal = ui->color->palette(); + pal.setColor(QPalette::Button, c); + ui->color->setAutoFillBackground(true); + ui->color->setPalette(pal); + ui->color->update(); + trace.setColor(c); +} diff --git a/Software/PC_Application/Traces/traceeditdialog.h b/Software/PC_Application/Traces/traceeditdialog.h new file mode 100644 index 0000000..60652dd --- /dev/null +++ b/Software/PC_Application/Traces/traceeditdialog.h @@ -0,0 +1,29 @@ +#ifndef TRACEEDITDIALOG_H +#define TRACEEDITDIALOG_H + +#include +#include "trace.h" + +namespace Ui { +class TraceEditDialog; +} + +class TraceEditDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TraceEditDialog(Trace &t, QWidget *parent = nullptr); + ~TraceEditDialog(); + +private slots: + void on_color_clicked(); + void on_buttonBox_accepted(); + +private: + void setColor(QColor c); + Ui::TraceEditDialog *ui; + Trace &trace; +}; + +#endif // TRACEEDITDIALOG_H diff --git a/Software/PC_Application/Traces/traceeditdialog.ui b/Software/PC_Application/Traces/traceeditdialog.ui new file mode 100644 index 0000000..b8aeb47 --- /dev/null +++ b/Software/PC_Application/Traces/traceeditdialog.ui @@ -0,0 +1,209 @@ + + + TraceEditDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 282 + 355 + + + + Edit Trace + + + + + + + + Name: + + + + + + + + + + Color: + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + Live Capture + + + true + + + GSource + + + + + + + From File + + + GSource + + + + + + + + + 0 + + + + + + + Type: + + + + + + + + Overwrite + + + + + Max hold + + + + + Min hold + + + + + + + + Parameter: + + + + + + + + S11 + + + + + S12 + + + + + S21 + + + + + S22 + + + + + + + + + + + + + + + + + Parameter: + + + + + + + + + + + + + + + + QDialogButtonBox::Ok + + + + + + + + TouchstoneImport + QWidget +
CustomWidgets/touchstoneimport.h
+ 1 +
+
+ + + + GSource + buttonClicked(int) + stack + setCurrentIndex(int) + + + -1 + -1 + + + 146 + 216 + + + + + + + +
diff --git a/Software/PC_Application/Traces/traceexportdialog.cpp b/Software/PC_Application/Traces/traceexportdialog.cpp new file mode 100644 index 0000000..873c926 --- /dev/null +++ b/Software/PC_Application/Traces/traceexportdialog.cpp @@ -0,0 +1,186 @@ +#include "traceexportdialog.h" +#include "ui_traceexportdialog.h" +#include +#include +#include "touchstone.h" +#include + +TraceExportDialog::TraceExportDialog(TraceModel &model, QWidget *parent) : + QDialog(parent), + ui(new Ui::TraceExportDialog), + model(model), + freqsSet(false) +{ + ui->setupUi(this); + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false); + ui->gbTraces->setLayout(new QGridLayout); + on_sbPorts_valueChanged(ui->sbPorts->value()); +} + +TraceExportDialog::~TraceExportDialog() +{ + delete ui; +} + +bool TraceExportDialog::setTrace(int portFrom, int portTo, Trace *t) +{ + if(portFrom < 0 || portFrom >= ui->sbPorts->value() || portTo < 0 || portTo >= ui->sbPorts->value()) { + // invalid port selection + return false; + } + auto c = cTraces[portTo][portFrom]; + if(t) { + for(int i=1;icount();i++) { + if(t == qvariant_cast(c->itemData(i))) { + // select this trace + c->setCurrentIndex(i); + return true; + } + } + // requested trace is not an option + return false; + } else { + // select 'none' option + c->setCurrentIndex(0); + return true; + } +} + +bool TraceExportDialog::setPortNum(int ports) +{ + if(ports < 1 || ports > 4) { + return false; + } + ui->sbPorts->setValue(ports); + return true; +} + +void TraceExportDialog::on_buttonBox_accepted() +{ + auto filename = QFileDialog::getSaveFileName(this, "Select file for exporting traces", "", "Touchstone files (*.s1p *.s2p *.s3p *.s4p)", nullptr, QFileDialog::DontUseNativeDialog); + if(filename.length() > 0) { + auto ports = ui->sbPorts->value(); + auto t = Touchstone(ports); + // add trace points to touchstone + for(unsigned int s=0;scurrentIndex() == 0) { + // missing trace, set to 0 + tData.S.push_back(0.0); + } else { + Trace *t = qvariant_cast(cTraces[i][j]->itemData(cTraces[i][j]->currentIndex())); + // extract frequency (will overwrite for each trace but all traces have the same frequency points anyway) + tData.frequency = t->sample(s).frequency; + // add S parameter from trace to touchstone + tData.S.push_back(t->sample(s).S); + } + } + } + t.AddDatapoint(tData); + } + Touchstone::Unit unit; + switch(ui->cUnit->currentIndex()) { + case 0: unit = Touchstone::Unit::Hz; break; + case 1: unit = Touchstone::Unit::kHz; break; + case 2: unit = Touchstone::Unit::MHz; break; + case 3: unit = Touchstone::Unit::GHz; break; + } + Touchstone::Format format; + switch(ui->cFormat->currentIndex()) { + case 0: format = Touchstone::Format::DBAngle; break; + case 1: format = Touchstone::Format::MagnitudeAngle; break; + case 2: format = Touchstone::Format::RealImaginary; break; + } + + t.toFile(filename.toStdString(), unit, format); + delete this; + } +} + +void TraceExportDialog::on_sbPorts_valueChanged(int ports) +{ + // remove the previous widgets + QGridLayout *layout = static_cast(ui->gbTraces->layout()); + QLayoutItem *child; + while ((child = layout->takeAt(0)) != 0) { + delete child->widget(); + delete child; + } + cTraces.clear(); + auto availableTraces = model.getTraces(); + for(int i=0;i()); + for(int j=0;jaddItem("None"); + for(auto t : availableTraces) { + 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(t)); + } + connect(c, qOverload(&QComboBox::currentIndexChanged), [=](int) { + selectionChanged(c); + }); + cTraces[i].push_back(c); + layout->addWidget(l, i, j*2); + layout->addWidget(c, i, j*2 + 1); + } + } +} + +void TraceExportDialog::selectionChanged(QComboBox *w) +{ + if(w->currentIndex() != 0 && !freqsSet) { + // the first trace has been selected, extract frequency info + Trace *t = qvariant_cast(w->itemData(w->currentIndex())); + points = t->size(); + ui->points->setText(QString::number(points)); + if(points > 0) { + lowerFreq = t->minFreq(); + upperFreq = t->maxFreq(); + ui->lowerFreq->setText(QString::number(lowerFreq)); + ui->upperFreq->setText(QString::number(upperFreq)); + } + freqsSet = true; + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(true); + // remove all trace options with incompatible frequencies + for(auto v1 : cTraces) { + for(auto c : v1) { + for(int i=1;icount();i++) { + Trace *t = qvariant_cast(c->itemData(i)); + if(t->size() != points || (points > 0 && (t->minFreq() != lowerFreq || t->maxFreq() != upperFreq))) { + // this trace is not available anymore + c->removeItem(i); + // decrement to check the next index in the next loop iteration + i--; + } + } + } + } + } else if(w->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 + return; + } + } + } + // all traces set for none + freqsSet = false; + ui->points->clear(); + ui->lowerFreq->clear(); + ui->upperFreq->clear(); + ui->buttonBox->button(QDialogButtonBox::Save)->setEnabled(false); + } +} diff --git a/Software/PC_Application/Traces/traceexportdialog.h b/Software/PC_Application/Traces/traceexportdialog.h new file mode 100644 index 0000000..a92f66c --- /dev/null +++ b/Software/PC_Application/Traces/traceexportdialog.h @@ -0,0 +1,38 @@ +#ifndef TRACEEXPORTDIALOG_H +#define TRACEEXPORTDIALOG_H + +#include +#include +#include "tracemodel.h" +#include + +namespace Ui { +class TraceExportDialog; +} + +class TraceExportDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TraceExportDialog(TraceModel &model, QWidget *parent = nullptr); + ~TraceExportDialog(); + bool setTrace(int portFrom, int portTo, Trace *t); + bool setPortNum(int ports); + +private slots: + void on_buttonBox_accepted(); + void on_sbPorts_valueChanged(int ports); + void selectionChanged(QComboBox *w); + +private: + Ui::TraceExportDialog *ui; + TraceModel &model; + std::vector> cTraces; + + unsigned int points; + double lowerFreq, upperFreq; + bool freqsSet; +}; + +#endif // TRACEEXPORTDIALOG_H diff --git a/Software/PC_Application/Traces/traceexportdialog.ui b/Software/PC_Application/Traces/traceexportdialog.ui new file mode 100644 index 0000000..c64a5a3 --- /dev/null +++ b/Software/PC_Application/Traces/traceexportdialog.ui @@ -0,0 +1,243 @@ + + + TraceExportDialog + + + + 0 + 0 + 516 + 486 + + + + Dialog + + + + + + + + + 0 + 0 + + + + Ports: + + + + + + + + 0 + 0 + + + + 1 + + + 4 + + + 2 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Traces + + + + + + + + + Touchstone Settings + + + + + + Unit: + + + + + + + + 0 + 0 + + + + 3 + + + + Hz + + + + + kHz + + + + + MHz + + + + + GHz + + + + + + + + Format: + + + + + + + + 0 + 0 + + + + 2 + + + + db/Angle + + + + + Mag/Angle + + + + + Real/Imag + + + + + + + + + + + Trace Info + + + + + + Points: + + + + + + + false + + + + + + + Lower Frequency: + + + + + + + false + + + + + + + Upper Frequency: + + + + + + + false + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Save + + + + + + + + + buttonBox + rejected() + TraceExportDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Software/PC_Application/Traces/traceimportdialog.cpp b/Software/PC_Application/Traces/traceimportdialog.cpp new file mode 100644 index 0000000..f0a91c7 --- /dev/null +++ b/Software/PC_Application/Traces/traceimportdialog.cpp @@ -0,0 +1,178 @@ +#include "traceimportdialog.h" +#include "ui_traceimportdialog.h" +#include +#include +#include +#include + +TraceImportDialog::TraceImportDialog(TraceModel &model, std::vector traces, QString prefix, QWidget *parent) : + QDialog(parent), + ui(new Ui::TraceImportDialog), + model(model) +{ + ui->setupUi(this); + tableModel = new TraceParameterModel(traces, prefix); + ui->tableView->setModel(tableModel); +} + +TraceImportDialog::~TraceImportDialog() +{ + delete ui; + delete tableModel; +} + +void TraceImportDialog::on_buttonBox_accepted() +{ + tableModel->import(model); +} + +TraceParameterModel::TraceParameterModel(std::vector traces, QString prefix, QObject *parent) + : QAbstractTableModel(parent), + traces(traces) +{ + int hue = 0; + for(auto t : traces) { + Parameter p; + p.name = prefix + t->name(); + p.color = QColor::fromHsl((hue++ * 30) % 360, 250, 128); + p.enabled = true; + p.trace = t->name(); + params.push_back(p); + } +} + +int TraceParameterModel::rowCount(const QModelIndex &) const { + return params.size(); +} + +int TraceParameterModel::columnCount(const QModelIndex &) const { + return 4; +} + +QVariant TraceParameterModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return QVariant(); + + if ((unsigned int) index.row() >= params.size()) + return QVariant(); + auto p = params[index.row()]; + if (index.column() == 0) { + if (role == Qt::DisplayRole) { + return p.trace; + } else { + return QVariant(); + } + } else if (index.column() == 1) { + if (role == Qt::CheckStateRole) { + if(p.enabled) { + return Qt::Checked; + } else { + return Qt::Unchecked; + } + } else { + return QVariant(); + } + } else if (index.column() == 2) { + if (role == Qt::BackgroundRole || role == Qt::EditRole) { + if(p.enabled) { + return p.color; + } else { + return (QColor) Qt::gray; + } + } else { + return QVariant(); + } + } else if (index.column() == 3) { + if (role == Qt::DisplayRole || role == Qt::EditRole) { + return p.name; + } else { + return QVariant(); + } + } else { + return QVariant(); + } +} + +QVariant TraceParameterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch(section) { + case 0: return "Parameter"; break; + case 1: return "Import"; break; + case 2: return "Color"; break; + case 3: return "Tracename"; break; + default: return QVariant(); break; + } + } else { + return QVariant(); + } +} + +bool TraceParameterModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if((unsigned int) index.row() >= params.size()) { + return false; + } + auto &p = params[index.row()]; + if(role == Qt::CheckStateRole && index.column()==1) { + if((Qt::CheckState)value.toInt() == Qt::Checked) { + p.enabled = true; + } else { + p.enabled = false; + } + dataChanged(this->index(index.row(),2), this->index(index.row(),3)); + return true; + } else if(role == Qt::EditRole && index.column() == 2) { + p.color = value.value(); + return true; + } else if(role == Qt::EditRole && index.column() == 3) { + p.name = value.toString(); + return true; + } + return false; +} + +Qt::ItemFlags TraceParameterModel::flags(const QModelIndex &index) const +{ + int flags = Qt::NoItemFlags; + if(index.column() == 0) { + flags |= Qt::ItemIsEnabled; + } else if(index.column() == 1) { + flags |= Qt::ItemIsUserCheckable; + flags |= Qt::ItemIsEnabled; + } else if(index.column() == 2) { + if(params[index.row()].enabled) { + flags |= Qt::ItemIsEnabled; + } + } else if(index.column() == 3) { + flags |= Qt::ItemIsEditable; + if(params[index.row()].enabled) { + flags |= Qt::ItemIsEnabled; + } + } + return (Qt::ItemFlags) flags; +} + +void TraceParameterModel::import(TraceModel &model) +{ + for(unsigned int i=0;isetColor(params[i].color); + traces[i]->setName(params[i].name); + model.addTrace(traces[i]); + } else { + delete traces[i]; + } + } +} + +void TraceImportDialog::on_tableView_doubleClicked(const QModelIndex &index) +{ + if(index.column() == 2 && tableModel->params[index.row()].enabled) { + auto initialColor = tableModel->params[index.row()].color; + auto newColor = QColorDialog::getColor(initialColor, this, "Select color", QColorDialog::DontUseNativeDialog); + if(newColor.isValid()) { + tableModel->setData(index, newColor); + } + } +} diff --git a/Software/PC_Application/Traces/traceimportdialog.h b/Software/PC_Application/Traces/traceimportdialog.h new file mode 100644 index 0000000..ff8aab8 --- /dev/null +++ b/Software/PC_Application/Traces/traceimportdialog.h @@ -0,0 +1,59 @@ +#ifndef TRACEIMPORTDIALOG_H +#define TRACEIMPORTDIALOG_H + +#include +#include "tracemodel.h" +#include + +namespace Ui { +class TraceImportDialog; +} + +class TraceParameterModel : public QAbstractTableModel +{ + Q_OBJECT + friend class TraceImportDialog; +public: + TraceParameterModel(std::vector traces, QString prefix, QObject *parent = 0); + ~TraceParameterModel(){}; + + 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; + + // adds all enabled traces to the model, deletes other traces + void import(TraceModel &model); +private: + class Parameter { + public: + bool enabled; + QString trace; + QString name; + QColor color; + }; + std::vector params; + std::vector traces; +}; + +class TraceImportDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TraceImportDialog(TraceModel &model, std::vector traces, QString prefix = QString(), QWidget *parent = nullptr); + ~TraceImportDialog(); + +private slots: + void on_buttonBox_accepted(); + void on_tableView_doubleClicked(const QModelIndex &index); + +private: + Ui::TraceImportDialog *ui; + TraceModel &model; + TraceParameterModel *tableModel; +}; + +#endif // TRACEIMPORTDIALOG_H diff --git a/Software/PC_Application/Traces/traceimportdialog.ui b/Software/PC_Application/Traces/traceimportdialog.ui new file mode 100644 index 0000000..9a51ce1 --- /dev/null +++ b/Software/PC_Application/Traces/traceimportdialog.ui @@ -0,0 +1,83 @@ + + + TraceImportDialog + + + + 0 + 0 + 396 + 420 + + + + Import Traces + + + + + + false + + + 21 + + + 80 + + + true + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + TraceImportDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + TraceImportDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Software/PC_Application/Traces/tracemarker.cpp b/Software/PC_Application/Traces/tracemarker.cpp new file mode 100644 index 0000000..4314e8b --- /dev/null +++ b/Software/PC_Application/Traces/tracemarker.cpp @@ -0,0 +1,122 @@ +#include "tracemarker.h" +#include + +TraceMarker::TraceMarker() + : parentTrace(nullptr), + frequency(1000000000), + number(1), + data(0) +{ + +} + +TraceMarker::~TraceMarker() +{ + if(parentTrace) { + parentTrace->removeMarker(this); + } + emit deleted(this); +} + +void TraceMarker::assignTrace(Trace *t) +{ + if(parentTrace) { + // remove connection from previous parent trace + parentTrace->removeMarker(this); + disconnect(parentTrace, &Trace::deleted, this, &TraceMarker::parentTraceDeleted); + disconnect(parentTrace, &Trace::dataChanged, this, &TraceMarker::traceDataChanged); + disconnect(parentTrace, &Trace::colorChanged, this, &TraceMarker::updateSymbol); + } + parentTrace = t; + connect(parentTrace, &Trace::deleted, this, &TraceMarker::parentTraceDeleted); + connect(parentTrace, &Trace::dataChanged, this, &TraceMarker::traceDataChanged); + connect(parentTrace, &Trace::colorChanged, this, &TraceMarker::updateSymbol); + constrainFrequency(); + updateSymbol(); + parentTrace->addMarker(this); +} + +Trace *TraceMarker::trace() +{ + return parentTrace; +} + +QString TraceMarker::readableData() +{ + auto db = 20*log10(abs(data)); + auto phase = arg(data); + return QString::number(db, 'g', 4) + "db@" + QString::number(phase*180/M_PI, 'g', 4); +} + +void TraceMarker::setFrequency(double freq) +{ + frequency = freq; + constrainFrequency(); +} + +void TraceMarker::parentTraceDeleted(Trace *t) +{ + if(t == parentTrace) { + delete this; + } +} + +void TraceMarker::traceDataChanged() +{ + // some data of the parent trace changed, check if marker data also changed + auto tracedata = parentTrace->getData(frequency); + if(tracedata != data) { + data = tracedata; + emit dataChanged(this); + } +} + +void TraceMarker::updateSymbol() +{ + constexpr int width = 15, height = 15; + symbol = QPixmap(width, height); + symbol.fill(Qt::transparent); + QPainter p(&symbol); + p.setRenderHint(QPainter::Antialiasing); + QPointF points[] = {QPointF(0,0),QPointF(width,0),QPointF(width/2,height)}; + auto traceColor = parentTrace->color(); + p.setPen(traceColor); + p.setBrush(traceColor); + p.drawConvexPolygon(points, 3); + auto brightness = traceColor.redF() * 0.299 + traceColor.greenF() * 0.587 + traceColor.blueF() * 0.114; + p.setPen((brightness > 0.6) ? Qt::black : Qt::white); + p.drawText(QRectF(0,0,width, height*2.0/3.0), Qt::AlignCenter, QString::number(number)); +} + +void TraceMarker::constrainFrequency() +{ + if(parentTrace && parentTrace->size() > 0) { + if(frequency > parentTrace->maxFreq()) { + frequency = parentTrace->maxFreq(); + } else if(frequency < parentTrace->minFreq()) { + frequency = parentTrace->minFreq(); + } + traceDataChanged(); + } +} + +int TraceMarker::getNumber() const +{ + return number; +} + +std::complex TraceMarker::getData() const +{ + return data; +} + +QPixmap &TraceMarker::getSymbol() +{ + return symbol; +} + +double TraceMarker::getFrequency() const +{ + return frequency; +} + diff --git a/Software/PC_Application/Traces/tracemarker.h b/Software/PC_Application/Traces/tracemarker.h new file mode 100644 index 0000000..2ac3563 --- /dev/null +++ b/Software/PC_Application/Traces/tracemarker.h @@ -0,0 +1,45 @@ +#ifndef TRACEMARKER_H +#define TRACEMARKER_H + +#include +#include +#include "trace.h" + +class TraceMarker : public QObject +{ + friend class TraceMarkerModel; + Q_OBJECT; +public: + TraceMarker(); + ~TraceMarker(); + void assignTrace(Trace *t); + Trace* trace(); + QString readableData(); + + double getFrequency() const; + std::complex getData() const; + + QPixmap& getSymbol(); + + int getNumber() const; + +public slots: + void setFrequency(double freq); +signals: + void deleted(TraceMarker *m); + void dataChanged(TraceMarker *m); + +private slots: + void parentTraceDeleted(Trace *t); + void traceDataChanged(); + void updateSymbol(); +private: + void constrainFrequency(); + Trace *parentTrace; + double frequency; + int number; + std::complex data; + QPixmap symbol; +}; + +#endif // TRACEMARKER_H diff --git a/Software/PC_Application/Traces/tracemarkermodel.cpp b/Software/PC_Application/Traces/tracemarkermodel.cpp new file mode 100644 index 0000000..b594cd6 --- /dev/null +++ b/Software/PC_Application/Traces/tracemarkermodel.cpp @@ -0,0 +1,228 @@ +#include "tracemarkermodel.h" +#include "unit.h" +#include +#include + +TraceMarkerModel::TraceMarkerModel(TraceModel &model, QObject *parent) + : QAbstractTableModel(parent), + model(model) +{ + markers.clear(); +} + +TraceMarker *TraceMarkerModel::createDefaultMarker() +{ + // find lowest free number + int number = 0; + bool used; + do { + number++; + used = false; + for(auto m : markers) { + if(m->number == number) { + used = true; + break; + } + } + } while (used); + auto marker = new TraceMarker(); + marker->number = number; + marker->frequency = 2150000000; + marker->assignTrace(model.trace(0)); + return marker; +} + +void TraceMarkerModel::addMarker(TraceMarker *t) +{ + beginInsertRows(QModelIndex(), markers.size(), markers.size()); + markers.push_back(t); + endInsertRows(); + connect(t, &TraceMarker::dataChanged, this, &TraceMarkerModel::markerDataChanged); + connect(t, &TraceMarker::deleted, this, qOverload(&TraceMarkerModel::removeMarker)); + emit markerAdded(t); +} + +void TraceMarkerModel::removeMarker(unsigned int index, bool delete_marker) +{ + if (index < markers.size()) { + beginRemoveRows(QModelIndex(), index, index); + if(delete_marker) { + delete markers[index]; + } + markers.erase(markers.begin() + index); + endRemoveRows(); + } +} + +void TraceMarkerModel::removeMarker(TraceMarker *m) +{ + auto it = std::find(markers.begin(), markers.end(), m); + if(it != markers.end()) { + removeMarker(it - markers.begin(), false); + } +} + +void TraceMarkerModel::markerDataChanged(TraceMarker *) +{ + emit dataChanged(index(0, ColIndexFreq), index(markers.size()-1, ColIndexData)); +} + +TraceMarker *TraceMarkerModel::marker(int index) +{ + return markers.at(index); +} + +int TraceMarkerModel::rowCount(const QModelIndex &) const +{ + return markers.size(); +} + +int TraceMarkerModel::columnCount(const QModelIndex &) const +{ + return 4; +} + +QVariant TraceMarkerModel::data(const QModelIndex &index, int role) const +{ + auto marker = markers[index.row()]; + switch(index.column()) { + case ColIndexNumber: + switch(role) { + case Qt::DisplayRole: return QVariant((unsigned int)marker->number); break; + } + case ColIndexTrace: + switch(role) { + case Qt::DisplayRole: + if(marker->parentTrace) { + return marker->parentTrace->name(); + } + break; + } + case ColIndexFreq: + switch(role) { + case Qt::DisplayRole: return Unit::ToString(marker->frequency, "Hz", " kMG", 6); break; + } + case ColIndexData: + switch(role) { + case Qt::DisplayRole: return marker->readableData(); break; + } + break; + } + return QVariant(); +} + +QVariant TraceMarkerModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { + switch(section) { + case ColIndexNumber: return "#"; break; + case ColIndexTrace: return "Trace"; break; + case ColIndexFreq: return "Frequency"; break; + case ColIndexData: return "Data"; break; + default: return QVariant(); break; + } + } else { + return QVariant(); + } +} + +bool TraceMarkerModel::setData(const QModelIndex &index, const QVariant &value, int) +{ + if((unsigned int) index.row() >= markers.size()) { + return false; + } + auto m = markers[index.row()]; + switch(index.column()) { + case ColIndexNumber: { + bool convertOk; + unsigned int number; + number = value.toUInt(&convertOk); + if(convertOk) { + m->number = number; + return true; + } + break; + } + case ColIndexTrace: { + auto trace = qvariant_cast(value); + m->assignTrace(trace); + } + break; + case ColIndexFreq: { + auto newval = Unit::FromString(value.toString(), "Hz", " kMG"); + if(!qIsNaN(newval)) { + m->setFrequency(newval); + } + } + break; + } + + return false; +} + +Qt::ItemFlags TraceMarkerModel::flags(const QModelIndex &index) const +{ + int flags = Qt::NoItemFlags; + switch(index.column()) { + case ColIndexNumber: flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable; break; + case ColIndexTrace: flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable; break; + case ColIndexFreq: flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable; break; + case ColIndexData: flags |= Qt::ItemIsEnabled; break; + } + return (Qt::ItemFlags) flags; +} + +std::vector TraceMarkerModel::getMarker() +{ + return markers; +} + +std::vector TraceMarkerModel::getMarker(Trace *t) +{ + std::vector attachedMarkers; + for(auto m : markers) { + if(m->parentTrace == t) { + attachedMarkers.push_back(m); + } + } + return attachedMarkers; +} + +TraceModel &TraceMarkerModel::getModel() +{ + return model; +} + +QWidget *TraceChooserDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &index) const +{ + auto model = (TraceMarkerModel*) index.model(); + auto c = new QComboBox(parent); + connect(c, qOverload(&QComboBox::currentIndexChanged), [c](int) { + c->clearFocus(); + }); + auto traces = model->getModel().getTraces(); + for(auto t : traces) { + c->addItem(t->name(), QVariant::fromValue(t)); + } + return c; +} + +void TraceChooserDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + auto model = (TraceMarkerModel*) index.model(); + auto marker = model->getMarker()[index.row()]; + auto c = (QComboBox*) editor; + for(int i=0;icount();i++) { + if(qvariant_cast(c->itemData(i)) == marker->trace()) { + c->setCurrentIndex(i); + return; + } + } +} + +void TraceChooserDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + auto markerModel = (TraceMarkerModel*) model; + auto c = (QComboBox*) editor; + markerModel->setData(index, c->itemData(c->currentIndex())); +} diff --git a/Software/PC_Application/Traces/tracemarkermodel.h b/Software/PC_Application/Traces/tracemarkermodel.h new file mode 100644 index 0000000..2774d07 --- /dev/null +++ b/Software/PC_Application/Traces/tracemarkermodel.h @@ -0,0 +1,61 @@ +#ifndef TRACEMARKERMODEL_H +#define TRACEMARKERMODEL_H + +#include +#include "tracemarker.h" +#include +#include "tracemodel.h" +#include + +class TraceChooserDelegate : public QItemDelegate +{ + Q_OBJECT; + QWidget *createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; +}; + +class TraceMarkerModel : public QAbstractTableModel +{ + Q_OBJECT +public: + TraceMarkerModel(TraceModel &model, QObject *parent = 0); + + enum { + ColIndexNumber = 0, + ColIndexTrace = 1, + ColIndexFreq = 2, + ColIndexData = 3, + }; + + 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; + + TraceMarker* createDefaultMarker(); + TraceMarker *marker(int index); + std::vector getMarker(); + std::vector getMarker(Trace *t); + TraceModel& getModel(); + +public slots: + void addMarker(TraceMarker *t); + void removeMarker(unsigned int index, bool delete_marker = true); + void removeMarker(TraceMarker *m); + + +signals: + void markerAdded(TraceMarker *t); + +private slots: + void markerDataChanged(TraceMarker *m); +private: + std::vector markers; + TraceModel &model; + +}; + +#endif // TRACEMARKERMODEL_H diff --git a/Software/PC_Application/Traces/tracemodel.cpp b/Software/PC_Application/Traces/tracemodel.cpp new file mode 100644 index 0000000..5015a7e --- /dev/null +++ b/Software/PC_Application/Traces/tracemodel.cpp @@ -0,0 +1,137 @@ +#include "tracemodel.h" +#include + +using namespace std; + +TraceModel::TraceModel(QObject *parent) + : QAbstractTableModel(parent) +{ + traces.clear(); +} + +void TraceModel::addTrace(Trace *t) +{ + beginInsertRows(QModelIndex(), traces.size(), traces.size()); + traces.push_back(t); + endInsertRows(); + emit traceAdded(t); +} + +void TraceModel::removeTrace(unsigned int index) +{ + if (index < traces.size()) { + beginRemoveRows(QModelIndex(), index, index); + auto trace = traces[index]; + delete trace; + traces.erase(traces.begin() + index); + endRemoveRows(); + emit traceRemoved(trace); + } +} + +Trace *TraceModel::trace(unsigned int index) +{ + return traces.at(index); +} + +void TraceModel::toggleVisibility(unsigned int index) +{ + if (index < traces.size()) { + traces[index]->setVisible(!traces[index]->isVisible()); + emit dataChanged(createIndex(index, 0), createIndex(index, 0)); + } +} + +void TraceModel::togglePause(unsigned int index) +{ + if (index < traces.size()) { + if(traces[index]->isPaused()) { + traces[index]->resume(); + } else { + traces[index]->pause(); + } + emit dataChanged(createIndex(index, 1), createIndex(index, 1)); + } +} + +int TraceModel::rowCount(const QModelIndex &) const +{ + return traces.size(); +} + +int TraceModel::columnCount(const QModelIndex &) const +{ + return 3; +} + +QVariant TraceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if ((unsigned int) index.row() >= traces.size()) + return QVariant(); + if (index.column() == 0) { + if (role == Qt::DecorationRole) { + if (traces[index.row()]->isVisible()) { + return QIcon(":/icons/visible.svg"); + } else { + return QIcon(":/icons/invisible.svg"); + } + } else { + return QVariant(); + } + } else if (index.column() == 1) { + if (role == Qt::DecorationRole && !traces[index.row()]->isTouchstone()) { + if (traces[index.row()]->isPaused()) { + return QIcon(":/icons/pause.svg"); + } else { + return QIcon(":/icons/play.svg"); + } + } else { + return QVariant(); + } + } else if (index.column() == 2) { + if (role == Qt::DisplayRole) { + return traces[index.row()]->name(); + } else if (role == Qt::ForegroundRole) { + return traces[index.row()]->color(); + } else { + return QVariant(); + } + } else { + return QVariant(); + } +} + +std::vector TraceModel::getTraces() +{ + return traces; +} + +void TraceModel::clearVNAData() +{ + for(auto t : traces) { + if (!t->isTouchstone()) { + // this trace is fed from live data + t->clear(); + } + } +} + +void TraceModel::addVNAData(Protocol::Datapoint d) +{ + for(auto t : traces) { + if (t->isLive()) { + Trace::Data td; + td.frequency = d.frequency; + switch(t->liveParameter()) { + case Trace::LiveParameter::S11: td.S = complex(d.real_S11, d.imag_S11); break; + case Trace::LiveParameter::S12: td.S = complex(d.real_S12, d.imag_S12); break; + case Trace::LiveParameter::S21: td.S = complex(d.real_S21, d.imag_S21); break; + case Trace::LiveParameter::S22: td.S = complex(d.real_S22, d.imag_S22); break; + } + t->addData(td); + } + } +} diff --git a/Software/PC_Application/Traces/tracemodel.h b/Software/PC_Application/Traces/tracemodel.h new file mode 100644 index 0000000..0ae29b6 --- /dev/null +++ b/Software/PC_Application/Traces/tracemodel.h @@ -0,0 +1,38 @@ +#ifndef TRACEMODEL_H +#define TRACEMODEL_H + +#include +#include "trace.h" +#include +#include "Device/device.h" + +class TraceModel : public QAbstractTableModel +{ + Q_OBJECT +public: + TraceModel(QObject *parent = 0); + + void addTrace(Trace *t); + void removeTrace(unsigned int index); + Trace *trace(unsigned int index); + void toggleVisibility(unsigned int index); + void togglePause(unsigned int index); + + 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; + + std::vector getTraces(); +signals: + void traceAdded(Trace *t); + void traceRemoved(Trace *t); + +public slots: + void clearVNAData(); + void addVNAData(Protocol::Datapoint d); + +private: + std::vector traces; +}; + +#endif // TRACEMODEL_H diff --git a/Software/PC_Application/Traces/traceplot.cpp b/Software/PC_Application/Traces/traceplot.cpp new file mode 100644 index 0000000..552a867 --- /dev/null +++ b/Software/PC_Application/Traces/traceplot.cpp @@ -0,0 +1,145 @@ +#include "traceplot.h" + +const QColor TracePlot::Background = QColor(0,0,0); +const QColor TracePlot::Border = QColor(255,255,255); +const QColor TracePlot::Divisions = QColor(255,255,255); +#include "tracemarker.h" + +std::set TracePlot::plots; + +TracePlot::TracePlot(QWidget *parent) : QWidget(parent) +{ + contextmenu = new QMenu(); + markedForDeletion = false; + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + lastUpdate = QTime::currentTime(); + plots.insert(this); +} + +TracePlot::~TracePlot() +{ + plots.erase(this); + delete contextmenu; +} + +void TracePlot::enableTrace(Trace *t, bool enabled) +{ + if(traces[t] != enabled) { + traces[t] = enabled; + if(enabled) { + // connect signals + connect(t, &Trace::dataChanged, this, &TracePlot::triggerReplot); + connect(t, &Trace::visibilityChanged, this, &TracePlot::triggerReplot); + connect(t, &Trace::markerAdded, this, &TracePlot::markerAdded); + connect(t, &Trace::markerRemoved, this, &TracePlot::markerRemoved); + } else { + // disconnect from notifications + disconnect(t, &Trace::dataChanged, this, &TracePlot::triggerReplot); + disconnect(t, &Trace::visibilityChanged, this, &TracePlot::triggerReplot); + disconnect(t, &Trace::markerAdded, this, &TracePlot::markerAdded); + disconnect(t, &Trace::markerRemoved, this, &TracePlot::markerRemoved); + } + updateContextMenu(); + triggerReplot(); + } +} + +void TracePlot::mouseDoubleClickEvent(QMouseEvent *) { + emit doubleClicked(this); +} + +void TracePlot::UpdateSpan(double fmin, double fmax) +{ + for(auto p : plots) { + p->setXAxis(fmin, fmax); + } +} + +void TracePlot::initializeTraceInfo(TraceModel &model) +{ + // Populate already present traces + auto tvect = model.getTraces(); + for(auto t : tvect) { + newTraceAvailable(t); + } + + // connect notification of traces added at later point + connect(&model, &TraceModel::traceAdded, this, &TracePlot::newTraceAvailable); +} + +void TracePlot::contextMenuEvent(QContextMenuEvent *event) +{ + contextmenu->exec(event->globalPos()); + if(markedForDeletion) { + emit deleted(this); + delete this; + } +} + +void TracePlot::updateContextMenu() +{ + contextmenu->clear(); + contextmenu->addSection("Traces"); + // Populate context menu + for(auto t : traces) { + auto action = new QAction(t.first->name()); + action->setCheckable(true); + if(t.second) { + action->setChecked(true); + } + connect(action, &QAction::toggled, [=](bool active) { + enableTrace(t.first, active); + }); + contextmenu->addAction(action); + } + contextmenu->addSeparator(); + auto close = new QAction("Close"); + contextmenu->addAction(close); + connect(close, &QAction::triggered, [=]() { + markedForDeletion = true; + }); +} + +std::set TracePlot::getPlots() +{ + return plots; +} + +void TracePlot::newTraceAvailable(Trace *t) +{ + if(supported(t)) { + traces[t] = false; + connect(t, &Trace::deleted, this, &TracePlot::traceDeleted); + connect(t, &Trace::nameChanged, this, &TracePlot::updateContextMenu); + connect(t, &Trace::typeChanged, this, &TracePlot::updateContextMenu); + } + updateContextMenu(); +} + +void TracePlot::traceDeleted(Trace *t) +{ + enableTrace(t, false); + traces.erase(t); + updateContextMenu(); + triggerReplot(); +} + +void TracePlot::triggerReplot() +{ + auto now = QTime::currentTime(); + if (lastUpdate.msecsTo(now) >= MinUpdateInterval) { + replot(); + lastUpdate = now; + } +} + +void TracePlot::markerAdded(TraceMarker *m) +{ + connect(m, &TraceMarker::dataChanged, this, &TracePlot::triggerReplot); + triggerReplot(); +} + +void TracePlot::markerRemoved(TraceMarker *) +{ + triggerReplot(); +} diff --git a/Software/PC_Application/Traces/traceplot.h b/Software/PC_Application/Traces/traceplot.h new file mode 100644 index 0000000..c6806e5 --- /dev/null +++ b/Software/PC_Application/Traces/traceplot.h @@ -0,0 +1,55 @@ +#ifndef TRACEPLOT_H +#define TRACEPLOT_H + +#include +#include "tracemodel.h" +#include +#include +#include + +class TracePlot : public QWidget +{ + Q_OBJECT +public: + TracePlot( QWidget *parent = nullptr); + ~TracePlot(); + + virtual void enableTrace(Trace *t, bool enabled); + void mouseDoubleClickEvent(QMouseEvent *event) override; + virtual void setXAxis(double min, double max){Q_UNUSED(min);Q_UNUSED(max)}; + + static std::set getPlots(); + static void UpdateSpan(double fmin, double fmax); + +signals: + void doubleClicked(QWidget *w); + void deleted(TracePlot*); + +protected: + static const QColor Background;// = QColor(0,0,0); + static const QColor Border;// = QColor(255,255,255); + static const QColor Divisions;// = QColor(255,255,255); + static constexpr int MinUpdateInterval = 100; + // need to be called in derived class constructor + void initializeTraceInfo(TraceModel &model); + void contextMenuEvent(QContextMenuEvent *event) override; + virtual void updateContextMenu(); + virtual bool supported(Trace *t) = 0; + virtual void replot(){}; + std::map traces; + QMenu *contextmenu; + QTime lastUpdate; + bool markedForDeletion; + + static std::set plots; + +protected slots: + void newTraceAvailable(Trace *t); + void traceDeleted(Trace *t); + void triggerReplot(); + virtual void markerAdded(TraceMarker *m); + virtual void markerRemoved(TraceMarker *m); + +}; + +#endif // TRACEPLOT_H diff --git a/Software/PC_Application/Traces/tracesmithchart.cpp b/Software/PC_Application/Traces/tracesmithchart.cpp new file mode 100644 index 0000000..5d56041 --- /dev/null +++ b/Software/PC_Application/Traces/tracesmithchart.cpp @@ -0,0 +1,184 @@ +#include "tracesmithchart.h" +#include +#include +#include +#include "tracemarker.h" +#include + +using namespace std; + +TraceSmithChart::TraceSmithChart(TraceModel &model, QWidget *parent) + : TracePlot(parent) +{ + chartLinesPen = QPen(palette().windowText(), 0.75); + thinPen = QPen(palette().windowText(), 0.25); + textPen = QPen(palette().windowText(), 0.25); + pointDataPen = QPen(QColor("red"), 4.0, Qt::SolidLine, Qt::RoundCap); + lineDataPen = QPen(QColor("blue"), 1.0); + initializeTraceInfo(model); +} + +QPoint TraceSmithChart::plotToPixel(std::complex S) +{ + QPoint ret; + ret.setX(S.real() * plotToPixelXScale + plotToPixelXOffset); + ret.setY(S.imag() * plotToPixelYScale + plotToPixelYOffset); + return ret; +} + +std::complex TraceSmithChart::pixelToPlot(const QPoint &pos) +{ + return complex((pos.x() - plotToPixelXOffset) / plotToPixelXScale, (pos.y() - plotToPixelYOffset) / plotToPixelYScale); +} + +void TraceSmithChart::mousePressEvent(QMouseEvent *event) +{ + auto clickPoint = event->pos(); + unsigned int closestDistance = numeric_limits::max(); + TraceMarker *closestMarker = nullptr; + for(auto t : traces) { + auto markers = t.first->getMarkers(); + for(auto m : markers) { + auto S = m->getData(); + auto markerPoint = plotToPixel(S); + auto yDiff = abs(markerPoint.y() - clickPoint.y()); + auto xDiff = abs(markerPoint.x() - clickPoint.x()); + unsigned int distance = xDiff * xDiff + yDiff * yDiff; + if(distance < closestDistance) { + closestDistance = distance; + closestMarker = m; + } + } + } + if(closestDistance <= 400) { + selectedMarker = closestMarker; + } else { + selectedMarker = nullptr; + } +} + +void TraceSmithChart::mouseMoveEvent(QMouseEvent *event) +{ + if(selectedMarker) { + auto t = selectedMarker->trace(); + auto mouseS = pixelToPlot(event->pos()); + auto samples = t->size(); + double closestDistance = numeric_limits::max(); + unsigned int closestIndex = 0; + for(unsigned int i=0;isample(i); + auto distance = norm(data.S - mouseS); + if(distance < closestDistance) { + closestDistance = distance; + closestIndex = i; + } + } + selectedMarker->setFrequency(t->sample(closestIndex).frequency); + } +} + +void TraceSmithChart::draw(QPainter * painter, double width_factor) { + painter->setPen(QPen(1.0 * width_factor)); + painter->setBrush(palette().windowText()); + painter->setRenderHint(QPainter::Antialiasing); + +// // Display parameter name +// QFont font = painter->font(); +// font.setPixelSize(48); +// font.setBold(true); +// painter->setFont(font); +// painter->drawText(-512, -512, title); + + // Outer circle + painter->setPen(QPen(Border, 1.5 * width_factor)); + QRectF rectangle(-smithCoordMax, -smithCoordMax, 2*smithCoordMax, 2*smithCoordMax); + painter->drawArc(rectangle, 0, 5760); + + constexpr int Circles = 6; + painter->setPen(QPen(Divisions, 0.5 * width_factor, Qt::DashLine)); + for(int i=1;idrawArc(rectangle, 0, 5760); + } + + painter->drawLine(-smithCoordMax, 0, smithCoordMax, 0); + constexpr std::array impedanceLines = {10, 25, 50, 100, 250}; + for(auto z : impedanceLines) { + z /= ReferenceImpedance; + auto radius = smithCoordMax * 1.0/z; + double span = M_PI - 2 * atan(radius/smithCoordMax); + span *= 5760 / (2 * M_PI); + QRectF rectangle(smithCoordMax - radius, -2*radius, 2 * radius, 2 * radius); + painter->drawArc(rectangle, 4320 - span, span); + rectangle = QRectF(smithCoordMax - radius, 0, 2 * radius, 2 * radius); + painter->drawArc(rectangle, 1440, span); + } + + for(auto t : traces) { + if(!t.second) { + // trace not enabled in plot + continue; + } + auto trace = t.first; + if(!trace->isVisible()) { + // trace marked invisible + continue; + } + painter->setPen(QPen(trace->color(), 1.5 * width_factor)); + int nPoints = trace->size(); + for(int i=1;isample(i-1).S; + auto now = trace->sample(i).S; + if(isnan(now.real())) { + break; + } + // scale to size of smith diagram + last *= smithCoordMax; + now *= smithCoordMax; + // draw line + painter->drawLine(std::real(last), -std::imag(last), std::real(now), -std::imag(now)); + } + auto markers = t.first->getMarkers(); + for(auto m : markers) { + auto coords = m->getData(); + coords *= smithCoordMax; + auto symbol = m->getSymbol(); + symbol = symbol.scaled(symbol.width()*width_factor, symbol.height()*width_factor); + painter->drawPixmap(coords.real() - symbol.width()/2, -coords.imag() - symbol.height(), symbol); + } + } +} + +void TraceSmithChart::replot() +{ + update(); +} + +void TraceSmithChart::paintEvent(QPaintEvent * /* the event */) +{ + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setBackground(QBrush(Background)); + painter.fillRect(0, 0, width(), height(), QBrush(Background)); + + double side = qMin(width(), height()) * screenUsage; + + painter.setViewport((width()-side)/2, (height()-side)/2, side, side); + painter.setWindow(-smithCoordMax, -smithCoordMax, 2*smithCoordMax, 2*smithCoordMax); + + plotToPixelXOffset = width()/2; + plotToPixelYOffset = height()/2; + plotToPixelXScale = side/2; + plotToPixelYScale = -side/2; + + draw(&painter, 2*smithCoordMax/side); +} + +bool TraceSmithChart::supported(Trace *t) +{ + if(t->isReflection()) { + return true; + } else { + return false; + } +} diff --git a/Software/PC_Application/Traces/tracesmithchart.h b/Software/PC_Application/Traces/tracesmithchart.h new file mode 100644 index 0000000..cb5931f --- /dev/null +++ b/Software/PC_Application/Traces/tracesmithchart.h @@ -0,0 +1,44 @@ +#ifndef TRACESMITHCHART_H +#define TRACESMITHCHART_H + +#include "traceplot.h" +#include + +class TraceSmithChart : public TracePlot +{ + Q_OBJECT +public: + TraceSmithChart(TraceModel &model, QWidget *parent = 0); + +protected: + static constexpr double ReferenceImpedance = 50.0; + static constexpr double screenUsage = 0.9; + static constexpr int smithCoordMax = 4096; + + QPoint plotToPixel(std::complex S); + std::complex pixelToPlot(const QPoint &pos); + + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + + bool supported(Trace *t) override; + void draw(QPainter * painter, double width_factor); + void replot() override; + QPen textPen; + QPen chartLinesPen; + QPen thinPen; + QPen pointDataPen; + QPen lineDataPen; + + /// Path for the thin arcs + QPainterPath thinArcsPath; + /// Path for the thick arcs + QPainterPath thickArcsPath; + + double plotToPixelXOffset, plotToPixelXScale; + double plotToPixelYOffset, plotToPixelYScale; + TraceMarker *selectedMarker; +}; + +#endif // TRACESMITHCHART_H diff --git a/Software/PC_Application/Traces/tracewidget.cpp b/Software/PC_Application/Traces/tracewidget.cpp new file mode 100644 index 0000000..b0d6fba --- /dev/null +++ b/Software/PC_Application/Traces/tracewidget.cpp @@ -0,0 +1,121 @@ +#include "tracewidget.h" +#include "ui_tracewidget.h" +#include "trace.h" +#include +#include "traceeditdialog.h" +#include "traceimportdialog.h" +#include "traceexportdialog.h" +#include + +TraceWidget::TraceWidget(TraceModel &model, QWidget *parent) : + QWidget(parent), + ui(new Ui::TraceWidget), + model(model) +{ + ui->setupUi(this); + ui->view->setModel(&model); + ui->view->setAutoScroll(false); + installEventFilter(this); + createCount = 0; +} + +TraceWidget::~TraceWidget() +{ + delete ui; +} + +void TraceWidget::on_add_clicked() +{ + createCount++; + auto t = new Trace("Trace #"+QString::number(createCount)); + t->setColor(QColor::fromHsl((createCount * 50) % 360, 250, 128)); + model.addTrace(t); +} + +void TraceWidget::on_remove_clicked() +{ + model.removeTrace(ui->view->currentIndex().row()); +} + +bool TraceWidget::eventFilter(QObject *, QEvent *event) +{ + if (event->type() == QEvent::KeyPress) { + int key = static_cast(event)->key(); + if(key == Qt::Key_Escape) { + ui->view->clearSelection(); + return true; + } else if(key == Qt::Key_Delete) { + model.removeTrace(ui->view->currentIndex().row()); + return true; + } + } + return false; +} + +void TraceWidget::on_edit_clicked() +{ + if(ui->view->currentIndex().isValid()) { + auto edit = new TraceEditDialog(*model.trace(ui->view->currentIndex().row())); + edit->show(); + } +} + +void TraceWidget::on_view_doubleClicked(const QModelIndex &index) +{ + if(index.column() == 2) { + auto edit = new TraceEditDialog(*model.trace(index.row())); + edit->show(); + } +} + +void TraceWidget::on_view_clicked(const QModelIndex &index) +{ + if(index.column()==0) { + model.toggleVisibility(index.row()); + } else if(index.column()==1) { + model.togglePause(index.row()); + } +} + +void TraceWidget::on_bImport_clicked() +{ + auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", "", "Touchstone files (*.s1p *.s2p *.s3p *.s4p)", nullptr, QFileDialog::DontUseNativeDialog); + if (filename.length() > 0) { + auto t = Touchstone::fromFile(filename.toStdString()); + std::vector traces; + for(unsigned int i=0;ifillFromTouchstone(t, i, filename); + unsigned int sink = i / t.ports() + 1; + unsigned int source = i % t.ports() + 1; + trace->setName("S"+QString::number(sink)+QString::number(source)); + traces.push_back(trace); + } + // contruct prefix from filename + // remove any directory names (keep only the filename itself) + int lastSlash = qMax(filename.lastIndexOf('/'), filename.lastIndexOf('\\')); + if(lastSlash != -1) { + filename.remove(0, lastSlash + 1); + } + // remove file type + filename.truncate(filename.indexOf('.')); + auto i = new TraceImportDialog(model, traces, filename+"_"); + i->show(); + } +} + +void TraceWidget::on_bExport_clicked() +{ + auto e = new TraceExportDialog(model); + // Attempt to set default traces (this will result in correctly populated + // 2 port export if the initial 4 traces have not been modified) + e->setPortNum(2); + auto traces = model.getTraces(); + for(unsigned int i=0;i<4;i++) { + if(i >= traces.size()) { + break; + } + e->setTrace(i%2, i/2, traces[i]); + } + e->show(); +} diff --git a/Software/PC_Application/Traces/tracewidget.h b/Software/PC_Application/Traces/tracewidget.h new file mode 100644 index 0000000..100758e --- /dev/null +++ b/Software/PC_Application/Traces/tracewidget.h @@ -0,0 +1,41 @@ +#ifndef TRACEWIDGET_H +#define TRACEWIDGET_H + +#include +#include "tracemodel.h" + +namespace Ui { +class TraceWidget; +} + +class TraceWidget : public QWidget +{ + Q_OBJECT + +public: + explicit TraceWidget(TraceModel &model, QWidget *parent = nullptr); + ~TraceWidget(); + +public slots: + void on_add_clicked(); + +private slots: + void on_remove_clicked(); + void on_edit_clicked(); + + void on_view_doubleClicked(const QModelIndex &index); + + void on_view_clicked(const QModelIndex &index); + + void on_bImport_clicked(); + + void on_bExport_clicked(); + +private: + bool eventFilter(QObject *obj, QEvent *event) override; + Ui::TraceWidget *ui; + TraceModel &model; + int createCount; +}; + +#endif // TRACEWIDGET_H diff --git a/Software/PC_Application/Traces/tracewidget.ui b/Software/PC_Application/Traces/tracewidget.ui new file mode 100644 index 0000000..e0ddffc --- /dev/null +++ b/Software/PC_Application/Traces/tracewidget.ui @@ -0,0 +1,179 @@ + + + TraceWidget + + + + 0 + 0 + 206 + 268 + + + + + 0 + 0 + + + + Form + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::SelectRows + + + QAbstractItemView::ScrollPerItem + + + false + + + false + + + 0 + + + 21 + + + true + + + false + + + 21 + + + + + + + + + + 0 + 0 + + + + Add + + + + + + + + + + 16 + 16 + + + + + + + + + 0 + 0 + + + + Delete + + + + + + + + + + + + + Import + + + + + + + :/icons/import.svg:/icons/import.svg + + + + + + + Export + + + + + + + :/icons/export.svg:/icons/export.svg + + + + + + + + 0 + 0 + + + + Edit + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/averaging.cpp b/Software/PC_Application/averaging.cpp new file mode 100644 index 0000000..442e30c --- /dev/null +++ b/Software/PC_Application/averaging.cpp @@ -0,0 +1,78 @@ +#include "averaging.h" + +using namespace std; + +Averaging::Averaging() +{ + averages = 1; +} + +void Averaging::reset() +{ + avg.clear(); +} + +void Averaging::setAverages(unsigned int a) +{ + averages = a; + reset(); +} + +Protocol::Datapoint Averaging::process(Protocol::Datapoint d) +{ + auto S11 = complex(d.real_S11, d.imag_S11); + auto S12 = complex(d.real_S12, d.imag_S12); + auto S21 = complex(d.real_S21, d.imag_S21); + auto S22 = complex(d.real_S22, d.imag_S22); + + if (d.pointNum == avg.size()) { + // add moving average entry + deque, 4>> deque; + avg.push_back(deque); + } + + if (d.pointNum < avg.size()) { + // can compute average + // get correct queue + auto deque = &avg[d.pointNum]; + // add newest sample to queue + array, 4> sample = {S11, S12, S21, S22}; + deque->push_back(sample); + if(deque->size() > averages) { + deque->pop_front(); + } + + // calculate average + complex sum[4]; + for(auto s : *deque) { + sum[0] += s[0]; + sum[1] += s[1]; + sum[2] += s[2]; + sum[3] += s[3]; + } + S11 = sum[0] / (double) (deque->size()); + S12 = sum[1] / (double) (deque->size()); + S21 = sum[2] / (double) (deque->size()); + S22 = sum[3] / (double) (deque->size()); + } + + d.real_S11 = S11.real(); + d.imag_S11 = S11.imag(); + d.real_S12 = S12.real(); + d.imag_S12 = S12.imag(); + d.real_S21 = S21.real(); + d.imag_S21 = S21.imag(); + d.real_S22 = S22.real(); + d.imag_S22 = S22.imag(); + + return d; +} + +unsigned int Averaging::getLevel() +{ + if(avg.size() > 0) { + return avg.back().size(); + } else { + return 0; + } +} diff --git a/Software/PC_Application/averaging.h b/Software/PC_Application/averaging.h new file mode 100644 index 0000000..d416d6f --- /dev/null +++ b/Software/PC_Application/averaging.h @@ -0,0 +1,23 @@ +#ifndef AVERAGING_H +#define AVERAGING_H + + +#include "Device/device.h" +#include +#include + +class Averaging +{ +public: + Averaging(); + void reset(); + void setAverages(unsigned int a); + Protocol::Datapoint process(Protocol::Datapoint d); + unsigned int getLevel(); +private: + std::vector, 4>>> avg; + int maxPoints; + unsigned int averages; +}; + +#endif // AVERAGING_H diff --git a/Software/PC_Application/icons.qrc b/Software/PC_Application/icons.qrc new file mode 100644 index 0000000..709426a --- /dev/null +++ b/Software/PC_Application/icons.qrc @@ -0,0 +1,26 @@ + + + + icons/add.png + icons/delete.png + icons/edit.png + icons/invisible.svg + icons/visible.svg + icons/pause.svg + icons/play.svg + icons/plus.svg + icons/export.svg + icons/import.svg + icons/close.svg + icons/horizontal.svg + icons/vertical.svg + icons/pCsC_small.png + icons/pCsL_small.png + icons/pLsC_small.png + icons/pLsL_small.png + icons/sCpC_small.png + icons/sCpL_small.png + icons/sLpC_small.png + icons/sLpL_small.png + + diff --git a/Software/PC_Application/icons/.gitignore b/Software/PC_Application/icons/.gitignore new file mode 100644 index 0000000..1a5ceba --- /dev/null +++ b/Software/PC_Application/icons/.gitignore @@ -0,0 +1,2 @@ +tex + diff --git a/Software/PC_Application/icons/add.png b/Software/PC_Application/icons/add.png new file mode 100644 index 0000000..6d4267b Binary files /dev/null and b/Software/PC_Application/icons/add.png differ diff --git a/Software/PC_Application/icons/close.svg b/Software/PC_Application/icons/close.svg new file mode 100644 index 0000000..c5e795d --- /dev/null +++ b/Software/PC_Application/icons/close.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/delete.png b/Software/PC_Application/icons/delete.png new file mode 100644 index 0000000..417656f Binary files /dev/null and b/Software/PC_Application/icons/delete.png differ diff --git a/Software/PC_Application/icons/edit.png b/Software/PC_Application/icons/edit.png new file mode 100644 index 0000000..fd5cdb6 Binary files /dev/null and b/Software/PC_Application/icons/edit.png differ diff --git a/Software/PC_Application/icons/export.svg b/Software/PC_Application/icons/export.svg new file mode 100644 index 0000000..63d1e45 --- /dev/null +++ b/Software/PC_Application/icons/export.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/horizontal.svg b/Software/PC_Application/icons/horizontal.svg new file mode 100644 index 0000000..d72be89 --- /dev/null +++ b/Software/PC_Application/icons/horizontal.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/import.svg b/Software/PC_Application/icons/import.svg new file mode 100644 index 0000000..ce928c3 --- /dev/null +++ b/Software/PC_Application/icons/import.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/invisible.svg b/Software/PC_Application/icons/invisible.svg new file mode 100644 index 0000000..02ae90a --- /dev/null +++ b/Software/PC_Application/icons/invisible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Software/PC_Application/icons/pCsC.png b/Software/PC_Application/icons/pCsC.png new file mode 100644 index 0000000..519202f Binary files /dev/null and b/Software/PC_Application/icons/pCsC.png differ diff --git a/Software/PC_Application/icons/pCsC_small.png b/Software/PC_Application/icons/pCsC_small.png new file mode 100644 index 0000000..50c0db9 Binary files /dev/null and b/Software/PC_Application/icons/pCsC_small.png differ diff --git a/Software/PC_Application/icons/pCsL.png b/Software/PC_Application/icons/pCsL.png new file mode 100644 index 0000000..51cd2e2 Binary files /dev/null and b/Software/PC_Application/icons/pCsL.png differ diff --git a/Software/PC_Application/icons/pCsL_small.png b/Software/PC_Application/icons/pCsL_small.png new file mode 100644 index 0000000..46a798e Binary files /dev/null and b/Software/PC_Application/icons/pCsL_small.png differ diff --git a/Software/PC_Application/icons/pLsC.png b/Software/PC_Application/icons/pLsC.png new file mode 100644 index 0000000..7f4940e Binary files /dev/null and b/Software/PC_Application/icons/pLsC.png differ diff --git a/Software/PC_Application/icons/pLsC_small.png b/Software/PC_Application/icons/pLsC_small.png new file mode 100644 index 0000000..e6d61b0 Binary files /dev/null and b/Software/PC_Application/icons/pLsC_small.png differ diff --git a/Software/PC_Application/icons/pLsL.png b/Software/PC_Application/icons/pLsL.png new file mode 100644 index 0000000..dd9d47e Binary files /dev/null and b/Software/PC_Application/icons/pLsL.png differ diff --git a/Software/PC_Application/icons/pLsL_small.png b/Software/PC_Application/icons/pLsL_small.png new file mode 100644 index 0000000..160151b Binary files /dev/null and b/Software/PC_Application/icons/pLsL_small.png differ diff --git a/Software/PC_Application/icons/pause.svg b/Software/PC_Application/icons/pause.svg new file mode 100644 index 0000000..34d64ac --- /dev/null +++ b/Software/PC_Application/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Software/PC_Application/icons/play.svg b/Software/PC_Application/icons/play.svg new file mode 100644 index 0000000..c54977e --- /dev/null +++ b/Software/PC_Application/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Software/PC_Application/icons/plus.svg b/Software/PC_Application/icons/plus.svg new file mode 100644 index 0000000..e408a39 --- /dev/null +++ b/Software/PC_Application/icons/plus.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Software/PC_Application/icons/sCpC.png b/Software/PC_Application/icons/sCpC.png new file mode 100644 index 0000000..349777e Binary files /dev/null and b/Software/PC_Application/icons/sCpC.png differ diff --git a/Software/PC_Application/icons/sCpC_small.png b/Software/PC_Application/icons/sCpC_small.png new file mode 100644 index 0000000..845cb55 Binary files /dev/null and b/Software/PC_Application/icons/sCpC_small.png differ diff --git a/Software/PC_Application/icons/sCpL.png b/Software/PC_Application/icons/sCpL.png new file mode 100644 index 0000000..ef38c62 Binary files /dev/null and b/Software/PC_Application/icons/sCpL.png differ diff --git a/Software/PC_Application/icons/sCpL_small.png b/Software/PC_Application/icons/sCpL_small.png new file mode 100644 index 0000000..e59bd44 Binary files /dev/null and b/Software/PC_Application/icons/sCpL_small.png differ diff --git a/Software/PC_Application/icons/sLpC.png b/Software/PC_Application/icons/sLpC.png new file mode 100644 index 0000000..f9dc1b8 Binary files /dev/null and b/Software/PC_Application/icons/sLpC.png differ diff --git a/Software/PC_Application/icons/sLpC_small.png b/Software/PC_Application/icons/sLpC_small.png new file mode 100644 index 0000000..318e481 Binary files /dev/null and b/Software/PC_Application/icons/sLpC_small.png differ diff --git a/Software/PC_Application/icons/sLpL.png b/Software/PC_Application/icons/sLpL.png new file mode 100644 index 0000000..64dc038 Binary files /dev/null and b/Software/PC_Application/icons/sLpL.png differ diff --git a/Software/PC_Application/icons/sLpL_small.png b/Software/PC_Application/icons/sLpL_small.png new file mode 100644 index 0000000..8adcd53 Binary files /dev/null and b/Software/PC_Application/icons/sLpL_small.png differ diff --git a/Software/PC_Application/icons/vertical.svg b/Software/PC_Application/icons/vertical.svg new file mode 100644 index 0000000..b3c1119 --- /dev/null +++ b/Software/PC_Application/icons/vertical.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/visible.svg b/Software/PC_Application/icons/visible.svg new file mode 100644 index 0000000..1b5cacc --- /dev/null +++ b/Software/PC_Application/icons/visible.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Software/PC_Application/main.cpp b/Software/PC_Application/main.cpp new file mode 100644 index 0000000..c422cb5 --- /dev/null +++ b/Software/PC_Application/main.cpp @@ -0,0 +1,22 @@ +#include + +#include +#include +#include "vna.h" +#include "valueinput.h" + +#include "Menu/menu.h" +#include "Menu/menuaction.h" +#include "Menu/menuvalue.h" + +#include "Calibration/calkit.h" +#include "touchstone.h" + +#include +int main(int argc, char *argv[]) { + QApplication a(argc, argv); + VNA vna; + vna.resize(1280, 800); + vna.show(); + a.exec(); +} diff --git a/Software/PC_Application/main.ui b/Software/PC_Application/main.ui new file mode 100644 index 0000000..d8b9f12 --- /dev/null +++ b/Software/PC_Application/main.ui @@ -0,0 +1,212 @@ + + + MainWindow + + + + 0 + 0 + 800 + 600 + + + + MainWindow + + + + + + 0 + 0 + 800 + 22 + + + + + File + + + + + + Device + + + + false + + + Connect to + + + + + + false + + + Default Calibration + + + + + + + + + + + + + + + Tools + + + + + + Window + + + + Docks + + + + + + Toolbars + + + + + + + + + Calibration + + + + + + + + + + + + + + + + + + .. + + + Quit + + + + + true + + + + .. + + + Update Device List + + + + + false + + + Disconnect + + + + + Dummy + + + + + Impedance Matching + + + + + false + + + Manual Control + + + + + Dummy + + + + + Dummy + + + + + true + + + Disabled + + + + + Calibration Data + + + + + Edit Calibration Kit + + + + + false + + + Import error terms as traces + + + + + Assign... + + + + + false + + + Remove + + + + + false + + + Firmware Update + + + + + + diff --git a/Software/PC_Application/qwtplotpiecewisecurve.cpp b/Software/PC_Application/qwtplotpiecewisecurve.cpp new file mode 100644 index 0000000..83293b6 --- /dev/null +++ b/Software/PC_Application/qwtplotpiecewisecurve.cpp @@ -0,0 +1,29 @@ +/* -*- mode: C++ ; c-file-style: "stroustrup" -*- ***************************** + * Qwt Widget Library + * Copyright (C) 1997 Josef Wilgen + * Copyright (C) 2002 Uwe Rathmann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the Qwt License, Version 1.0 + *****************************************************************************/ + +#include "qwtplotpiecewisecurve.h" + +void QwtPlotPiecewiseCurve::drawCurve(QPainter *p, int style, const QwtScaleMap &xMap, const QwtScaleMap &yMap, const QRectF &canvasRect, int from, int to) const +{ + if (to < 0) + to = dataSize() - 1; + + int first, last = from; + while (last <= to) + { + first = last; + while (first <= to && (isNaN(sample(first).y()))) + ++first; + last = first; + while (last <= to && !isNaN(sample(last).y())) + ++last; + if (first <= to) + QwtPlotCurve::drawCurve(p, style, xMap, yMap, canvasRect, first, last - 1); + } +} diff --git a/Software/PC_Application/qwtplotpiecewisecurve.h b/Software/PC_Application/qwtplotpiecewisecurve.h new file mode 100644 index 0000000..2d6ee4c --- /dev/null +++ b/Software/PC_Application/qwtplotpiecewisecurve.h @@ -0,0 +1,61 @@ +/* -*- mode: C++ ; c-file-style: "stroustrup" -*- ***************************** + * Qwt Widget Library + * Copyright (C) 1997 Josef Wilgen + * Copyright (C) 2002 Uwe Rathmann + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the Qwt License, Version 1.0 + *****************************************************************************/ + +#ifndef QWT_PLOT_PIECEWISE_CURVE_H +#define QWT_PLOT_PIECEWISE_CURVE_H + +#include "qwt_plot_curve.h" + +/*! + \brief A class which draws piecewise curves + + This class can be used to display data with missing (NaN) values as a + piecewise curve in the x-y plane. +*/ +class QWT_EXPORT QwtPlotPiecewiseCurve: public QwtPlotCurve +{ +public: + explicit QwtPlotPiecewiseCurve(); + explicit QwtPlotPiecewiseCurve(const QwtText &title); + explicit QwtPlotPiecewiseCurve(const QString &title); + ~QwtPlotPiecewiseCurve(); + + virtual void drawCurve(QPainter *p, int style, + const QwtScaleMap &xMap, const QwtScaleMap &yMap, + const QRectF &canvasRect, int from, int to) const override; + +private: + static bool isNaN(double x); +}; + +inline QwtPlotPiecewiseCurve::QwtPlotPiecewiseCurve(): QwtPlotCurve() +{ +} + +inline QwtPlotPiecewiseCurve::QwtPlotPiecewiseCurve(const QwtText &title): + QwtPlotCurve(title) +{ +} + +inline QwtPlotPiecewiseCurve::QwtPlotPiecewiseCurve(const QString &title): + QwtPlotCurve(title) +{ +} + +inline QwtPlotPiecewiseCurve::~QwtPlotPiecewiseCurve() +{ + +} + +inline bool QwtPlotPiecewiseCurve::isNaN(double x) +{ + return x != x; +} + +#endif diff --git a/Software/PC_Application/resources.qrc b/Software/PC_Application/resources.qrc new file mode 100644 index 0000000..7646d2b --- /dev/null +++ b/Software/PC_Application/resources.qrc @@ -0,0 +1 @@ + diff --git a/Software/PC_Application/touchstone.cpp b/Software/PC_Application/touchstone.cpp new file mode 100644 index 0000000..d77f839 --- /dev/null +++ b/Software/PC_Application/touchstone.cpp @@ -0,0 +1,367 @@ +#include "touchstone.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +Touchstone::Touchstone(unsigned int ports) +{ + this->m_ports = ports; + m_datapoints.clear(); +} + +void Touchstone::AddDatapoint(Touchstone::Datapoint p) +{ + if (p.S.size() != m_ports * m_ports) { + throw runtime_error("Invalid number of parameters"); + } + + bool needs_sort = false; + if (m_datapoints.size() > 0 && m_datapoints.back().frequency >= p.frequency) { + needs_sort = true; + } + m_datapoints.push_back(p); + if(needs_sort) { + sort(m_datapoints.begin(), m_datapoints.end(), [](Datapoint &a, Datapoint &b) { + return a.frequency < b.frequency; + }); + } +} + +void Touchstone::toFile(string filename, Unit unit, Format format) +{ + // strip any potential file name extension and apply snp convention + if(filename.find_last_of('.') != string::npos) { + filename.erase(filename.find_last_of('.')); + } + filename.append(".s" + to_string(m_ports) + "p"); + + // create file + ofstream file; + file.open(filename); + file << std::fixed << std::setprecision(12); + + // write option line + file << "# "; + switch(unit) { + case Unit::Hz: file << "HZ "; break; + case Unit::kHz: file << "KHZ "; break; + case Unit::MHz: file << "MHZ "; break; + case Unit::GHz: file << "GHZ "; break; + } + // only S parameters supported so far + file << "S "; + switch(format) { + case Format::DBAngle: file << "DB "; break; + case Format::RealImaginary: file << "RI "; break; + case Format::MagnitudeAngle: file << "MA "; break; + } + // reference impedance is always 50 ohm + file << "R 50\n"; + + auto printParameter = [format](ostream &out, complex &c) { + switch (format) { + case Format::RealImaginary: + out << c.real() << " " << c.imag(); + break; + case Format::MagnitudeAngle: + out << abs(c) << " " << arg(c) / M_PI * 180.0; + break; + case Format::DBAngle: + out << 20*log10(abs(c)) << " " << arg(c) / M_PI * 180.0; + break; + } + }; + + for(auto p : m_datapoints) { + switch(unit) { + case Unit::Hz: file << p.frequency; break; + case Unit::kHz: file << p.frequency / 1e3; break; + case Unit::MHz: file << p.frequency / 1e6; break; + case Unit::GHz: file << p.frequency / 1e9; break; + } + file << " "; + // special cases for 1 and 2 port + if (m_ports == 1) { + printParameter(file, p.S[0]); + file << "\n"; + } else if (m_ports == 2){ + printParameter(file, p.S[0]); + // touchstone expects S11 S21 S12 S22 order, swap S12 and S21 + file << " "; + printParameter(file, p.S[2]); + file << " "; + printParameter(file, p.S[1]); + file << " "; + printParameter(file, p.S[3]); + file << "\n"; + } else { + // print parameters in matrix form + for(unsigned int i=0;i '9' + || filename[index_extension+3] != 'p') { + throw runtime_error("Invalid filename extension"); + } + unsigned int ports = filename[index_extension + 2] - '0'; + auto ret = Touchstone(ports); + + Unit unit = Unit::GHz; + Format format = Format::RealImaginary; + + bool option_line_found = false; + unsigned int parameter_cnt = 0; + + Datapoint point; + + string line; + while(getline(file, line)) { + // remove comments + auto comment = line.find_first_of('!'); + if(comment != string::npos) { + line.erase(comment); + } + // remove leading whitespace + size_t first = line.find_first_not_of(" \t"); + if (string::npos == first) { + // string does only contain whitespace, skip line + continue; + } + line.erase(0, first); + + if (line[0] == '#') { + // this is the option line + if (option_line_found) { + throw runtime_error("Additional option line present"); + } + option_line_found = true; + transform(line.begin(), line.end(), line.begin(), ::toupper); + // check individual options + istringstream iss(line); + bool last_R = false; + string s; + // throw away the option line start character + iss >> s; + for(;iss>>s;) { + if(last_R) { + last_R = false; + // check reference impedance + if (stoi(s, nullptr, 10) != 50) { + throw runtime_error("Invalid reference impedance, only 50Ohm is supported"); + } + break; + } + if (!s.compare("HZ")) { + unit = Unit::Hz; + } else if (!s.compare("KHZ")) { + unit = Unit::kHz; + } else if (!s.compare("MHZ")) { + unit = Unit::MHz; + } else if (!s.compare("GHZ")) { + unit = Unit::GHz; + } else if (!s.compare("S")) { + // S parameter, nothing to do + } else if (!s.compare("Y")) { + throw runtime_error("Y parameters not supported"); + } else if (!s.compare("Z")) { + throw runtime_error("Z parameters not supported"); + } else if (!s.compare("G")) { + throw runtime_error("G parameters not supported"); + } else if (!s.compare("H")) { + throw runtime_error("H parameters not supported"); + } else if(!s.compare("MA")) { + format = Format::MagnitudeAngle; + } else if(!s.compare("DB")) { + format = Format::DBAngle; + } else if(!s.compare("RI")) { + format = Format::RealImaginary; + } else if(!s.compare("R")) { + // next option is the reference impedance + last_R = true; + } else { + throw runtime_error("Unexpected option in option line"); + } + } + } else { + // not the option line + if(!option_line_found) { + throw runtime_error("First dataline before option line"); + } + auto parseDatapoint = [format](istream &in) -> complex { + double part1, part2; + in >> part1; + in >> part2; + complex ret; + switch(format) { + case Format::MagnitudeAngle: + ret = polar(part1, part2 / 180.0 * M_PI); + break; + case Format::DBAngle: + ret = polar(pow(10, part1/20), part2 / 180.0 * M_PI); + break; + case Format::RealImaginary: + ret = complex(part1, part2); + break; + } + return ret; + }; + istringstream iss(line); + if (parameter_cnt == 0) { + iss >> point.frequency; + point.S.clear(); + switch(unit) { + case Unit::Hz: break; + case Unit::kHz: point.frequency *= 1e3; break; + case Unit::MHz: point.frequency *= 1e6; break; + case Unit::GHz: point.frequency *= 1e9; break; + } + } + unsigned int parameters_per_line; + if(ports == 1) { + parameters_per_line = 1; + } else if(ports == 3) { + parameters_per_line = 3; + } else { + parameters_per_line = 4; + } + unsigned int parameters_per_point = ports * ports; + for(unsigned int i=0;i= parameters_per_point) { + parameter_cnt = 0; + if(ports == 2) { + // 2 port touchstone has S11 S21 S12 S22 order, swap S12 and S21 + swap(point.S[1], point.S[2]); + } + ret.AddDatapoint(point); + break; + } + } + } + } + return ret; +} + +double Touchstone::minFreq() +{ + if (m_datapoints.size() > 0) { + return m_datapoints.front().frequency; + } else { + return numeric_limits::quiet_NaN(); + } +} + +double Touchstone::maxFreq() +{ + if (m_datapoints.size() > 0) { + return m_datapoints.back().frequency; + } else { + return numeric_limits::quiet_NaN(); + } +} + +Touchstone::Datapoint Touchstone::interpolate(double frequency) +{ + if(m_datapoints.size() == 0) { + throw runtime_error("Trying to interpolate empty touchstone data"); + } + // Check if requested frequency is outside of points and return first/last datapoint respectively + if(frequency <= m_datapoints.front().frequency) { + return m_datapoints.front(); + } else if(frequency >= m_datapoints.back().frequency) { + return m_datapoints.back(); + } + // frequency within points, interpolate + auto lower = lower_bound(m_datapoints.begin(), m_datapoints.end(), frequency, [](const Datapoint &lhs, double rhs) -> bool { + return lhs.frequency < rhs; + }); + auto lowPoint = *lower; + advance(lower, 1); + auto highPoint = *lower; + double alpha = (frequency - lowPoint.frequency) / (highPoint.frequency - lowPoint.frequency); + Datapoint ret; + ret.frequency = frequency; + for(unsigned int i=0;i= m_ports || port2 >= m_ports || port1 == port2) { + throw runtime_error("Invalid port number"); + } + unsigned int S11_index = port1 * m_ports + port1; + unsigned int S22_index = port2 * m_ports + port2; + unsigned int S12_index = port1 * m_ports + port2; + unsigned int S21_index = port2 * m_ports + port1; + if(m_ports == 2) { + swap(S21_index, S12_index); + } + for(auto &p : m_datapoints) { + auto S11 = p.S[S11_index]; + auto S12 = p.S[S12_index]; + auto S21 = p.S[S21_index]; + auto S22 = p.S[S22_index]; + p.S.clear(); + p.S.push_back(S11); + p.S.push_back(S21); + p.S.push_back(S12); + p.S.push_back(S22); + } + m_ports = 2; +} + +void Touchstone::reduceTo1Port(unsigned int port) +{ + if (port >= m_ports) { + throw runtime_error("Invalid port number"); + } + if(m_ports == 1) { + // already at one port, nothing to do + return; + } + unsigned int S11_index = port * m_ports + port; + for(auto &p : m_datapoints) { + auto S11 = p.S[S11_index]; + p.S.clear(); + p.S.push_back(S11); + } + m_ports = 1; +} diff --git a/Software/PC_Application/touchstone.h b/Software/PC_Application/touchstone.h new file mode 100644 index 0000000..b355df0 --- /dev/null +++ b/Software/PC_Application/touchstone.h @@ -0,0 +1,49 @@ +#ifndef TOUCHSTONE_H +#define TOUCHSTONE_H + +#include +#include +#include + +class Touchstone +{ +public: + enum class Unit { + Hz, + kHz, + MHz, + GHz, + }; + + enum class Format { + DBAngle, + MagnitudeAngle, + RealImaginary, + }; + + class Datapoint { + public: + double frequency; + std::vector> S; + }; + + Touchstone(unsigned int m_ports); + void AddDatapoint(Datapoint p); + void toFile(std::string filename, Unit unit = Unit::GHz, Format format = Format::RealImaginary); + static Touchstone fromFile(std::string filename); + double minFreq(); + double maxFreq(); + unsigned int points() { return m_datapoints.size(); }; + Datapoint point(int index) { return m_datapoints.at(index); }; + Datapoint interpolate(double frequency); + // remove all paramaters except the ones regarding port1 and port2 (port cnt starts at 0) + void reduceTo2Port(unsigned int port1, unsigned int port2); + // remove all paramaters except the ones from port (port cnt starts at 0) + void reduceTo1Port(unsigned int port); + unsigned int ports() { return m_ports; } +private: + unsigned int m_ports; + std::vector m_datapoints; +}; + +#endif // TOUCHSTONE_H diff --git a/Software/PC_Application/unit.cpp b/Software/PC_Application/unit.cpp new file mode 100644 index 0000000..197beb4 --- /dev/null +++ b/Software/PC_Application/unit.cpp @@ -0,0 +1,94 @@ +#include "unit.h" +#include +#include +#include +#include + +using namespace std; + +double Unit::FromString(QString string, QString unit, QString prefixes) +{ + if(string.size() == 0) { + return std::numeric_limits::quiet_NaN(); + } + // remove unit if present + if(string.endsWith(unit, Qt::CaseInsensitive)) { + string.chop(unit.size()); + } + // check if last char is a valid prefix + double factor = 1.0; + if(prefixes.contains(string.at(string.size()-1))) { + QChar prefix = string.at(string.size()-1); + factor = SIPrefixToFactor(prefix.toLatin1()); + string.chop(1); + } + bool convertOk; + auto value = string.toDouble(&convertOk); + if(!convertOk) { + return std::numeric_limits::quiet_NaN(); + } + return value * factor; +} + +QString Unit::ToString(double value, QString unit, QString prefixes, int precision) +{ + // change label text + QString sValue; + if(isnan(value) || isinf(value)) { + sValue.append("NaN"); + return sValue; + } else if(value == 0.0) { + sValue.append("0 "); + } else { + if(value < 0) { + sValue.append('-'); + value = -value; + } + int preDotDigits = log10(value) + 2; + int prefixIndex = prefixes.indexOf(' '); + while(preDotDigits > 3 && prefixIndex < prefixes.length() - 1) { + value /= 1000.0; + preDotDigits -= 3; + prefixIndex++; + } + while(preDotDigits<=1 && prefixIndex > 0) { + value *= 1000.0; + preDotDigits += 3; + prefixIndex--; + } + stringstream ss; + ss << std::fixed; + if(preDotDigits >= 0) { + if(precision - preDotDigits + 1 < 0) { + ss << std::setprecision(0); + } else { + ss << std::setprecision(precision - preDotDigits + 1); + } + } else { + ss << std::setprecision(precision - 1); + } + ss << value; + sValue.append(QString::fromStdString(ss.str())); + sValue.append(prefixes[prefixIndex]); + } + sValue.append(unit); + return sValue; +} + +double Unit::SIPrefixToFactor(char prefix) +{ + switch(prefix) { + case 'f': return 1e-15; break; + case 'p': return 1e-12; break; + case 'n': return 1e-9; break; + case 'u': return 1e-6; break; + case 'm': return 1e-3; break; + case ' ': return 1e0; break; + case 'k': return 1e3; break; + case 'M': return 1e6; break; + case 'G': return 1e9; break; + case 'T': return 1e12; break; + case 'P': return 1e15; break; + default: return 0; break; + } +} diff --git a/Software/PC_Application/unit.h b/Software/PC_Application/unit.h new file mode 100644 index 0000000..c80d1fb --- /dev/null +++ b/Software/PC_Application/unit.h @@ -0,0 +1,14 @@ +#ifndef UNIT_H +#define UNIT_H + +#include + +class Unit +{ +public: + static double FromString(QString string, QString unit = QString(), QString prefixes = " "); + static QString ToString(double value, QString unit = QString(), QString prefixes = " ", int precision = 6); + static double SIPrefixToFactor(char prefix); +}; + +#endif // UNIT_H diff --git a/Software/PC_Application/valueinput.cpp b/Software/PC_Application/valueinput.cpp new file mode 100644 index 0000000..d5507bc --- /dev/null +++ b/Software/PC_Application/valueinput.cpp @@ -0,0 +1,148 @@ +#include "valueinput.h" +#include +#include +#include +#include + +constexpr QSize ValueInput::minButtonSize; + +ValueInput::ValueInput(std::vector units, QString name, QString initialValue) +{ + auto fontButton = QFont("Arial", 12); + auto fontLabel = QFont("Arial", 30); + + input = initialValue; + // always use dots instead of comma + input.replace(',', '.'); + this->units = units; + setWindowTitle(name); + + // Create layout + auto layout = new QGridLayout(); + + // Add Label + label = new QLabel(); + layout->addWidget(label, 0, 0, 2, 4); + label->setText(input); + label->setAlignment(Qt::AlignCenter); + label->setFont(fontLabel); + + // Create buttons and add to layout + for(int i=0;i<3;i++) { + for(int j=0;j<3;j++) { + int number = i+1 + j*3; + auto name = QString::number(number); + auto button = new QPushButton(name); + button->setFont(fontButton); + button->setMinimumSize(minButtonSize); + connect(button, &QPushButton::clicked, [=]() {this->AddToInput(name);}); + layout->addWidget(button, j+2, i); + } + } + bDot = new QPushButton("."); + bDot->setFont(fontButton); + bDot->setMinimumSize(minButtonSize); + if(input.contains('.')) { + bDot->setDisabled(true); + } + layout->addWidget(bDot, 5, 0); + connect(bDot, &QPushButton::clicked, [=]() {this->AddToInput(".");}); + auto bZero = new QPushButton("0"); + bZero->setFont(fontButton); + bZero->setMinimumSize(minButtonSize); + connect(bZero, &QPushButton::clicked, [=]() {this->AddToInput("0");}); + layout->addWidget(bZero, 5, 1); + auto bSign = new QPushButton("+/-"); + bSign->setFont(fontButton); + bSign->setMinimumSize(minButtonSize); + layout->addWidget(bSign, 5, 2); + connect(bSign, SIGNAL(clicked()), this, SLOT(ChangeSign())); + + auto bDel = new QPushButton("Backspace"); + bDel->setMinimumSize(minButtonSize); + layout->addWidget(bDel, 6, 0, 1, 3); + connect(bDel, SIGNAL(clicked()), this, SLOT(Backspace())); + + auto bAbort = new QPushButton("Abort"); + bAbort->setMinimumSize(minButtonSize); + layout->addWidget(bAbort, 6, 3); + connect(bAbort, SIGNAL(clicked()), this, SLOT(Abort())); + + // Add unit inputs + if(units.size() > 4) { + units.resize(4); + } + for(unsigned int i=0;isetFont(fontButton); + bUnit->setMinimumSize(minButtonSize); + connect(bUnit, &QPushButton::clicked, [=](){this->UnitPressed(units[i].factor);}); + layout->addWidget(bUnit, i+2, 3); + } + + setLayout(layout); + setWindowModality(Qt::ApplicationModal); + show(); +} + +void ValueInput::keyPressEvent(QKeyEvent *event) { + auto key = event->key(); + if(key >= '0' && key <= '9') { + AddToInput((QChar) key); + event->accept(); + } else if((key == '.' || key == ',') && bDot->isEnabled()) { + AddToInput("."); + event->accept(); + } else if(key == Qt::Key_Escape) { + Abort(); + event->accept(); + } else if(key == Qt::Key_Backspace) { + Backspace(); + event->accept(); + } else if(key == '-') { + ChangeSign(); + event->accept(); + } +} + +void ValueInput::AddToInput(QString a) +{ + input.append(a); + label->setText(input); + if(a == '.') { + bDot->setDisabled(true); + } +} + +void ValueInput::ChangeSign() +{ + if(input.at(0) == '-') { + input.remove(0, 1); + } else { + input.prepend('-'); + } + label->setText(input); +} + +void ValueInput::Backspace() +{ + if(input.size() > 0) { + if(input.at(input.size()-1) == '.') { + bDot->setEnabled(true); + } + input.chop(1); + label->setText(input); + } +} + +void ValueInput::Abort() +{ + close(); +} + +void ValueInput::UnitPressed(double factor) +{ + double value = input.toDouble() * factor; + emit ValueChanged(value); + close(); +} diff --git a/Software/PC_Application/valueinput.h b/Software/PC_Application/valueinput.h new file mode 100644 index 0000000..d8a4c1c --- /dev/null +++ b/Software/PC_Application/valueinput.h @@ -0,0 +1,44 @@ +#ifndef VALUEINPUT_H +#define VALUEINPUT_H + +#include +#include +#include +#include +#include + +class ValueInput : public QWidget +{ + Q_OBJECT +public: + class Unit { + public: + Unit(){}; + Unit(QString name, double factor): + name(name), factor(factor){}; + QString name; + double factor; + }; + ValueInput(std::vector units, QString name = "Value input", QString initialValue = QString()); + +signals: + void ValueChanged(double value); + +protected: + void keyPressEvent(QKeyEvent *event) override; + +private slots: + void AddToInput(QString a); + void ChangeSign(); + void Backspace(); + void Abort(); + void UnitPressed(double factor); +private: + static constexpr QSize minButtonSize = QSize(50, 50); + QString input; + QLabel *label; + QPushButton *bDot; + std::vector units; +}; + +#endif // VALUEINPUT_H diff --git a/Software/PC_Application/vna.cpp b/Software/PC_Application/vna.cpp new file mode 100644 index 0000000..2916f9b --- /dev/null +++ b/Software/PC_Application/vna.cpp @@ -0,0 +1,1077 @@ +#include "vna.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "valueinput.h" +#include +#include +#include +#include +#include +#include "Menu/menu.h" +#include "Menu/menuaction.h" +#include "Menu/menuvalue.h" +#include "Menu/menubool.h" +#include +#include +#include +#include +#include +#include +#include "unit.h" +#include "CustomWidgets/toggleswitch.h" +#include "Device/manualcontroldialog.h" +#include "Traces/tracemodel.h" +#include "Traces/tracewidget.h" +#include "Traces/tracesmithchart.h" +#include "Traces/tracebodeplot.h" +#include "Traces/traceimportdialog.h" +#include "CustomWidgets/tilewidget.h" +#include +#include "Traces/markerwidget.h" +#include "Tools/impedancematchdialog.h" +#include "Calibration/calibrationtracedialog.h" +#include "ui_main.h" +#include "Device/firmwareupdatedialog.h" + +using namespace std; + +constexpr Protocol::SweepSettings VNA::defaultSweep; + +VNA::VNA(QWidget *parent) + : QMainWindow(parent) + , deviceActionGroup(new QActionGroup(this)) + , ui(new Ui::MainWindow) +{ + QCoreApplication::setOrganizationName("VNA"); + QCoreApplication::setOrganizationName("Application"); + + settings = defaultSweep; + averages = 1; + calValid = false; + calMeasuring = false; + device = nullptr; + calDialog.reset(); + + ui->setupUi(this); +// ui->statusbar->insertPermanentWidget(0, &lDeviceStatus); +// ui->statusbar->insertPermanentWidget(1, new QPushButton("Test")); + ui->statusbar->addWidget(&lConnectionStatus); + auto div1 = new QFrame; + div1->setFrameShape(QFrame::VLine); + ui->statusbar->addWidget(div1); + ui->statusbar->addWidget(&lDeviceInfo); + ui->statusbar->addWidget(new QLabel, 1); + //ui->statusbar->setStyleSheet("QStatusBar::item { border: 1px solid black; };"); + + CreateToolbars(); + // UI connections + connect(ui->actionUpdate_Device_List, &QAction::triggered, this, &VNA::UpdateDeviceList); + connect(ui->actionDisconnect, &QAction::triggered, this, &VNA::DisconnectDevice); + connect(ui->actionQuit, &QAction::triggered, this, &VNA::close); + connect(ui->actionManual_Control, &QAction::triggered, this, &VNA::StartManualControl); + connect(ui->actionImpedance_Matching, &QAction::triggered, this, &VNA::StartImpedanceMatching); + connect(ui->actionEdit_Calibration_Kit, &QAction::triggered, [=](){ + cal.getCalibrationKit().edit(); + }); + connect(ui->actionImport_error_terms_as_traces, &QAction::triggered, [=](){ + auto import = new TraceImportDialog(traceModel, cal.getErrorTermTraces()); + import->show(); + }); + connect(ui->actionTracedata, &QAction::triggered, [=](){ + auto dialog = new CalibrationTraceDialog(&cal); + connect(dialog, &CalibrationTraceDialog::triggerMeasurement, this, &VNA::StartCalibrationMeasurement); + connect(dialog, &CalibrationTraceDialog::applyCalibration, this, &VNA::ApplyCalibration); + connect(this, &VNA::CalibrationMeasurementComplete, dialog, &CalibrationTraceDialog::measurementComplete); + dialog->show(); + }); + connect(ui->actionAssignDefaultCal, &QAction::triggered, [=](){ + if(device) { + auto key = "DefaultCalibration"+device->serial(); + QSettings settings; + auto filename = QFileDialog::getOpenFileName(nullptr, "Load calibration data", settings.value(key).toString(), "Calibration files (*.cal)", nullptr, QFileDialog::DontUseNativeDialog); + if(!filename.isEmpty()) { + settings.setValue(key, filename); + ui->actionRemoveDefaultCal->setEnabled(true); + } + } + }); + connect(ui->actionRemoveDefaultCal, &QAction::triggered, [=](){ + QSettings settings; + settings.remove("DefaultCalibration"+device->serial()); + ui->actionRemoveDefaultCal->setEnabled(false); + }); + connect(ui->actionFirmware_Update, &QAction::triggered, [=](){ + if(device) { + auto fw_update = new FirmwareUpdateDialog(*device); + fw_update->exec(); + } + }); + + + setWindowTitle("VNA"); + + markerModel = new TraceMarkerModel(traceModel); + + // Create status panel + auto statusLayout = new QVBoxLayout(); + statusLayout->setSpacing(0); + QFont statusFont( "Arial", 8); + { + auto l = new QLabel("Start Frequency:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lStart.setAlignment(Qt::AlignRight); + lStart.setFont(statusFont); + statusLayout->addWidget(&lStart); + + l = new QLabel("Center Frequency:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lCenter.setAlignment(Qt::AlignRight); + lCenter.setFont(statusFont); + statusLayout->addWidget(&lCenter); + + l = new QLabel("Stop Frequency:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lStop.setAlignment(Qt::AlignRight); + lStop.setFont(statusFont); + statusLayout->addWidget(&lStop); + + l = new QLabel("Span:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lSpan.setAlignment(Qt::AlignRight); + lSpan.setFont(statusFont); + statusLayout->addWidget(&lSpan); + + statusLayout->addStretch(); + + l = new QLabel("Points:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lPoints.setAlignment(Qt::AlignRight); + lPoints.setFont(statusFont); + statusLayout->addWidget(&lPoints); + + l = new QLabel("IF Bandwidth:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lBandwidth.setAlignment(Qt::AlignRight); + lBandwidth.setFont(statusFont); + statusLayout->addWidget(&lBandwidth); + + l = new QLabel("Averages:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lAverages.setAlignment(Qt::AlignRight); + lAverages.setFont(statusFont); + statusLayout->addWidget(&lAverages); + + statusLayout->addStretch(); + + l = new QLabel("Calibration:"); + l->setAlignment(Qt::AlignLeft); + l->setFont(statusFont); + statusLayout->addWidget(l); + lCalibration.setAlignment(Qt::AlignRight); + lCalibration.setFont(statusFont); + statusLayout->addWidget(&lCalibration); + } + statusLayout->addStretch(); + + auto tw = new TraceWidget(traceModel); + // Create default traces + auto tS11 = new Trace("S11", Qt::yellow); + tS11->fromLivedata(Trace::LivedataType::Overwrite, Trace::LiveParameter::S11); + traceModel.addTrace(tS11); + auto tS12 = new Trace("S12", Qt::blue); + tS12->fromLivedata(Trace::LivedataType::Overwrite, Trace::LiveParameter::S12); + traceModel.addTrace(tS12); + auto tS21 = new Trace("S21", Qt::green); + tS21->fromLivedata(Trace::LivedataType::Overwrite, Trace::LiveParameter::S21); + traceModel.addTrace(tS21); + auto tS22 = new Trace("S22", Qt::red); + tS22->fromLivedata(Trace::LivedataType::Overwrite, Trace::LiveParameter::S22); + traceModel.addTrace(tS22); + + auto tracesmith1 = new TraceSmithChart(traceModel); + tracesmith1->enableTrace(tS11, true); + auto tracesmith2 = new TraceSmithChart(traceModel); + tracesmith2->enableTrace(tS22, true); + + auto tracebode1 = new TraceBodePlot(traceModel); + tracebode1->enableTrace(tS12, true); + auto tracebode2 = new TraceBodePlot(traceModel); + tracebode2->enableTrace(tS21, true); + + auto tiles = new TileWidget(traceModel); + tiles->splitVertically(); + tiles->Child1()->splitHorizontally(); + tiles->Child2()->splitHorizontally(); + tiles->Child1()->Child1()->setPlot(tracesmith1); + tiles->Child1()->Child2()->setPlot(tracebode1); + tiles->Child2()->Child1()->setPlot(tracebode2); + tiles->Child2()->Child2()->setPlot(tracesmith2); + + auto menuLayout = new QStackedLayout; + auto mMain = new Menu(*menuLayout); + + auto mFrequency = new Menu(*menuLayout, "Frequency"); + auto mCenter = new MenuValue("Center Frequency", (settings.f_start + settings.f_stop)/2, "Hz", " kMG", 6); + mFrequency->addItem(mCenter); + auto mStart = new MenuValue("Start Frequency", settings.f_start, "Hz", " kMG", 6); + mFrequency->addItem(mStart); + auto mStop = new MenuValue("Stop Frequency", settings.f_stop, "Hz", " kMG", 6); + mFrequency->addItem(mStop); + mFrequency->finalize(); + mMain->addMenu(mFrequency); + + auto mSpan = new Menu(*menuLayout, "Span"); + auto mSpanWidth = new MenuValue("Span", settings.f_stop - settings.f_start, "Hz", " kMG", 6); + mSpan->addItem(mSpanWidth); + auto mSpanZoomIn = new MenuAction("Zoom in"); + mSpan->addItem(mSpanZoomIn); + auto mSpanZoomOut = new MenuAction("Zoom out"); + mSpan->addItem(mSpanZoomOut); + auto mSpanFull = new MenuAction("Full span"); + mSpan->addItem(mSpanFull); + mSpan->finalize(); + mMain->addMenu(mSpan); + + auto mAcquisition = new Menu(*menuLayout, "Acquisition"); + auto mdbm = new MenuValue("Source Level", -10, "dbm", " "); + mAcquisition->addItem(mdbm); + auto mPoints = new MenuValue("Points", settings.points, "", " "); + mAcquisition->addItem(mPoints); + auto mBandwidth = new MenuValue("IF Bandwidth", settings.if_bandwidth, "Hz", " k", 3); + mAcquisition->addItem(mBandwidth); + auto mAverages = new MenuValue("Averages", averages); + mAcquisition->addItem(mAverages); + mAcquisition->finalize(); + mMain->addMenu(mAcquisition); + + auto mCalibration = new Menu(*menuLayout, "Calibration"); + auto mCalPort1 = new Menu(*menuLayout, "Port 1"); + auto mCalPort1Open = new MenuAction("Port 1 Open"); + auto mCalPort1Short = new MenuAction("Port 1 Short"); + auto mCalPort1Load = new MenuAction("Port 1 Load"); + mCalPort1->addItem(mCalPort1Short); + mCalPort1->addItem(mCalPort1Open); + mCalPort1->addItem(mCalPort1Load); + mCalPort1->finalize(); + mCalibration->addMenu(mCalPort1); + auto mCalPort2 = new Menu(*menuLayout, "Port 2"); + auto mCalPort2Open = new MenuAction("Port 2 Open"); + auto mCalPort2Short = new MenuAction("Port 2 Short"); + auto mCalPort2Load = new MenuAction("Port 2 Load"); + mCalPort2->addItem(mCalPort2Short); + mCalPort2->addItem(mCalPort2Open); + mCalPort2->addItem(mCalPort2Load); + mCalPort2->finalize(); + mCalibration->addMenu(mCalPort2); + auto mCalThrough = new MenuAction("Through"); + auto mCalIsolation = new MenuAction("Isolation"); + mCalibration->addItem(mCalThrough); + mCalibration->addItem(mCalIsolation); + + mCalSOL1 = new MenuAction("Apply Port 1 SOL"); + mCalibration->addItem(mCalSOL1); + mCalSOL1->setDisabled(true); + + mCalSOL2 = new MenuAction("Apply Port 2 SOL"); + mCalibration->addItem(mCalSOL2); + mCalSOL2->setDisabled(true); + + mCalFullSOLT = new MenuAction("Apply full SOLT"); + mCalibration->addItem(mCalFullSOLT); + mCalFullSOLT->setDisabled(true); + + auto mCalSave = new MenuAction("Save to file"); + mCalibration->addItem(mCalSave); + + auto mCalLoad = new MenuAction("Load from file"); + mCalibration->addItem(mCalLoad); + + auto mEditKit = new MenuAction("Edit CalKit"); + mCalibration->addItem(mEditKit); + + mCalibration->finalize(); + mMain->addMenu(mCalibration); + + auto mSystem = new Menu(*menuLayout, "System"); + auto aManual = new MenuAction("Manual Control"); + auto aMatchDialog = new MenuAction("Impedance Matching"); + mSystem->addItem(aManual); + mSystem->addItem(aMatchDialog); + mSystem->finalize(); + mMain->addMenu(mSystem); + + mMain->finalize(); + + // Frequency and span connections + // setting values + connect(mCenter, &MenuValue::valueChanged, this, &VNA::SetCenterFreq); + connect(mStart, &MenuValue::valueChanged, this, &VNA::SetStartFreq); + connect(mStop, &MenuValue::valueChanged, this, &VNA::SetStopFreq); + connect(mSpanWidth, &MenuValue::valueChanged, this, &VNA::SetSpan); + connect(mSpanZoomIn, &MenuAction::triggered, this, &VNA::SpanZoomIn); + connect(mSpanZoomOut, &MenuAction::triggered, this, &VNA::SpanZoomOut); + connect(mSpanFull, &MenuAction::triggered, this, &VNA::SetFullSpan); + // readback and update line edits + connect(this, &VNA::startFreqChanged, mStart, &MenuValue::setValueQuiet); + connect(this, &VNA::stopFreqChanged, mStop, &MenuValue::setValueQuiet); + connect(this, &VNA::centerFreqChanged, mCenter, &MenuValue::setValueQuiet); + connect(this, &VNA::spanChanged, mSpanWidth, &MenuValue::setValueQuiet); + + // Acquisition connections + // setting values + connect(mPoints, &MenuValue::valueChanged, [=](double newval){ + SetPoints(newval); + }); + connect(mdbm, &MenuValue::valueChanged, this, &VNA::SetSourceLevel); + connect(mBandwidth, &MenuValue::valueChanged, this, &VNA::SetIFBandwidth); + connect(mAverages, &MenuValue::valueChanged, [=](double newval){ + SetAveraging(newval); + }); + // readback and update line edits + connect(this, &VNA::sourceLevelChanged, mdbm, &MenuValue::setValueQuiet); + connect(this, &VNA::pointsChanged, [=](int newval) { + mPoints->setValueQuiet(newval); + }); + connect(this, &VNA::IFBandwidthChanged, mBandwidth, &MenuValue::setValueQuiet); + connect(this, &VNA::averagingChanged, [=](int newval) { + mAverages->setValueQuiet(newval); + }); + + connect(mCalPort1Open, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port1Open); + }); + connect(mCalPort1Short, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port1Short); + }); + connect(mCalPort1Load, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port1Load); + }); + connect(mCalPort2Open, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port2Open); + }); + connect(mCalPort2Short, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port2Short); + }); + connect(mCalPort2Load, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Port2Load); + }); + connect(mCalThrough, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Through); + }); + connect(mCalIsolation, &MenuAction::triggered, [=](){ + StartCalibrationMeasurement(Calibration::Measurement::Isolation); + }); + connect(mCalSOL1, &MenuAction::triggered, [=](){ + ApplyCalibration(Calibration::Type::Port1SOL); + }); + connect(mCalSOL2, &MenuAction::triggered, [=](){ + ApplyCalibration(Calibration::Type::Port2SOL); + }); + connect(mCalFullSOLT, &MenuAction::triggered, [=](){ + ApplyCalibration(Calibration::Type::FullSOLT); + }); + + connect(mCalSave, &MenuAction::triggered, [=](){ + cal.saveToFile(); + }); + + connect(mCalLoad, &MenuAction::triggered, [=](){ + if(cal.openFromFile()) { + // Check if applying calibration is available + if(cal.calculationPossible(Calibration::Type::Port1SOL)) { + mCalSOL1->setEnabled(true); + } + if(cal.calculationPossible(Calibration::Type::Port2SOL)) { + mCalSOL2->setEnabled(true); + } + if(cal.calculationPossible(Calibration::Type::FullSOLT)) { + mCalFullSOLT->setEnabled(true); + } + } + }); + + connect(mEditKit, &MenuAction::triggered, [=](){ + cal.getCalibrationKit().edit(); + }); + + // Manual control trigger + connect(aManual, &MenuAction::triggered, this, &VNA::StartManualControl); + connect(aMatchDialog, &MenuAction::triggered, this, &VNA::StartImpedanceMatching); + + setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::BottomLeftCorner, Qt::LeftDockWidgetArea); + setCorner(Qt::TopRightCorner, Qt::RightDockWidgetArea); + setCorner(Qt::BottomRightCorner, Qt::RightDockWidgetArea); + + auto mainWidget = new QWidget; + auto mainLayout = new QHBoxLayout; + mainWidget->setLayout(mainLayout); + auto statusWidget = new QWidget; + statusWidget->setLayout(statusLayout); +// statusWidget->setFixedWidth(150); + auto statusDock = new QDockWidget("Status"); + statusDock->setWidget(statusWidget); +// statusDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + addDockWidget(Qt::LeftDockWidgetArea, statusDock); + + auto tracesDock = new QDockWidget("Traces"); + tracesDock->setWidget(tw); +// tracesDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + addDockWidget(Qt::LeftDockWidgetArea, tracesDock); + +// mainLayout->addWidget(statusWidget); + mainLayout->addWidget(tiles); + auto menuWidget = new QWidget; + menuWidget->setLayout(menuLayout); +// menuWidget->setFixedWidth(180); + auto menuDock = new QDockWidget("Menu"); + menuDock->setWidget(menuWidget); +// menuDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + addDockWidget(Qt::RightDockWidgetArea, menuDock); +// mainLayout->addWidget(menuWidget); + + auto markerWidget = new MarkerWidget(*markerModel); + + auto markerDock = new QDockWidget("Marker"); + markerDock->setWidget(markerWidget); +// markerDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable); + addDockWidget(Qt::BottomDockWidgetArea, markerDock); + + auto logDock = new QDockWidget("Device Log"); + logDock->setWidget(&deviceLog); + addDockWidget(Qt::BottomDockWidgetArea, logDock); + + setCentralWidget(mainWidget); + + // status and menu dock hidden by default + menuDock->close(); + statusDock->close(); + + // fill dock/toolbar hide/show menu and set initial state if available + QSettings settings; + ui->menuDocks->clear(); + for(auto d : findChildren()) { + ui->menuDocks->addAction(d->toggleViewAction()); + bool hidden = settings.value("dock_"+d->windowTitle(), d->isHidden()).toBool(); + if(hidden) { + d->close(); + } else { + d->show(); + } + } + ui->menuToolbars->clear(); + for(auto t : findChildren()) { + ui->menuToolbars->addAction(t->toggleViewAction()); + bool hidden = settings.value("toolbar_"+t->windowTitle(), t->isHidden()).toBool(); + if(hidden) { + t->close(); + } else { + t->show(); + } + } + + restoreGeometry(settings.value("geometry").toByteArray()); + restoreState(settings.value("windowState").toByteArray()); + + qRegisterMetaType("Datapoint"); + + ConstrainAndUpdateFrequencies(); + + // List available devices + if(UpdateDeviceList()) { + // at least one device available + ConnectToDevice(); + } +} + +void VNA::closeEvent(QCloseEvent *event) +{ + QSettings settings; + // save dock/toolbar visibility + for(auto d : findChildren()) { + settings.setValue("dock_"+d->windowTitle(), d->isHidden()); + } + ui->menuToolbars->clear(); + for(auto t : findChildren()) { + settings.setValue("toolbar_"+t->windowTitle(), t->isHidden()); + } + settings.setValue("geometry", saveGeometry()); + settings.setValue("windowState", saveState()); + QMainWindow::closeEvent(event); +} + +void VNA::NewDatapoint(Protocol::Datapoint d) +{ + if(calMeasuring) { + if(!calWaitFirst || d.pointNum == 0) { + calWaitFirst = false; + cal.addMeasurement(calMeasurement, d); + if(d.pointNum == settings.points - 1) { + calMeasuring = false; + emit CalibrationMeasurementComplete(calMeasurement); + // Check if applying calibration is available + if(cal.calculationPossible(Calibration::Type::Port1SOL)) { + mCalSOL1->setEnabled(true); + } + if(cal.calculationPossible(Calibration::Type::Port2SOL)) { + mCalSOL2->setEnabled(true); + } + if(cal.calculationPossible(Calibration::Type::FullSOLT)) { + mCalFullSOLT->setEnabled(true); + } + } + calDialog.setValue(d.pointNum + 1); + } + } + if(calValid) { + cal.correctMeasurement(d); + } + d = average.process(d); + traceModel.addVNAData(d); + emit dataChanged(); + if(d.pointNum == settings.points - 1) { + UpdateStatusPanel(); + } +} + +void VNA::UpdateStatusPanel() +{ + lStart.setText(Unit::ToString(settings.f_start, "Hz", " kMG", 4)); + lCenter.setText(Unit::ToString((settings.f_start + settings.f_stop)/2, "Hz", " kMG", 4)); + lStop.setText(Unit::ToString(settings.f_stop, "Hz", " kMG", 4)); + lSpan.setText(Unit::ToString(settings.f_stop - settings.f_start, "Hz", " kMG", 4)); + lPoints.setText(QString::number(settings.points)); + lBandwidth.setText(Unit::ToString(settings.if_bandwidth, "Hz", " k", 2)); + lAverages.setText(QString::number(average.getLevel()) + "/" + QString::number(averages)); + if(calValid) { + switch(cal.getInterpolation(settings)) { + case Calibration::InterpolationType::Extrapolate: + lCalibration.setText("Enabled/Extrapolating"); + break; + case Calibration::InterpolationType::Interpolate: + lCalibration.setText("Enabled/Interpolating"); + break; + case Calibration::InterpolationType::Exact: + case Calibration::InterpolationType::Unchanged: + lCalibration.setText("Enabled"); + break; + default: + lCalibration.setText("Unknown"); + break; + } + } else { + lCalibration.setText("Off"); + } +} + +void VNA::SettingsChanged() +{ + if(device) { + device->Configure(settings); + } + average.reset(); + traceModel.clearVNAData(); + UpdateStatusPanel(); + TracePlot::UpdateSpan(settings.f_start, settings.f_stop); +} + +void VNA::ConnectToDevice(QString serial) +{ + if(device) { + DisconnectDevice(); + } + try { + qDebug() << "Attempting to connect to device..."; + device = new Device(serial); + lConnectionStatus.setText("Connected to " + device->serial()); + qInfo() << "Connected to " << device->serial(); + lDeviceInfo.setText(device->getLastDeviceInfoString()); + device->Configure(settings); + connect(device, &Device::DatapointReceived, this, &VNA::NewDatapoint); + connect(device, &Device::LogLineReceived, &deviceLog, &DeviceLog::addLine); + connect(device, &Device::ConnectionLost, this, &VNA::DeviceConnectionLost); + connect(device, &Device::DeviceInfoUpdated, [this]() { + lDeviceInfo.setText(device->getLastDeviceInfoString()); + }); + ui->actionDisconnect->setEnabled(true); + ui->actionManual_Control->setEnabled(true); + ui->menuDefault_Calibration->setEnabled(true); + ui->actionFirmware_Update->setEnabled(true); + // Check if default calibration exists and attempt to load it + QSettings settings; + auto key = "DefaultCalibration"+device->serial(); + if (settings.contains(key)) { + auto filename = settings.value(key).toString(); + qDebug() << "Attempting to load default calibration file \"" << filename << "\""; + if(QFile::exists(filename)) { + cal.openFromFile(filename); + ApplyCalibration(cal.getType()); + } + ui->actionRemoveDefaultCal->setEnabled(true); + } else { + qDebug() << "No default calibration file set for this device"; + ui->actionRemoveDefaultCal->setEnabled(false); + } + } catch (const runtime_error e) { + DisconnectDevice(); + UpdateDeviceList(); + } +} + +void VNA::DisconnectDevice() +{ + if(device) { + delete device; + device = nullptr; + } + ui->actionDisconnect->setEnabled(false); + ui->actionManual_Control->setEnabled(false); + ui->menuDefault_Calibration->setEnabled(false); + ui->actionFirmware_Update->setEnabled(false); + if(deviceActionGroup->checkedAction()) { + deviceActionGroup->checkedAction()->setChecked(false); + } + lConnectionStatus.setText("No device connected"); + lDeviceInfo.setText("No device information available yet"); +} + +void VNA::DeviceConnectionLost() +{ + DisconnectDevice(); + QMessageBox::warning(this, "Disconnected", "The USB connection to the device has been lost"); + UpdateDeviceList(); +} + +void VNA::CreateToolbars() +{ + // Sweep toolbar + auto tb_sweep = new QToolBar("Sweep", this); + auto eStart = new SIUnitEdit("Hz", " kMG", 6); + eStart->setFixedWidth(100); + eStart->setToolTip("Start frequency"); + connect(eStart, &SIUnitEdit::valueChanged, this, &VNA::SetStartFreq); + connect(this, &VNA::startFreqChanged, eStart, &SIUnitEdit::setValueQuiet); + tb_sweep->addWidget(new QLabel("Start:")); + tb_sweep->addWidget(eStart); + + auto eCenter = new SIUnitEdit("Hz", " kMG", 6); + eCenter->setFixedWidth(100); + eCenter->setToolTip("Center frequency"); + connect(eCenter, &SIUnitEdit::valueChanged, this, &VNA::SetCenterFreq); + connect(this, &VNA::centerFreqChanged, eCenter, &SIUnitEdit::setValueQuiet); + tb_sweep->addWidget(new QLabel("Center:")); + tb_sweep->addWidget(eCenter); + + auto eStop = new SIUnitEdit("Hz", " kMG", 6); + eStop->setFixedWidth(100); + eStop->setToolTip("Stop frequency"); + connect(eStop, &SIUnitEdit::valueChanged, this, &VNA::SetStopFreq); + connect(this, &VNA::stopFreqChanged, eStop, &SIUnitEdit::setValueQuiet); + tb_sweep->addWidget(new QLabel("Stop:")); + tb_sweep->addWidget(eStop); + + auto eSpan = new SIUnitEdit("Hz", " kMG", 6); + eSpan->setFixedWidth(100); + eSpan->setToolTip("Span"); + connect(eSpan, &SIUnitEdit::valueChanged, this, &VNA::SetSpan); + connect(this, &VNA::spanChanged, eSpan, &SIUnitEdit::setValueQuiet); + tb_sweep->addWidget(new QLabel("Span:")); + tb_sweep->addWidget(eSpan); + + auto bFull = new QPushButton(QIcon::fromTheme("zoom-fit-best"), ""); + bFull->setToolTip("Full span"); + connect(bFull, &QPushButton::clicked, this, &VNA::SetFullSpan); + tb_sweep->addWidget(bFull); + + auto bZoomIn = new QPushButton(QIcon::fromTheme("zoom-in"), ""); + bZoomIn->setToolTip("Zoom in"); + connect(bZoomIn, &QPushButton::clicked, this, &VNA::SpanZoomIn); + tb_sweep->addWidget(bZoomIn); + + auto bZoomOut = new QPushButton(QIcon::fromTheme("zoom-out"), ""); + bZoomOut->setToolTip("Zoom out"); + connect(bZoomOut, &QPushButton::clicked, this, &VNA::SpanZoomOut); + tb_sweep->addWidget(bZoomOut); + + addToolBar(tb_sweep); + + // Acquisition toolbar + auto tb_acq = new QToolBar("Acquisition", this); + auto dbm = new QDoubleSpinBox(); + dbm->setValue(settings.cdbm_excitation * 100); + dbm->setFixedWidth(95); + dbm->setRange(-42.0, -10.0); + dbm->setSingleStep(0.25); + dbm->setSuffix("dbm"); + dbm->setToolTip("Stimulus level"); + connect(dbm, qOverload(&QDoubleSpinBox::valueChanged), this, &VNA::SetSourceLevel); + connect(this, &VNA::sourceLevelChanged, dbm, &QDoubleSpinBox::setValue); + tb_acq->addWidget(new QLabel("Level:")); + tb_acq->addWidget(dbm); + + auto points = new QSpinBox(); + points->setFixedWidth(55); + points->setRange(1, 4501); + points->setValue(settings.points); + points->setSingleStep(100); + points->setToolTip("Points/sweep"); + connect(points, qOverload(&QSpinBox::valueChanged), this, &VNA::SetPoints); + connect(this, &VNA::pointsChanged, points, &QSpinBox::setValue); + tb_acq->addWidget(new QLabel("Points:")); + tb_acq->addWidget(points); + + auto eBandwidth = new SIUnitEdit("Hz", " k", 3); + eBandwidth->setValueQuiet(settings.if_bandwidth); + eBandwidth->setFixedWidth(70); + eBandwidth->setToolTip("IF bandwidth"); + connect(eBandwidth, &SIUnitEdit::valueChanged, this, &VNA::SetIFBandwidth); + connect(this, &VNA::IFBandwidthChanged, eBandwidth, &SIUnitEdit::setValueQuiet); + tb_acq->addWidget(new QLabel("IF BW:")); + tb_acq->addWidget(eBandwidth); + + addToolBar(tb_acq); + + // Reference toolbar + auto tb_reference = new QToolBar("Reference", this); + tb_reference->addWidget(new QLabel("Ref:")); + toolbars.referenceType = new QComboBox(); + toolbars.referenceType->addItem("Int"); + toolbars.referenceType->addItem("Ext"); + auto refInAuto = new QCheckBox("Auto"); + refInAuto->setChecked(true); + toolbars.referenceType->setEnabled(false); + connect(refInAuto, &QCheckBox::clicked, [this](bool checked) { + // TODO change device settings + toolbars.referenceType->setEnabled(!checked); + }); + tb_reference->addWidget(toolbars.referenceType); + tb_reference->addWidget(refInAuto); + tb_reference->addSeparator(); + tb_reference->addWidget(new QLabel("Ref out:")); + auto refOutEnabled = new QCheckBox(); + auto refOutFreq = new QComboBox(); + refOutFreq->addItem("10 MHz"); + refOutFreq->addItem("100 MHz"); + tb_reference->addWidget(refOutEnabled); + tb_reference->addWidget(refOutFreq); + + addToolBar(tb_reference); + + // Calibration toolbar (and populate calibration menu) + auto tb_cal = new QToolBar("Calibration"); + tb_cal->addWidget(new QLabel("Calibration:")); + auto cbEnableCal = new QCheckBox; + tb_cal->addWidget(cbEnableCal); + auto cbType = new QComboBox(); + auto calMenuGroup = new QActionGroup(this); + calMenuGroup->addAction(ui->actionCalDisabled); + for(auto type : Calibration::Types()) { + cbType->addItem(Calibration::TypeToString(type), (int) type); + auto menuAction = new QAction(Calibration::TypeToString(type)); + calMenuGroup->addAction(menuAction); + connect(menuAction, &QAction::triggered, [=](){ + ApplyCalibration(type); + }); + connect(this, &VNA::CalibrationApplied, [=](Calibration::Type applied){ + if(type == applied) { + menuAction->setChecked(true); + } + }); + menuAction->setCheckable(true); + ui->menuCalibration->insertAction(ui->actionCalDisabled, menuAction); + } + + auto calToolbarLambda = [=]() { + if(cbEnableCal->isChecked()) { + // Get requested calibration type from combobox + ApplyCalibration((Calibration::Type) cbType->itemData(cbType->currentIndex()).toInt()); + } else { + DisableCalibration(); + } + }; + + // Calibration connections + connect(cbEnableCal, &QCheckBox::stateChanged, calToolbarLambda); + connect(cbType, qOverload(&QComboBox::currentIndexChanged), calToolbarLambda); + connect(this, &VNA::CalibrationDisabled, [=](){ + cbType->blockSignals(true); + cbEnableCal->blockSignals(true); + ui->actionCalDisabled->setChecked(true); + cbEnableCal->setCheckState(Qt::CheckState::Unchecked); + cbType->blockSignals(false); + cbEnableCal->blockSignals(false); + }); + connect(ui->actionCalDisabled, &QAction::triggered, this, &VNA::DisableCalibration); + connect(this, &VNA::CalibrationApplied, [=](Calibration::Type applied){ + cbType->blockSignals(true); + cbEnableCal->blockSignals(true); + for(int i=0;icount();i++) { + if(cbType->itemData(i).toInt() == (int) applied) { + cbType->setCurrentIndex(i); + break; + } + } + cbEnableCal->setCheckState(Qt::CheckState::Checked); + cbType->blockSignals(false); + cbEnableCal->blockSignals(false); + }); + + tb_cal->addWidget(cbType); + + addToolBar(tb_cal); +} + +int VNA::UpdateDeviceList() +{ + ui->menuConnect_to->clear(); + auto devices = Device::GetDevices(); + if(devices.size()) { + for(auto d : devices) { + auto connectAction = ui->menuConnect_to->addAction(d); + deviceActionGroup->addAction(connectAction); + connectAction->setCheckable(true); + if(device && d == device->serial()) { + connectAction->setChecked(true); + } + connect(connectAction, &QAction::triggered, [this, connectAction, d]() { + ConnectToDevice(d); + if(device) { + // connectAction might have been unchecked if it was a reconnect to the already connected device + connectAction->setChecked(true); + } + }); + } + ui->menuConnect_to->setEnabled(true); + } else { + // no devices available, disable connection option + ui->menuConnect_to->setEnabled(false); + } + return devices.size(); +} + +void VNA::StartManualControl() +{ + auto control = new ManualControlDialog(*device, this); + connect(control, &QDialog::finished, [this](){ + SettingsChanged(); + }); + control->show(); +} + +void VNA::StartImpedanceMatching() +{ + auto dialog = new ImpedanceMatchDialog(*markerModel); + dialog->show(); +} + +void VNA::SetStartFreq(double freq) +{ + settings.f_start = freq; + if(settings.f_stop < freq) { + settings.f_stop = freq; + } + ConstrainAndUpdateFrequencies(); +} + +void VNA::SetStopFreq(double freq) +{ + settings.f_stop = freq; + if(settings.f_start > freq) { + settings.f_start = freq; + } + ConstrainAndUpdateFrequencies(); +} + +void VNA::SetCenterFreq(double freq) +{ + auto old_span = settings.f_stop - settings.f_start; + if (freq > old_span / 2) { + settings.f_start = freq - old_span / 2; + settings.f_stop = freq + old_span / 2; + } else { + settings.f_start = 0; + settings.f_stop = 2 * freq; + } + ConstrainAndUpdateFrequencies(); +} + +void VNA::SetSpan(double span) +{ + auto old_center = (settings.f_start + settings.f_stop) / 2; + if(old_center > span / 2) { + settings.f_start = old_center - span / 2; + } else { + settings.f_start = 0; + } + settings.f_stop = old_center + span / 2; + ConstrainAndUpdateFrequencies(); +} + +void VNA::SetFullSpan() +{ + settings.f_start = 0; + settings.f_stop = maxFreq; + ConstrainAndUpdateFrequencies(); +} + +void VNA::SpanZoomIn() +{ + auto center = (settings.f_start + settings.f_stop) / 2; + auto old_span = settings.f_stop - settings.f_start; + settings.f_start = center - old_span / 4; + settings.f_stop = center + old_span / 4; + ConstrainAndUpdateFrequencies(); +} + +void VNA::SpanZoomOut() +{ + auto center = (settings.f_start + settings.f_stop) / 2; + auto old_span = settings.f_stop - settings.f_start; + if(center > old_span) { + settings.f_start = center - old_span; + } else { + settings.f_start = 0; + } + settings.f_stop = center + old_span; + ConstrainAndUpdateFrequencies(); +} + +void VNA::SetSourceLevel(double level) +{ + // TODO remove hardcoded limits + if(level > -10.0) { + level = -10.0; + } else if(level < -42.0) { + level = -42.0; + } + emit sourceLevelChanged(level); + settings.cdbm_excitation = level * 100; + SettingsChanged(); +} + +void VNA::SetPoints(unsigned int points) +{ + // TODO remove hardcoded limits + if (points < 1) { + points = 1; + } else if(points > 4501) { + points = 4501; + } + emit pointsChanged(points); + settings.points = points; + SettingsChanged(); +} + +void VNA::SetIFBandwidth(double bandwidth) +{ + settings.if_bandwidth = bandwidth; + emit IFBandwidthChanged(bandwidth); + SettingsChanged(); +} + +void VNA::SetAveraging(unsigned int averages) +{ + this->averages = averages; + average.setAverages(averages); + emit averagingChanged(averages); + SettingsChanged(); +} + +void VNA::DisableCalibration(bool force) +{ + if(calValid || force) { + calValid = false; + ui->actionImport_error_terms_as_traces->setEnabled(false); + emit CalibrationDisabled(); + average.reset(); + } +} + +void VNA::ApplyCalibration(Calibration::Type type) +{ + if(cal.calculationPossible(type)) { + try { + cal.constructErrorTerms(type); + calValid = true; + average.reset(); + ui->actionImport_error_terms_as_traces->setEnabled(true); + emit CalibrationApplied(type); + } catch (runtime_error e) { + QMessageBox::critical(this, "Calibration failure", e.what()); + DisableCalibration(true); + } + } else { + // Not all required traces available + // TODO start tracedata dialog with required traces + QMessageBox::information(this, "Missing calibration traces", "Not all calibration traces for this type of calibration have been measured. The calibration can be enabled after the missing traces have been acquired."); + DisableCalibration(true); + auto traceDialog = new CalibrationTraceDialog(&cal, type); + connect(traceDialog, &CalibrationTraceDialog::triggerMeasurement, this, &VNA::StartCalibrationMeasurement); + connect(traceDialog, &CalibrationTraceDialog::applyCalibration, this, &VNA::ApplyCalibration); + connect(this, &VNA::CalibrationMeasurementComplete, traceDialog, &CalibrationTraceDialog::measurementComplete); + traceDialog->show(); + } +} + +void VNA::StartCalibrationMeasurement(Calibration::Measurement m) +{ + // Trigger sweep to start from beginning + SettingsChanged(); + calMeasurement = m; + // Delete any already captured data of this measurement + cal.clearMeasurement(m); + calWaitFirst = true; + calMeasuring = true; + QString text = "Measuring \""; + text.append(Calibration::MeasurementToString(m)); + text.append("\" parameters."); + calDialog.setRange(0, settings.points); + calDialog.setLabelText(text); + calDialog.setCancelButtonText("Abort"); + calDialog.setWindowTitle("Taking calibration measurement..."); + calDialog.setValue(0); + calDialog.setWindowModality(Qt::ApplicationModal); + // always show the dialog + calDialog.setMinimumDuration(0); + connect(&calDialog, &QProgressDialog::canceled, [=]() { + // the user aborted the calibration measurement + calMeasuring = false; + cal.clearMeasurement(calMeasurement); + }); +} + +void VNA::ConstrainAndUpdateFrequencies() +{ + if(settings.f_stop > maxFreq) { + settings.f_stop = maxFreq; + } + if(settings.f_start > settings.f_stop) { + settings.f_start = settings.f_stop; + } + emit startFreqChanged(settings.f_start); + emit stopFreqChanged(settings.f_stop); + emit spanChanged(settings.f_stop - settings.f_start); + emit centerFreqChanged((settings.f_stop + settings.f_start)/2); + SettingsChanged(); +} diff --git a/Software/PC_Application/vna.h b/Software/PC_Application/vna.h new file mode 100644 index 0000000..23e2ab5 --- /dev/null +++ b/Software/PC_Application/vna.h @@ -0,0 +1,125 @@ +#ifndef VNA_H +#define VNA_H + +#include +#include +#include +#include +#include "Device/device.h" +#include "Traces/traceplot.h" +#include "Calibration/calibration.h" +#include +#include "Menu/menuaction.h" +#include "Traces/tracemodel.h" +#include "Traces/tracemarkermodel.h" +#include "averaging.h" +#include "Device/devicelog.h" + +namespace Ui { +class MainWindow; +} + +class VNA : public QMainWindow +{ + Q_OBJECT +public: + VNA(QWidget *parent = nullptr); +protected: + void closeEvent(QCloseEvent *event) override; +private: + static constexpr double minFreq = 0; + static constexpr double maxFreq = 6000000000; + static constexpr Protocol::SweepSettings defaultSweep = { + .f_start = 1000000, + .f_stop = (uint64_t) maxFreq, + .points = 501, + .if_bandwidth = 1000, + .cdbm_excitation = 0, + }; +private slots: + void NewDatapoint(Protocol::Datapoint d); + void ConnectToDevice(QString serial = QString()); + void DisconnectDevice(); + int UpdateDeviceList(); + void StartManualControl(); + void StartImpedanceMatching(); + // Sweep control + void SetStartFreq(double freq); + void SetStopFreq(double freq); + void SetCenterFreq(double freq); + void SetSpan(double span); + void SetFullSpan(); + void SpanZoomIn(); + void SpanZoomOut(); + // Acquisition control + void SetSourceLevel(double level); + void SetPoints(unsigned int points); + void SetIFBandwidth(double bandwidth); + void SetAveraging(unsigned int averages); + // Calibration + void DisableCalibration(bool force = false); + void ApplyCalibration(Calibration::Type type); + void StartCalibrationMeasurement(Calibration::Measurement m); + +signals: + void CalibrationMeasurementComplete(Calibration::Measurement m); + +private: + void UpdateStatusPanel(); + void SettingsChanged(); + void DeviceConnectionLost(); + void CreateToolbars(); + void ConstrainAndUpdateFrequencies(); + + struct { + QComboBox *referenceType; + } toolbars; + + Device *device; + DeviceLog deviceLog; + QString deviceSerial; + QActionGroup *deviceActionGroup; + Protocol::SweepSettings settings; + unsigned int averages; + TraceModel traceModel; + TraceMarkerModel *markerModel; + Averaging average; + + // Calibration + Calibration cal; + bool calValid; + Calibration::Measurement calMeasurement; + bool calMeasuring; + bool calWaitFirst; + QProgressDialog calDialog; + + // Calibration menu + MenuAction *mCalSOL1, *mCalSOL2, *mCalFullSOLT; + + // Status Labels + QLabel lStart, lCenter, lStop, lSpan, lPoints, lBandwidth; + QLabel lCalibration; + QLabel lAverages; + + // Status bar widgets + QLabel lConnectionStatus; + QLabel lDeviceInfo; + + Ui::MainWindow *ui; +signals: + void dataChanged(); + void startFreqChanged(double freq); + void stopFreqChanged(double freq); + void centerFreqChanged(double freq); + void spanChanged(double span); + + void sourceLevelChanged(double level); + void pointsChanged(unsigned int points); + void IFBandwidthChanged(double bandwidth); + void averagingChanged(unsigned int averages); + + void CalibrationDisabled(); + void CalibrationApplied(Calibration::Type type); +}; + +#endif // VNA_H diff --git a/Software/VNA_embedded/Application/Communication/Protocol.cpp b/Software/VNA_embedded/Application/Communication/Protocol.cpp index 64b21ce..6fe0587 100644 --- a/Software/VNA_embedded/Application/Communication/Protocol.cpp +++ b/Software/VNA_embedded/Application/Communication/Protocol.cpp @@ -150,7 +150,7 @@ static Protocol::Datapoint DecodeDatapoint(uint8_t *buf) { e.get(d.pointNum); return d; } -static uint16_t EncodeDatapoint(Protocol::Datapoint d, uint8_t *buf, +static int16_t EncodeDatapoint(Protocol::Datapoint d, uint8_t *buf, uint16_t bufSize) { Encoder e(buf, bufSize); e.add(d.real_S11); @@ -176,7 +176,7 @@ static Protocol::SweepSettings DecodeSweepSettings(uint8_t *buf) { e.get(d.cdbm_excitation); return d; } -static uint16_t EncodeSweepSettings(Protocol::SweepSettings d, uint8_t *buf, +static int16_t EncodeSweepSettings(Protocol::SweepSettings d, uint8_t *buf, uint16_t bufSize) { Encoder e(buf, bufSize); e.add(d.f_start); @@ -204,7 +204,7 @@ static Protocol::DeviceInfo DecodeDeviceInfo(uint8_t *buf) { e.get(d.temperatures.MCU); return d; } -static uint16_t EncodeDeviceInfo(Protocol::DeviceInfo d, uint8_t *buf, +static int16_t EncodeDeviceInfo(Protocol::DeviceInfo d, uint8_t *buf, uint16_t bufSize) { Encoder e(buf, bufSize); e.add(d.FW_major); @@ -243,7 +243,7 @@ static Protocol::ManualStatus DecodeStatus(uint8_t *buf) { d.LO_locked = e.getBits(1); return d; } -static uint16_t EncodeStatus(Protocol::ManualStatus d, uint8_t *buf, +static int16_t EncodeStatus(Protocol::ManualStatus d, uint8_t *buf, uint16_t bufSize) { Encoder e(buf, bufSize); e.add(d.port1min); @@ -291,7 +291,7 @@ static Protocol::ManualControl DecodeManualControl(uint8_t *buf) { e.get(d.Samples); return d; } -static uint16_t EncodeManualControl(Protocol::ManualControl d, uint8_t *buf, +static int16_t EncodeManualControl(Protocol::ManualControl d, uint8_t *buf, uint16_t bufSize) { Encoder e(buf, bufSize); e.addBits(d.SourceHighCE, 1); @@ -318,6 +318,26 @@ static uint16_t EncodeManualControl(Protocol::ManualControl d, uint8_t *buf, return e.getSize(); } +static Protocol::FirmwarePacket DecodeFirmwarePacket(uint8_t *buf) { + Protocol::FirmwarePacket d; + // simple packet format, memcpy is faster than using the decoder + memcpy(&d.address, buf, 4); + buf += 4; + memcpy(d.data, buf, Protocol::FirmwareChunkSize); + return d; +} +static int16_t EncodeFirmwarePacket(const Protocol::FirmwarePacket &d, uint8_t *buf, uint16_t bufSize) { + if(bufSize < 4 + Protocol::FirmwareChunkSize) { + // unable to encode, not enough space + return -1; + } + // simple packet format, memcpy is faster than using the encoder + memcpy(buf, &d.address, 4); + buf += 4; + memcpy(buf, d.data, Protocol::FirmwareChunkSize); + return 4 + Protocol::FirmwareChunkSize; +} + uint16_t Protocol::DecodeBuffer(uint8_t *buf, uint16_t len, PacketInfo *info) { if (!info || !len) { info->type = PacketType::None; @@ -377,6 +397,12 @@ uint16_t Protocol::DecodeBuffer(uint8_t *buf, uint16_t len, PacketInfo *info) { case PacketType::ManualControl: info->manual = DecodeManualControl(&data[4]); break; + case PacketType::FirmwarePacket: + info->firmware = DecodeFirmwarePacket(&data[4]); + break; + case PacketType::Ack: + // no payload, nothing to do + break; case PacketType::None: break; } @@ -385,7 +411,7 @@ uint16_t Protocol::DecodeBuffer(uint8_t *buf, uint16_t len, PacketInfo *info) { } uint16_t Protocol::EncodePacket(PacketInfo packet, uint8_t *dest, uint16_t destsize) { - uint16_t payload_size = 0; + int16_t payload_size = 0; switch (packet.type) { case PacketType::Datapoint: payload_size = EncodeDatapoint(packet.datapoint, &dest[4], destsize - 8); @@ -402,10 +428,16 @@ uint16_t Protocol::EncodePacket(PacketInfo packet, uint8_t *dest, uint16_t dests case PacketType::ManualControl: payload_size = EncodeManualControl(packet.manual, &dest[4], destsize - 8); break; + case PacketType::FirmwarePacket: + payload_size = EncodeFirmwarePacket(packet.firmware, &dest[4], destsize - 8); + break; + case PacketType::Ack: + // no payload, nothing to do + break; case PacketType::None: break; } - if (payload_size == 0 || payload_size + 8 > destsize) { + if (payload_size < 0 || payload_size + 8 > destsize) { // encoding failed, buffer too small return 0; } diff --git a/Software/VNA_embedded/Application/Communication/Protocol.hpp b/Software/VNA_embedded/Application/Communication/Protocol.hpp index 75a43fd..c2949d0 100644 --- a/Software/VNA_embedded/Application/Communication/Protocol.hpp +++ b/Software/VNA_embedded/Application/Communication/Protocol.hpp @@ -83,6 +83,13 @@ using ManualControl = struct _manualControl { uint32_t Samples; }; + +static constexpr uint16_t FirmwareChunkSize = 256; +using FirmwarePacket = struct _firmwarePacket { + uint32_t address; + uint8_t data[FirmwareChunkSize]; +}; + enum class PacketType : uint8_t { None, Datapoint, @@ -90,6 +97,8 @@ enum class PacketType : uint8_t { Status, ManualControl, DeviceInfo, + FirmwarePacket, + Ack, }; using PacketInfo = struct _packetinfo { @@ -100,6 +109,7 @@ using PacketInfo = struct _packetinfo { DeviceInfo info; ManualControl manual; ManualStatus status; + FirmwarePacket firmware; }; };