kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
commit
e2d79b7d35
|
@ -38,6 +38,24 @@ class Analysis:
|
|||
def reset(self):
|
||||
pass
|
||||
|
||||
def calculateRolloff(self, location1, location2):
|
||||
from NanoVNASaver.NanoVNASaver import NanoVNASaver
|
||||
if location1 == location2:
|
||||
return 0, 0
|
||||
frequency1 = self.app.data21[location1].freq
|
||||
frequency2 = self.app.data21[location2].freq
|
||||
gain1 = NanoVNASaver.gain(self.app.data21[location1])
|
||||
gain2 = NanoVNASaver.gain(self.app.data21[location2])
|
||||
frequency_factor = frequency2 / frequency1
|
||||
if frequency_factor < 1:
|
||||
frequency_factor = 1 / frequency_factor
|
||||
attenuation = abs(gain1 - gain2)
|
||||
logger.debug("Measured points: %d Hz and %d Hz", frequency1, frequency2)
|
||||
logger.debug("%f dB over %f factor", attenuation, frequency_factor)
|
||||
octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2))
|
||||
decade_attenuation = attenuation / math.log10(frequency_factor)
|
||||
return octave_attenuation, decade_attenuation
|
||||
|
||||
|
||||
class LowPassAnalysis(Analysis):
|
||||
def __init__(self, app):
|
||||
|
@ -154,16 +172,21 @@ class LowPassAnalysis(Analysis):
|
|||
six_db_cutoff_frequency = self.app.data21[six_db_location].freq
|
||||
self.six_db_label.setText(NanoVNASaver.formatFrequency(six_db_cutoff_frequency))
|
||||
|
||||
six_db_attenuation = NanoVNASaver.gain(self.app.data21[six_db_location])
|
||||
max_attenuation = NanoVNASaver.gain(self.app.data21[len(self.app.data21) - 1])
|
||||
frequency_factor = self.app.data21[len(self.app.data21) - 1].freq / six_db_cutoff_frequency
|
||||
attenuation = (max_attenuation - six_db_attenuation)
|
||||
logger.debug("Measured points: %d Hz and %d Hz", six_db_cutoff_frequency, self.app.data21[len(self.app.data21) - 1].freq)
|
||||
logger.debug("%d dB over %f factor", attenuation, frequency_factor)
|
||||
octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2))
|
||||
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
decade_attenuation = attenuation / math.log10(frequency_factor)
|
||||
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
ten_db_location = -1
|
||||
for i in range(cutoff_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(cutoff_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(six_db_location, len(self.app.data21)):
|
||||
|
@ -173,16 +196,23 @@ class LowPassAnalysis(Analysis):
|
|||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location < 0:
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
self.db_per_octave_label.setText("Not calculated")
|
||||
self.db_per_decade_label.setText("Not calculated")
|
||||
|
||||
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)")
|
||||
|
||||
|
@ -303,16 +333,21 @@ class HighPassAnalysis(Analysis):
|
|||
six_db_cutoff_frequency = self.app.data21[six_db_location].freq
|
||||
self.six_db_label.setText(NanoVNASaver.formatFrequency(six_db_cutoff_frequency))
|
||||
|
||||
six_db_attenuation = NanoVNASaver.gain(self.app.data21[six_db_location])
|
||||
max_attenuation = NanoVNASaver.gain(self.app.data21[len(self.app.data21) - 1])
|
||||
frequency_factor = self.app.data21[len(self.app.data21) - 1].freq / six_db_cutoff_frequency
|
||||
attenuation = (max_attenuation - six_db_attenuation)
|
||||
logger.debug("Measured points: %d Hz and %d Hz", six_db_cutoff_frequency, self.app.data21[len(self.app.data21) - 1].freq)
|
||||
logger.debug("%d dB over %f factor", attenuation, frequency_factor)
|
||||
octave_attenuation = attenuation / (math.log10(frequency_factor) / math.log10(2))
|
||||
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
decade_attenuation = attenuation / math.log10(frequency_factor)
|
||||
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
ten_db_location = -1
|
||||
for i in range(cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(six_db_location, -1, -1):
|
||||
|
@ -322,16 +357,23 @@ class HighPassAnalysis(Analysis):
|
|||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location < 0:
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
self.db_per_octave_label.setText("Not calculated")
|
||||
self.db_per_decade_label.setText("Not calculated")
|
||||
|
||||
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)")
|
||||
|
||||
|
@ -364,11 +406,13 @@ class BandPassAnalysis(Analysis):
|
|||
|
||||
self.center_frequency_label = QtWidgets.QLabel()
|
||||
self.span_label = QtWidgets.QLabel()
|
||||
self.six_db_span_label = QtWidgets.QLabel()
|
||||
self.quality_label = QtWidgets.QLabel()
|
||||
|
||||
layout.addRow("Center frequency:", self.center_frequency_label)
|
||||
layout.addRow("Span:", self.span_label)
|
||||
layout.addRow("Bandwidth (-3 dB):", self.span_label)
|
||||
layout.addRow("Quality factor:", self.quality_label)
|
||||
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
|
||||
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
|
@ -390,6 +434,10 @@ class BandPassAnalysis(Analysis):
|
|||
|
||||
def reset(self):
|
||||
self.result_label.clear()
|
||||
self.span_label.clear()
|
||||
self.quality_label.clear()
|
||||
self.six_db_span_label.clear()
|
||||
|
||||
self.upper_cutoff_label.clear()
|
||||
self.upper_six_db_label.clear()
|
||||
self.upper_sixty_db_label.clear()
|
||||
|
@ -537,35 +585,47 @@ class BandPassAnalysis(Analysis):
|
|||
lower_six_db_cutoff_frequency = self.app.data21[lower_six_db_location].freq
|
||||
self.lower_six_db_label.setText(NanoVNASaver.formatFrequency(lower_six_db_cutoff_frequency))
|
||||
|
||||
lower_six_db_attenuation = NanoVNASaver.gain(self.app.data21[lower_six_db_location])
|
||||
lower_max_attenuation = NanoVNASaver.gain(self.app.data21[0])
|
||||
frequency_factor = self.app.data21[0].freq / lower_six_db_cutoff_frequency
|
||||
lower_attenuation = (lower_max_attenuation - lower_six_db_attenuation)
|
||||
logger.debug("Measured points: %d Hz and %d Hz", lower_six_db_cutoff_frequency, self.app.data21[0].freq)
|
||||
logger.debug("%d dB over %f factor", lower_attenuation, frequency_factor)
|
||||
octave_attenuation = lower_attenuation / (math.log10(frequency_factor) / math.log10(2))
|
||||
self.lower_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
decade_attenuation = lower_attenuation / math.log10(frequency_factor)
|
||||
self.lower_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
ten_db_location = -1
|
||||
for i in range(lower_cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
lower_sixty_db_location = -1
|
||||
twenty_db_location = -1
|
||||
for i in range(lower_cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(lower_six_db_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
lower_sixty_db_location = i
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if lower_sixty_db_location < 0:
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.lower_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.upper_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.lower_sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.lower_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.lower_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
lower_sixty_db_cutoff_frequency = self.app.data21[lower_sixty_db_location].freq
|
||||
self.lower_sixty_db_label.setText(NanoVNASaver.formatFrequency(lower_sixty_db_cutoff_frequency))
|
||||
self.lower_db_per_octave_label.setText("Not calculated")
|
||||
self.lower_db_per_decade_label.setText("Not calculated")
|
||||
|
||||
# Upper roll-off
|
||||
|
||||
|
@ -583,35 +643,324 @@ class BandPassAnalysis(Analysis):
|
|||
upper_six_db_cutoff_frequency = self.app.data21[upper_six_db_location].freq
|
||||
self.upper_six_db_label.setText(NanoVNASaver.formatFrequency(upper_six_db_cutoff_frequency))
|
||||
|
||||
upper_six_db_attenuation = NanoVNASaver.gain(self.app.data21[upper_six_db_location])
|
||||
upper_max_attenuation = NanoVNASaver.gain(self.app.data21[0])
|
||||
frequency_factor = self.app.data21[0].freq / upper_six_db_cutoff_frequency
|
||||
upper_attenuation = (upper_max_attenuation - upper_six_db_attenuation)
|
||||
logger.debug("Measured points: %d Hz and %d Hz", upper_six_db_cutoff_frequency, self.app.data21[0].freq)
|
||||
logger.debug("%d dB over %f factor", upper_attenuation, frequency_factor)
|
||||
octave_attenuation = upper_attenuation / (math.log10(frequency_factor) / math.log10(2))
|
||||
self.upper_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
decade_attenuation = upper_attenuation / math.log10(frequency_factor)
|
||||
self.upper_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
|
||||
|
||||
upper_sixty_db_location = -1
|
||||
self.six_db_span_label.setText(NanoVNASaver.formatFrequency(six_db_span))
|
||||
|
||||
ten_db_location = -1
|
||||
for i in range(upper_cutoff_location, len(self.app.data21), 1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(upper_cutoff_location, len(self.app.data21), 1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(upper_six_db_location, len(self.app.data21), 1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
upper_sixty_db_location = i
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if upper_sixty_db_location < 0:
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.upper_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.upper_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.upper_sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.upper_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.upper_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
upper_sixty_db_cutoff_frequency = self.app.data21[upper_sixty_db_location].freq
|
||||
self.upper_sixty_db_label.setText(NanoVNASaver.formatFrequency(upper_sixty_db_cutoff_frequency))
|
||||
self.upper_db_per_octave_label.setText("Not calculated")
|
||||
self.upper_db_per_decade_label.setText("Not calculated")
|
||||
|
||||
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
|
||||
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)\n" +
|
||||
"Insufficient data for analysis. Increase segment count.")
|
||||
else:
|
||||
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)")
|
||||
|
||||
|
||||
class BandStopAnalysis(Analysis):
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self._widget = QtWidgets.QWidget()
|
||||
|
||||
layout = QtWidgets.QFormLayout()
|
||||
self._widget.setLayout(layout)
|
||||
layout.addRow(QtWidgets.QLabel("Band stop filter analysis"))
|
||||
self.result_label = QtWidgets.QLabel()
|
||||
self.lower_cutoff_label = QtWidgets.QLabel()
|
||||
self.lower_six_db_label = QtWidgets.QLabel()
|
||||
self.lower_sixty_db_label = QtWidgets.QLabel()
|
||||
self.lower_db_per_octave_label = QtWidgets.QLabel()
|
||||
self.lower_db_per_decade_label = QtWidgets.QLabel()
|
||||
|
||||
self.upper_cutoff_label = QtWidgets.QLabel()
|
||||
self.upper_six_db_label = QtWidgets.QLabel()
|
||||
self.upper_sixty_db_label = QtWidgets.QLabel()
|
||||
self.upper_db_per_octave_label = QtWidgets.QLabel()
|
||||
self.upper_db_per_decade_label = QtWidgets.QLabel()
|
||||
layout.addRow("Result:", self.result_label)
|
||||
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
self.center_frequency_label = QtWidgets.QLabel()
|
||||
self.span_label = QtWidgets.QLabel()
|
||||
self.six_db_span_label = QtWidgets.QLabel()
|
||||
self.quality_label = QtWidgets.QLabel()
|
||||
|
||||
layout.addRow("Center frequency:", self.center_frequency_label)
|
||||
layout.addRow("Bandwidth (-3 dB):", self.span_label)
|
||||
layout.addRow("Quality factor:", self.quality_label)
|
||||
layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label)
|
||||
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
layout.addRow(QtWidgets.QLabel("Lower side:"))
|
||||
layout.addRow("Cutoff frequency:", self.lower_cutoff_label)
|
||||
layout.addRow("-6 dB point:", self.lower_six_db_label)
|
||||
layout.addRow("-60 dB point:", self.lower_sixty_db_label)
|
||||
layout.addRow("Roll-off:", self.lower_db_per_octave_label)
|
||||
layout.addRow("Roll-off:", self.lower_db_per_decade_label)
|
||||
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
layout.addRow(QtWidgets.QLabel("Upper side:"))
|
||||
layout.addRow("Cutoff frequency:", self.upper_cutoff_label)
|
||||
layout.addRow("-6 dB point:", self.upper_six_db_label)
|
||||
layout.addRow("-60 dB point:", self.upper_sixty_db_label)
|
||||
layout.addRow("Roll-off:", self.upper_db_per_octave_label)
|
||||
layout.addRow("Roll-off:", self.upper_db_per_decade_label)
|
||||
|
||||
def reset(self):
|
||||
self.result_label.clear()
|
||||
self.span_label.clear()
|
||||
self.quality_label.clear()
|
||||
self.six_db_span_label.clear()
|
||||
|
||||
self.upper_cutoff_label.clear()
|
||||
self.upper_six_db_label.clear()
|
||||
self.upper_sixty_db_label.clear()
|
||||
self.upper_db_per_octave_label.clear()
|
||||
self.upper_db_per_decade_label.clear()
|
||||
|
||||
self.lower_cutoff_label.clear()
|
||||
self.lower_six_db_label.clear()
|
||||
self.lower_sixty_db_label.clear()
|
||||
self.lower_db_per_octave_label.clear()
|
||||
self.lower_db_per_decade_label.clear()
|
||||
|
||||
def runAnalysis(self):
|
||||
from NanoVNASaver.NanoVNASaver import NanoVNASaver
|
||||
self.reset()
|
||||
|
||||
if len(self.app.data21) == 0:
|
||||
logger.debug("No data to analyse")
|
||||
self.result_label.setText("No data to analyse.")
|
||||
return
|
||||
|
||||
peak_location = -1
|
||||
peak_db = NanoVNASaver.gain(self.app.data21[0])
|
||||
for i in range(len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if db > peak_db:
|
||||
peak_db = db
|
||||
peak_location = i
|
||||
|
||||
logger.debug("Found peak of %f at %d", peak_db, self.app.data[peak_location].freq)
|
||||
|
||||
lower_cutoff_location = -1
|
||||
pass_band_db = peak_db
|
||||
for i in range(len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 3:
|
||||
# We found the cutoff location
|
||||
lower_cutoff_location = i
|
||||
break
|
||||
|
||||
lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq
|
||||
lower_cutoff_gain = NanoVNASaver.gain(self.app.data21[lower_cutoff_location]) - pass_band_db
|
||||
|
||||
if lower_cutoff_gain < -4:
|
||||
logger.debug("Lower cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
|
||||
lower_cutoff_gain)
|
||||
|
||||
logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency)
|
||||
|
||||
self.lower_cutoff_label.setText(NanoVNASaver.formatFrequency(lower_cutoff_frequency) +
|
||||
" (" + str(round(lower_cutoff_gain, 1)) + " dB)")
|
||||
|
||||
self.app.markers[1].setFrequency(str(lower_cutoff_frequency))
|
||||
self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency))
|
||||
|
||||
upper_cutoff_location = -1
|
||||
for i in range(len(self.app.data21)-1, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 3:
|
||||
# We found the cutoff location
|
||||
upper_cutoff_location = i
|
||||
break
|
||||
|
||||
upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq
|
||||
upper_cutoff_gain = NanoVNASaver.gain(self.app.data21[upper_cutoff_location]) - pass_band_db
|
||||
if upper_cutoff_gain < -4:
|
||||
logger.debug("Upper cutoff frequency found at %f dB - insufficient data points for true -3 dB point.",
|
||||
upper_cutoff_gain)
|
||||
|
||||
logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency)
|
||||
|
||||
self.upper_cutoff_label.setText(NanoVNASaver.formatFrequency(upper_cutoff_frequency) +
|
||||
" (" + str(round(upper_cutoff_gain, 1)) + " dB)")
|
||||
self.app.markers[2].setFrequency(str(upper_cutoff_frequency))
|
||||
self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency))
|
||||
|
||||
span = upper_cutoff_frequency - lower_cutoff_frequency
|
||||
center_frequency = math.sqrt(lower_cutoff_frequency * upper_cutoff_frequency)
|
||||
q = center_frequency / span
|
||||
|
||||
self.span_label.setText(NanoVNASaver.formatFrequency(span))
|
||||
self.center_frequency_label.setText(NanoVNASaver.formatFrequency(center_frequency))
|
||||
self.quality_label.setText(str(round(q, 2)))
|
||||
|
||||
self.app.markers[0].setFrequency(str(round(center_frequency)))
|
||||
self.app.markers[0].frequencyInput.setText(str(round(center_frequency)))
|
||||
|
||||
# Lower roll-off
|
||||
|
||||
lower_six_db_location = -1
|
||||
for i in range(lower_cutoff_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 6:
|
||||
# We found 6dB location
|
||||
lower_six_db_location = i
|
||||
break
|
||||
|
||||
if lower_six_db_location < 0:
|
||||
self.result_label.setText("Lower 6 dB location not found.")
|
||||
return
|
||||
lower_six_db_cutoff_frequency = self.app.data21[lower_six_db_location].freq
|
||||
self.lower_six_db_label.setText(NanoVNASaver.formatFrequency(lower_six_db_cutoff_frequency))
|
||||
|
||||
ten_db_location = -1
|
||||
for i in range(lower_cutoff_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(lower_cutoff_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(lower_six_db_location, len(self.app.data21)):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.lower_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.lower_sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.lower_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.lower_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
self.lower_db_per_octave_label.setText("Not calculated")
|
||||
self.lower_db_per_decade_label.setText("Not calculated")
|
||||
|
||||
# Upper roll-off
|
||||
|
||||
upper_six_db_location = -1
|
||||
for i in range(upper_cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 6:
|
||||
# We found 6dB location
|
||||
upper_six_db_location = i
|
||||
break
|
||||
|
||||
if upper_six_db_location < 0:
|
||||
self.result_label.setText("Upper 6 dB location not found.")
|
||||
return
|
||||
upper_six_db_cutoff_frequency = self.app.data21[upper_six_db_location].freq
|
||||
self.upper_six_db_label.setText(NanoVNASaver.formatFrequency(upper_six_db_cutoff_frequency))
|
||||
|
||||
six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency
|
||||
|
||||
self.six_db_span_label.setText(NanoVNASaver.formatFrequency(six_db_span))
|
||||
|
||||
ten_db_location = -1
|
||||
for i in range(upper_cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 10:
|
||||
# We found 6dB location
|
||||
ten_db_location = i
|
||||
break
|
||||
|
||||
twenty_db_location = -1
|
||||
for i in range(upper_cutoff_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 20:
|
||||
# We found 6dB location
|
||||
twenty_db_location = i
|
||||
break
|
||||
|
||||
sixty_db_location = -1
|
||||
for i in range(upper_six_db_location, -1, -1):
|
||||
db = NanoVNASaver.gain(self.app.data21[i])
|
||||
if (pass_band_db - db) > 60:
|
||||
# We found 60dB location! Wow.
|
||||
sixty_db_location = i
|
||||
break
|
||||
|
||||
if sixty_db_location > 0:
|
||||
sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
|
||||
self.upper_sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency))
|
||||
else:
|
||||
# # We derive 60 dB instead
|
||||
# factor = 10 * (-54 / decade_attenuation)
|
||||
# sixty_db_cutoff_frequency = round(six_db_cutoff_frequency + six_db_cutoff_frequency * factor)
|
||||
# self.sixty_db_label.setText(NanoVNASaver.formatFrequency(sixty_db_cutoff_frequency) + " (derived)")
|
||||
self.upper_sixty_db_label.setText("Not calculated")
|
||||
|
||||
if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
|
||||
octave_attenuation, decade_attenuation = self.calculateRolloff(ten_db_location, twenty_db_location)
|
||||
self.upper_db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
|
||||
self.upper_db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
|
||||
else:
|
||||
self.upper_db_per_octave_label.setText("Not calculated")
|
||||
self.upper_db_per_decade_label.setText("Not calculated")
|
||||
|
||||
if upper_cutoff_gain < -4 or lower_cutoff_gain < -4:
|
||||
self.result_label.setText("Analysis complete (" + str(len(self.app.data)) + " points)\n" +
|
||||
|
|
|
@ -417,6 +417,11 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.notes_textedit.clear()
|
||||
|
||||
def calculate(self):
|
||||
if self.app.btnStopSweep.isEnabled():
|
||||
# Currently sweeping
|
||||
self.app.showError("Unable to apply calibration while a sweep is running. " +
|
||||
"Please stop the sweep and try again.")
|
||||
return
|
||||
# TODO: Error handling for all the fields.
|
||||
if self.use_ideal_values.isChecked():
|
||||
self.app.calibration.useIdealShort = True
|
||||
|
@ -475,7 +480,16 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
return
|
||||
filedialog = QtWidgets.QFileDialog(self)
|
||||
filedialog.setDefaultSuffix("cal")
|
||||
filename, _ = filedialog.getSaveFileName(filter="Calibration Files (*.cal);;All files (*.*)")
|
||||
filedialog.setNameFilter("Calibration Files (*.cal);;All files (*.*)")
|
||||
filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
||||
selected = filedialog.exec()
|
||||
if selected:
|
||||
filename = filedialog.selectedFiles()[0]
|
||||
else:
|
||||
return
|
||||
if filename == "":
|
||||
logger.debug("No file name selected.")
|
||||
return
|
||||
self.app.calibration.notes = self.notes_textedit.toPlainText().splitlines()
|
||||
if filename and self.app.calibration.saveCalibration(filename):
|
||||
self.app.settings.setValue("CalibrationFile", filename)
|
||||
|
|
|
@ -31,6 +31,8 @@ class Chart(QtWidgets.QWidget):
|
|||
secondarySweepColor = QtCore.Qt.darkMagenta
|
||||
referenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
|
||||
referenceColor.setAlpha(64)
|
||||
secondaryReferenceColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.blue)
|
||||
secondaryReferenceColor.setAlpha(64)
|
||||
backgroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.white)
|
||||
foregroundColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.lightGray)
|
||||
textColor: QtGui.QColor = QtGui.QColor(QtCore.Qt.black)
|
||||
|
@ -65,6 +67,10 @@ class Chart(QtWidgets.QWidget):
|
|||
self.referenceColor = color
|
||||
self.update()
|
||||
|
||||
def setSecondaryReferenceColor(self, color : QtGui.QColor):
|
||||
self.secondaryReferenceColor = color
|
||||
self.update()
|
||||
|
||||
def setBackgroundColor(self, color: QtGui.QColor):
|
||||
self.backgroundColor = color
|
||||
pal = self.palette()
|
||||
|
@ -1614,9 +1620,9 @@ class RealImaginaryChart(FrequencyChart):
|
|||
prev_y_im = self.getImYPosition(self.data[i-1])
|
||||
|
||||
# Real part first
|
||||
line_pen.setColor(self.sweepColor)
|
||||
qp.setPen(line_pen)
|
||||
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
|
||||
line_pen.setColor(self.sweepColor)
|
||||
qp.setPen(line_pen)
|
||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
|
||||
|
@ -1625,10 +1631,10 @@ class RealImaginaryChart(FrequencyChart):
|
|||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
|
||||
|
||||
# Imag part first
|
||||
# Imag part second
|
||||
line_pen.setColor(self.secondarySweepColor)
|
||||
qp.setPen(line_pen)
|
||||
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
|
||||
line_pen.setColor(self.secondarySweepColor)
|
||||
qp.setPen(line_pen)
|
||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
|
||||
|
@ -1639,7 +1645,7 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
primary_pen.setColor(self.referenceColor)
|
||||
line_pen.setColor(self.referenceColor)
|
||||
secondary_pen.setColor(self.referenceColor)
|
||||
secondary_pen.setColor(self.secondaryReferenceColor)
|
||||
qp.setPen(primary_pen)
|
||||
if len(self.reference) > 0:
|
||||
c = QtGui.QColor(self.referenceColor)
|
||||
|
@ -1647,7 +1653,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(20, 14, 25, 14) # Alpha might be low, so we draw twice
|
||||
qp.drawLine(20, 14, 25, 14)
|
||||
c = QtGui.QColor(self.secondaryReferenceColor)
|
||||
c.setAlpha(255)
|
||||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.chartWidth, 14, self.leftMargin + self.chartWidth + 5, 14)
|
||||
|
||||
for i in range(len(self.reference)):
|
||||
|
@ -1667,10 +1678,10 @@ class RealImaginaryChart(FrequencyChart):
|
|||
prev_y_re = self.getReYPosition(self.reference[i-1])
|
||||
prev_y_im = self.getImYPosition(self.reference[i-1])
|
||||
|
||||
line_pen.setColor(self.secondaryReferenceColor)
|
||||
qp.setPen(line_pen)
|
||||
# Real part first
|
||||
if self.isPlotable(x, y_re) and self.isPlotable(prev_x, prev_y_re):
|
||||
line_pen.setColor(self.referenceColor)
|
||||
qp.setPen(line_pen)
|
||||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
elif self.isPlotable(x, y_re) and not self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(x, y_re, prev_x, prev_y_re)
|
||||
|
@ -1679,10 +1690,10 @@ class RealImaginaryChart(FrequencyChart):
|
|||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
qp.drawLine(prev_x, prev_y_re, new_x, new_y)
|
||||
|
||||
# Imag part first
|
||||
line_pen.setColor(self.secondaryReferenceColor)
|
||||
qp.setPen(line_pen)
|
||||
# Imag part second
|
||||
if self.isPlotable(x, y_im) and self.isPlotable(prev_x, prev_y_im):
|
||||
line_pen.setColor(self.secondarySweepColor)
|
||||
qp.setPen(line_pen)
|
||||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
elif self.isPlotable(x, y_im) and not self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(x, y_im, prev_x, prev_y_im)
|
||||
|
|
|
@ -13,15 +13,277 @@
|
|||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import collections
|
||||
import logging
|
||||
import re
|
||||
from time import sleep
|
||||
from typing import List
|
||||
|
||||
import serial
|
||||
|
||||
Datapoint = collections.namedtuple('Datapoint', 'freq re im')
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VNA:
|
||||
pass
|
||||
name = "VNA"
|
||||
|
||||
def __init__(self, app, serialPort: serial.Serial):
|
||||
from NanoVNASaver.NanoVNASaver import NanoVNASaver
|
||||
self.app: NanoVNASaver = app
|
||||
self.serial = serialPort
|
||||
self.version: Version = Version("0.0.0")
|
||||
|
||||
@staticmethod
|
||||
def getVNA(app, serialPort: serial.Serial) -> 'VNA':
|
||||
logger.info("Finding correct VNA type")
|
||||
tmp_vna = VNA(app, serialPort)
|
||||
tmp_vna.flushSerialBuffers()
|
||||
firmware = tmp_vna.readFirmware()
|
||||
if firmware.find("NanoVNA-H") > 0:
|
||||
return NanoVNA_H(app, serialPort)
|
||||
if firmware.find("NanoVNA-F") > 0:
|
||||
return NanoVNA_F(app, serialPort)
|
||||
elif firmware.find("NanoVNA") > 0:
|
||||
return NanoVNA(app, serialPort)
|
||||
return InvalidVNA(app, serialPort)
|
||||
|
||||
def readFrequencies(self) -> List[str]:
|
||||
pass
|
||||
|
||||
def readValues11(self) -> List[str]:
|
||||
pass
|
||||
|
||||
def readValues21(self) -> List[str]:
|
||||
pass
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
pass
|
||||
|
||||
def isValid(self):
|
||||
return False
|
||||
|
||||
def flushSerialBuffers(self):
|
||||
if self.app.serialLock.acquire():
|
||||
self.serial.write(b"\r\n\r\n")
|
||||
sleep(0.1)
|
||||
self.serial.reset_input_buffer()
|
||||
self.serial.reset_output_buffer()
|
||||
sleep(0.1)
|
||||
self.app.serialLock.release()
|
||||
|
||||
def readFirmware(self) -> str:
|
||||
if self.app.serialLock.acquire():
|
||||
result = ""
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
self.serial.write("info\r".encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.01)
|
||||
while data != "ch> ":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading firmware data: %s", exc)
|
||||
finally:
|
||||
self.app.serialLock.release()
|
||||
return result
|
||||
else:
|
||||
logger.error("Unable to acquire serial lock to read firmware.")
|
||||
return ""
|
||||
|
||||
def readValues(self, value) -> List[str]:
|
||||
logger.debug("VNA reading %s", value)
|
||||
if self.app.serialLock.acquire():
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
|
||||
# Then send the command to read data
|
||||
self.serial.write(str(value + "\r").encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.05)
|
||||
while data != "ch> ":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
values = result.split("\r\n")
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading %s: %s", value, exc)
|
||||
return []
|
||||
finally:
|
||||
self.app.serialLock.release()
|
||||
return values[1:-1]
|
||||
else:
|
||||
logger.error("Unable to acquire serial lock to read %s", value)
|
||||
return []
|
||||
|
||||
def writeSerial(self, command):
|
||||
if not self.serial.is_open:
|
||||
logger.warning("Writing without serial port being opened (%s)", command)
|
||||
return
|
||||
if self.app.serialLock.acquire():
|
||||
try:
|
||||
self.serial.write(str(command + "\r").encode('ascii'))
|
||||
self.serial.readline()
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while writing to serial port (%s): %s", command, exc)
|
||||
finally:
|
||||
self.app.serialLock.release()
|
||||
return
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
self.writeSerial("sweep " + str(start) + " " + str(stop) + " 101")
|
||||
|
||||
|
||||
class InvalidVNA(VNA):
|
||||
name = "Invalid"
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
return
|
||||
|
||||
def resetSweep(self, start, stop):
|
||||
return
|
||||
|
||||
def writeSerial(self, command):
|
||||
return
|
||||
|
||||
def readFirmware(self):
|
||||
return
|
||||
|
||||
def readFrequencies(self) -> List[int]:
|
||||
return []
|
||||
|
||||
def readValues11(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def readValues21(self) -> List[str]:
|
||||
return []
|
||||
|
||||
def readValues(self, value):
|
||||
return
|
||||
|
||||
def flushSerialBuffers(self):
|
||||
return
|
||||
|
||||
|
||||
class NanoVNA(VNA):
|
||||
pass
|
||||
name = "NanoVNA"
|
||||
|
||||
def __init__(self, app, serialPort):
|
||||
super().__init__(app, serialPort)
|
||||
self.version = Version(self.readVersion())
|
||||
|
||||
logger.debug("Testing against 0.2.0")
|
||||
if self.version.version_string.find("extended with scan") > 0:
|
||||
logger.debug("Incompatible scan command detected.")
|
||||
self.useScan = False
|
||||
elif self.version >= Version("0.2.0"):
|
||||
logger.debug("Newer than 0.2.0, using new scan command.")
|
||||
self.useScan = True
|
||||
else:
|
||||
logger.debug("Older than 0.2.0, using old sweep command.")
|
||||
self.useScan = False
|
||||
|
||||
def isValid(self):
|
||||
return True
|
||||
|
||||
def readFrequencies(self) -> List[str]:
|
||||
return self.readValues("frequencies")
|
||||
|
||||
def readValues11(self) -> List[str]:
|
||||
return self.readValues("data 1")
|
||||
|
||||
def readValues21(self) -> List[str]:
|
||||
return self.readValues("data 1")
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
self.writeSerial("sweep " + str(start) + " " + str(stop) + " 101")
|
||||
self.writeSerial("resume")
|
||||
|
||||
def readVersion(self):
|
||||
logger.debug("Reading version info.")
|
||||
if not self.serial.is_open:
|
||||
return
|
||||
if self.app.serialLock.acquire():
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
self.serial.write("version\r".encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.1)
|
||||
while "ch>" not in data:
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
values = result.splitlines()
|
||||
logger.debug("Found version info: %s", values[1])
|
||||
return values[1]
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading firmware version: %s", exc)
|
||||
finally:
|
||||
self.app.serialLock.release()
|
||||
return
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
if self.useScan:
|
||||
self.writeSerial("scan " + str(start) + " " + str(stop) + " 101")
|
||||
else:
|
||||
self.writeSerial("sweep " + str(start) + " " + str(stop) + " 101")
|
||||
sleep(1)
|
||||
|
||||
|
||||
class NanoVNA_H(NanoVNA):
|
||||
name = "NanoVNA-H"
|
||||
|
||||
|
||||
class NanoVNA_F(NanoVNA):
|
||||
pass
|
||||
name = "NanoVNA-F"
|
||||
|
||||
|
||||
class Version:
|
||||
major = 0
|
||||
minor = 0
|
||||
revision = 0
|
||||
note = ""
|
||||
version_string =""
|
||||
|
||||
def __init__(self, version_string):
|
||||
self.version_string = version_string
|
||||
results = re.match(r"(.*\D+)?(\d+)\.(\d+)\.(\d+)(.*)", version_string)
|
||||
if results:
|
||||
self.major = int(results.group(2))
|
||||
self.minor = int(results.group(3))
|
||||
self.revision = int(results.group(4))
|
||||
self.note = results.group(5)
|
||||
logger.debug("Parsed version as %d.%d.%d%s", self.major, self.minor, self.revision, self.note)
|
||||
|
||||
@staticmethod
|
||||
def getVersion(major: int, minor: int, revision: int, note=""):
|
||||
return Version(str(major) + "." + str(minor) + "." + str(revision) + note)
|
||||
|
||||
def __gt__(self, other: "Version"):
|
||||
return self.major > other.major or self.major == other.major and self.minor > other.minor or \
|
||||
self.major == other.major and self.minor == other.minor and self.revision > other.revision
|
||||
|
||||
def __lt__(self, other: "Version"):
|
||||
return other > self
|
||||
|
||||
def __ge__(self, other: "Version"):
|
||||
return self > other or self == other
|
||||
|
||||
def __le__(self, other: "Version"):
|
||||
return self < other or self == other
|
||||
|
||||
def __eq__(self, other: "Version"):
|
||||
return self.major == other.major and self.minor == other.minor and self.revision == other.revision and \
|
||||
self.note == other.note
|
||||
|
|
|
@ -28,13 +28,14 @@ from PyQt5 import QtWidgets, QtCore, QtGui
|
|||
from PyQt5.QtCore import QModelIndex
|
||||
from serial.tools import list_ports
|
||||
|
||||
from NanoVNASaver.Hardware import VNA, InvalidVNA, Version
|
||||
from .Chart import Chart, PhaseChart, VSWRChart, PolarChart, SmithChart, LogMagChart, QualityFactorChart, TDRChart, \
|
||||
RealImaginaryChart
|
||||
from .Calibration import CalibrationWindow, Calibration
|
||||
from .Marker import Marker
|
||||
from .SweepWorker import SweepWorker
|
||||
from .Touchstone import Touchstone
|
||||
from .Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, BandPassAnalysis
|
||||
from .Analysis import Analysis, LowPassAnalysis, HighPassAnalysis, BandPassAnalysis, BandStopAnalysis
|
||||
from .about import version as ver
|
||||
|
||||
Datapoint = collections.namedtuple('Datapoint', 'freq re im')
|
||||
|
@ -62,8 +63,13 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
"NanoVNASaver", "NanoVNASaver")
|
||||
print("Settings: " + self.settings.fileName())
|
||||
self.threadpool = QtCore.QThreadPool()
|
||||
self.vna: VNA = InvalidVNA()
|
||||
self.worker = SweepWorker(self)
|
||||
|
||||
self.worker.signals.updated.connect(self.dataUpdated)
|
||||
self.worker.signals.finished.connect(self.sweepFinished)
|
||||
self.worker.signals.sweepError.connect(self.showSweepError)
|
||||
|
||||
self.bands = BandsModel()
|
||||
|
||||
self.noSweeps = 1 # Number of sweeps to run
|
||||
|
@ -399,53 +405,37 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.fileWindow = QtWidgets.QWidget()
|
||||
self.fileWindow.setWindowTitle("Files")
|
||||
self.fileWindow.setWindowIcon(self.icon)
|
||||
self.fileWindow.setMinimumWidth(200)
|
||||
shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self.fileWindow, self.fileWindow.hide)
|
||||
file_window_layout = QtWidgets.QVBoxLayout()
|
||||
self.fileWindow.setLayout(file_window_layout)
|
||||
|
||||
reference_file_control_box = QtWidgets.QGroupBox("Import file")
|
||||
reference_file_control_layout = QtWidgets.QFormLayout(reference_file_control_box)
|
||||
self.referenceFileNameInput = QtWidgets.QLineEdit("")
|
||||
btn_reference_file_picker = QtWidgets.QPushButton("...")
|
||||
btn_reference_file_picker.setMaximumWidth(25)
|
||||
btn_reference_file_picker.clicked.connect(self.pickReferenceFile)
|
||||
reference_file_name_layout = QtWidgets.QHBoxLayout()
|
||||
reference_file_name_layout.addWidget(self.referenceFileNameInput)
|
||||
reference_file_name_layout.addWidget(btn_reference_file_picker)
|
||||
load_file_control_box = QtWidgets.QGroupBox("Import file")
|
||||
load_file_control_box.setMaximumWidth(300)
|
||||
load_file_control_layout = QtWidgets.QFormLayout(load_file_control_box)
|
||||
|
||||
reference_file_control_layout.addRow(QtWidgets.QLabel("Filename"), reference_file_name_layout)
|
||||
file_window_layout.addWidget(reference_file_control_box)
|
||||
|
||||
btn_load_reference = QtWidgets.QPushButton("Load reference")
|
||||
btn_load_reference.clicked.connect(self.loadReferenceFile)
|
||||
btn_load_sweep = QtWidgets.QPushButton("Load as sweep")
|
||||
btn_load_sweep.clicked.connect(self.loadSweepFile)
|
||||
reference_file_control_layout.addRow(btn_load_reference)
|
||||
reference_file_control_layout.addRow(btn_load_sweep)
|
||||
btn_load_reference = QtWidgets.QPushButton("Load reference")
|
||||
btn_load_reference.clicked.connect(self.loadReferenceFile)
|
||||
load_file_control_layout.addRow(btn_load_sweep)
|
||||
load_file_control_layout.addRow(btn_load_reference)
|
||||
|
||||
file_control_box = QtWidgets.QGroupBox()
|
||||
file_control_box.setTitle("Export file")
|
||||
file_control_box.setMaximumWidth(300)
|
||||
file_control_layout = QtWidgets.QFormLayout(file_control_box)
|
||||
self.fileNameInput = QtWidgets.QLineEdit("")
|
||||
btn_file_picker = QtWidgets.QPushButton("...")
|
||||
btn_file_picker.setMaximumWidth(25)
|
||||
btn_file_picker.clicked.connect(self.pickFile)
|
||||
file_name_layout = QtWidgets.QHBoxLayout()
|
||||
file_name_layout.addWidget(self.fileNameInput)
|
||||
file_name_layout.addWidget(btn_file_picker)
|
||||
file_window_layout.addWidget(load_file_control_box)
|
||||
|
||||
file_control_layout.addRow(QtWidgets.QLabel("Filename"), file_name_layout)
|
||||
save_file_control_box = QtWidgets.QGroupBox("Export file")
|
||||
save_file_control_box.setMaximumWidth(300)
|
||||
save_file_control_layout = QtWidgets.QFormLayout(save_file_control_box)
|
||||
|
||||
self.btnExportFile = QtWidgets.QPushButton("Export data S1P")
|
||||
self.btnExportFile.clicked.connect(self.exportFileS1P)
|
||||
file_control_layout.addRow(self.btnExportFile)
|
||||
btnExportFile = QtWidgets.QPushButton("Save file (S1P)")
|
||||
btnExportFile.clicked.connect(self.exportFileS1P)
|
||||
save_file_control_layout.addRow(btnExportFile)
|
||||
|
||||
self.btnExportFile = QtWidgets.QPushButton("Export data S2P")
|
||||
self.btnExportFile.clicked.connect(self.exportFileS2P)
|
||||
file_control_layout.addRow(self.btnExportFile)
|
||||
btnExportFile = QtWidgets.QPushButton("Save file (S2P)")
|
||||
btnExportFile.clicked.connect(self.exportFileS2P)
|
||||
save_file_control_layout.addRow(btnExportFile)
|
||||
|
||||
file_window_layout.addWidget(file_control_box)
|
||||
file_window_layout.addWidget(save_file_control_box)
|
||||
|
||||
btn_open_file_window = QtWidgets.QPushButton("Files ...")
|
||||
btn_open_file_window.clicked.connect(self.displayFileWindow)
|
||||
|
@ -467,15 +457,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.displaySetupWindow = DisplaySettingsWindow(self)
|
||||
btn_display_setup.clicked.connect(self.displaySettingsWindow)
|
||||
|
||||
self.aboutWindow = AboutWindow(self)
|
||||
|
||||
btn_about = QtWidgets.QPushButton("About ...")
|
||||
btn_about.setMaximumWidth(250)
|
||||
btn_about.clicked.connect(lambda: QtWidgets.QMessageBox.about(self, "About NanoVNASaver",
|
||||
"NanoVNASaver version "
|
||||
+ NanoVNASaver.version +
|
||||
"\n\n\N{COPYRIGHT SIGN} Copyright 2019 Rune B. Broberg\n" +
|
||||
"This program comes with ABSOLUTELY NO WARRANTY\n" +
|
||||
"This program is licensed under the GNU General Public License version 3\n\n" +
|
||||
"See https://mihtjel.github.io/nanovna-saver/ for further details"))
|
||||
|
||||
btn_about.clicked.connect(self.displayAboutWindow)
|
||||
|
||||
button_grid = QtWidgets.QGridLayout()
|
||||
button_grid.addWidget(btn_open_file_window, 0, 0)
|
||||
|
@ -484,14 +471,6 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
button_grid.addWidget(btn_about, 1, 1)
|
||||
left_column.addLayout(button_grid)
|
||||
|
||||
################################################################################################################
|
||||
# Right side
|
||||
################################################################################################################
|
||||
|
||||
self.worker.signals.updated.connect(self.dataUpdated)
|
||||
self.worker.signals.finished.connect(self.sweepFinished)
|
||||
self.worker.signals.sweepError.connect(self.showSweepError)
|
||||
|
||||
logger.debug("Finished building interface")
|
||||
|
||||
def rescanSerialPort(self):
|
||||
|
@ -511,27 +490,23 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
return port
|
||||
return ""
|
||||
|
||||
def pickReferenceFile(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(directory=self.referenceFileNameInput.text(),
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
if filename != "":
|
||||
self.referenceFileNameInput.setText(filename)
|
||||
|
||||
def pickFile(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(directory=self.fileNameInput.text(),
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
if filename != "":
|
||||
self.fileNameInput.setText(filename)
|
||||
|
||||
def exportFileS1P(self):
|
||||
logger.debug("Save S1P file to %s", self.fileNameInput.text())
|
||||
filedialog = QtWidgets.QFileDialog(self)
|
||||
filedialog.setDefaultSuffix("s1p")
|
||||
filedialog.setNameFilter("Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
||||
selected = filedialog.exec()
|
||||
if selected:
|
||||
filename = filedialog.selectedFiles()[0]
|
||||
else:
|
||||
return
|
||||
if filename == "":
|
||||
logger.debug("No file name selected.")
|
||||
return
|
||||
logger.debug("Save S1P file to %s", filename)
|
||||
if len(self.data) == 0:
|
||||
logger.warning("No data stored, nothing written.")
|
||||
return
|
||||
filename = self.fileNameInput.text()
|
||||
if filename == "":
|
||||
logger.warning("No filename entered, nothing saved.")
|
||||
return
|
||||
try:
|
||||
logger.debug("Opening %s for writing", filename)
|
||||
file = open(filename, "w+")
|
||||
|
@ -547,14 +522,22 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
return
|
||||
|
||||
def exportFileS2P(self):
|
||||
logger.debug("Save S2P file to %s", self.fileNameInput.text())
|
||||
filedialog = QtWidgets.QFileDialog(self)
|
||||
filedialog.setDefaultSuffix("s2p")
|
||||
filedialog.setNameFilter("Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
||||
selected = filedialog.exec()
|
||||
if selected:
|
||||
filename = filedialog.selectedFiles()[0]
|
||||
else:
|
||||
return
|
||||
if filename == "":
|
||||
logger.debug("No file name selected.")
|
||||
return
|
||||
logger.debug("Save S2P file to %s", filename)
|
||||
if len(self.data) == 0 or len(self.data21) == 0:
|
||||
logger.warning("No data stored, nothing written.")
|
||||
return
|
||||
filename = self.fileNameInput.text()
|
||||
if filename == "":
|
||||
logger.warning("No filename entered, nothing saved.")
|
||||
return
|
||||
try:
|
||||
logger.debug("Opening %s for writing", filename)
|
||||
file = open(filename, "w+")
|
||||
|
@ -577,15 +560,6 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.startSerial()
|
||||
return
|
||||
|
||||
def flushSerialBuffers(self):
|
||||
if self.serialLock.acquire():
|
||||
self.serial.write(b"\r\n\r\n")
|
||||
sleep(0.1)
|
||||
self.serial.reset_input_buffer()
|
||||
self.serial.reset_output_buffer()
|
||||
sleep(0.1)
|
||||
self.serialLock.release()
|
||||
|
||||
def startSerial(self):
|
||||
if self.serialLock.acquire():
|
||||
self.serialPort = self.serialPortInput.text()
|
||||
|
@ -602,12 +576,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.serialLock.release()
|
||||
sleep(0.05)
|
||||
|
||||
self.flushSerialBuffers()
|
||||
sleep(0.05)
|
||||
self.vna = VNA.getVNA(self, self.serial)
|
||||
self.worker.setVNA(self.vna)
|
||||
|
||||
logger.info(self.readFirmware())
|
||||
logger.info(self.vna.readFirmware())
|
||||
|
||||
frequencies = self.readValues("frequencies")
|
||||
frequencies = self.vna.readFrequencies()
|
||||
if frequencies:
|
||||
logger.info("Read starting frequency %s and end frequency %s", frequencies[0], frequencies[100])
|
||||
if int(frequencies[0]) == int(frequencies[100]) and (self.sweepStartInput.text() == "" or self.sweepEndInput.text() == ""):
|
||||
|
@ -620,7 +594,6 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
else:
|
||||
logger.warning("No frequencies read")
|
||||
return
|
||||
|
||||
logger.debug("Starting initial sweep")
|
||||
self.sweep()
|
||||
return
|
||||
|
@ -632,22 +605,6 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.serialLock.release()
|
||||
self.btnSerialToggle.setText("Connect to NanoVNA")
|
||||
|
||||
def writeSerial(self, command):
|
||||
if not self.serial.is_open:
|
||||
logger.warning("Writing without serial port being opened (%s)", command)
|
||||
return
|
||||
if self.serialLock.acquire():
|
||||
try:
|
||||
self.serial.write(str(command + "\r").encode('ascii'))
|
||||
self.serial.readline()
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while writing to serial port (%s): %s", command, exc)
|
||||
self.serialLock.release()
|
||||
return
|
||||
|
||||
def setSweep(self, start, stop):
|
||||
self.writeSerial("sweep " + str(start) + " " + str(stop) + " 101")
|
||||
|
||||
def toggleSweepSettings(self, disabled):
|
||||
self.sweepStartInput.setDisabled(disabled)
|
||||
self.sweepEndInput.setDisabled(disabled)
|
||||
|
@ -676,61 +633,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
if self.sweepCountInput.text().isdigit():
|
||||
self.settings.setValue("Segments", self.sweepCountInput.text())
|
||||
|
||||
logger.debug("Starting worker thread")
|
||||
self.threadpool.start(self.worker)
|
||||
|
||||
def stopSweep(self):
|
||||
self.worker.stopped = True
|
||||
|
||||
def readFirmware(self):
|
||||
if self.serialLock.acquire():
|
||||
result = ""
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
# Then send the command to read data
|
||||
self.serial.write("info\r".encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.01)
|
||||
while "ch>" not in data:
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading firmware data: %s", exc)
|
||||
self.serialLock.release()
|
||||
return result
|
||||
else:
|
||||
logger.error("Unable to acquire serial lock to read firmware.")
|
||||
return ""
|
||||
|
||||
def readValues(self, value):
|
||||
if self.serialLock.acquire():
|
||||
try:
|
||||
data = "a"
|
||||
while data != "":
|
||||
data = self.serial.readline().decode('ascii')
|
||||
|
||||
# Then send the command to read data
|
||||
self.serial.write(str(value + "\r").encode('ascii'))
|
||||
result = ""
|
||||
data = ""
|
||||
sleep(0.05)
|
||||
while "ch>" not in data:
|
||||
data = self.serial.readline().decode('ascii')
|
||||
result += data
|
||||
values = result.split("\r\n")
|
||||
except serial.SerialException as exc:
|
||||
logger.exception("Exception while reading %s: %s", value, exc)
|
||||
self.serialLock.release()
|
||||
return
|
||||
|
||||
self.serialLock.release()
|
||||
return values[1:102]
|
||||
else:
|
||||
logger.error("Unable to acquire serial lock to read %s", value)
|
||||
return
|
||||
|
||||
def saveData(self, data, data12, source=None):
|
||||
if self.dataLock.acquire(blocking=True):
|
||||
self.data = data
|
||||
|
@ -912,8 +820,9 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
return
|
||||
if self.sweepCountInput.text().isdigit():
|
||||
segments = int(self.sweepCountInput.text())
|
||||
fstep = fspan / (segments * 101)
|
||||
self.sweepStepLabel.setText(self.formatShortFrequency(fstep) + "/step")
|
||||
if segments > 0:
|
||||
fstep = fspan / (segments * 101)
|
||||
self.sweepStepLabel.setText(self.formatShortFrequency(fstep) + "/step")
|
||||
|
||||
@staticmethod
|
||||
def formatFrequency(freq):
|
||||
|
@ -1027,16 +936,16 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.btnResetReference.setDisabled(True)
|
||||
|
||||
def loadReferenceFile(self):
|
||||
filename = self.referenceFileNameInput.text()
|
||||
if filename is not "":
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
if filename != "":
|
||||
self.resetReference()
|
||||
t = Touchstone(filename)
|
||||
t.load()
|
||||
self.setReference(t.s11data, t.s21data, filename)
|
||||
|
||||
def loadSweepFile(self):
|
||||
filename = self.referenceFileNameInput.text()
|
||||
if filename is not "":
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
if filename != "":
|
||||
self.data = []
|
||||
self.data21 = []
|
||||
t = Touchstone(filename)
|
||||
|
@ -1071,9 +980,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.analysis_window.show()
|
||||
QtWidgets.QApplication.setActiveWindow(self.analysis_window)
|
||||
|
||||
def displayAboutWindow(self):
|
||||
self.aboutWindow.show()
|
||||
QtWidgets.QApplication.setActiveWindow(self.aboutWindow)
|
||||
|
||||
def showError(self, text):
|
||||
error_message = QtWidgets.QErrorMessage(self)
|
||||
error_message.showMessage(text)
|
||||
QtWidgets.QMessageBox.warning(self, "Error", text)
|
||||
|
||||
def showSweepError(self):
|
||||
self.showError(self.worker.error_message)
|
||||
|
@ -1168,6 +1080,18 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
display_options_layout.addRow("Reference color", self.btnReferenceColorPicker)
|
||||
|
||||
self.btnSecondaryReferenceColorPicker = QtWidgets.QPushButton("█")
|
||||
self.btnSecondaryReferenceColorPicker.setFixedWidth(20)
|
||||
self.secondaryReferenceColor = self.app.settings.value("SecondaryReferenceColor",
|
||||
defaultValue=QtGui.QColor(0, 0, 255, 32),
|
||||
type=QtGui.QColor)
|
||||
self.setSecondaryReferenceColor(self.secondaryReferenceColor)
|
||||
self.btnSecondaryReferenceColorPicker.clicked.connect(lambda: self.setSecondaryReferenceColor(
|
||||
QtWidgets.QColorDialog.getColor(self.secondaryReferenceColor,
|
||||
options=QtWidgets.QColorDialog.ShowAlphaChannel)))
|
||||
|
||||
display_options_layout.addRow("Second reference color", self.btnSecondaryReferenceColorPicker)
|
||||
|
||||
layout.addWidget(display_options_box)
|
||||
|
||||
color_options_box = QtWidgets.QGroupBox("Chart colors")
|
||||
|
@ -1465,6 +1389,18 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
for c in self.app.charts:
|
||||
c.setReferenceColor(color)
|
||||
|
||||
def setSecondaryReferenceColor(self, color):
|
||||
if color.isValid():
|
||||
self.secondaryReferenceColor = color
|
||||
p = self.btnSecondaryReferenceColorPicker.palette()
|
||||
p.setColor(QtGui.QPalette.ButtonText, color)
|
||||
self.btnSecondaryReferenceColorPicker.setPalette(p)
|
||||
self.app.settings.setValue("SecondaryReferenceColor", color)
|
||||
self.app.settings.sync()
|
||||
|
||||
for c in self.app.charts:
|
||||
c.setSecondaryReferenceColor(color)
|
||||
|
||||
def setShowBands(self, show_bands):
|
||||
self.app.bands.enabled = show_bands
|
||||
self.app.bands.settings.setValue("ShowBands", show_bands)
|
||||
|
@ -1485,6 +1421,155 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QApplication.setActiveWindow(self.bandsWindow)
|
||||
|
||||
|
||||
class AboutWindow(QtWidgets.QWidget):
|
||||
def __init__(self, app: NanoVNASaver):
|
||||
super().__init__()
|
||||
self.app = app
|
||||
|
||||
self.setWindowTitle("About NanoVNASaver")
|
||||
self.setWindowIcon(self.app.icon)
|
||||
top_layout = QtWidgets.QHBoxLayout()
|
||||
self.setLayout(top_layout)
|
||||
#self.setAutoFillBackground(True)
|
||||
pal = self.palette()
|
||||
pal.setColor(QtGui.QPalette.Background, QtGui.QColor("white"))
|
||||
self.setPalette(pal)
|
||||
shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
|
||||
|
||||
icon_layout = QtWidgets.QVBoxLayout()
|
||||
top_layout.addLayout(icon_layout)
|
||||
icon = QtWidgets.QLabel()
|
||||
icon.setPixmap(self.app.icon.pixmap(128, 128))
|
||||
icon_layout.addWidget(icon)
|
||||
icon_layout.addStretch()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
top_layout.addLayout(layout)
|
||||
|
||||
layout.addWidget(QtWidgets.QLabel("NanoVNASaver version " + NanoVNASaver.version))
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
layout.addWidget(QtWidgets.QLabel("\N{COPYRIGHT SIGN} Copyright 2019 Rune B. Broberg"))
|
||||
layout.addWidget(QtWidgets.QLabel("This program comes with ABSOLUTELY NO WARRANTY"))
|
||||
layout.addWidget(QtWidgets.QLabel("This program is licensed under the GNU General Public License version 3"))
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
link_label = QtWidgets.QLabel("For further details, see: " +
|
||||
"<a href=\"https://mihtjel.github.io/nanovna-saver/\">" +
|
||||
"https://mihtjel.github.io/nanovna-saver/</a>")
|
||||
link_label.setOpenExternalLinks(True)
|
||||
layout.addWidget(link_label)
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
|
||||
self.versionLabel = QtWidgets.QLabel("NanoVNA Firmware Version: Not connected.")
|
||||
layout.addWidget(self.versionLabel)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
btn_check_version = QtWidgets.QPushButton("Check for updates")
|
||||
btn_check_version.clicked.connect(self.findUpdates)
|
||||
|
||||
self.updateLabel = QtWidgets.QLabel("Last checked: ")
|
||||
self.updateCheckBox = QtWidgets.QCheckBox("Check for updates on startup")
|
||||
|
||||
self.updateCheckBox.toggled.connect(self.updateSettings)
|
||||
|
||||
check_for_updates = self.app.settings.value("CheckForUpdates", "Ask")
|
||||
if check_for_updates == "Yes":
|
||||
self.updateCheckBox.setChecked(True)
|
||||
self.findUpdates(automatic = True)
|
||||
elif check_for_updates == "No":
|
||||
self.updateCheckBox.setChecked(False)
|
||||
else:
|
||||
logger.debug("Starting timer")
|
||||
QtCore.QTimer.singleShot(2000, self.askAboutUpdates)
|
||||
|
||||
update_hbox = QtWidgets.QHBoxLayout()
|
||||
update_hbox.addWidget(btn_check_version)
|
||||
update_form = QtWidgets.QFormLayout()
|
||||
update_hbox.addLayout(update_form)
|
||||
update_hbox.addStretch()
|
||||
update_form.addRow(self.updateLabel)
|
||||
update_form.addRow(self.updateCheckBox)
|
||||
layout.addLayout(update_hbox)
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
btn_ok = QtWidgets.QPushButton("Ok")
|
||||
btn_ok.clicked.connect(lambda: self.close())
|
||||
layout.addWidget(btn_ok)
|
||||
|
||||
def show(self):
|
||||
super().show()
|
||||
self.updateLabels()
|
||||
|
||||
def updateLabels(self):
|
||||
if self.app.vna.isValid():
|
||||
logger.debug("Valid VNA")
|
||||
v: Version = self.app.vna.version
|
||||
self.versionLabel.setText("NanoVNA Firmware Version: " + self.app.vna.name + " " + v.version_string)
|
||||
|
||||
def updateSettings(self):
|
||||
if self.updateCheckBox.isChecked():
|
||||
self.app.settings.setValue("CheckForUpdates", "Yes")
|
||||
else:
|
||||
self.app.settings.setValue("CheckForUpdates", "No")
|
||||
|
||||
def askAboutUpdates(self):
|
||||
logger.debug("Asking about automatic update checks")
|
||||
selection = QtWidgets.QMessageBox.question(self.app, "Enable checking for updates?",
|
||||
"Would you like NanoVNA-Saver to check for updates automatically?")
|
||||
if selection == QtWidgets.QMessageBox.Yes:
|
||||
self.updateCheckBox.setChecked(True)
|
||||
self.app.settings.setValue("CheckForUpdates", "Yes")
|
||||
self.findUpdates()
|
||||
elif selection == QtWidgets.QMessageBox.No:
|
||||
self.updateCheckBox.setChecked(False)
|
||||
self.app.settings.setValue("CheckForUpdates", "No")
|
||||
QtWidgets.QMessageBox.information(self.app, "Checking for updates disabled",
|
||||
"You can check for updates using the \"About\" window.")
|
||||
else:
|
||||
self.app.settings.setValue("CheckForUpdates", "Ask")
|
||||
|
||||
def findUpdates(self, automatic=False):
|
||||
from urllib import request, error
|
||||
import json
|
||||
update_url = "http://mihtjel.dk/nanovna-saver/latest.json"
|
||||
|
||||
try:
|
||||
updates = json.load(request.urlopen(update_url, timeout=3))
|
||||
latest_version = Version(updates['version'])
|
||||
latest_url = updates['url']
|
||||
except error.HTTPError as e:
|
||||
logger.exception("Checking for updates produced an HTTP exception: %s", e)
|
||||
return
|
||||
except json.JSONDecodeError as e:
|
||||
logger.exception("Checking for updates provided an unparseable file: %s", e)
|
||||
return
|
||||
|
||||
logger.info("Latest version is " + latest_version.version_string)
|
||||
this_version = Version(NanoVNASaver.version)
|
||||
logger.info("This is " + this_version.version_string)
|
||||
if latest_version > this_version:
|
||||
logger.info("New update available: %s!", latest_version)
|
||||
if automatic:
|
||||
QtWidgets.QMessageBox.information(self, "Updates available",
|
||||
"There is a new update for NanoVNA-Saver available!\n" +
|
||||
"Version " + latest_version.version_string + "\n\n" +
|
||||
"Press \"About\" to find the update.")
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(self, "Updates available",
|
||||
"There is a new update for NanoVNA-Saver available!")
|
||||
self.updateLabel.setText("<a href=\"" + latest_url + "\">New version available</a>.")
|
||||
self.updateLabel.setOpenExternalLinks(True)
|
||||
else:
|
||||
# Probably don't show a message box, just update the screen?
|
||||
# Maybe consider showing it if not an automatic update.
|
||||
#
|
||||
# QtWidgets.QMessageBox.information(self, "No updates available", "There are no new updates available.")
|
||||
#
|
||||
self.updateLabel.setText("Last checked: " + strftime("%Y-%m-%d %H:%M:%S", localtime()))
|
||||
return
|
||||
|
||||
|
||||
class TDRWindow(QtWidgets.QWidget):
|
||||
def __init__(self, app: NanoVNASaver):
|
||||
super().__init__()
|
||||
|
@ -1900,6 +1985,7 @@ class AnalysisWindow(QtWidgets.QWidget):
|
|||
self.analysis_list.addItem("Low-pass filter", LowPassAnalysis(self.app))
|
||||
self.analysis_list.addItem("Band-pass filter", BandPassAnalysis(self.app))
|
||||
self.analysis_list.addItem("High-pass filter", HighPassAnalysis(self.app))
|
||||
self.analysis_list.addItem("Band-stop filter", BandStopAnalysis(self.app))
|
||||
select_analysis_layout.addRow("Analysis type", self.analysis_list)
|
||||
self.analysis_list.currentIndexChanged.connect(self.updateSelection)
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ from PyQt5.QtCore import pyqtSlot, pyqtSignal
|
|||
import NanoVNASaver
|
||||
import logging
|
||||
|
||||
from NanoVNASaver.Hardware import VNA, InvalidVNA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Datapoint = collections.namedtuple('Datapoint', 'freq re im')
|
||||
|
@ -41,6 +43,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
logger.info("Initializing SweepWorker")
|
||||
self.signals = WorkerSignals()
|
||||
self.app = app
|
||||
self.vna: VNA = InvalidVNA()
|
||||
self.noSweeps = 1
|
||||
self.setAutoDelete(False)
|
||||
self.percentage = 0
|
||||
|
@ -156,7 +159,8 @@ class SweepWorker(QtCore.QRunnable):
|
|||
logger.debug("Resetting NanoVNA sweep to full range: %d to %d",
|
||||
NanoVNASaver.parseFrequency(self.app.sweepStartInput.text()),
|
||||
NanoVNASaver.parseFrequency(self.app.sweepEndInput.text()))
|
||||
self.app.setSweep(NanoVNASaver.parseFrequency(self.app.sweepStartInput.text()), NanoVNASaver.parseFrequency(self.app.sweepEndInput.text()))
|
||||
self.vna.resetSweep(NanoVNASaver.parseFrequency(self.app.sweepStartInput.text()),
|
||||
NanoVNASaver.parseFrequency(self.app.sweepEndInput.text()))
|
||||
|
||||
self.percentage = 100
|
||||
logger.debug("Sending \"finished\" signal")
|
||||
|
@ -248,10 +252,10 @@ class SweepWorker(QtCore.QRunnable):
|
|||
|
||||
for valueset in values:
|
||||
avg = np.average(valueset, 0) # avg becomes a 2-value array of the location of the average
|
||||
new_valueset = valueset
|
||||
for n in range(count):
|
||||
max_deviance = 0
|
||||
max_idx = -1
|
||||
new_valueset = valueset
|
||||
for i in range(len(new_valueset)):
|
||||
deviance = abs(new_valueset[i][0] - avg[0])**2 + abs(new_valueset[i][1] - avg[1])**2
|
||||
if deviance > max_deviance:
|
||||
|
@ -270,9 +274,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
|
||||
def readSegment(self, start, stop):
|
||||
logger.debug("Setting sweep range to %d to %d", start, stop)
|
||||
self.app.setSweep(start, stop)
|
||||
sleep(1) # TODO This long delay seems to fix the weird data transitions we were seeing by getting partial
|
||||
# sweeps. Clearly something needs to be done, maybe at firmware level, to address this fully.
|
||||
self.vna.setSweep(start, stop)
|
||||
|
||||
# Let's check the frequencies first:
|
||||
frequencies = self.readFreq()
|
||||
|
@ -296,7 +298,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
while not done:
|
||||
done = True
|
||||
returndata = []
|
||||
tmpdata = self.app.readValues(data)
|
||||
tmpdata = self.vna.readValues(data)
|
||||
if not tmpdata:
|
||||
logger.warning("Read no values")
|
||||
raise NanoVNAValueException("Failed reading data: Returned no values.")
|
||||
|
@ -338,7 +340,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
while not done:
|
||||
done = True
|
||||
returnfreq = []
|
||||
tmpfreq = self.app.readValues("frequencies")
|
||||
tmpfreq = self.vna.readFrequencies()
|
||||
if not tmpfreq:
|
||||
logger.warning("Read no frequencies")
|
||||
raise NanoVNAValueException("Failed reading frequencies: Returned no values.")
|
||||
|
@ -368,6 +370,9 @@ class SweepWorker(QtCore.QRunnable):
|
|||
except:
|
||||
return
|
||||
|
||||
def setVNA(self, vna):
|
||||
self.vna = vna
|
||||
|
||||
|
||||
class NanoVNAValueException(Exception):
|
||||
pass
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
version = '0.1.1'
|
||||
version = '0.1.2'
|
||||
debug = False
|
||||
|
|
Ładowanie…
Reference in New Issue