diff --git a/cfg/terascan.cfg b/cfg/terascan.cfg index e15af4b..f6f5b3d 100644 --- a/cfg/terascan.cfg +++ b/cfg/terascan.cfg @@ -1,6 +1,6 @@ global: # list of modules to load when starting - startup_modules: [photon_counts_time_average_gui, terascan_gui] + startup_modules: [photon_counts_time_average_gui, powermeter_gui, terascan_gui] # Module server configuration for accessing qudi GUI/logic/hardware modules from remote clients remote_modules_server: @@ -44,6 +44,11 @@ gui: swabian_timetagger: 'swabian_timetagger' options: ring_buffer_length_s: 5 + + powermeter_gui: + module.Class: 'powermeter.powermeter_gui.PowermeterGui' + connect: + powermeter_logic: powermeter_logic logic: terascan_logic: @@ -59,6 +64,11 @@ logic: mode_hop_overlap_med: 0.001 # in nm, from the SolsTiS control panel. This is how far back we go to discard data every time a mode hop occurs mode_hop_overlap_fine: 0.00025 # "" + powermeter_logic: + module.Class: 'powermeter.powermeter_logic.PowermeterLogic' + connect: + powermeter: powermeter_thorlabs + # daq_reader_logic: # module.Class: 'common.daq_reader_logic.DAQReaderLogic' # connect: @@ -117,6 +127,12 @@ hardware: host_ip_addr: '192.168.1.225' # IP address of control computer laser_ip_addr: '192.168.1.222' # IP address of laser laser_port: 39900 # Port number to connect on + + powermeter_thorlabs: + module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter' + options: + average_count: 5 + update_period: 100 # in ms, hardware will emit data ~10 times/second # Dummy Hardware: # daq_reader_dummy: diff --git a/src/qudi/gui/powermeter/powermeter_gui.py b/src/qudi/gui/powermeter/powermeter_gui.py new file mode 100644 index 0000000..69d1c1d --- /dev/null +++ b/src/qudi/gui/powermeter/powermeter_gui.py @@ -0,0 +1,53 @@ +from PySide2 import QtCore +from qudi.core.module import GuiBase +from qudi.core.connector import Connector +from qudi.core.statusvariable import StatusVar + +from .powermeter_main_window import PowermeterMainWindow + +class PowermeterGui(GuiBase): + """ + GUI module to display power meter readings with a multiplication factor. + + Example config: + powermeter_gui: + module.Class: 'powermeter.powermeter_gui.PowermeterGui' + connect: + powermeter_logic: powermeter_logic + """ + # Connector to the logic module (which emits power readings) + _powermeter_logic = Connector(name='powermeter_logic', interface='PowermeterLogic') + # StatusVar to store the multiplication factor between sessions + _multiplication_factor = StatusVar(name='multiplication_factor', default=1.0) + + def on_activate(self): + self._mw = PowermeterMainWindow() + # Initialize the main window's multiplication factor from the status variable. + self._mw.set_multiplication_factor(self._multiplication_factor) + # Set the spinbox value accordingly. + self._mw.mult_factor_spinbox.setValue(self._multiplication_factor) + # Connect spinbox changes to our handler. + self._mw.mult_factor_spinbox.valueChanged.connect(self._on_mult_factor_changed) + # Connect logic's power update signal to the main window update slot. + self._powermeter_logic().sigPowerUpdated.connect( + self._mw.update_power, QtCore.Qt.QueuedConnection + ) + self.show() + + def on_deactivate(self): + self._powermeter_logic().sigPowerUpdated.disconnect(self._mw.update_power) + self._mw.mult_factor_spinbox.valueChanged.disconnect(self._on_mult_factor_changed) + self._mw.close() + + def show(self): + self._mw.show() + self._mw.raise_() + + @QtCore.Slot(float) + def _on_mult_factor_changed(self, factor: float): + """ + Slot called when the multiplication factor is changed via the spinbox. + Updates the StatusVar and informs the main window. + """ + self._multiplication_factor = factor + self._mw.set_multiplication_factor(factor) diff --git a/src/qudi/gui/powermeter/powermeter_main_window.py b/src/qudi/gui/powermeter/powermeter_main_window.py new file mode 100644 index 0000000..10345d7 --- /dev/null +++ b/src/qudi/gui/powermeter/powermeter_main_window.py @@ -0,0 +1,102 @@ +from PySide2 import QtWidgets, QtCore, QtGui + +class PowermeterMainWindow(QtWidgets.QMainWindow): + """ + Main window for displaying the Thorlabs Power Meter reading. + Features: + - Automatic unit conversion. + - A multiplication factor control (for correcting losses) in the lower right, + where the user types in the value (no increment arrows). + - Dynamic font scaling: as the window is resized, the power reading text scales. + """ + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle('Thorlabs Power Meter') + self.resize(400, 180) + + # Initialize multiplication factor (default 1.0) + self._mult_factor = 1.0 + + # Set up central widget and main layout + central_widget = QtWidgets.QWidget() + self.setCentralWidget(central_widget) + main_layout = QtWidgets.QVBoxLayout(central_widget) + main_layout.setContentsMargins(10, 10, 10, 10) + main_layout.setSpacing(5) + + # Create power reading label with initial font + self.power_label = QtWidgets.QLabel("--", self) + self.power_label.setAlignment(QtCore.Qt.AlignCenter) + initial_font = QtGui.QFont("Arial", 24) + self.power_label.setFont(initial_font) + main_layout.addWidget(self.power_label) + + # Add a stretch to push the following controls to the bottom + main_layout.addStretch() + + # Create a horizontal layout for the multiplication factor controls + h_layout = QtWidgets.QHBoxLayout() + h_layout.addStretch() # Pushes items to the right side + + self.mult_factor_label = QtWidgets.QLabel("Multiplication Factor:", self) + self.mult_factor_spinbox = QtWidgets.QDoubleSpinBox(self) + self.mult_factor_spinbox.setDecimals(4) + self.mult_factor_spinbox.setRange(0.0, 1000.0) + self.mult_factor_spinbox.setSingleStep(0.1) + # Remove the up/down arrows so the user types the value + self.mult_factor_spinbox.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.mult_factor_spinbox.setValue(self._mult_factor) + + h_layout.addWidget(self.mult_factor_label) + h_layout.addWidget(self.mult_factor_spinbox) + main_layout.addLayout(h_layout) + + @QtCore.Slot(float) + def update_power(self, power: float): + """ + Update the label text with the new power reading. + The raw power (in Watts) is multiplied by the multiplication factor, then + converted into a suitable unit (pW, nW, µW, mW, or W). + """ + corrected_power = power * self._mult_factor + abs_pwr = abs(corrected_power) + + if abs_pwr < 1e-12: + display_val = corrected_power + display_unit = "W" + elif abs_pwr < 1e-9: + display_val = corrected_power * 1e12 + display_unit = "pW" + elif abs_pwr < 1e-6: + display_val = corrected_power * 1e9 + display_unit = "nW" + elif abs_pwr < 1e-3: + display_val = corrected_power * 1e6 + display_unit = "µW" + elif abs_pwr < 1: + display_val = corrected_power * 1e3 + display_unit = "mW" + else: + display_val = corrected_power + display_unit = "W" + + self.power_label.setText(f"{display_val:.4f} {display_unit}") + + def set_multiplication_factor(self, factor: float): + """ + Update the multiplication factor. + """ + self._mult_factor = factor + + def resizeEvent(self, event): + """ + Dynamically scale the font size of the power reading label based on the + available width of the central widget. + Adjust the divisor (here, 10) to fine-tune scaling. + """ + available_width = self.centralWidget().width() + new_font_size = max(12, int(available_width / 10)) + font = self.power_label.font() + font.setPointSize(new_font_size) + self.power_label.setFont(font) + super().resizeEvent(event) diff --git a/src/qudi/hardware/powermeter/thorlabs_power_meter.py b/src/qudi/hardware/powermeter/thorlabs_power_meter.py index 54b2e9c..549ef1b 100644 --- a/src/qudi/hardware/powermeter/thorlabs_power_meter.py +++ b/src/qudi/hardware/powermeter/thorlabs_power_meter.py @@ -1,72 +1,111 @@ from PySide2 import QtCore - -from ThorlabsPM100 import ThorlabsPM100, USBTMC -# ref: https://github.com/clade/ThorlabsPM100 +import pyvisa +from pymeasure.instruments.thorlabs import ThorlabsPM100USB from qudi.core.configoption import ConfigOption from qudi.util.mutex import RecursiveMutex from qudi.interface.simple_powermeter_interface import SimplePowermeterInterface + class ThorlabsPowerMeter(SimplePowermeterInterface): - """ Hardware class for Thorlabs power meter. Assumes only one meter is attached - Example config for copy-paste: + """ + Hardware class for a Thorlabs power meter (via pymeasure). + Assumes exactly one meter is attached. - powermeter_thorlabs: - # This module assumes only one thorlabs meter is attached - module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter' + Example config for copy-paste: - options: - average_count: 5 # Internal samples to average in the power meter - update_interval: 0 # Period in ms to check for data updates. Integers only. 0 is as fast as possible + powermeter_thorlabs: + module.Class: 'powermeter.thorlabs_power_meter.ThorlabsPowerMeter' + options: + average_count: 5 # Internal samples to average in the power meter + update_period: 0 # Timer interval in ms; 0 = as fast as possible """ - _averages = ConfigOption( - name='average_count', - default=5 - ) - - _update_interval = ConfigOption(name='update_period', - default=0, - missing='info') - - - # Run this in its own thread: + _averages = ConfigOption(name='average_count', default=5) + _update_interval = ConfigOption(name='update_period', default=0, missing='info') + + # This module runs in its own thread: _threaded = True - + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__timer = None self._thread_lock = RecursiveMutex() + self._meter = None def on_activate(self): - inst = USBTMC() - self._meter = ThorlabsPM100(inst=inst) - self._meter.sense.power.dc.range.auto = "ON" # auto-range - self._meter.input.pdiode.filter.lpass.state = 0 # high bandwidth, 1 for low + """ + Called when Qudi activates the module. We open the first VISA resource + that appears, instantiate the PM100USB driver from pymeasure, + and set up a QTimer to poll periodically. + """ + rm = pyvisa.ResourceManager() + devices = rm.list_resources() + + if not devices: + self.log.error("No VISA resources found. Cannot open Thorlabs PM100.") + return + + # Open the first available device (adjust if you have multiple meters) + device = devices[0] + self.log.info(f"Opening resource: {device}") + self._meter = ThorlabsPM100USB(device) + + # Example: set auto-range if supported + self._meter.auto_range = True + + # Set the averaging if supported self.set_average_count(self._averages) - + + # Start a timer to periodically read data self.__timer = QtCore.QTimer() self.__timer.timeout.connect(self.__data_update) self.__timer.setSingleShot(False) self.__timer.start(int(self._update_interval)) - def on_deactivate(self): - if (self.__timer is not None): + """ + Called when Qudi deactivates the module. + Clean up the timer and close the device. + """ + if self.__timer is not None: self.__timer.stop() self.__timer.timeout.disconnect() self.__timer = None - - def get_power(self): - return self._meter.read + # If needed, close the device + if self._meter is not None: + try: + self._meter.shutdown() # Only if the pymeasure driver supports it + except AttributeError: + pass + self._meter = None + + def get_power(self) -> float: + """ + Retrieve the current power reading from the device (in Watts). + """ + if self._meter is None: + return 0.0 + return self._meter.power # from the pymeasure PM100USB driver def set_average_count(self, count: int): + """ + Set the sample averaging on the power meter, if supported. + """ self._averages = count - self._meter.sense.average.count = self._averages - - + if self._meter is not None: + # The actual pymeasure property might differ; adjust as needed. + try: + self._meter.average_count = count + except AttributeError: + pass # Not implemented by the driver? + def __data_update(self): + """ + Called by the QTimer to emit new data for any logic or GUI module + connected to sigNewData. + """ with self._thread_lock: data = self.get_power() - self.sigNewData.emit(data) \ No newline at end of file + self.sigNewData.emit(data) diff --git a/src/qudi/logic/powermeter_logic.py b/src/qudi/logic/powermeter_logic.py new file mode 100644 index 0000000..28dc4b4 --- /dev/null +++ b/src/qudi/logic/powermeter_logic.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from PySide2 import QtCore +from qudi.core.module import LogicBase +from qudi.core.connector import Connector +from qudi.util.mutex import RecursiveMutex + +class PowermeterLogic(LogicBase): + """ + Logic module for displaying the Thorlabs power meter readings. + It connects to the hardware module, listens for new data, and re-emits + a dedicated signal to any GUI module. + """ + + # Connector to the hardware module that implements SimplePowermeterInterface + _powermeter = Connector(name='powermeter', interface='SimplePowermeterInterface') + + # Signal that the GUI can connect to for new power readings + sigPowerUpdated = QtCore.Signal(float) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._thread_lock = RecursiveMutex() + + def on_activate(self): + """ Called when this logic module is activated by Qudi. """ + # Connect hardware's new-data signal to our internal slot + self._powermeter().sigNewData.connect(self._on_new_data, QtCore.Qt.QueuedConnection) + + def on_deactivate(self): + """ Called when this logic module is deactivated. """ + # Safely disconnect signals + self._powermeter().sigNewData.disconnect(self._on_new_data) + + @QtCore.Slot(float) + def _on_new_data(self, power: float): + """ Internal slot that receives new power readings from the hardware. """ + with self._thread_lock: + # Simply re-emit the data for the GUI + self.sigPowerUpdated.emit(power)