Merge branch 'upstream_Development' into feature/refactor_marker

pull/113/head
Holger Mueller 2019-12-08 18:20:35 +01:00
commit 6a4800d361
4 zmienionych plików z 286 dodań i 45 usunięć

Wyświetl plik

@ -15,9 +15,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
import re
import struct
from time import sleep
from typing import List
import numpy as np
from PyQt5 import QtGui
import serial
logger = logging.getLogger(__name__)
@ -25,28 +29,33 @@ logger = logging.getLogger(__name__)
class VNA:
name = "VNA"
validateInput = True
features = []
def __init__(self, app, serialPort: serial.Serial):
def __init__(self, app, serial_port: serial.Serial):
from NanoVNASaver.NanoVNASaver import NanoVNASaver
self.app: NanoVNASaver = app
self.serial = serialPort
self.serial = serial_port
self.version: Version = Version("0.0.0")
@staticmethod
def getVNA(app, serialPort: serial.Serial) -> 'VNA':
def getVNA(app, serial_port: serial.Serial) -> 'VNA':
logger.info("Finding correct VNA type")
tmp_vna = VNA(app, serialPort)
tmp_vna = VNA(app, serial_port)
tmp_vna.flushSerialBuffers()
firmware = tmp_vna.readFirmware()
if firmware.find("NanoVNA-H") > 0:
return NanoVNA_H(app, serialPort)
logger.info("Type: NanoVNA-H")
return NanoVNA_H(app, serial_port)
if firmware.find("NanoVNA-F") > 0:
return NanoVNA_F(app, serialPort)
logger.info("Type: NanoVNA-F")
return NanoVNA_F(app, serial_port)
elif firmware.find("NanoVNA") > 0:
return NanoVNA(app, serialPort)
logger.info("Type: Generic NanoVNA")
return NanoVNA(app, serial_port)
else:
logger.warning("Did not recognize NanoVNA type from firmware.")
return NanoVNA(app, serialPort)
return NanoVNA(app, serial_port)
def readFrequencies(self) -> List[str]:
pass
@ -63,6 +72,15 @@ class VNA:
def isValid(self):
return False
def getFeatures(self) -> List[str]:
return self.features
def getCalibration(self) -> str:
return "Unknown"
def getScreenshot(self) -> QtGui.QPixmap:
return QtGui.QPixmap()
def flushSerialBuffers(self):
if self.app.serialLock.acquire():
self.serial.write(b"\r\n\r\n")
@ -177,24 +195,81 @@ class InvalidVNA(VNA):
class NanoVNA(VNA):
name = "NanoVNA"
def __init__(self, app, serialPort):
super().__init__(app, serialPort)
def __init__(self, app, serial_port):
super().__init__(app, serial_port)
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.features.append("Incompatible scan command")
self.useScan = False
elif self.version >= Version("0.2.0"):
logger.debug("Newer than 0.2.0, using new scan command.")
self.features.append("New scan command")
self.useScan = True
else:
logger.debug("Older than 0.2.0, using old sweep command.")
self.features.append("Original sweep method")
self.useScan = False
def isValid(self):
return True
def getCalibration(self) -> str:
logger.debug("Reading calibration info.")
if not self.serial.is_open:
return "Not connected."
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("cal\r".encode('ascii'))
result = ""
data = ""
sleep(0.1)
while "ch>" not in data:
data = self.serial.readline().decode('ascii')
result += data
values = result.splitlines()
return values[1]
except serial.SerialException as exc:
logger.exception("Exception while reading calibration info: %s", exc)
finally:
self.app.serialLock.release()
return "Unknown"
def getScreenshot(self) -> QtGui.QPixmap:
logger.debug("Capturing screenshot...")
if not self.serial.is_open:
return QtGui.QPixmap()
if self.app.serialLock.acquire():
try:
data = "a"
while data != "":
data = self.serial.readline().decode('ascii')
self.serial.write("capture\r".encode('ascii'))
timeout = self.serial.timeout
self.serial.timeout = 2
self.serial.readline()
image_data = self.serial.read(320 * 240 * 2)
self.serial.timeout = timeout
rgb_data = struct.unpack(">76800H", image_data)
rgb_array = np.array(rgb_data, dtype=np.uint32)
rgba_array = (0xFF000000 +
((rgb_array & 0xF800) << 8) +
((rgb_array & 0x07E0) << 5) +
((rgb_array & 0x001F) << 3))
image = QtGui.QImage(rgba_array, 320, 240, QtGui.QImage.Format_ARGB32)
logger.debug("Captured screenshot")
return QtGui.QPixmap(image)
except serial.SerialException as exc:
logger.exception("Exception while capturing screenshot: %s", exc)
finally:
self.app.serialLock.release()
return QtGui.QPixmap()
def readFrequencies(self) -> List[str]:
return self.readValues("frequencies")
@ -254,7 +329,7 @@ class Version:
minor = 0
revision = 0
note = ""
version_string =""
version_string = ""
def __init__(self, version_string):
self.version_string = version_string
@ -286,3 +361,6 @@ class Version:
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
def __str__(self):
return str(self.major) + "." + str(self.minor) + "." + str(self.revision) + str(self.note)

