diff --git a/src/nqrduck_autotm/controller.py b/src/nqrduck_autotm/controller.py index 279c7b2..e21525d 100644 --- a/src/nqrduck_autotm/controller.py +++ b/src/nqrduck_autotm/controller.py @@ -1,4 +1,5 @@ import logging +import time import numpy as np import json import time @@ -6,9 +7,10 @@ from serial.tools.list_ports import comports from PyQt6.QtTest import QTest from PyQt6 import QtSerialPort from PyQt6.QtCore import QThread, pyqtSignal, pyqtSlot, Qt +from PyQt6.QtCore import QTimer from PyQt6.QtWidgets import QApplication from nqrduck.module.module_controller import ModuleController -from .model import S11Data, LookupTable +from .model import S11Data, ElectricalLookupTable, MechanicalLookupTable, SavedPosition, Stepper logger = logging.getLogger(__name__) @@ -16,6 +18,60 @@ logger = logging.getLogger(__name__) class AutoTMController(ModuleController): BAUDRATE = 115200 + def on_loading(self): + """This method is called when the module is loaded. + It sets up the serial connection and connects the signals and slots. + """ + logger.debug("Setting up serial connection") + self.find_devices() + + # Connect signals + self.module.model.serial_data_received.connect(self.process_frequency_sweep_data) + self.module.model.serial_data_received.connect(self.process_measurement_data) + self.module.model.serial_data_received.connect(self.process_calibration_data) + self.module.model.serial_data_received.connect(self.process_voltage_sweep_result) + self.module.model.serial_data_received.connect(self.print_info) + self.module.model.serial_data_received.connect(self.read_position_data) + self.module.model.serial_data_received.connect(self.process_reflection_data) + self.module.model.serial_data_received.connect(self.process_position_sweep_result) + self.module.model.serial_data_received.connect(self.process_signalpath_data) + + @pyqtSlot(str, object) + def process_signals(self, key: str, value: object) -> None: + logger.debug("Received signal: %s", key) + if key == "set_tune_and_match": + self.tune_and_match(value) + + def tune_and_match(self, frequency: float) -> None: + """ This method is called when this module already has a LUT table. It should then tune and match the probe coil to the specified frequency. + """ + if self.module.model.LUT is None: + logger.error("Could not tune and match. No LUT available.") + return + elif self.module.model.LUT.TYPE == "Electrical": + tuning_voltage, matching_voltage = self.module.model.LUT.get_voltages(frequency) + confirmation = self.set_voltages(str(tuning_voltage), str(matching_voltage)) + # We need to change the signal pathway to preamp to measure the reflection + self.switch_to_atm() + reflection = self.read_reflection(frequency) + # We need to change the signal pathway back to atm to perform a measurement + self.switch_to_preamp() + self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection) + + elif self.module.model.LUT.TYPE == "Mechanical": + tuning_position, matching_position = self.module.model.LUT.get_positions(frequency) + self.go_to_position(tuning_position, matching_position) + self.switch_to_atm() + # Switch to atm to measure the reflection + reflection = self.read_reflection(frequency) + # Switch back to preamp to perform a measurement + self.switch_to_preamp() + + # The Lime doesn"t like it if we send the command to switch to atm and then immediately send the command to measure the reflection. + # So we wait a bit before starting the measurement + + QTimer.singleShot(100, lambda: self.module.nqrduck_signal.emit("confirm_tune_and_match", reflection)) + def find_devices(self) -> None: """Scan for available serial devices and add them to the model as available devices.""" logger.debug("Scanning for available serial devices") @@ -62,6 +118,12 @@ class AutoTMController(ModuleController): self.module.model.serial = serial logger.debug("Connected to device %s", device) + + # On opening of the command we set the switch position to atm + self.switch_to_atm() + + self.set_voltages("0", "0") + except Exception as e: logger.error("Could not connect to device %s: %s", device, e) @@ -79,7 +141,7 @@ class AutoTMController(ModuleController): MAX_FREQUENCY = 200e6 # Hz try: - start_frequence = start_frequency.replace(",", ".") + start_frequency = start_frequency.replace(",", ".") stop_frequency = stop_frequency.replace(",", ".") start_frequency = float(start_frequency) * 1e6 stop_frequency = float(stop_frequency) * 1e6 @@ -130,98 +192,177 @@ class AutoTMController(ModuleController): self.module.model.clear_data_points() self.module.view.create_frequency_sweep_spinner_dialog() + @pyqtSlot(str) + def process_frequency_sweep_data(self, text : str) -> None: + """This method is called when data is received from the serial connection during a frequency sweep. + It processes the data and adds it to the model. + """ + if text.startswith("f") and self.module.view.frequency_sweep_spinner.isVisible(): + text = text[1:].split("r") + frequency = float(text[0]) + return_loss, phase = map(float, text[1].split("p")) + self.module.model.add_data_point(frequency, return_loss, phase) + + @pyqtSlot(str) + def process_measurement_data(self, text : str) -> None: + """This method is called when data is received from the serial connection during a measurement. + It processes the data and adds it to the model. + """ + if self.module.model.active_calibration is None and text.startswith("r"): + logger.debug("Measurement finished") + self.module.model.measurement = S11Data( + self.module.model.data_points.copy() + ) + self.finish_frequency_sweep() + + @pyqtSlot(str) + def process_calibration_data(self, text : str) -> None: + """This method is called when data is received from the serial connection during a calibration. + It processes the data and adds it to the model. + + Args: + calibration_type (str): The type of calibration that is being performed. + """ + if text.startswith("r") and self.module.model.active_calibration in ["short", "open", "load"]: + calibration_type = self.module.model.active_calibration + logger.debug(f"{calibration_type.capitalize()} calibration finished") + setattr(self.module.model, f"{calibration_type}_calibration", + S11Data(self.module.model.data_points.copy())) + self.module.model.active_calibration = None + self.module.view.frequency_sweep_spinner.hide() + + @pyqtSlot(str) + def process_voltage_sweep_result(self, text : str) -> None: + """This method is called when data is received from the serial connection during a voltage sweep. + It processes the data and adds it to the model. + + Args: + text (str): The data received from the serial connection. + """ + if text.startswith("v"): + text = text[1:].split("t") + tuning_voltage, matching_voltage = map(float, text) + LUT = self.module.model.el_lut + if LUT is not None: + if LUT.is_incomplete(): + logger.debug("Received voltage sweep result: Tuning %s Matching %s", tuning_voltage, matching_voltage) + LUT.add_voltages(tuning_voltage, matching_voltage) + self.continue_or_finish_voltage_sweep(LUT) + + self.module.model.tuning_voltage = tuning_voltage + self.module.model.matching_voltage = matching_voltage + logger.debug("Updated voltages: Tuning %s Matching %s", self.module.model.tuning_voltage, self.module.model.matching_voltage) + + def finish_frequency_sweep(self): + """This method is called when a frequency sweep is finished. + It hides the frequency sweep spinner dialog and adds the data to the model. + """ + self.module.view.frequency_sweep_spinner.hide() + self.module.model.frequency_sweep_stop = time.time() + duration = self.module.model.frequency_sweep_stop - self.module.model.frequency_sweep_start + self.module.view.add_info_text(f"Frequency sweep finished in {duration:.2f} seconds") + + def continue_or_finish_voltage_sweep(self, LUT): + """This method is called when a voltage sweep is finished. + It checks if the voltage sweep is finished or if the next voltage sweep should be started. + + Args: + LUT (LookupTable): The lookup table that is being generated. + """ + if LUT.is_incomplete(): + # Start the next voltage sweep + self.start_next_voltage_sweep(LUT) + else: + # Finish voltage sweep + self.finish_voltage_sweep(LUT) + + def start_next_voltage_sweep(self, LUT): + """This method is called when a voltage sweep is finished. + It starts the next voltage sweep. + + Args: + LUT (LookupTable): The lookup table that is being generated. + """ + next_frequency = LUT.get_next_frequency() + # We write the first command to the serial connection + if self.module.view._ui_form.prevVoltagecheckBox.isChecked(): + # Command format is soo + # We use the currently set voltages + command = "s%so%so%s" % (next_frequency, self.module.model.tuning_voltage, self.module.model.matching_voltage) + else: + command = "s%s" % (next_frequency) + + LUT.started_frequency = next_frequency + logger.debug("Starting next voltage sweep: %s", command) + self.send_command(command) + + def finish_voltage_sweep(self, LUT): + """This method is called when a voltage sweep is finished. + It hides the voltage sweep spinner dialog and adds the data to the model. + + Args: + LUT (LookupTable): The lookup table that is being generated.""" + logger.debug("Voltage sweep finished") + self.module.view.el_LUT_spinner.hide() + self.module.model.LUT = LUT + self.module.model.voltage_sweep_stop = time.time() + duration = self.module.model.voltage_sweep_stop - self.module.model.voltage_sweep_start + self.module.view.add_info_text(f"Voltage sweep finished in {duration:.2f} seconds") + self.module.nqrduck_signal.emit("LUT_finished", LUT) + + @pyqtSlot(str) + def print_info(self, text : str) -> None: + """This method is called when data is received from the serial connection. + It prints the data to the info text box. + + Args: + text (str): The data received from the serial connection. + """ + if text.startswith("i"): + text = text[1:] + self.module.view.add_info_text(text) + elif text.startswith("e"): + text = text[1:] + self.module.view.add_error_text(text) + + @pyqtSlot(str) + def read_position_data(self, text : str) -> None: + """This method is called when data is received from the serial connection.""" + if text.startswith("p"): + # Format is pm + text = text[1:].split("m") + tuning_position, matching_position = map(int, text) + self.module.model.tuning_stepper.position = tuning_position + self.module.model.matching_stepper.position = matching_position + self.module.model.tuning_stepper.homed = True + self.module.model.matching_stepper.homed = True + logger.debug("Tuning position: %s, Matching position: %s", tuning_position, matching_position) + self.module.view.on_active_stepper_changed() + def on_ready_read(self) -> None: """This method is called when data is received from the serial connection.""" serial = self.module.model.serial + while serial.canReadLine(): - text = serial.readLine().data().decode() - text = text.rstrip("\r\n") - # logger.debug("Received data: %s", text) - # If the text starts with 'f' and the frequency sweep spinner is visible we know that the data is a data point - # then we have the data for the return loss and the phase at a certain frequency - if ( - text.startswith("f") - and self.module.view.frequency_sweep_spinner.isVisible() - ): - text = text[1:].split("r") - frequency = float(text[0]) - return_loss, phase = map(float, text[1].split("p")) - self.module.model.add_data_point(frequency, return_loss, phase) - # If the text starts with 'r' and no calibration is active we know that the data is a measurement - elif text.startswith("r") and self.module.model.active_calibration == None: - logger.debug("Measurement finished") - self.module.model.measurement = S11Data( - self.module.model.data_points.copy() - ) - self.module.view.frequency_sweep_spinner.hide() - self.module.model.frequency_sweep_stop = time.time() - self.module.view.add_info_text( - "Frequency sweep finished in %.2f seconds" - % ( - self.module.model.frequency_sweep_stop - - self.module.model.frequency_sweep_start - ) - ) - # If the text starts with 'r' and a short calibration is active we know that the data is a short calibration - elif ( - text.startswith("r") and self.module.model.active_calibration == "short" - ): - logger.debug("Short calibration finished") - self.module.model.short_calibration = S11Data( - self.module.model.data_points.copy() - ) - self.module.model.active_calibration = None - self.module.view.frequency_sweep_spinner.hide() - # If the text starts with 'r' and an open calibration is active we know that the data is an open calibration - elif ( - text.startswith("r") and self.module.model.active_calibration == "open" - ): - logger.debug("Open calibration finished") - self.module.model.open_calibration = S11Data( - self.module.model.data_points.copy() - ) - self.module.model.active_calibration = None - self.module.view.frequency_sweep_spinner.hide() - # If the text starts with 'r' and a load calibration is active we know that the data is a load calibration - elif ( - text.startswith("r") and self.module.model.active_calibration == "load" - ): - logger.debug("Load calibration finished") - self.module.model.load_calibration = S11Data( - self.module.model.data_points.copy() - ) - self.module.model.active_calibration = None - self.module.view.frequency_sweep_spinner.hide() - # If the text starts with 'i' we know that the data is an info message - elif text.startswith("i"): - text = "ATM Info: " + text[1:] - self.module.view.add_info_text(text) - # If the text starts with 'e' we know that the data is an error message - elif text.startswith("e"): - text = "ATM Error: " + text[1:] - self.module.view.add_info_text(text) - # If the text starts with 'v' we know that the data is a voltage sweep result - elif text.startswith("v"): - text = text[1:] - text = text.split("t") - matching_voltage = float(text[0]) - tuning_voltage = float(text[1]) - # Now we add the datapoint to the current LUT - LUT = self.module.model.LUT - logger.debug( - "Received voltage sweep result: %s %s", - matching_voltage, - tuning_voltage, - ) - LUT.add_voltages(matching_voltage, tuning_voltage) + text = serial.readLine().data().decode().rstrip("\r\n") + logger.debug("Received data: %s", text) - # Start the next voltage sweep if there are more voltages to sweep - if LUT.is_incomplete(): - next_frequency = LUT.get_next_frequency() - command = "s%s" % next_frequency - LUT.started_frequency = next_frequency - logger.debug("Starting next voltage sweep: %s", command) - self.send_command(command) + self.module.model.serial_data_received.emit(text) + + @pyqtSlot(str) + def process_reflection_data(self, text): + """This method is called when data is received from the serial connection. + It processes the data and adds it to the model. + + Args: + text (str): The data received from the serial connection. + """ + if text.startswith("m"): + text = text[1:] + return_loss, phase = map(float, text.split("p")) + self.module.model.last_reflection = (return_loss, phase) + + ### Calibration Stuff ### def on_short_calibration( self, start_frequency: float, stop_frequency: float @@ -370,6 +511,37 @@ class AutoTMController(ModuleController): self.module.model.open_calibration = S11Data.from_json(data["open"]) self.module.model.load_calibration = S11Data.from_json(data["load"]) + def save_measurement(self, filename: str) -> None: + """Save measurement to file. + + Args: + filename (str): Path to file. + """ + logger.debug("Saving measurement.") + if not self.module.model.measurement: + logger.debug("No measurement to save.") + return + + measurement = self.module.model.measurement.to_json() + + with open(filename, "w") as f: + json.dump(measurement, f) + + def load_measurement(self, filename: str) -> None: + """Load measurement from file. + + Args: + filename (str): Path to file. + """ + + logger.debug("Loading measurement.") + + with open(filename, "r") as f: + measurement = json.load(f) + self.module.model.measurement = S11Data.from_json(measurement) + + ### Voltage Control ### + def set_voltages(self, tuning_voltage: str, matching_voltage: str) -> None: """This method is called when the set voltages button is pressed. It writes the specified tuning and matching voltage to the serial connection. @@ -380,6 +552,8 @@ class AutoTMController(ModuleController): """ logger.debug("Setting voltages") MAX_VOLTAGE = 5 # V + timeout_duration = 15 # timeout in seconds + try: tuning_voltage = tuning_voltage.replace(",", ".") matching_voltage = matching_voltage.replace(",", ".") @@ -411,15 +585,35 @@ class AutoTMController(ModuleController): matching_voltage, ) - command = "v%sv%s" % (matching_voltage, tuning_voltage) - self.send_command(command) + if tuning_voltage == self.module.model.tuning_voltage and matching_voltage == self.module.model.matching_voltage: + logger.debug("Voltages already set") + return + + command = "v%sv%s" % (tuning_voltage, matching_voltage) + + start_time = time.time() - def generate_lut( + confirmation = self.send_command(command) + while matching_voltage != self.module.model.matching_voltage and tuning_voltage != self.module.model.tuning_voltage: + QApplication.processEvents() + # Check for timeout + if time.time() - start_time > timeout_duration: + logger.error("Voltage setting timed out") + break + + logger.debug("Voltages set successfully") + return confirmation + else: + logger.error("Could not set voltages") + return confirmation + + ### Electrical Lookup Table ### + + def generate_electrical_lut( self, start_frequency: str, stop_frequency: str, frequency_step: str, - voltage_resolution: str, ) -> None: """This method is called when the generate LUT button is pressed. It generates a lookup table for the specified frequency range and voltage resolution. @@ -428,20 +622,17 @@ class AutoTMController(ModuleController): start_frequency (str): The start frequency in Hz. stop_frequency (str): The stop frequency in Hz. frequency_step (str): The frequency step in Hz. - voltage_resolution (str): The voltage resolution in V. """ logger.debug("Generating LUT") try: start_frequency = start_frequency.replace(",", ".") stop_frequency = stop_frequency.replace(",", ".") frequency_step = frequency_step.replace(",", ".") - voltage_resolution = voltage_resolution.replace(",", ".") start_frequency = float(start_frequency) stop_frequency = float(stop_frequency) frequency_step = float(frequency_step) - voltage_resolution = float(voltage_resolution) except ValueError: - error = "Could not generate LUT. Start frequency, stop frequency, frequency step and voltage resolution must be floats" + error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be floats" logger.error(error) self.module.view.add_info_text(error) return @@ -450,9 +641,8 @@ class AutoTMController(ModuleController): start_frequency < 0 or stop_frequency < 0 or frequency_step < 0 - or voltage_resolution < 0 ): - error = "Could not generate LUT. Start frequency, stop frequency, frequency step and voltage resolution must be positive" + error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive" logger.error(error) self.module.view.add_info_text(error) return @@ -463,48 +653,101 @@ class AutoTMController(ModuleController): self.module.view.add_info_text(error) return - if frequency_step > (stop_frequency - start_frequency): + # - 0.1 is to prevent float errors + if frequency_step - 0.1 > (stop_frequency - start_frequency): error = "Could not generate LUT. Frequency step must be smaller than the frequency range" logger.error(error) self.module.view.add_info_text(error) return logger.debug( - "Generating LUT from %s MHz to %s MHz with a frequency step of %s MHz and a voltage resolution of %s V", + "Generating LUT from %s MHz to %s MHz with a frequency step of %s MHz", start_frequency, stop_frequency, frequency_step, - voltage_resolution, ) + self.switch_to_atm() + # self.set_voltages("0", "0") + # We create the lookup table - LUT = LookupTable( - start_frequency, stop_frequency, frequency_step, voltage_resolution + LUT = ElectricalLookupTable( + start_frequency, stop_frequency, frequency_step ) LUT.started_frequency = start_frequency - self.module.model.LUT = LUT # We write the first command to the serial connection - command = "s%s" % (start_frequency) + if self.module.view._ui_form.prevVoltagecheckBox.isChecked(): + # Command format is soo + # We use the currently set voltages + logger.debug("Starting preset Voltage sweep with voltage Tuning: %s V and Matching: %s V", self.module.model.tuning_voltage, self.module.model.matching_voltage) + command = "s%so%so%s" % (start_frequency, self.module.model.tuning_voltage, self.module.model.matching_voltage) + else: + command = "s%s" % (start_frequency) + + # For timing of the voltage sweep + self.module.model.voltage_sweep_start = time.time() confirmation = self.send_command(command) - if not confirmation: - return + # If the command was send successfully, we set the LUT + if confirmation: + self.module.model.el_lut = LUT + self.module.view.create_el_LUT_spinner_dialog() def switch_to_preamp(self) -> None: """This method is used to send the command 'cp' to the atm system. This switches the signal pathway of the atm system to 'RX' to 'Preamp'. This is the mode for either NQR or NMR measurements or if on wants to check the tuning of the probe coil on a network analyzer. """ + if self.module.model.signal_path == "preamp": + logger.debug("Already in preamp") + return + + TIMEOUT = 1 # s logger.debug("Switching to preamp") self.send_command("cp") + start_time = time.time() + while self.module.model.signal_path != "preamp": + QApplication.processEvents() + # Check for timeout + if time.time() - start_time > TIMEOUT: + logger.error("Switching to preamp timed out") + break + def switch_to_atm(self) -> None: """This method is used to send the command 'ca' to the atm system. This switches the signal pathway of the atm system to 'RX' to 'ATM. In this state the atm system can be used to measure the reflection coefficient of the probecoils. """ + if self.module.model.signal_path == "atm": + logger.debug("Already in atm mode") + return + + TIMEOUT = 1 # s logger.debug("Switching to atm") self.send_command("ca") + start_time = time.time() + while self.module.model.signal_path != "atm": + QApplication.processEvents() + # Check for timeout + if time.time() - start_time > TIMEOUT: + logger.error("Switching to atm timed out") + break + + def process_signalpath_data(self, text : str) -> None: + """This method is called when data is received from the serial connection. + It processes the data and adds it to the model. + + Args: + text (str): The data received from the serial connection. + """ + if text.startswith("c"): + text = text[1:] + if text == "p": + self.module.model.signal_path = "preamp" + elif text == "a": + self.module.model.signal_path = "atm" + def send_command(self, command: str) -> bool: """This method is used to send a command to the active serial connection. @@ -544,7 +787,7 @@ class AutoTMController(ModuleController): logger.debug("Confirmation: %s", confirmation) if confirmation == "c": - logger.debug("Command send successfully") + logger.debug("Command sent successfully") return True else: logger.error("Could not send command. No confirmation received") @@ -557,9 +800,378 @@ class AutoTMController(ModuleController): logger.error("Could not send command. %s", e) self.module.view.add_error_text("Could not send command. %s" % e) + ### Stepper Motor Control ### + def homing(self) -> None: """This method is used to send the command 'h' to the atm system. This command is used to home the stepper motors of the atm system. """ logger.debug("Homing") self.send_command("h") + self.module.model.tuning_stepper.last_direction = 1 + self.module.model.matching_stepper.last_direction = 1 + + @pyqtSlot(str) + def on_stepper_changed(self, stepper: str) -> None: + """This method is called when the stepper position is changed. + It sends the command to the atm system to change the stepper position. + + Args: + stepper (str): The stepper that is being changed. Either 'tuning' or 'matching'. + """ + logger.debug("Stepper %s changed", stepper) + stepper = stepper.lower() + if stepper == "tuning": + self.module.model.active_stepper = self.module.model.tuning_stepper + elif stepper == "matching": + self.module.model.active_stepper = self.module.model.matching_stepper + + def validate_position(self, future_position: int, stepper : Stepper) -> bool: + """Validate the stepper's future position.""" + if future_position < 0: + self.module.view.add_error_text("Could not move stepper. Stepper position cannot be negative") + return False + + if future_position > stepper.MAX_STEPS: + self.module.view.add_error_text(f"Could not move stepper. Stepper position cannot be larger than {stepper.MAX_STEPS}") + return False + + return True + + def calculate_steps_for_absolute_move(self, target_position: int, stepper : Stepper) -> int: + """Calculate the number of steps for an absolute move.""" + current_position = stepper.position + return target_position - current_position + + def send_stepper_command(self, steps: int, stepper : Stepper) -> None: + """Send a command to the stepper motor based on the number of steps.""" + # Here we handle backlash of the tuner + # Determine the direction of the current steps + backlash = 0 + current_direction = np.sign(steps) # This will be -1,or 1 + if stepper.TYPE == "Tuning": + logger.debug("Stepper last direction: %s", stepper.last_direction) + logger.debug("Current direction: %s", current_direction) + if stepper.last_direction != current_direction: + backlash = stepper.BACKLASH_STEPS * current_direction + + stepper.last_direction = current_direction + logger.debug("Stepper last direction: %s", stepper.last_direction) + + motor_identifier = stepper.TYPE.lower()[0] + command = f"m{motor_identifier}{steps},{backlash}" + confirmation = self.send_command(command) + return confirmation + + def on_relative_move(self, steps: str, stepper: Stepper = None) -> None: + """This method is called when the relative move button is pressed.""" + timeout_duration = 15 # timeout in seconds + start_time = time.time() + + if stepper is None: + stepper = self.module.model.active_stepper + + stepper_position = stepper.position + future_position = stepper.position + int(steps) + if future_position == stepper_position: + logger.debug("Stepper already at position") + return + + if self.validate_position(future_position, stepper): + confirmation = self.send_stepper_command(int(steps), stepper) # Convert the steps string to an integer + + while stepper_position == stepper.position: + QApplication.processEvents() + # Check for timeout + if time.time() - start_time > timeout_duration: + logger.error("Relative move timed out") + break # or handle timeout differently + + return confirmation + + def on_absolute_move(self, steps: str, stepper: Stepper = None) -> None: + """This method is called when the absolute move button is pressed.""" + timeout_duration = 15 # timeout in seconds + start_time = time.time() + + if stepper is None: + stepper = self.module.model.active_stepper + + stepper_position = stepper.position + future_position = int(steps) + + if future_position == stepper_position: + logger.debug("Stepper already at position") + return + + if self.validate_position(future_position, stepper): + actual_steps = self.calculate_steps_for_absolute_move(future_position, stepper) + confirmation = self.send_stepper_command(actual_steps, stepper) + + while stepper_position == stepper.position: + QApplication.processEvents() + # Check for timeout + if time.time() - start_time > timeout_duration: + logger.error("Absolute move timed out") + break # or handle timeout differently + + return confirmation + + ### Position Saving and Loading ### + + 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) + + def on_go_to_position(self, position: SavedPosition) -> None: + """Go to the specified position. + + Args: + position (SavedPosition): The position to go to. + """ + logger.debug("Going to position: %s", position) + confirmation = self.on_absolute_move(position.tuning_position, self.module.model.tuning_stepper) + if confirmation: + self.on_absolute_move(position.matching_position, self.module.model.matching_stepper) + + def on_delete_position(self, position: SavedPosition) -> None: + """Delete the specified position. + + Args: + position (SavedPosition): The position to delete. + """ + logger.debug("Deleting position: %s", position) + self.module.model.delete_saved_position(position) + + + #### Mechanical tuning and matching #### + + def generate_mechanical_lut(self, start_frequency: str, stop_frequency: str, frequency_step: str) -> None: + """Generate a lookup table for the specified frequency range and voltage resolution. + + Args: + start_frequency (str): The start frequency in Hz. + stop_frequency (str): The stop frequency in Hz. + frequency_step (str): The frequency step in Hz. + """ + try: + start_frequency = start_frequency.replace(",", ".") + stop_frequency = stop_frequency.replace(",", ".") + frequency_step = frequency_step.replace(",", ".") + start_frequency = float(start_frequency) + stop_frequency = float(stop_frequency) + frequency_step = float(frequency_step) + except ValueError: + error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be floats" + logger.error(error) + self.module.view.add_info_text(error) + return + + if ( + start_frequency < 0 + or stop_frequency < 0 + or frequency_step < 0 + ): + error = "Could not generate LUT. Start frequency, stop frequency, frequency step must be positive" + logger.error(error) + self.module.view.add_info_text(error) + return + + if start_frequency > stop_frequency: + error = "Could not generate LUT. Start frequency must be smaller than stop frequency" + logger.error(error) + self.module.view.add_info_text(error) + return + + # - 0.1 is to prevent float errors + if frequency_step - 0.1 > (stop_frequency - start_frequency): + error = "Could not generate LUT. Frequency step must be smaller than the frequency range" + logger.error(error) + self.module.view.add_info_text(error) + return + + logger.debug( + "Generating LUT from %s MHz to %s MHz with a frequency step of %s MHz", + start_frequency, + stop_frequency, + frequency_step, + ) + + self.switch_to_atm() + + # We create the lookup table + LUT = MechanicalLookupTable( + start_frequency, stop_frequency, frequency_step + ) + + # Lock GUI + self.module.view.create_mech_LUT_spinner_dialog() + + self.module.model.mech_lut = LUT + + self.start_next_mechTM(LUT) + + + def start_next_mechTM(self, LUT): + """Start the next mechanical tuning and matching sweep.""" + + next_frequency = LUT.get_next_frequency() + LUT.started_frequency = next_frequency + logger.debug("Starting next mechanical tuning and matching:") + + # Now we vary the tuning capacitor position and matching capacitor position + # Step size tuner: + TUNER_STEP_SIZE = 10 + # Step size matcher: + MATCHER_STEP_SIZE = 50 + + TUNING_RANGE = 40 + MATCHING_RANGE = 500 + + tuning_backlash = self.module.model.tuning_stepper.BACKLASH_STEPS + # I'm not sure about this value ... + matching_backlash = 0 + + # Command for the position sweep: pt,,,m,,," + tuning_last_direction = self.module.model.tuning_stepper.last_direction + matching_last_direction = self.module.model.matching_stepper.last_direction + command = f"p{next_frequency}t{TUNING_RANGE},{TUNER_STEP_SIZE},{tuning_backlash},{tuning_last_direction}m{MATCHING_RANGE},{MATCHER_STEP_SIZE},{matching_backlash},{matching_last_direction}" + + confirmation = self.send_command(command) + + @pyqtSlot(str) + def process_position_sweep_result(self, text): + if text.startswith("z"): + text = text[1:] + # Format is z,m, + text = text.split("m") + tuning_position, tuning_last_direction = map(int, text[0].split(",")) + matching_position, matching_last_direction = map(int, text[1].split(",")) + + # Keep backlash compensation consistent + self.module.model.tuning_stepper.last_direction = tuning_last_direction + self.module.model.matching_stepper.last_direction = matching_last_direction + + # Update the positions + self.module.model.tuning_stepper.position = tuning_position + self.module.model.matching_stepper.position = matching_position + self.module.view.on_active_stepper_changed() + + logger.debug("Tuning position: %s, Matching position: %s", tuning_position, matching_position) + + LUT = self.module.model.mech_lut + logger.debug("Received position sweep result: %s %s", matching_position, tuning_position) + LUT.add_positions(tuning_position, matching_position) + self.continue_or_finish_position_sweep(LUT) + + def continue_or_finish_position_sweep(self, LUT): + """Continue or finish the position sweep.""" + if LUT.is_incomplete(): + self.start_next_mechTM(LUT) + else: + self.finish_position_sweep(LUT) + + def finish_position_sweep(self, LUT): + """Finish the position sweep.""" + logger.debug("Finished position sweep") + self.module.model.mech_lut = LUT + self.module.model.LUT = LUT + self.module.view.mech_LUT_spinner.hide() + self.module.nqrduck_signal.emit("LUT_finished", LUT) + + def go_to_position(self, tuning_position : int, matching_position : int) -> None: + """Go to the specified position. + + Args: + position (SavedPosition): The position to go to. + """ + confirmation = self.on_absolute_move(tuning_position, self.module.model.tuning_stepper) + if confirmation: + confirmation = self.on_absolute_move(matching_position, self.module.model.matching_stepper) + if confirmation: + return True + + + # This method isn't used anymore but it might be useful in the future so I'll keep it here + def read_reflection(self, frequency) -> float: + """Starts a reflection measurement and reads the reflection at the specified frequency.""" + # We send the command to the atm system + command = f"r{frequency}" + try: + confirmation = self.send_command(command) + QApplication.processEvents() + if confirmation: + reflection = self.module.model.last_reflection + + # Set the timeout duration (e.g., 5 seconds) + timeout_duration = 5 + # Record the start time + start_time = time.time() + + # Wait for reflection data until the timeout is reached + while reflection is None: + # Check if the timeout has been reached + if time.time() - start_time > timeout_duration: + logger.error("Reading reflection timed out after %d seconds", timeout_duration) + self.module.view.add_error_text(f"Could not read reflection. Timed out after {timeout_duration} seconds") + return None + + # Refresh the reflection data + reflection = self.module.model.last_reflection + QApplication.processEvents() + + # Reset the reflection cache + self.module.model.last_reflection = None + + magnitude = reflection[0] + CENTER_POINT_MAGNITUDE = 900 # mV + MAGNITUDE_SLOPE = 30 # dB/mV + magnitude = (magnitude - CENTER_POINT_MAGNITUDE) / MAGNITUDE_SLOPE + + return -magnitude + + else: + logger.error("Could not read reflection. No confirmation received") + self.module.view.add_error_text("Could not read reflection. No confirmation received") + return None + + except Exception as e: + logger.error("Could not read reflection. %s", e) + self.module.view.add_error_text(f"Could not read reflection. {e}") + return None + + diff --git a/src/nqrduck_autotm/model.py b/src/nqrduck_autotm/model.py index 8ecc223..26fd7c3 100644 --- a/src/nqrduck_autotm/model.py +++ b/src/nqrduck_autotm/model.py @@ -10,6 +10,7 @@ logger = logging.getLogger(__name__) class S11Data: + FILE_EXTENSION = "s11" # Conversion factors - the data is generally sent and received in mV # These values are used to convert the data to dB and degrees CENTER_POINT_MAGNITUDE = 900 # mV @@ -139,6 +140,9 @@ class S11Data: * phase_sign[i] ) + # Murks: The last point is always wrong so just set it to the previous value + phase_data_corrected[-1] = phase_data_corrected[-2] + return phase_data_corrected def to_json(self): @@ -167,26 +171,102 @@ class LookupTable: start_frequency: float, stop_frequency: float, frequency_step: float, - voltage_resolution: float, ) -> None: self.start_frequency = start_frequency self.stop_frequency = stop_frequency self.frequency_step = frequency_step - self.voltage_resolution = voltage_resolution # This is the frequency at which the tuning and matching process was started self.started_frequency = None + + def get_entry_number(self, frequency: float) -> int: + """This method returns the entry number of the given frequency. + Args: + frequency (float): The frequency for which the entry number should be returned. + + Returns: + int: The entry number of the given frequency. + """ + # Round to closest integer + return int(round((frequency - self.start_frequency) / self.frequency_step)) + +class Stepper: + + def __init__(self) -> None: + 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, + } + +class TuningStepper(Stepper): + TYPE = "Tuning" + MAX_STEPS = 1e6 + BACKLASH_STEPS = 60 + + def __init__(self) -> None: + super().__init__() + # Backlash stepper + self.last_direction = None + +class MatchingStepper(Stepper): + TYPE = "Matching" + MAX_STEPS = 1e6 + + BACKLASH_STEPS = 0 + + def __init__(self) -> None: + super().__init__() + self.last_direction = None + +class ElectricalLookupTable(LookupTable): + TYPE = "Electrical" + + def __init__(self, start_frequency: float, stop_frequency: float, frequency_step: float) -> None: + super().__init__(start_frequency, stop_frequency, frequency_step) self.init_voltages() def init_voltages(self) -> None: """Initialize the lookup table with default values.""" for frequency in np.arange( - self.start_frequency, self.stop_frequency, self.frequency_step + self.start_frequency, self.stop_frequency + self.frequency_step, self.frequency_step ): self.started_frequency = frequency self.add_voltages(None, None) + def add_voltages(self, tuning_voltage: float, matching_voltage: float) -> None: + """Add a tuning and matching voltage for the last started frequency to the lookup table. + + Args: + tuning_voltage (float): The tuning voltage for the given frequency. + matching_voltage (float): The matching voltage for the given frequency.""" + self.data[self.started_frequency] = (tuning_voltage, matching_voltage) + + def get_voltages(self, frequency: float) -> tuple: + """Get the tuning and matching voltage for the given frequency. + + Args: + frequency (float): The frequency for which the tuning and matching voltage should be returned. + + Returns: + tuple: The tuning and matching voltage for the given frequency. + """ + entry_number = self.get_entry_number(frequency) + key = list(self.data.keys())[entry_number] + return self.data[key] + def is_incomplete(self) -> bool: """This method returns True if the lookup table is incomplete, i.e. if there are frequencies for which no the tuning or matching voltage is none. @@ -214,19 +294,78 @@ class LookupTable: return None - def add_voltages(self, tuning_voltage: float, matching_voltage: float) -> None: - """Add a tuning and matching voltage for the last started frequency to the lookup table. +class MechanicalLookupTable(LookupTable): + # Hmm duplicate code + TYPE = "Mechanical" + + + def __init__(self, start_frequency: float, stop_frequency: float, frequency_step: float) -> None: + super().__init__(start_frequency, stop_frequency, frequency_step) + self.init_positions() + + def init_positions(self) -> None: + """Initialize the lookup table with default values.""" + for frequency in np.arange( + self.start_frequency, self.stop_frequency + self.frequency_step, self.frequency_step + ): + self.started_frequency = frequency + self.add_positions(None, None) + + def add_positions(self, tuning_position: int, matching_position: int) -> None: + """Add a tuning and matching position for the last started frequency to the lookup table. Args: - tuning_voltage (float): The tuning voltage for the given frequency. - matching_voltage (float): The matching voltage for the given frequency.""" - self.data[self.started_frequency] = (tuning_voltage, matching_voltage) + tuning_position (int): The tuning position for the given frequency. + matching_position (int): The matching position for the given frequency.""" + self.data[self.started_frequency] = (tuning_position, matching_position) + def get_positions(self, frequency: float) -> tuple: + """Get the tuning and matching position for the given frequency. + Args: + frequency (float): The frequency for which the tuning and matching position should be returned. + + Returns: + tuple: The tuning and matching position for the given frequency. + """ + entry_number = self.get_entry_number(frequency) + key = list(self.data.keys())[entry_number] + return self.data[key] + + def is_incomplete(self) -> bool: + """This method returns True if the lookup table is incomplete, + i.e. if there are frequencies for which no the tuning or matching position is none. + + Returns: + bool: True if the lookup table is incomplete, False otherwise. + """ + return any( + [ + tuning_position is None or matching_position is None + for tuning_position, matching_position in self.data.values() + ] + ) + + def get_next_frequency(self) -> float: + """This method returns the next frequency for which the tuning and matching position is not yet set. + + Returns: + float: The next frequency for which the tuning and matching position is not yet set. + """ + + for frequency, (tuning_position, matching_position) in self.data.items(): + if tuning_position is None or matching_position is None: + return frequency + + return None class AutoTMModel(ModuleModel): + available_devices_changed = pyqtSignal(list) serial_changed = pyqtSignal(QSerialPort) data_points_changed = pyqtSignal(list) + active_stepper_changed = pyqtSignal(Stepper) + saved_positions_changed = pyqtSignal(list) + serial_data_received = pyqtSignal(str) short_calibration_finished = pyqtSignal(S11Data) open_calibration_finished = pyqtSignal(S11Data) @@ -240,6 +379,24 @@ class AutoTMModel(ModuleModel): self.calibration = None self.serial = None + self.tuning_stepper = TuningStepper() + self.matching_stepper = MatchingStepper() + self.active_stepper = self.tuning_stepper + + self.saved_positions = [] + + self.el_lut = None + self.mech_lut = None + self.LUT = None + + self.last_reflection = None + + self.tuning_voltage = None + self.matching_voltage = None + + # AutoTM system or preamp + self.signal_path = None + @property def available_devices(self): return self._available_devices @@ -273,6 +430,25 @@ 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) + + def delete_saved_position(self, position: SavedPosition) -> None: + """Delete a saved position from the model.""" + self.saved_positions.remove(position) + self.saved_positions_changed.emit(self.saved_positions) + @property def measurement(self): """The measurement property is used to store the current measurement. @@ -285,6 +461,15 @@ class AutoTMModel(ModuleModel): self._measurement = value self.measurement_finished.emit(value) + @property + def active_stepper(self): + return self._active_stepper + + @active_stepper.setter + def active_stepper(self, value): + self._active_stepper = value + self.active_stepper_changed.emit(value) + # Calibration properties @property diff --git a/src/nqrduck_autotm/resources/autotm_widget.ui b/src/nqrduck_autotm/resources/autotm_widget.ui index 2b2838b..ec69b53 100644 --- a/src/nqrduck_autotm/resources/autotm_widget.ui +++ b/src/nqrduck_autotm/resources/autotm_widget.ui @@ -7,7 +7,7 @@ 0 0 1280 - 862 + 1089 @@ -26,6 +26,7 @@ + 75 true @@ -76,10 +77,70 @@ + + + + + 75 + true + + + + T&M Settings: + + + + + + + + + 80.099999999999994 + + + + + + + Stop Frequency (MHz) + + + + + + + 80.000000000000000 + + + + + + + Start Frequency (MHz) + + + + + + + Frequency Step (MHz) + + + + + + + 0.100000000000000 + + + + + + 75 true @@ -91,86 +152,17 @@ - 0 + 1 Mechanical - + - + - - - Home - - - - - - - Step Size: - - - - - - - -1000 - - - 1000 - - - 500 - - - - - - - Tuning Stepper: - - - - - - - - - - - - - - - + - - - - - - - Matching Stepper: - - - - - - - - - - - - - - - + - - - - - + Home @@ -178,17 +170,127 @@ + + + 75 + true + + Stepper Control: + + + + 1000000 + + + + + + + + Tuning + + + + + Matching + + + + + + + + - + + + + + + + + + + + + + + + Stepper: + + + + + + + Absolute: + + + + + + + 0 + + + 1000000 + + + 500 + + + + + + + Step Size: + + + + + + + Go + + + + + + + Position: + + + + + + + 0 + + + - + - Start Position + Saved Positions + + + + + + + Generate LUT + + + + + + + View LUT @@ -211,66 +313,12 @@ Electrical - - - - - Voltage Resolution - - - - - - - - - - Voltage Tuning - - - - - - - - - - - - - - true - - - - Generate LUT: - - - - - - - - - - Start Voltage Sweep - - - - - - - Stop Frequency (MHz) - - - - - - + + 75 true @@ -279,15 +327,41 @@ - - + + - RF Switch: + Voltage Tuning + + + + + + + + + + Generate LUT + + + + + + + + + + + 75 + true + + + + Generate LUT: - + View LUT @@ -307,34 +381,10 @@ - - + + - Start Frequency (MHz) - - - - - - - - - - Frequency Step (MHz) - - - - - - - Preamplifier - - - - - - - ATM + Start from previous Voltage @@ -342,10 +392,42 @@ + + + + + 75 + true + + + + RF Switch: + + + + + + + + + ATM + + + + + + + Preamplifier + + + + + + 75 true @@ -363,17 +445,10 @@ - - + + - MHz - - - - - - - 100 + Stop Frequency: @@ -384,13 +459,6 @@ - - - - Stop Frequency: - - - @@ -398,6 +466,20 @@ + + + + 100 + + + + + + + MHz + + + @@ -425,6 +507,7 @@ + 75 true @@ -443,8 +526,8 @@ 0 0 - 297 - 68 + 291 + 83 @@ -464,6 +547,41 @@ + + + + + + + + Import Measurement + + + + + + + Export Measurement + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + diff --git a/src/nqrduck_autotm/resources/duck_kick.gif b/src/nqrduck_autotm/resources/duck_kick.gif deleted file mode 100644 index 0b7b08e..0000000 Binary files a/src/nqrduck_autotm/resources/duck_kick.gif and /dev/null differ diff --git a/src/nqrduck_autotm/view.py b/src/nqrduck_autotm/view.py index bcf0fe9..89bd753 100644 --- a/src/nqrduck_autotm/view.py +++ b/src/nqrduck_autotm/view.py @@ -21,6 +21,7 @@ from nqrduck.contrib.mplwidget import MplWidget from nqrduck.assets.icons import Logos from nqrduck.assets.animations import DuckAnimations from .widget import Ui_Form +from .model import S11Data logger = logging.getLogger(__name__) @@ -34,11 +35,14 @@ class AutoTMView(ModuleView): self._ui_form.setupUi(self) self.widget = widget - self.frequency_sweep_spinner = self.FrequencySweepSpinner(self) + self.frequency_sweep_spinner = self.LoadingSpinner(self) self.frequency_sweep_spinner.hide() # Disable the connectButton while no devices are selected self._ui_form.connectButton.setDisabled(True) + self._ui_form.decreaseButton.setEnabled(False) + self._ui_form.increaseButton.setEnabled(False) + self._ui_form.absoluteGoButton.setEnabled(False) # On clicking of the refresh button scan for available usb devices self._ui_form.refreshButton.clicked.connect(self.module.controller.find_devices) @@ -61,18 +65,28 @@ class AutoTMView(ModuleView): ) ) - # On clicking of the generateLUTButton call the generate_lut method + # On clicking of the generateLUTButton call the generate_mechanical_lut method self._ui_form.generateLUTButton.clicked.connect( - lambda: self.module.controller.generate_lut( + lambda: self.module.controller.generate_electrical_lut( + self._ui_form.startfrequencyBox.text(), + self._ui_form.stopfrequencyBox.text(), + self._ui_form.frequencystepBox.text(), + ) + ) + + # On clicking of the generateLUTButton call the generate_electrical_lut method + self._ui_form.mechLUTButton.clicked.connect( + lambda: self.module.controller.generate_mechanical_lut( self._ui_form.startfrequencyBox.text(), self._ui_form.stopfrequencyBox.text(), self._ui_form.frequencystepBox.text(), - self._ui_form.resolutionBox.text(), ) ) # On clicking of the viewLUTButton call the view_lut method - self._ui_form.viewLUTButton.clicked.connect(self.view_lut) + self._ui_form.viewelLUTButton.clicked.connect(self.view_el_lut) + + self._ui_form.viewmechLUTButton.clicked.connect(self.view_mech_lut) # On clicking of the setvoltagesButton call the set_voltages method self._ui_form.setvoltagesButton.clicked.connect( @@ -97,7 +111,7 @@ class AutoTMView(ModuleView): ) # On clicking of the homingButton call the homing method - self._ui_form.starpositionButton.clicked.connect(self.module.controller.homing) + self._ui_form.homeButton.clicked.connect(self.module.controller.homing) # Connect the measurement finished signal to the plot_measurement slot self.module.model.measurement_finished.connect(self.plot_measurement) @@ -112,11 +126,35 @@ class AutoTMView(ModuleView): self._ui_form.startButton.setIcon(Logos.Play_16x16()) self._ui_form.startButton.setIconSize(self._ui_form.startButton.size()) + # Stepper selection + self._ui_form.stepperselectBox.currentIndexChanged.connect(lambda: self.module.controller.on_stepper_changed(self._ui_form.stepperselectBox.currentText())) + self._ui_form.increaseButton.clicked.connect(lambda: self.module.controller.on_relative_move(self._ui_form.stepsizeBox.text())) + self._ui_form.decreaseButton.clicked.connect(lambda: self.module.controller.on_relative_move("-" + self._ui_form.stepsizeBox.text())) + + self._ui_form.absoluteGoButton.clicked.connect(lambda: self.module.controller.on_absolute_move(self._ui_form.absoluteposBox.text())) + + # 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) + + # Import and export buttons + + self._ui_form.exportButton.setIcon(Logos.Save16x16()) + self._ui_form.exportButton.setIconSize(self._ui_form.exportButton.size()) + self._ui_form.exportButton.clicked.connect(self.on_export_button_clicked) + + self._ui_form.importButton.setIcon(Logos.Load16x16()) + self._ui_form.importButton.setIconSize(self._ui_form.importButton.size()) + self._ui_form.importButton.clicked.connect(self.on_import_button_clicked) + self.init_plot() self.init_labels() def init_labels(self) -> None: """Makes some of the labels bold for better readability.""" + self._ui_form.tmsettingsLabel.setStyleSheet("font-weight: bold;") self._ui_form.titleconnectionLabel.setStyleSheet("font-weight: bold;") self._ui_form.titlefrequencyLabel.setStyleSheet("font-weight: bold;") self._ui_form.titletypeLabel.setStyleSheet("font-weight: bold;") @@ -126,7 +164,7 @@ class AutoTMView(ModuleView): """Initialize the S11 plot.""" ax = self._ui_form.S11Plot.canvas.ax ax.set_xlabel("Frequency (MHz)") - ax.set_ylabel("S11 (dB)") + ax.set_ylabel("S11 (dB)", loc="center") ax.set_title("S11") ax.grid(True) ax.set_xlim(0, 100) @@ -156,6 +194,10 @@ class AutoTMView(ModuleView): self._ui_form.connectButton.setEnabled(False) logger.debug("Updated available devices list") + def on_stepper_changed(): + """Update the stepper position label according to the current stepper position.""" + logger.debug("Updating stepper position label") + @pyqtSlot() def on_connect_button_clicked(self) -> None: """This method is called when the connect button is clicked. @@ -184,6 +226,38 @@ class AutoTMView(ModuleView): logger.debug("Updated serial connection label") + @pyqtSlot() + def on_active_stepper_changed(self) -> None: + """Update the stepper position label according to the current stepper position.""" + logger.debug("Updating stepper position label") + self._ui_form.stepperposLabel.setText(str(self.module.model.active_stepper.position)) + logger.debug("Updated stepper position label") + + # Only allow position change when stepper is homed + if self.module.model.active_stepper.homed: + self._ui_form.decreaseButton.setEnabled(True) + self._ui_form.increaseButton.setEnabled(True) + self._ui_form.absoluteGoButton.setEnabled(True) + self._ui_form.positionButton.setEnabled(True) + self._ui_form.mechLUTButton.setEnabled(True) + self._ui_form.viewmechLUTButton.setEnabled(True) + else: + self._ui_form.decreaseButton.setEnabled(False) + self._ui_form.increaseButton.setEnabled(False) + self._ui_form.absoluteGoButton.setEnabled(False) + self._ui_form.positionButton.setEnabled(False) + self._ui_form.mechLUTButton.setEnabled(False) + self._ui_form.viewmechLUTButton.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. @@ -226,7 +300,9 @@ class AutoTMView(ModuleView): else: magnitude_ax.plot(frequency, return_loss_db, color="blue") - self.phase_ax.set_ylabel("|Phase (deg)|") + self.phase_ax.yaxis.tick_right() + self.phase_ax.yaxis.set_label_position("right") + self.phase_ax.set_ylabel("Phase (deg)") self.phase_ax.plot(frequency, phase, color="orange", linestyle="--") # self.phase_ax.invert_yaxis() @@ -288,21 +364,237 @@ class AutoTMView(ModuleView): def create_frequency_sweep_spinner_dialog(self) -> None: """Creates a frequency sweep spinner dialog.""" - self.frequency_sweep_spinner = self.FrequencySweepSpinner(self) + self.frequency_sweep_spinner = self.LoadingSpinner("Performing frequency sweep ...", self) self.frequency_sweep_spinner.show() - def view_lut(self) -> None: - """Creates a new Dialog that shows the currently active LUT.""" + def create_el_LUT_spinner_dialog(self) -> None: + """Creates a electrical LUT spinner dialog.""" + self.el_LUT_spinner = self.LoadingSpinner("Generating electrical LUT ...", self) + self.el_LUT_spinner.show() + + def create_mech_LUT_spinner_dialog(self) -> None: + """Creates a mechanical LUT spinner dialog.""" + self.mech_LUT_spinner = self.LoadingSpinner("Generating mechanical LUT ...", self) + self.mech_LUT_spinner.show() + + def view_el_lut(self) -> None: + """Creates a new Dialog that shows the currently active electrical LUT.""" logger.debug("View LUT") + if self.module.model.el_lut is None: + logger.debug("No LUT available") + self.add_error_text("No LUT available") + return self.lut_window = self.LutWindow(self.module) self.lut_window.show() - class FrequencySweepSpinner(QDialog): + def view_mech_lut(self) -> None: + """Creates a new Dialog that shows the currently active mechanical LUT.""" + logger.debug("View mechanical LUT") + if self.module.model.mech_lut is None: + logger.debug("No LUT available") + self.add_error_text("No LUT available") + return + self.lut_window = self.LutWindow(self.module) + self.lut_window.show() + + @pyqtSlot() + def on_export_button_clicked(self) -> None: + """Slot for when the export button is clicked.""" + logger.debug("Export button clicked") + file_manager = self.QFileManager(S11Data.FILE_EXTENSION, parent=self.widget) + file_name = file_manager.saveFileDialog() + if file_name: + self.module.controller.save_measurement(file_name) + + @pyqtSlot() + def on_import_button_clicked(self) -> None: + """Slot for when the import button is clicked.""" + logger.debug("Import button clicked") + file_manager = self.QFileManager(S11Data.FILE_EXTENSION, parent=self.widget) + file_name = file_manager.loadFileDialog() + if file_name: + self.module.controller.load_measurement(file_name) + + 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(5) + self.table_widget.setHorizontalHeaderLabels( + ["Frequency (MHz)", "Tuning Position", "Matching Position", "Button", "Delete"] + ) + + self.table_widget.setColumnWidth(0, 150) + self.table_widget.setColumnWidth(1, 200) + self.table_widget.setColumnWidth(2, 200) + self.table_widget.setColumnWidth(3, 100) + self.table_widget.setColumnWidth(4, 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) + ) + go_button = QPushButton("Go") + go_button.clicked.connect( + lambda _, position=position: self.module.controller.on_go_to_position( + position + ) + ) + self.table_widget.setCellWidget(row, 3, go_button) + + delete_button = QPushButton("Delete") + delete_button.clicked.connect( + lambda _, position=position: self.module.controller.on_delete_position( + position + ) + ) + self.table_widget.setCellWidget(row, 4, delete_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.""" - def __init__(self, parent=None): + def __init__(self, text : str, parent=None): super().__init__(parent) - self.setWindowTitle("Frequency sweep") + self.setWindowTitle("Loading") self.setModal(True) self.setWindowFlag(Qt.WindowType.FramelessWindowHint) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) @@ -312,7 +604,7 @@ class AutoTMView(ModuleView): self.spinner_label.setMovie(self.spinner_movie) self.layout = QVBoxLayout(self) - self.layout.addWidget(QLabel("Performing frequency sweep...")) + self.layout.addWidget(QLabel(text)) self.layout.addWidget(self.spinner_label) self.spinner_movie.start() @@ -324,16 +616,31 @@ class AutoTMView(ModuleView): self.setParent(parent) self.setWindowTitle("LUT") + # Set size + self.resize(800, 800) + # Add vertical main layout main_layout = QVBoxLayout() + LUT = self.module.model.LUT + # Create table widget self.table_widget = QTableWidget() - self.table_widget.setColumnCount(3) - self.table_widget.setHorizontalHeaderLabels( - ["Frequency (MHz)", "Matching Voltage", "Tuning Voltage"] - ) - LUT = self.module.model.LUT + self.table_widget.setColumnCount(4) + self.table_widget.setColumnWidth(0, 150) + self.table_widget.setColumnWidth(1, 200) + self.table_widget.setColumnWidth(2, 200) + self.table_widget.setColumnWidth(3, 100) + + if LUT.TYPE == "Mechanical": + self.table_widget.setHorizontalHeaderLabels( + ["Frequency (MHz)", "Tuning Position", "Matching Position"] + ) + elif LUT.TYPE == "Electrical": + self.table_widget.setHorizontalHeaderLabels( + ["Frequency (MHz)", "Tuning Voltage", "Matching Voltage"] + ) + for row, frequency in enumerate(LUT.data.keys()): self.table_widget.insertRow(row) self.table_widget.setItem(row, 0, QTableWidgetItem(str(frequency))) @@ -344,20 +651,38 @@ class AutoTMView(ModuleView): row, 2, QTableWidgetItem(str(LUT.data[frequency][1])) ) + # Button to test the specific entry in the LUT + test_button = QPushButton("Test") + # For electrical probe coils the matching voltage is the first entry in the LUT + if LUT.TYPE == "Electrical": + tuning_voltage = str(LUT.data[frequency][0]) + matching_voltage = str(LUT.data[frequency][1]) + test_button.clicked.connect( + lambda _, tuning_voltage=tuning_voltage, matching_voltage=matching_voltage: self.module.controller.set_voltages( + tuning_voltage, matching_voltage + ) + ) + # For mechanical probe coils the tuning voltage is the first entry in the LUT + elif LUT.TYPE == "Mechanical": + tuning_position = str(LUT.data[frequency][0]) + matching_position = str(LUT.data[frequency][1]) + test_button.clicked.connect( + lambda _, tuning_position=tuning_position, matching_position=matching_position: self.module.controller.go_to_position( + tuning_position, matching_position + ) + ) + + self.table_widget.setCellWidget(row, 3, test_button) + # Add table widget to main layout main_layout.addWidget(self.table_widget) - - # Add Test LUT button - test_lut_button = QPushButton("Test LUT") - test_lut_button.clicked.connect(self.test_lut) - main_layout.addWidget(test_lut_button) - self.setLayout(main_layout) def test_lut(self): """This method is called when the Test LUT button is clicked. It sets all of the voltages from the lut with a small delay. One can then view the matching on a seperate VNA. """ + # This should be in the controller for frequency in self.module.model.LUT.data.keys(): tuning_voltage = str(self.module.model.LUT.data[frequency][1]) matching_voltage = str(self.module.model.LUT.data[frequency][0]) diff --git a/src/nqrduck_autotm/widget.py b/src/nqrduck_autotm/widget.py index d0a4ac4..9758e6b 100644 --- a/src/nqrduck_autotm/widget.py +++ b/src/nqrduck_autotm/widget.py @@ -1,6 +1,6 @@ -# Form implementation generated from reading ui file '../Modules/nqrduck-autotm/src/nqrduck_autotm/resources/autotm_widget.ui' +# Form implementation generated from reading ui file 'Modules/nqrduck-autotm/src/nqrduck_autotm/resources/autotm_widget.ui' # -# Created by: PyQt6 UI code generator 6.4.2 +# Created by: PyQt6 UI code generator 6.5.1 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. @@ -12,7 +12,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(1280, 862) + Form.resize(1280, 1089) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -25,6 +25,7 @@ class Ui_Form(object): self.titleconnectionLabel = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setBold(True) + font.setWeight(75) self.titleconnectionLabel.setFont(font) self.titleconnectionLabel.setObjectName("titleconnectionLabel") self.verticalLayout_2.addWidget(self.titleconnectionLabel) @@ -50,9 +51,41 @@ class Ui_Form(object): self.connectButton = QtWidgets.QPushButton(parent=Form) self.connectButton.setObjectName("connectButton") self.verticalLayout_2.addWidget(self.connectButton) + self.tmsettingsLabel = QtWidgets.QLabel(parent=Form) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.tmsettingsLabel.setFont(font) + self.tmsettingsLabel.setObjectName("tmsettingsLabel") + self.verticalLayout_2.addWidget(self.tmsettingsLabel) + self.gridLayout_8 = QtWidgets.QGridLayout() + self.gridLayout_8.setObjectName("gridLayout_8") + self.stopfrequencyBox = QtWidgets.QDoubleSpinBox(parent=Form) + self.stopfrequencyBox.setProperty("value", 80.1) + self.stopfrequencyBox.setObjectName("stopfrequencyBox") + self.gridLayout_8.addWidget(self.stopfrequencyBox, 1, 1, 1, 1) + self.label_13 = QtWidgets.QLabel(parent=Form) + self.label_13.setObjectName("label_13") + self.gridLayout_8.addWidget(self.label_13, 1, 0, 1, 1) + self.startfrequencyBox = QtWidgets.QDoubleSpinBox(parent=Form) + self.startfrequencyBox.setProperty("value", 80.0) + self.startfrequencyBox.setObjectName("startfrequencyBox") + self.gridLayout_8.addWidget(self.startfrequencyBox, 0, 1, 1, 1) + self.label_12 = QtWidgets.QLabel(parent=Form) + self.label_12.setObjectName("label_12") + self.gridLayout_8.addWidget(self.label_12, 0, 0, 1, 1) + self.label_14 = QtWidgets.QLabel(parent=Form) + self.label_14.setObjectName("label_14") + self.gridLayout_8.addWidget(self.label_14, 2, 0, 1, 1) + self.frequencystepBox = QtWidgets.QDoubleSpinBox(parent=Form) + self.frequencystepBox.setProperty("value", 0.1) + self.frequencystepBox.setObjectName("frequencystepBox") + self.gridLayout_8.addWidget(self.frequencystepBox, 2, 1, 1, 1) + self.verticalLayout_2.addLayout(self.gridLayout_8) self.titletypeLabel = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setBold(True) + font.setWeight(75) self.titletypeLabel.setFont(font) self.titletypeLabel.setObjectName("titletypeLabel") self.verticalLayout_2.addWidget(self.titletypeLabel) @@ -64,46 +97,65 @@ class Ui_Form(object): self.verticalLayout.setObjectName("verticalLayout") self.gridLayout_4 = QtWidgets.QGridLayout() self.gridLayout_4.setObjectName("gridLayout_4") - self.homematcherButton = QtWidgets.QPushButton(parent=self.mechTab) - self.homematcherButton.setObjectName("homematcherButton") - self.gridLayout_4.addWidget(self.homematcherButton, 5, 1, 1, 1) - self.label_17 = QtWidgets.QLabel(parent=self.mechTab) - self.label_17.setObjectName("label_17") - self.gridLayout_4.addWidget(self.label_17, 1, 0, 1, 1) - self.stepsizeBox = QtWidgets.QSpinBox(parent=self.mechTab) - self.stepsizeBox.setMinimum(-1000) - self.stepsizeBox.setMaximum(1000) - self.stepsizeBox.setProperty("value", 500) - self.stepsizeBox.setObjectName("stepsizeBox") - self.gridLayout_4.addWidget(self.stepsizeBox, 1, 1, 1, 1) - self.label_18 = QtWidgets.QLabel(parent=self.mechTab) - self.label_18.setObjectName("label_18") - self.gridLayout_4.addWidget(self.label_18, 2, 0, 1, 3) - self.decreasetunerButton = QtWidgets.QPushButton(parent=self.mechTab) - self.decreasetunerButton.setObjectName("decreasetunerButton") - self.gridLayout_4.addWidget(self.decreasetunerButton, 3, 0, 1, 1) - self.increasetunerButton = QtWidgets.QPushButton(parent=self.mechTab) - self.increasetunerButton.setObjectName("increasetunerButton") - self.gridLayout_4.addWidget(self.increasetunerButton, 3, 2, 1, 1) - self.label_19 = QtWidgets.QLabel(parent=self.mechTab) - self.label_19.setObjectName("label_19") - self.gridLayout_4.addWidget(self.label_19, 4, 0, 1, 3) - self.decreasematcherButton = QtWidgets.QPushButton(parent=self.mechTab) - self.decreasematcherButton.setObjectName("decreasematcherButton") - self.gridLayout_4.addWidget(self.decreasematcherButton, 5, 0, 1, 1) - self.increasematcherButton = QtWidgets.QPushButton(parent=self.mechTab) - self.increasematcherButton.setObjectName("increasematcherButton") - self.gridLayout_4.addWidget(self.increasematcherButton, 5, 2, 1, 1) - self.hometunerButton = QtWidgets.QPushButton(parent=self.mechTab) - self.hometunerButton.setObjectName("hometunerButton") - self.gridLayout_4.addWidget(self.hometunerButton, 3, 1, 1, 1) + self.homeButton = QtWidgets.QPushButton(parent=self.mechTab) + self.homeButton.setObjectName("homeButton") + self.gridLayout_4.addWidget(self.homeButton, 5, 1, 1, 1) self.label_16 = QtWidgets.QLabel(parent=self.mechTab) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_16.setFont(font) self.label_16.setObjectName("label_16") self.gridLayout_4.addWidget(self.label_16, 0, 0, 1, 3) + self.absoluteposBox = QtWidgets.QSpinBox(parent=self.mechTab) + self.absoluteposBox.setMaximum(1000000) + self.absoluteposBox.setObjectName("absoluteposBox") + self.gridLayout_4.addWidget(self.absoluteposBox, 6, 1, 1, 1) + self.stepperselectBox = QtWidgets.QComboBox(parent=self.mechTab) + self.stepperselectBox.setObjectName("stepperselectBox") + self.stepperselectBox.addItem("") + self.stepperselectBox.addItem("") + self.gridLayout_4.addWidget(self.stepperselectBox, 1, 1, 1, 1) + self.decreaseButton = QtWidgets.QPushButton(parent=self.mechTab) + self.decreaseButton.setObjectName("decreaseButton") + self.gridLayout_4.addWidget(self.decreaseButton, 5, 0, 1, 1) + self.increaseButton = QtWidgets.QPushButton(parent=self.mechTab) + self.increaseButton.setObjectName("increaseButton") + self.gridLayout_4.addWidget(self.increaseButton, 5, 2, 1, 1) + self.label_18 = QtWidgets.QLabel(parent=self.mechTab) + self.label_18.setObjectName("label_18") + self.gridLayout_4.addWidget(self.label_18, 1, 0, 1, 1) + self.label_20 = QtWidgets.QLabel(parent=self.mechTab) + self.label_20.setObjectName("label_20") + self.gridLayout_4.addWidget(self.label_20, 6, 0, 1, 1) + self.stepsizeBox = QtWidgets.QSpinBox(parent=self.mechTab) + self.stepsizeBox.setMinimum(0) + self.stepsizeBox.setMaximum(1000000) + self.stepsizeBox.setProperty("value", 500) + self.stepsizeBox.setObjectName("stepsizeBox") + self.gridLayout_4.addWidget(self.stepsizeBox, 3, 1, 1, 1) + self.label_17 = QtWidgets.QLabel(parent=self.mechTab) + self.label_17.setObjectName("label_17") + self.gridLayout_4.addWidget(self.label_17, 3, 0, 1, 1) + self.absoluteGoButton = QtWidgets.QPushButton(parent=self.mechTab) + self.absoluteGoButton.setObjectName("absoluteGoButton") + self.gridLayout_4.addWidget(self.absoluteGoButton, 6, 2, 1, 1) + self.label_4 = QtWidgets.QLabel(parent=self.mechTab) + self.label_4.setObjectName("label_4") + self.gridLayout_4.addWidget(self.label_4, 2, 0, 1, 1) + self.stepperposLabel = QtWidgets.QLabel(parent=self.mechTab) + self.stepperposLabel.setObjectName("stepperposLabel") + self.gridLayout_4.addWidget(self.stepperposLabel, 2, 1, 1, 1) self.verticalLayout.addLayout(self.gridLayout_4) - self.starpositionButton = QtWidgets.QPushButton(parent=self.mechTab) - self.starpositionButton.setObjectName("starpositionButton") - self.verticalLayout.addWidget(self.starpositionButton) + self.positionButton = QtWidgets.QPushButton(parent=self.mechTab) + self.positionButton.setObjectName("positionButton") + self.verticalLayout.addWidget(self.positionButton) + self.mechLUTButton = QtWidgets.QPushButton(parent=self.mechTab) + self.mechLUTButton.setObjectName("mechLUTButton") + self.verticalLayout.addWidget(self.mechLUTButton) + self.viewmechLUTButton = QtWidgets.QPushButton(parent=self.mechTab) + self.viewmechLUTButton.setObjectName("viewmechLUTButton") + self.verticalLayout.addWidget(self.viewmechLUTButton) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.verticalLayout.addItem(spacerItem) self.verticalLayout.setStretch(1, 1) @@ -112,77 +164,66 @@ class Ui_Form(object): self.elecTab.setObjectName("elecTab") self.gridLayout_3 = QtWidgets.QGridLayout(self.elecTab) self.gridLayout_3.setObjectName("gridLayout_3") - self.label_4 = QtWidgets.QLabel(parent=self.elecTab) - self.label_4.setObjectName("label_4") - self.gridLayout_3.addWidget(self.label_4, 5, 0, 1, 1) - self.stopfrequencyBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.stopfrequencyBox.setObjectName("stopfrequencyBox") - self.gridLayout_3.addWidget(self.stopfrequencyBox, 7, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(parent=self.elecTab) - self.label_2.setObjectName("label_2") - self.gridLayout_3.addWidget(self.label_2, 1, 0, 1, 1) - self.tuningBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.tuningBox.setObjectName("tuningBox") - self.gridLayout_3.addWidget(self.tuningBox, 1, 1, 1, 1) - self.frequencystepBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.frequencystepBox.setObjectName("frequencystepBox") - self.gridLayout_3.addWidget(self.frequencystepBox, 8, 1, 1, 1) - self.label_11 = QtWidgets.QLabel(parent=self.elecTab) - font = QtGui.QFont() - font.setBold(True) - self.label_11.setFont(font) - self.label_11.setObjectName("label_11") - self.gridLayout_3.addWidget(self.label_11, 4, 0, 1, 1) - self.startfrequencyBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.startfrequencyBox.setObjectName("startfrequencyBox") - self.gridLayout_3.addWidget(self.startfrequencyBox, 6, 1, 1, 1) - self.generateLUTButton = QtWidgets.QPushButton(parent=self.elecTab) - self.generateLUTButton.setObjectName("generateLUTButton") - self.gridLayout_3.addWidget(self.generateLUTButton, 9, 0, 1, 2) - self.label_13 = QtWidgets.QLabel(parent=self.elecTab) - self.label_13.setObjectName("label_13") - self.gridLayout_3.addWidget(self.label_13, 7, 0, 1, 1) - self.resolutionBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.resolutionBox.setObjectName("resolutionBox") - self.gridLayout_3.addWidget(self.resolutionBox, 5, 1, 1, 1) self.label_9 = QtWidgets.QLabel(parent=self.elecTab) font = QtGui.QFont() font.setBold(True) + font.setWeight(75) self.label_9.setFont(font) self.label_9.setObjectName("label_9") self.gridLayout_3.addWidget(self.label_9, 0, 0, 1, 1) - self.label_15 = QtWidgets.QLabel(parent=self.elecTab) - self.label_15.setObjectName("label_15") - self.gridLayout_3.addWidget(self.label_15, 11, 0, 1, 1) - self.viewLUTButton = QtWidgets.QPushButton(parent=self.elecTab) - self.viewLUTButton.setObjectName("viewLUTButton") - self.gridLayout_3.addWidget(self.viewLUTButton, 10, 0, 1, 2) + self.label_2 = QtWidgets.QLabel(parent=self.elecTab) + self.label_2.setObjectName("label_2") + self.gridLayout_3.addWidget(self.label_2, 1, 0, 1, 1) + self.matchingBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) + self.matchingBox.setObjectName("matchingBox") + self.gridLayout_3.addWidget(self.matchingBox, 2, 1, 1, 1) + self.generateLUTButton = QtWidgets.QPushButton(parent=self.elecTab) + self.generateLUTButton.setObjectName("generateLUTButton") + self.gridLayout_3.addWidget(self.generateLUTButton, 8, 0, 1, 2) + self.tuningBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) + self.tuningBox.setObjectName("tuningBox") + self.gridLayout_3.addWidget(self.tuningBox, 1, 1, 1, 1) + self.label_11 = QtWidgets.QLabel(parent=self.elecTab) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_11.setFont(font) + self.label_11.setObjectName("label_11") + self.gridLayout_3.addWidget(self.label_11, 4, 0, 1, 1) + self.viewelLUTButton = QtWidgets.QPushButton(parent=self.elecTab) + self.viewelLUTButton.setObjectName("viewelLUTButton") + self.gridLayout_3.addWidget(self.viewelLUTButton, 10, 0, 1, 2) self.setvoltagesButton = QtWidgets.QPushButton(parent=self.elecTab) self.setvoltagesButton.setObjectName("setvoltagesButton") self.gridLayout_3.addWidget(self.setvoltagesButton, 3, 0, 1, 2) self.label_3 = QtWidgets.QLabel(parent=self.elecTab) self.label_3.setObjectName("label_3") self.gridLayout_3.addWidget(self.label_3, 2, 0, 1, 1) - self.label_12 = QtWidgets.QLabel(parent=self.elecTab) - self.label_12.setObjectName("label_12") - self.gridLayout_3.addWidget(self.label_12, 6, 0, 1, 1) - self.matchingBox = QtWidgets.QDoubleSpinBox(parent=self.elecTab) - self.matchingBox.setObjectName("matchingBox") - self.gridLayout_3.addWidget(self.matchingBox, 2, 1, 1, 1) - self.label_14 = QtWidgets.QLabel(parent=self.elecTab) - self.label_14.setObjectName("label_14") - self.gridLayout_3.addWidget(self.label_14, 8, 0, 1, 1) - self.switchpreampButton = QtWidgets.QPushButton(parent=self.elecTab) - self.switchpreampButton.setObjectName("switchpreampButton") - self.gridLayout_3.addWidget(self.switchpreampButton, 12, 0, 1, 1) - self.switchATMButton = QtWidgets.QPushButton(parent=self.elecTab) - self.switchATMButton.setObjectName("switchATMButton") - self.gridLayout_3.addWidget(self.switchATMButton, 12, 1, 1, 1) + self.prevVoltagecheckBox = QtWidgets.QCheckBox(parent=self.elecTab) + self.prevVoltagecheckBox.setObjectName("prevVoltagecheckBox") + self.gridLayout_3.addWidget(self.prevVoltagecheckBox, 9, 0, 1, 1) self.typeTab.addTab(self.elecTab, "") self.verticalLayout_2.addWidget(self.typeTab) + self.rfswitchLabel = QtWidgets.QLabel(parent=Form) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.rfswitchLabel.setFont(font) + self.rfswitchLabel.setObjectName("rfswitchLabel") + self.verticalLayout_2.addWidget(self.rfswitchLabel) + self.gridLayout_7 = QtWidgets.QGridLayout() + self.gridLayout_7.setObjectName("gridLayout_7") + self.switchATMButton = QtWidgets.QPushButton(parent=Form) + self.switchATMButton.setObjectName("switchATMButton") + self.gridLayout_7.addWidget(self.switchATMButton, 0, 0, 1, 1) + self.switchpreampButton = QtWidgets.QPushButton(parent=Form) + self.switchpreampButton.setObjectName("switchpreampButton") + self.gridLayout_7.addWidget(self.switchpreampButton, 0, 1, 1, 1) + self.verticalLayout_2.addLayout(self.gridLayout_7) self.titlefrequencyLabel = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setBold(True) + font.setWeight(75) self.titlefrequencyLabel.setFont(font) self.titlefrequencyLabel.setObjectName("titlefrequencyLabel") self.verticalLayout_2.addWidget(self.titlefrequencyLabel) @@ -191,21 +232,21 @@ class Ui_Form(object): self.startEdit = QtWidgets.QLineEdit(parent=Form) self.startEdit.setObjectName("startEdit") self.gridLayout.addWidget(self.startEdit, 0, 1, 1, 1) - self.label_8 = QtWidgets.QLabel(parent=Form) - self.label_8.setObjectName("label_8") - self.gridLayout.addWidget(self.label_8, 1, 2, 1, 1) - self.stopEdit = QtWidgets.QLineEdit(parent=Form) - self.stopEdit.setObjectName("stopEdit") - self.gridLayout.addWidget(self.stopEdit, 1, 1, 1, 1) - self.label_6 = QtWidgets.QLabel(parent=Form) - self.label_6.setObjectName("label_6") - self.gridLayout.addWidget(self.label_6, 0, 2, 1, 1) self.label_7 = QtWidgets.QLabel(parent=Form) self.label_7.setObjectName("label_7") self.gridLayout.addWidget(self.label_7, 1, 0, 1, 1) + self.label_6 = QtWidgets.QLabel(parent=Form) + self.label_6.setObjectName("label_6") + self.gridLayout.addWidget(self.label_6, 0, 2, 1, 1) self.label_5 = QtWidgets.QLabel(parent=Form) self.label_5.setObjectName("label_5") self.gridLayout.addWidget(self.label_5, 0, 0, 1, 1) + self.stopEdit = QtWidgets.QLineEdit(parent=Form) + self.stopEdit.setObjectName("stopEdit") + self.gridLayout.addWidget(self.stopEdit, 1, 1, 1, 1) + self.label_8 = QtWidgets.QLabel(parent=Form) + self.label_8.setObjectName("label_8") + self.gridLayout.addWidget(self.label_8, 1, 2, 1, 1) self.verticalLayout_2.addLayout(self.gridLayout) self.startButton = QtWidgets.QPushButton(parent=Form) self.startButton.setObjectName("startButton") @@ -219,6 +260,7 @@ class Ui_Form(object): self.titleinfoLabel = QtWidgets.QLabel(parent=Form) font = QtGui.QFont() font.setBold(True) + font.setWeight(75) self.titleinfoLabel.setFont(font) self.titleinfoLabel.setObjectName("titleinfoLabel") self.verticalLayout_2.addWidget(self.titleinfoLabel) @@ -226,7 +268,7 @@ class Ui_Form(object): self.scrollArea.setWidgetResizable(True) self.scrollArea.setObjectName("scrollArea") self.scrollAreaWidgetContents = QtWidgets.QWidget() - self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 297, 68)) + self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 291, 83)) self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.verticalLayout_2.addWidget(self.scrollArea) @@ -241,11 +283,25 @@ class Ui_Form(object): self.S11Plot.setSizePolicy(sizePolicy) self.S11Plot.setObjectName("S11Plot") self.verticalLayout_5.addWidget(self.S11Plot) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.verticalLayout_4 = QtWidgets.QVBoxLayout() + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.importButton = QtWidgets.QPushButton(parent=Form) + self.importButton.setObjectName("importButton") + self.verticalLayout_4.addWidget(self.importButton) + self.exportButton = QtWidgets.QPushButton(parent=Form) + self.exportButton.setObjectName("exportButton") + self.verticalLayout_4.addWidget(self.exportButton) + self.horizontalLayout.addLayout(self.verticalLayout_4) + spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout.addItem(spacerItem1) + self.verticalLayout_5.addLayout(self.horizontalLayout) self.horizontalLayout_2.addLayout(self.verticalLayout_5) self.horizontalLayout_2.setStretch(1, 1) self.retranslateUi(Form) - self.typeTab.setCurrentIndex(0) + self.typeTab.setCurrentIndex(1) QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): @@ -256,43 +312,50 @@ class Ui_Form(object): self.label.setText(_translate("Form", "Port:")) self.label_10.setText(_translate("Form", "Connected to:")) self.connectButton.setText(_translate("Form", "Connect")) - self.titletypeLabel.setText(_translate("Form", "T&M Type:")) - self.homematcherButton.setText(_translate("Form", "Home")) - self.label_17.setText(_translate("Form", "Step Size:")) - self.label_18.setText(_translate("Form", "Tuning Stepper:")) - self.decreasetunerButton.setText(_translate("Form", "-")) - self.increasetunerButton.setText(_translate("Form", "+")) - self.label_19.setText(_translate("Form", "Matching Stepper:")) - self.decreasematcherButton.setText(_translate("Form", "-")) - self.increasematcherButton.setText(_translate("Form", "+")) - self.hometunerButton.setText(_translate("Form", "Home")) - self.label_16.setText(_translate("Form", "Stepper Control:")) - self.starpositionButton.setText(_translate("Form", "Start Position")) - self.typeTab.setTabText(self.typeTab.indexOf(self.mechTab), _translate("Form", "Mechanical")) - self.label_4.setText(_translate("Form", "Voltage Resolution")) - self.label_2.setText(_translate("Form", "Voltage Tuning")) - self.label_11.setText(_translate("Form", "Generate LUT:")) - self.generateLUTButton.setText(_translate("Form", "Start Voltage Sweep")) + self.tmsettingsLabel.setText(_translate("Form", "T&M Settings:")) self.label_13.setText(_translate("Form", "Stop Frequency (MHz)")) - self.label_9.setText(_translate("Form", "Set Voltages:")) - self.label_15.setText(_translate("Form", "RF Switch:")) - self.viewLUTButton.setText(_translate("Form", "View LUT")) - self.setvoltagesButton.setText(_translate("Form", "Set Voltages")) - self.label_3.setText(_translate("Form", "Voltage Matching")) self.label_12.setText(_translate("Form", "Start Frequency (MHz)")) self.label_14.setText(_translate("Form", "Frequency Step (MHz)")) - self.switchpreampButton.setText(_translate("Form", "Preamplifier")) - self.switchATMButton.setText(_translate("Form", "ATM")) + self.titletypeLabel.setText(_translate("Form", "T&M Type:")) + self.homeButton.setText(_translate("Form", "Home")) + self.label_16.setText(_translate("Form", "Stepper Control:")) + self.stepperselectBox.setItemText(0, _translate("Form", "Tuning")) + self.stepperselectBox.setItemText(1, _translate("Form", "Matching")) + self.decreaseButton.setText(_translate("Form", "-")) + self.increaseButton.setText(_translate("Form", "+")) + self.label_18.setText(_translate("Form", "Stepper:")) + self.label_20.setText(_translate("Form", "Absolute:")) + self.label_17.setText(_translate("Form", "Step Size:")) + self.absoluteGoButton.setText(_translate("Form", "Go")) + self.label_4.setText(_translate("Form", "Position:")) + self.stepperposLabel.setText(_translate("Form", "0")) + self.positionButton.setText(_translate("Form", "Saved Positions")) + self.mechLUTButton.setText(_translate("Form", "Generate LUT")) + self.viewmechLUTButton.setText(_translate("Form", "View LUT")) + self.typeTab.setTabText(self.typeTab.indexOf(self.mechTab), _translate("Form", "Mechanical")) + self.label_9.setText(_translate("Form", "Set Voltages:")) + self.label_2.setText(_translate("Form", "Voltage Tuning")) + self.generateLUTButton.setText(_translate("Form", "Generate LUT")) + self.label_11.setText(_translate("Form", "Generate LUT:")) + self.viewelLUTButton.setText(_translate("Form", "View LUT")) + self.setvoltagesButton.setText(_translate("Form", "Set Voltages")) + self.label_3.setText(_translate("Form", "Voltage Matching")) + self.prevVoltagecheckBox.setText(_translate("Form", "Start from previous Voltage")) self.typeTab.setTabText(self.typeTab.indexOf(self.elecTab), _translate("Form", "Electrical")) + self.rfswitchLabel.setText(_translate("Form", "RF Switch:")) + self.switchATMButton.setText(_translate("Form", "ATM")) + self.switchpreampButton.setText(_translate("Form", "Preamplifier")) self.titlefrequencyLabel.setText(_translate("Form", "Frequency Sweep:")) self.startEdit.setText(_translate("Form", "80")) - self.label_8.setText(_translate("Form", "MHz")) - self.stopEdit.setText(_translate("Form", "100")) - self.label_6.setText(_translate("Form", "MHz")) self.label_7.setText(_translate("Form", "Stop Frequency:")) + self.label_6.setText(_translate("Form", "MHz")) self.label_5.setText(_translate("Form", "Start Frequency:")) + self.stopEdit.setText(_translate("Form", "100")) + self.label_8.setText(_translate("Form", "MHz")) self.startButton.setText(_translate("Form", "Start Sweep")) self.calibrationButton.setText(_translate("Form", "Calibrate")) self.pushButton_3.setText(_translate("Form", "T&M Settings")) self.titleinfoLabel.setText(_translate("Form", "Info Box:")) + self.importButton.setText(_translate("Form", "Import Measurement")) + self.exportButton.setText(_translate("Form", "Export Measurement")) from nqrduck.contrib.mplwidget import MplWidget