nqrduck-autotm/src/nqrduck_autotm/view.py
2023-08-09 11:57:34 +02:00

352 lines
No EOL
14 KiB
Python

import logging
from datetime import datetime
from pathlib import Path
from PyQt6.QtGui import QMovie
from PyQt6.QtSerialPort import QSerialPort
from PyQt6.QtWidgets import QWidget, QLabel, QVBoxLayout, QApplication, QHBoxLayout, QLineEdit, QPushButton, QDialog
from PyQt6.QtCore import pyqtSlot, Qt
from nqrduck.module.module_view import ModuleView
from nqrduck.contrib.mplwidget import MplWidget
from .widget import Ui_Form
logger = logging.getLogger(__name__)
class AutoTMView(ModuleView):
def __init__(self, module):
super().__init__(module)
widget = QWidget()
self._ui_form = Ui_Form()
self._ui_form.setupUi(self)
self.widget = widget
# Disable the connectButton while no devices are selected
self._ui_form.connectButton.setDisabled(True)
# On clicking of the refresh button scan for available usb devices
self._ui_form.refreshButton.clicked.connect(self.module.controller.find_devices)
# Connect the available devices changed signal to the on_available_devices_changed slot
self.module.model.available_devices_changed.connect(self.on_available_devices_changed)
# Connect the serial changed signal to the on_serial_changed slot
self.module.model.serial_changed.connect(self.on_serial_changed)
# On clicking of the connect button call the connect method
self._ui_form.connectButton.clicked.connect(self.on_connect_button_clicked)
# On clicking of the start button call the start_frequency_sweep method
self._ui_form.startButton.clicked.connect(lambda: self.module.controller.start_frequency_sweep(
float(self._ui_form.startEdit.text()),
float(self._ui_form.stopEdit.text())
))
# On clicking of the calibration button call the on_calibration_button_clicked method
self._ui_form.calibrationButton.clicked.connect(self.on_calibration_button_clicked)
# Connect the measurement finished signal to the plot_measurement slot
self.module.model.measurement_finished.connect(self.plot_measurement)
# Add a vertical layout to the info box
self._ui_form.scrollAreaWidgetContents.setLayout(QVBoxLayout())
self._ui_form.scrollAreaWidgetContents.layout().setAlignment(Qt.AlignmentFlag.AlignTop)
self.init_plot()
self.init_labels()
def init_labels(self) -> None:
"""Makes some of the labels bold for better readability.
"""
self._ui_form.titleconnectionLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titlefrequencyLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titletypeLabel.setStyleSheet("font-weight: bold;")
self._ui_form.titleinfoLabel.setStyleSheet("font-weight: bold;")
def init_plot(self) -> None:
"""Initialize the S11 plot. """
ax = self._ui_form.S11Plot.canvas.ax
ax.set_xlabel("Frequency (MHz)")
ax.set_ylabel("S11 (dB)")
ax.set_title("S11")
ax.grid(True)
ax.set_xlim(0, 100)
ax.set_ylim(-100, 0)
self._ui_form.S11Plot.canvas.draw()
def on_calibration_button_clicked(self) -> None:
"""This method is called when the calibration button is clicked.
It opens the calibration window.
"""
logger.debug("Calibration button clicked")
self.calibration_window = self.CalibrationWindow(self.module)
self.calibration_window.show()
@pyqtSlot(list)
def on_available_devices_changed(self, available_devices : list) -> None:
"""Update the available devices list in the view. """
logger.debug("Updating available devices list")
self._ui_form.portBox.clear()
self._ui_form.portBox.addItems(available_devices)
# Enable the connectButton if there are available devices
if available_devices:
self._ui_form.connectButton.setEnabled(True)
else:
self._ui_form.connectButton.setEnabled(False)
logger.debug("Updated available devices list")
@pyqtSlot()
def on_connect_button_clicked(self) -> None:
"""This method is called when the connect button is clicked.
It calls the connect method of the controller with the currently selected device.
"""
logger.debug("Connect button clicked")
selected_device = self._ui_form.portBox.currentText()
self.module.controller.connect(selected_device)
@pyqtSlot(QSerialPort)
def on_serial_changed(self, serial : QSerialPort) -> None:
"""Update the serial 'connectionLabel' according to the current serial connection.
Args:
serial (serial.Serial): The current serial connection."""
logger.debug("Updating serial connection label")
if serial.isOpen():
self._ui_form.connectionLabel.setText(serial.portName())
self.add_info_text("Connected to device %s" % serial.portName())
else:
self._ui_form.connectionLabel.setText("Disconnected")
logger.debug("Updated serial connection label")
def plot_measurement(self, data : "S11Data") -> None:
"""Update the S11 plot with the current data points.
Args:
data_points (list): List of data points to plot.
"""
frequency = data.frequency
return_loss_db = data.return_loss_db
phase = data.phase_deg
# Calibration test:
#calibration = self.module.model.calibration
#e_00 = calibration[0]
#e11 = calibration[1]
#delta_e = calibration[2]
#y_corr = [(data_point - e_00[i]) / (data_point * e11[i] - delta_e[i]) for i, data_point in enumerate(y)]
#import numpy as np
#y = [data_point[1] for data_point in self.module.model.data_points]
#open_calibration = [data_point[1] for data_point in self.module.model.open_calibration]
#load_calibration = [data_point[1] for data_point in self.module.model.load_calibration]
#short_calibration = [data_point[1] for data_point in self.module.model.short_calibration]
#y_corr = np.array(y) - np.array(load_calibration)
#y_corr = y_corr - np.mean(y_corr)
phase_ax = self._ui_form.S11Plot.canvas.ax.twinx()
phase_ax.set_ylabel("Phase (deg)")
phase_ax.plot(frequency, phase, color="orange", linestyle="--")
phase_ax.set_ylim(-180, 180)
phase_ax.invert_yaxis()
magnitude_ax = self._ui_form.S11Plot.canvas.ax
magnitude_ax.clear()
magnitude_ax.set_xlabel("Frequency (MHz)")
magnitude_ax.set_ylabel("S11 (dB)")
magnitude_ax.set_title("S11")
magnitude_ax.grid(True)
magnitude_ax.plot(frequency, return_loss_db, color="blue")
# make the y axis go down instead of up
magnitude_ax.invert_yaxis()
self._ui_form.S11Plot.canvas.draw()
self._ui_form.S11Plot.canvas.flush_events()
# Wait for the signals to be processed before adding the info text
QApplication.processEvents()
def add_info_text(self, text : str) -> None:
""" Adds text to the info text box.
Args:
text (str): Text to add to the info text box.
"""
# Add a timestamp to the text
timestamp = datetime.now().strftime("%H:%M:%S")
text = "[%s] %s" % (timestamp, text)
text_label = QLabel(text)
text_label.setStyleSheet("font-size: 25px;")
self._ui_form.scrollAreaWidgetContents.layout().addWidget(text_label)
self._ui_form.scrollArea.verticalScrollBar().setValue(self._ui_form.scrollArea.verticalScrollBar().maximum())
def create_frequency_sweep_spinner_dialog(self) -> None:
"""Creates a frequency sweep spinner dialog. """
self.frequency_sweep_spinner = self.FrequencySweepSpinner()
self.frequency_sweep_spinner.show()
class FrequencySweepSpinner(QDialog):
"""This class implements a spinner dialog that is shown during a frequency sweep."""
def __init__(self):
super().__init__()
self.setWindowTitle("Frequency sweep")
self.setModal(True)
self.setWindowFlag(Qt.WindowType.FramelessWindowHint)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
path = Path(__file__).parent
self.spinner_movie = QMovie(str(path / "resources/duck_kick.gif"))
self.spinner_label = QLabel(self)
self.spinner_label.setMovie(self.spinner_movie)
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.spinner_label)
self.spinner_movie.start()
class CalibrationWindow(QWidget):
def __init__(self, module, parent=None):
super().__init__()
self.module = module
self.setParent(parent)
self.setWindowTitle("Calibration")
# Add vertical main layout
main_layout = QVBoxLayout()
# Add horizontal layout for the frequency range
frequency_layout = QHBoxLayout()
main_layout.addLayout(frequency_layout)
frequency_label = QLabel("Frequency range")
frequency_layout.addWidget(frequency_label)
start_edit = QLineEdit()
start_edit.setPlaceholderText("Start")
frequency_layout.addWidget(start_edit)
stop_edit = QLineEdit()
stop_edit.setPlaceholderText("Stop")
frequency_layout.addWidget(stop_edit)
unit_label = QLabel("MHz")
frequency_layout.addWidget(unit_label)
# Add horizontal layout for the calibration type
type_layout = QHBoxLayout()
main_layout.addLayout(type_layout)
# Add vertical layout for short calibration
short_layout = QVBoxLayout()
short_button = QPushButton("Short")
short_button.clicked.connect(lambda: self.module.controller.on_short_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Short plot widget
self.short_plot = MplWidget()
short_layout.addWidget(self.short_plot)
short_layout.addWidget(short_button)
type_layout.addLayout(short_layout)
# Add vertical layout for open calibration
open_layout = QVBoxLayout()
open_button = QPushButton("Open")
open_button.clicked.connect(lambda: self.module.controller.on_open_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Open plot widget
self.open_plot = MplWidget()
open_layout.addWidget(self.open_plot)
open_layout.addWidget(open_button)
type_layout.addLayout(open_layout)
# Add vertical layout for load calibration
load_layout = QVBoxLayout()
load_button = QPushButton("Load")
load_button.clicked.connect(lambda: self.module.controller.on_load_calibration(
float(start_edit.text()),
float(stop_edit.text())
))
# Load plot widget
self.load_plot = MplWidget()
load_layout.addWidget(self.load_plot)
load_layout.addWidget(load_button)
type_layout.addLayout(load_layout)
# Add vertical layout for save calibration
data_layout = QVBoxLayout()
# Export button
export_button = QPushButton("Export")
export_button.clicked.connect(self.on_export_button_clicked)
data_layout.addWidget(export_button)
# Import button
import_button = QPushButton("Import")
import_button.clicked.connect(self.on_import_button_clicked)
data_layout.addWidget(import_button)
# Apply button
apply_button = QPushButton("Apply calibration")
apply_button.clicked.connect(self.on_apply_button_clicked)
data_layout.addWidget(apply_button)
main_layout.addLayout(data_layout)
self.setLayout(main_layout)
# Connect the calibration finished signals to the on_calibration_finished slot
self.module.model.short_calibration_finished.connect(self.on_short_calibration_finished)
self.module.model.open_calibration_finished.connect(self.on_open_calibration_finished)
self.module.model.load_calibration_finished.connect(self.on_load_calibration_finished)
def on_short_calibration_finished(self, short_calibration : "S11Data") -> None:
self.on_calibration_finished("short", self.short_plot, short_calibration)
def on_open_calibration_finished(self, open_calibration : "S11Data") -> None:
self.on_calibration_finished("open", self.open_plot, open_calibration)
def on_load_calibration_finished(self, load_calibration : "S11Data") -> None:
self.on_calibration_finished("load", self.load_plot, load_calibration)
def on_calibration_finished(self, type : str, widget: MplWidget, data :"S11Data") -> None:
"""This method is called when a calibration has finished.
It plots the calibration data on the given widget.
"""
frequency = data.frequency
return_loss_db = data.return_loss_db
phase = data.phase_deg
phase_ax = widget.canvas.ax.twinx()
phase_ax.set_ylabel("Phase (deg)")
phase_ax.plot(frequency, phase, color="orange", linestyle="--")
phase_ax.set_ylim(-180, 180)
phase_ax.invert_yaxis()
magnitude_ax = widget.canvas.ax
magnitude_ax.clear()
magnitude_ax.set_xlabel("Frequency (MHz)")
magnitude_ax.set_ylabel("S11 (dB)")
magnitude_ax.set_title("S11")
magnitude_ax.grid(True)
magnitude_ax.plot(frequency, return_loss_db, color="blue")
# make the y axis go down instead of up
magnitude_ax.invert_yaxis()
widget.canvas.draw()
widget.canvas.flush_events()
def on_export_button_clicked(self) -> None:
"""This method is called when the export button is clicked. """
pass
def on_import_button_clicked(self) -> None:
"""This method is called when the import button is clicked. """
pass
def on_apply_button_clicked(self) -> None:
"""This method is called when the apply button is clicked. """
self.module.controller.calculate_calibration()
# Close the calibration window
self.close()