diff --git a/src/nqrduck_autotm/controller.py b/src/nqrduck_autotm/controller.py index da92aac..657f0bf 100644 --- a/src/nqrduck_autotm/controller.py +++ b/src/nqrduck_autotm/controller.py @@ -8,7 +8,7 @@ from PyQt6 import QtSerialPort from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot, Qt from PyQt6.QtWidgets import QApplication from nqrduck.module.module_controller import ModuleController -from .model import S11Data, ElectricalLookupTable, MechanicalLookupTable +from .model import S11Data, ElectricalLookupTable, MechanicalLookupTable, SavedPosition logger = logging.getLogger(__name__) @@ -671,5 +671,43 @@ class AutoTMController(ModuleController): actual_steps = self.calculate_steps_for_absolute_move(future_position) self.send_stepper_command(actual_steps) + def load_positions(self, path : str) -> None: + """Load the saved positions from a json file. + + Args: + path (str): The path to the json file. + """ + # First clear the old positions + self.module.model.saved_positions = [] + + with open(path, "r") as f: + positions = json.load(f) + for position in positions: + logger.debug("Loading position: %s", position) + self.add_position(position["frequency"], position["tuning_position"], position["matching_position"]) + + + def save_positions(self, path: str) -> None: + """Save the current positions to a json file. + + Args: + path (str): The path to the json file. + """ + positions = self.module.model.saved_positions + with open(path, "w") as f: + json_position = [position.to_json() for position in positions] + json.dump(json_position, f) + + def add_position(self, frequency: str, tuning_position: str, matching_position: str) -> None: + """Add a position to the lookup table. + + Args: + frequency (str): The frequency of the position. + tuning_position (str): The tuning position. + matching_position (str): The matching position. + """ + logger.debug("Adding new position at %s MHz", frequency) + self.module.model.add_saved_position(frequency, tuning_position, matching_position) + diff --git a/src/nqrduck_autotm/model.py b/src/nqrduck_autotm/model.py index 6c89896..c1d15ae 100644 --- a/src/nqrduck_autotm/model.py +++ b/src/nqrduck_autotm/model.py @@ -223,6 +223,25 @@ class Stepper: self.homed = False self.position = 0 +class SavedPosition(): + """This class is used to store a saved position for tuning and matching of electrical probeheads.""" + def __init__(self, frequency: float, tuning_position : int, matching_position : int) -> None: + self.frequency = frequency + self.tuning_position = tuning_position + self.matching_position = matching_position + + def to_json(self): + return { + "frequency": self.frequency, + "tuning_position": self.tuning_position, + "matching_position": self.matching_position, + } + + @classmethod + def from_json(cls, json): + logger.debug(json) + return cls(json[0], json[1], json[2]) + class TuningStepper(Stepper): TYPE = "Tuning" MAX_STEPS = 1e6 @@ -275,6 +294,7 @@ class AutoTMModel(ModuleModel): serial_changed = pyqtSignal(QSerialPort) data_points_changed = pyqtSignal(list) active_stepper_changed = pyqtSignal(Stepper) + saved_positions_changed = pyqtSignal(list) short_calibration_finished = pyqtSignal(S11Data) open_calibration_finished = pyqtSignal(S11Data) @@ -292,6 +312,8 @@ class AutoTMModel(ModuleModel): self.matching_stepper = MatchingStepper() self.active_stepper = self.tuning_stepper + self.saved_positions = [] + @property def available_devices(self): return self._available_devices @@ -325,6 +347,20 @@ class AutoTMModel(ModuleModel): self.data_points.clear() self.data_points_changed.emit(self.data_points) + @property + def saved_positions(self): + return self._saved_positions + + @saved_positions.setter + def saved_positions(self, value): + self._saved_positions = value + self.saved_positions_changed.emit(value) + + def add_saved_position(self, frequency: float, tuning_position: int, matching_position: int) -> None: + """Add a saved position to the model.""" + self.saved_positions.append(SavedPosition(frequency, tuning_position, matching_position)) + self.saved_positions_changed.emit(self.saved_positions) + @property def measurement(self): """The measurement property is used to store the current measurement. diff --git a/src/nqrduck_autotm/view.py b/src/nqrduck_autotm/view.py index 9330bc7..14c6fbe 100644 --- a/src/nqrduck_autotm/view.py +++ b/src/nqrduck_autotm/view.py @@ -124,6 +124,9 @@ class AutoTMView(ModuleView): # Active stepper changed self.module.model.active_stepper_changed.connect(self.on_active_stepper_changed) + # Position Button + self._ui_form.positionButton.clicked.connect(self.on_position_button_clicked) + self.init_plot() self.init_labels() @@ -218,6 +221,15 @@ class AutoTMView(ModuleView): self._ui_form.increaseButton.setEnabled(False) self._ui_form.absoluteGoButton.setEnabled(False) + @pyqtSlot() + def on_position_button_clicked(self) -> None: + """This method is called when the position button is clicked. + It opens the position window. + """ + logger.debug("Position button clicked") + self.position_window = self.StepperSavedPositionsWindow(self.module, self) + self.position_window.show() + def plot_measurement(self, data: "S11Data") -> None: """Update the S11 plot with the current data points. @@ -336,6 +348,165 @@ class AutoTMView(ModuleView): self.lut_window = self.LutWindow(self.module) self.lut_window.show() + class StepperSavedPositionsWindow(QDialog): + def __init__(self, module, parent=None): + super().__init__(parent) + self.setParent(parent) + self.module = module + self.setWindowTitle("Saved positions") + # make window larger + self.resize(800, 800) + + # Add vertical main layout + main_layout = QVBoxLayout() + + # Create table widget + self.table_widget = QTableWidget() + self.table_widget.setColumnCount(4) + self.table_widget.setHorizontalHeaderLabels( + ["Frequency", "Tuning Position", "Matching Position", "Button"] + ) + + self.table_widget.setColumnWidth(0, 100) + self.table_widget.setColumnWidth(1, 200) + self.table_widget.setColumnWidth(2, 200) + self.table_widget.setColumnWidth(3, 100) + self.on_saved_positions_changed() + + # Add a 'Load Position' button (File selector) + load_position_button = QPushButton("Load Positions File") + load_position_button.clicked.connect(self.on_load_position_button_clicked) + main_layout.addWidget(load_position_button) + + # Add a 'Save Position' button (File selector) + save_position_button = QPushButton("Save Positions File") + save_position_button.clicked.connect(self.on_save_position_button_clicked) + main_layout.addWidget(save_position_button) + + # Add a 'New Position' button + new_position_button = QPushButton("New Position") + new_position_button.clicked.connect(self.on_new_position_button_clicked) + main_layout.addWidget(new_position_button) + + # Add table widget to main layout + main_layout.addWidget(self.table_widget) + + # On saved positions changed + self.module.model.saved_positions_changed.connect(self.on_saved_positions_changed) + + + self.setLayout(main_layout) + + def file_selector(self, mode) -> str: + """Opens a file selector and returns the selected file.""" + filedialog = QFileDialog() + if mode == "load": + filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptOpen) + elif mode == "save": + filedialog.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + filedialog.setNameFilter("position files (*.pos)") + filedialog.setDefaultSuffix("pos") + filedialog.exec() + filename = filedialog.selectedFiles()[0] + return filename + + def on_load_position_button_clicked(self) -> None: + """File picker for loading a position from a file.""" + filename = self.file_selector("load") + logger.debug("Loading position from %s" % filename) + self.module.controller.load_positions(filename) + + def on_save_position_button_clicked(self) -> None: + """File picker for saving a position to a file.""" + filename = self.file_selector("save") + logger.debug("Saving position to %s" % filename) + self.module.controller.save_positions(filename) + + def on_new_position_button_clicked(self) -> None: + """Opens a new position dialog.""" + logger.debug("New position button clicked") + self.new_position_window = self.NewPositionWindow(self.module, self) + self.new_position_window.show() + + + def on_saved_positions_changed(self) -> None: + """This method is called when the saved positions changed. + It updates the table widget. + """ + logger.debug("Updating saved positions table") + self.table_widget.clearContents() + self.table_widget.setRowCount(0) + + for row, position in enumerate(self.module.model.saved_positions): + self.table_widget.insertRow(row) + self.table_widget.setItem(row, 0, QTableWidgetItem(str(position.frequency))) + self.table_widget.setItem( + row, 1, QTableWidgetItem(position.tuning_position) + ) + self.table_widget.setItem( + row, 2, QTableWidgetItem(position.matching_position) + ) + button = QPushButton("Go") + self.table_widget.setCellWidget(row, 3, button) + logger.debug("Updated saved positions table") + + class NewPositionWindow(QDialog): + def __init__(self, module, parent=None): + super().__init__(parent) + self.setParent(parent) + self.module = module + self.setWindowTitle("New Position") + + # 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") + frequency_layout.addWidget(frequency_label) + frequency_edit = QLineEdit() + frequency_layout.addWidget(frequency_edit) + unit_label = QLabel("MHz") + frequency_layout.addWidget(unit_label) + frequency_layout.addStretch() + + # Add horizontal layout for the calibration type + type_layout = QHBoxLayout() + main_layout.addLayout(type_layout) + + # Add vertical layout for short calibration + tuning_layout = QVBoxLayout() + tuning_label = QLabel("Tuning Position") + tuning_layout.addWidget(tuning_label) + tuning_edit = QLineEdit() + tuning_layout.addWidget(tuning_edit) + type_layout.addLayout(tuning_layout) + + # Add vertical layout for open calibration + matching_layout = QVBoxLayout() + matching_label = QLabel("Matching Position") + matching_layout.addWidget(matching_label) + matching_edit = QLineEdit() + matching_layout.addWidget(matching_edit) + type_layout.addLayout(matching_layout) + + # Add vertical layout for save calibration + data_layout = QVBoxLayout() + # Apply button + apply_button = QPushButton("Apply") + apply_button.clicked.connect(lambda: self.on_apply_button_clicked(frequency_edit.text(), tuning_edit.text(), matching_edit.text())) + data_layout.addWidget(apply_button) + + main_layout.addLayout(data_layout) + + self.setLayout(main_layout) + + def on_apply_button_clicked(self, frequency: str, tuning_position: str, matching_position: str) -> None: + """This method is called when the apply button is clicked.""" + self.module.controller.add_position(frequency, tuning_position, matching_position) + # Close the calibration window + self.close() class LoadingSpinner(QDialog): """This class implements a spinner dialog that is shown during a frequency sweep."""