Wyświetl plik

@ -448,10 +448,17 @@ class NanoVNASaver(QtWidgets.QWidget):
serial_port_input_layout.addWidget(btn_rescan_serial_port)
serial_control_layout.addRow(QtWidgets.QLabel("Serial port"), serial_port_input_layout)
serial_button_layout = QtWidgets.QHBoxLayout()
self.btnSerialToggle = QtWidgets.QPushButton("Connect to NanoVNA")
self.btnSerialToggle.clicked.connect(self.serialButtonClick)
serial_control_layout.addRow(self.btnSerialToggle)
serial_button_layout.addWidget(self.btnSerialToggle, stretch=1)
self.deviceSettingsWindow = DeviceSettingsWindow(self)
self.btnDeviceSettings = QtWidgets.QPushButton("Manage")
self.btnDeviceSettings.clicked.connect(self.displayDeviceSettingsWindow)
serial_button_layout.addWidget(self.btnDeviceSettings, stretch=0)
serial_control_layout.addRow(serial_button_layout)
left_column.addWidget(serial_control_box)
################################################################################################################
@ -783,8 +790,10 @@ class NanoVNASaver(QtWidgets.QWidget):
min_gain_freq = d.freq
if max_gain_freq > -1:
self.s21_min_gain_label.setText(str(round(min_gain, 3)) + " dB @ " + RFTools.formatFrequency(min_gain_freq))
self.s21_max_gain_label.setText(str(round(max_gain, 3)) + " dB @ " + RFTools.formatFrequency(max_gain_freq))
self.s21_min_gain_label.setText(
str(round(min_gain, 3)) + " dB @ " + RFTools.formatFrequency(min_gain_freq))
self.s21_max_gain_label.setText(
str(round(max_gain, 3)) + " dB @ " + RFTools.formatFrequency(max_gain_freq))
else:
self.s21_min_gain_label.setText("")
self.s21_max_gain_label.setText("")
@ -830,7 +839,7 @@ class NanoVNASaver(QtWidgets.QWidget):
if self.sweepCountInput.text().isdigit():
segments = int(self.sweepCountInput.text())
if segments > 0:
fstep = fspan / (segments * 101)
fstep = fspan / (segments * 101 - 1)
self.sweepStepLabel.setText(RFTools.formatShortFrequency(fstep) + "/step")
def setReference(self, s11data=None, s21data=None, source=None):
@ -919,6 +928,10 @@ class NanoVNASaver(QtWidgets.QWidget):
self.sweepSettingsWindow.show()
QtWidgets.QApplication.setActiveWindow(self.sweepSettingsWindow)
def displayDeviceSettingsWindow(self):
self.deviceSettingsWindow.show()
QtWidgets.QApplication.setActiveWindow(self.deviceSettingsWindow)
def displayCalibrationWindow(self):
self.calibrationWindow.show()
QtWidgets.QApplication.setActiveWindow(self.calibrationWindow)
@ -2027,7 +2040,8 @@ class SweepSettingsWindow(QtWidgets.QWidget):
settings_layout.addRow(QtWidgets.QLabel("Averaging allows discarding outlying samples to get better averages."))
settings_layout.addRow(QtWidgets.QLabel("Common values are 3/0, 5/2, 9/4 and 25/6."))
self.continuous_sweep_radiobutton.toggled.connect(lambda: self.app.worker.setContinuousSweep(self.continuous_sweep_radiobutton.isChecked()))
self.continuous_sweep_radiobutton.toggled.connect(
lambda: self.app.worker.setContinuousSweep(self.continuous_sweep_radiobutton.isChecked()))
self.averaged_sweep_radiobutton.toggled.connect(self.updateAveraging)
self.averages.textEdited.connect(self.updateAveraging)
self.truncates.textEdited.connect(self.updateAveraging)
@ -2546,3 +2560,151 @@ class MarkerSettingsWindow(QtWidgets.QWidget):
item.setCheckState(QtCore.Qt.Checked)
self.model.appendRow(item)
self.fieldSelectionView.setModel(self.model)
class DeviceSettingsWindow(QtWidgets.QWidget):
def __init__(self, app: NanoVNASaver):
super().__init__()
self.app = app
self.setWindowTitle("Device settings")
self.setWindowIcon(self.app.icon)
shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
status_box = QtWidgets.QGroupBox("Status")
status_layout = QtWidgets.QFormLayout(status_box)
self.statusLabel = QtWidgets.QLabel("Not connected.")
status_layout.addRow("Status:", self.statusLabel)
self.calibrationStatusLabel = QtWidgets.QLabel("Not connected.")
status_layout.addRow("Calibration:", self.calibrationStatusLabel)
status_layout.addRow(QtWidgets.QLabel("Features:"))
self.featureList = QtWidgets.QListWidget()
status_layout.addRow(self.featureList)
settings_box = QtWidgets.QGroupBox("Settings")
settings_layout = QtWidgets.QFormLayout(settings_box)
self.chkValidateInputData = QtWidgets.QCheckBox("Validate received data")
self.chkValidateInputData.setChecked(True)
self.chkValidateInputData.stateChanged.connect(self.updateValidation)
settings_layout.addRow("Validation", self.chkValidateInputData)
control_layout = QtWidgets.QHBoxLayout()
self.btnRefresh = QtWidgets.QPushButton("Refresh")
self.btnRefresh.clicked.connect(self.updateFields)
control_layout.addWidget(self.btnRefresh)
self.screenshotWindow = ScreenshotWindow()
self.btnCaptureScreenshot = QtWidgets.QPushButton("Screenshot")
self.btnCaptureScreenshot.clicked.connect(self.captureScreenshot)
control_layout.addWidget(self.btnCaptureScreenshot)
layout.addWidget(status_box)
layout.addWidget(settings_box)
layout.addLayout(control_layout)
def show(self):
super().show()
self.updateFields()
def updateFields(self):
if self.app.vna.isValid():
self.statusLabel.setText("Connected to " + self.app.vna.name + ".")
if self.app.worker.running:
self.calibrationStatusLabel.setText("(Sweep running)")
else:
self.calibrationStatusLabel.setText(self.app.vna.getCalibration())
self.featureList.clear()
self.featureList.addItem(self.app.vna.name + " v" + str(self.app.vna.version))
for item in self.app.vna.getFeatures():
self.featureList.addItem(item)
else:
self.statusLabel.setText("Not connected.")
self.calibrationStatusLabel.setText("Not connected.")
self.featureList.clear()
self.featureList.addItem("Not connected.")
def updateValidation(self, validate_data: bool):
self.app.vna.validateInput = validate_data
def captureScreenshot(self):
if not self.app.worker.running:
pixmap = self.app.vna.getScreenshot()
self.screenshotWindow.setScreenshot(pixmap)
self.screenshotWindow.show()
else:
# TODO: Tell the user no screenshots while sweep is running?
# TODO: Consider having a list of widgets that want to be disabled when a sweep is running?
pass
class ScreenshotWindow(QtWidgets.QLabel):
pix = None
def __init__(self):
super().__init__()
self.setWindowTitle("Screenshot")
# TODO : self.setWindowIcon(self.app.icon)
shortcut = QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.action_original_size = QtWidgets.QAction("Original size")
self.action_original_size.triggered.connect(lambda: self.setScale(1))
self.action_2x_size = QtWidgets.QAction("2x size")
self.action_2x_size.triggered.connect(lambda: self.setScale(2))
self.action_3x_size = QtWidgets.QAction("3x size")
self.action_3x_size.triggered.connect(lambda: self.setScale(3))
self.action_4x_size = QtWidgets.QAction("4x size")
self.action_4x_size.triggered.connect(lambda: self.setScale(4))
self.action_5x_size = QtWidgets.QAction("5x size")
self.action_5x_size.triggered.connect(lambda: self.setScale(5))
self.addAction(self.action_original_size)
self.addAction(self.action_2x_size)
self.addAction(self.action_3x_size)
self.addAction(self.action_4x_size)
self.addAction(self.action_5x_size)
self.action_save_screenshot = QtWidgets.QAction("Save image")
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
self.addAction(self.action_save_screenshot)
def setScreenshot(self, pixmap: QtGui.QPixmap):
if self.pix is None:
self.resize(pixmap.size())
self.pix = pixmap
self.setPixmap(self.pix.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation))
w, h = pixmap.width(), pixmap.height()
self.action_original_size.setText("Original size (" + str(w) + "x" + str(h) + ")")
self.action_2x_size.setText("2x size (" + str(w * 2) + "x" + str(h * 2) + ")")
self.action_3x_size.setText("3x size (" + str(w * 3) + "x" + str(h * 3) + ")")
self.action_4x_size.setText("4x size (" + str(w * 4) + "x" + str(h * 4) + ")")
self.action_5x_size.setText("5x size (" + str(w * 5) + "x" + str(h * 5) + ")")
def saveScreenshot(self):
if self.pix is not None:
logger.info("Saving screenshot to file...")
filename, _ = QtWidgets.QFileDialog.getSaveFileName(parent=self, caption="Save image",
filter="PNG (*.png);;All files (*.*)")
logger.debug("Filename: %s", filename)
if filename != "":
self.pixmap().save(filename)
else:
logger.warning("The user got shown an empty screenshot window?")
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
super().resizeEvent(a0)
if self.pixmap() is not None:
self.setPixmap(self.pix.scaled(self.size(), QtCore.Qt.KeepAspectRatio, QtCore.Qt.FastTransformation))
def setScale(self, scale):
width, height = self.pix.size().width() * scale, self.pix.size().height() * scale
self.resize(width, height)

