Merge pull request #36 from mihtjel/Development

Development
pull/42/head^2 v0.1.2
mihtjel 2019-10-11 17:52:21 +02:00 zatwierdzone przez GitHub
commit e2d79b7d35
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
7 zmienionych plików z 971 dodań i 244 usunięć

Wyświetl plik

@ -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" +

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

Wyświetl plik

@ -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

Wyświetl plik

@ -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