Source code for QInstrument.instruments.Novanta.Opus.instrument

import logging
from QInstrument.lib.QSerialInstrument import QSerialInstrument


logger = logging.getLogger(__name__)


[docs] class QOpus(QSerialInstrument): '''Laser Quantum Opus Continuous-wave Laser The Opus command interface uses plain text: - Queries end in ``?`` and return a value with a units suffix (e.g. ``POWER?`` → ``'0123.4mW'``). - Setpoint commands use ``CMD=value`` syntax (e.g. ``POWER=100.0``). - Boolean commands are sent as plain mnemonics (``ON``, ``OFF``). Properties ========== Control ------- power : float [mW] Getter returns the actual output power. Setter transmits a new power setpoint, clamped to ``maximum_power``. maximum_power : float [mW] Software upper bound on the power setpoint. Default: 1000. Not discoverable from the hardware; set at startup and persisted via the saved configuration. wavelength : float [nm] Emission wavelength of the specific laser unit. Default: 532. Not discoverable from the hardware; set at startup and persisted via the saved configuration. current : float [%] Diode current as a percentage of maximum. emission : bool True: laser emission enabled. False: emission disabled. Status (read-only) ------------------ status : bool True if the laser is ENABLED (interlock satisfied and ready to emit). False if DISABLED (keyswitch off or enable button not pressed); a warning is logged when DISABLED is received. laser_temperature : float [°C] Laser head temperature. psu_temperature : float [°C] Power supply unit temperature. ''' comm = dict(baudRate=QSerialInstrument.BaudRate.Baud19200, dataBits=QSerialInstrument.DataBits.Data8, stopBits=QSerialInstrument.StopBits.OneStop, parity=QSerialInstrument.Parity.NoParity, flowControl=QSerialInstrument.FlowControl.NoFlowControl, eol='\r\n', timeout=500) def _registerProperties(self) -> None: '''Register all instrument properties via ``registerProperty()``. Called once from ``__init__``. Subclasses that extend the property set should call ``super()._registerProperties()`` first. ``maximum_power`` and ``wavelength`` are not discoverable from the hardware at runtime. They are initialised to defaults here and restored from the saved configuration on the first widget show. ''' self._maximum_power = getattr(type(self), 'MAXIMUM_POWER', 1000.) self._wavelength = 532. self._power: float = 0. register = self.registerProperty register('power', ptype=float, getter=self._getPower, setter=self._setPower) register('maximum_power', ptype=float, getter=lambda: self._maximum_power, setter=self._setMaximumPower, minimum=0.) register('wavelength', ptype=float, getter=lambda: self._wavelength, setter=lambda v: setattr(self, '_wavelength', float(v))) register('current', ptype=float, getter=self._getCurrent, setter=lambda v: self.handshake(f'CURRENT={float(v)}')) register('emission', ptype=bool, getter=lambda: self._power > 0, setter=self._setEmission) register('status', ptype=bool, setter=None, getter=self._getStatus) register('version', ptype=str, setter=None, getter=self.version) register('laser_temperature', ptype=float, setter=None, getter=lambda: self._parseTemp('LASTEMP?')) register('psu_temperature', ptype=float, setter=None, getter=lambda: self._parseTemp('PSUTEMP?'))
[docs] def identify(self) -> bool: '''Return True if the connected device identifies as an Opus laser. Queries the firmware version and checks for the ``'MPC-D'`` controller model token in the response. On success, sends ``CONTROL=POWER`` to ensure the controller regulates output power rather than diode current for all subsequent setpoint commands. ''' if 'MPC-D' not in self.version(): return False self.handshake('CONTROL=POWER') return True
def _getPower(self) -> float: '''Query and return the actual output power [mW]. The instrument responds with a value and ``mW`` suffix (e.g. ``'0123.4mW'``). Caches the result in ``_power`` so the ``emission`` getter can read it without a second query. ''' response = self.handshake('POWER?') if response: self._power = float(response.rstrip('mW')) return self._power def _getCurrent(self) -> float: '''Query and return the diode current [%]. The instrument responds with a value and ``%`` suffix (e.g. ``'050.0%'``). ''' response = self.handshake('CURRENT?') return float(response.rstrip('%')) if response else 0. def _parseTemp(self, cmd: str) -> float: '''Query a temperature command and return the value [°C]. Strips trailing unit characters (``C``) before conversion. Parameters ---------- cmd : str Temperature query mnemonic (``'LASTEMP?'`` or ``'PSUTEMP?'``). ''' response = self.handshake(cmd) return float(response.rstrip(' C')) if response else 0. def _getStatus(self) -> bool: '''Query and return the laser enable status. Returns ------- bool True if the laser responds ``ENABLED``. False if the laser responds ``DISABLED``; a warning is logged in that case because the condition requires user intervention (keyswitch or enable button). ''' response = self.handshake('STATUS?') if response == 'DISABLED': logger.warning( 'Opus laser is DISABLED: ' 'check keyswitch and front-panel enable button') return False return response == 'ENABLED' def _setPower(self, v: float) -> None: '''Transmit a new power setpoint, clamped to ``maximum_power``. Parameters ---------- v : float Requested output power [mW]. ''' limit = self._maximum_power clamped = min(float(v), limit) if clamped < float(v): logger.warning( f'power {v:.1f} mW exceeds maximum_power ' f'{limit:.1f} mW; clamped to {clamped:.1f} mW') self.handshake(f'POWER={clamped}') def _setMaximumPower(self, v: float) -> None: '''Set the software upper bound on the power setpoint. Values at or below zero are rejected and a warning is logged. Parameters ---------- v : float New maximum output power [mW]. ''' v = float(v) if v <= 0.: logger.warning( f'maximum_power {v:.1f} mW must be positive; ignored') return self._maximum_power = v def _setEmission(self, state: bool) -> None: '''Enable or disable laser emission. Parameters ---------- state : bool True to enable (``ON``), False to disable (``OFF``). ''' self.handshake('ON' if bool(state) else 'OFF')
[docs] def version(self) -> str: '''Return the firmware version string.''' return self.handshake('VERSION?')
[docs] def timers(self) -> list[str]: '''Return laser and PSU on-time readings. Sends ``TIMERS?`` and collects response lines until a line not containing ``'Hours'`` is received. The terminating line is discarded. Returns ------- list[str] Lines containing timer readings (e.g. ``'Laser head: 12345 Hours'``). ''' self.transmit('TIMERS?') lines = [] while True: line = self.receive() if 'Hours' not in line: break lines.append(line) return lines
if __name__ == '__main__': QOpus.example() __all__ = ['QOpus']