Wyświetl plik

@ -51,6 +51,7 @@ class SweepWorker(QtCore.QRunnable):
self.rawData11: List[Datapoint] = []
self.rawData21: List[Datapoint] = []
self.stopped = False
self.running = False
self.continuousSweep = False
self.averaging = False
self.averages = 3
@ -61,9 +62,11 @@ class SweepWorker(QtCore.QRunnable):
@pyqtSlot()
def run(self):
logger.info("Initializing SweepWorker")
self.running = True
self.percentage = 0
if not self.app.serial.is_open:
logger.debug("Attempted to run without being connected to the NanoVNA")
self.running = False
return
if int(self.app.sweepCountInput.text()) > 0:
@ -88,6 +91,7 @@ class SweepWorker(QtCore.QRunnable):
self.app.sweepEndInput.text())
self.error_message = "Unable to parse frequency inputs - check start and stop fields."
self.stopped = True
self.running = False
self.signals.sweepError.emit()
return
@ -137,6 +141,7 @@ class SweepWorker(QtCore.QRunnable):
except NanoVNAValueException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.fatalSweepError.emit()
while self.continuousSweep and not self.stopped:
@ -154,6 +159,7 @@ class SweepWorker(QtCore.QRunnable):
except NanoVNAValueException as e:
self.error_message = str(e)
self.stopped = True
self.running = False
self.signals.fatalSweepError.emit()
# Reset the device to show the full range
@ -166,45 +172,42 @@ class SweepWorker(QtCore.QRunnable):
self.percentage = 100
logger.debug("Sending \"finished\" signal")
self.signals.finished.emit()
self.running = False
return
def updateData(self, values11, values21, offset, segment_size = 101):
def updateData(self, values11, values21, offset, segment_size=101):
# Update the data from (i*101) to (i+1)*101
logger.debug("Calculating data and inserting in existing data at offset %d", offset)
for i in range(len(values11)):
re, im = values11[i]
re21, im21 = values21[i]
freq = self.data11[offset * segment_size + i].freq
rawData11 = Datapoint(freq, re, im)
rawData21 = Datapoint(freq, re21, im21)
# TODO: Use applyCalibration instead
if self.app.calibration.isCalculated:
re, im = self.app.calibration.correct11(re, im, freq)
if self.app.calibration.isValid2Port():
re21, im21 = self.app.calibration.correct21(re21, im21, freq)
raw_data11 = Datapoint(freq, re, im)
raw_data21 = Datapoint(freq, re21, im21)
data11, data21 = self.applyCalibration([raw_data11], [raw_data21])
self.data11[offset * segment_size + i] = Datapoint(freq, re, im)
self.data21[offset * segment_size + i] = Datapoint(freq, re21, im21)
self.rawData11[offset * segment_size + i] = rawData11
self.rawData21[offset * segment_size + i] = rawData21
self.data11[offset * segment_size + i] = data11
self.data21[offset * segment_size + i] = data21
self.rawData11[offset * segment_size + i] = raw_data11
self.rawData21[offset * segment_size + i] = raw_data21
logger.debug("Saving data to application (%d and %d points)", len(self.data11), len(self.data21))
self.app.saveData(self.data11, self.data21)
logger.debug("Sending \"updated\" signal")
self.signals.updated.emit()
def saveData(self, frequencies, values11, values21):
rawData11 = []
rawData21 = []
raw_data11 = []
raw_data21 = []
logger.debug("Calculating data including corrections")
for i in range(len(values11)):
re, im = values11[i]
re21, im21 = values21[i]
freq = frequencies[i]
rawData11 += [Datapoint(freq, re, im)]
rawData21 += [Datapoint(freq, re21, im21)]
self.data11, self.data21 = self.applyCalibration(rawData11, rawData21)
self.rawData11 = rawData11
self.rawData21 = rawData21
raw_data11 += [Datapoint(freq, re, im)]
raw_data21 += [Datapoint(freq, re21, im21)]
self.data11, self.data21 = self.applyCalibration(raw_data11, raw_data21)
self.rawData11 = raw_data11
self.rawData21 = raw_data21
logger.debug("Saving data to application (%d and %d points)", len(self.data11), len(self.data21))
self.app.saveData(self.data11, self.data21)
logger.debug("Sending \"updated\" signal")
@ -213,7 +216,6 @@ class SweepWorker(QtCore.QRunnable):
def applyCalibration(self, raw_data11: List[Datapoint], raw_data21: List[Datapoint]) ->\
(List[Datapoint], List[Datapoint]):
if self.offsetDelay != 0:
logger.debug("Applying offset delay of %f ps.", self.offsetDelay * 10e12)
tmp = []
for d in raw_data11:
tmp.append(Calibration.correctDelay11(d, self.offsetDelay))
@ -230,7 +232,6 @@ class SweepWorker(QtCore.QRunnable):
data21: List[Datapoint] = []
if self.app.calibration.isValid1Port():
logger.debug("Applying S11 calibration.")
for d in raw_data11:
re, im = self.app.calibration.correct11(d.re, d.im, d.freq)
data11.append(Datapoint(d.freq, re, im))
@ -238,7 +239,6 @@ class SweepWorker(QtCore.QRunnable):
data11 = raw_data11
if self.app.calibration.isValid2Port():
logger.debug("Applying S21 calibration.")
for d in raw_data21:
re, im = self.app.calibration.correct21(d.re, d.im, d.freq)
data21.append(Datapoint(d.freq, re, im))
@ -273,7 +273,8 @@ class SweepWorker(QtCore.QRunnable):
return freq, return11, return21
def truncate(self, values: List[List[tuple]], count):
@staticmethod
def truncate(values: List[List[tuple]], count):
logger.debug("Truncating from %d values to %d", len(values), len(values) - count)
if count < 1:
return values
@ -336,11 +337,11 @@ class SweepWorker(QtCore.QRunnable):
for d in tmpdata:
a, b = d.split(" ")
try:
if float(a) < -9.5 or float(a) > 9.5:
if self.vna.validateInput and float(a) < -9.5 or float(a) > 9.5:
logger.warning("Got a non-float data value: %s (%s)", d, a)
logger.debug("Re-reading %s", data)
done = False
elif float(b) < -9.5 or float(b) > 9.5:
elif self.vna.validateInput and float(b) < -9.5 or float(b) > 9.5:
logger.warning("Got a non-float data value: %s (%s)", d, b)
logger.debug("Re-reading %s", data)
done = False
@ -389,15 +390,15 @@ class SweepWorker(QtCore.QRunnable):
returnfreq.append(int(f))
return returnfreq
def setContinuousSweep(self, continuousSweep: bool):
self.continuousSweep = continuousSweep
def setContinuousSweep(self, continuous_sweep: bool):
self.continuousSweep = continuous_sweep
def setAveraging(self, averaging: bool, averages: str, truncates: str):
self.averaging = averaging
try:
self.averages = int(averages)
self.truncates = int(truncates)
except:
except ValueError:
return
def setVNA(self, vna):

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.2.1'
version = '0.2.2alpha'
debug = False