Source code for photons.equipment.laser_superk

"""
SuperK Fianium laser from NKT Photonics.
"""
from binascii import hexlify
from ctypes import c_ubyte
from enum import IntEnum
from math import nan
from time import sleep

from msl.equipment import EquipmentRecord
from msl.equipment.resources import NKT
from msl.qt import QtCore
from msl.qt import Signal

from .base import BaseEquipment
from .base import equipment
from ..log import logger


[docs] class ID60(IntEnum): """Register IDs for "SK Fianium" (Module type 0x0060).""" INLET_TEMPERATURE = 0x11 EMISSION = 0x30 MODE = 0x31 INTERLOCK = 0x32 PULSE_PICKER_RATIO = 0x34 WATCHDOG_INTERVAL = 0x36 POWER_LEVEL = 0x37 CURRENT_LEVEL = 0x38 NIM_DELAY = 0x39 SERIAL_NUMBER = 0x65 STATUS_BITS = 0x66 SYSTEM_TYPE = 0x6B USER_TEXT = 0x6C
[docs] class ID88(IntEnum): """Register IDs for "SuperK G3 Mainboard" (Module type 0x0088).""" INLET_TEMPERATURE = 0x11 EMISSION = 0x30 MODE = 0x31 INTERLOCK = 0x32 DATETIME = 0x33 PULSE_PICKER_RATIO = 0x34 WATCHDOG_INTERVAL = 0x36 CURRENT_LEVEL = 0x37 PULSE_PICKER_NIM_DELAY = 0x39 MAINBOARD_NIM_DELAY = 0x3A USER_CONFIG = 0x3B MAX_PULSE_PICKER_RATIO = 0x3D STATUS_BITS = 0x66 ERROR_CODE = 0x67 USER_TEXT = 0x8D
[docs] class ID61(IntEnum): """Register IDs for "SuperK Front panel" (Module type 0x61).""" PANEL_LOCK = 0x3D DISPLAY_TEXT = 0x72 ERROR_FLASH = 0x8D
[docs] class ID89(IntEnum): """Register IDs for "SuperK G3 Front Panel" (Module type 0x0089). According to the NKT engineers, there are no front-panel registers available. This means that the PANEL_LOCK, DISPLAY_TEXT and ERROR_FLASH do not exist. """
[docs] class OperatingModes(IntEnum): """The operating modes for a SuperK Fianium laser.""" CONSTANT_CURRENT = 0 CONSTANT_POWER = 1 MODULATED_CURRENT = 2 MODULATED_POWER = 3 POWER_LOCK = 4
[docs] @equipment(manufacturer=r'^NKT', model=r'F?S473') class SuperK(BaseEquipment): _callbacks_registered = False connection: NKT OperatingModes = OperatingModes DEVICE_ID = 0x0F FRONT_PANEL_ID = 0x01 MODULE_TYPE_0x60 = 0x60 MODULE_TYPE_0x88 = 0x88 # the DeviceStatusCallback is not reliable when changing the level manually # on the front panel, this is one reason why the front panel gets locked # when communication is established level_changed: QtCore.SignalInstance = Signal(float) # level value emission_changed: QtCore.SignalInstance = Signal(bool) # on/off state mode_changed: QtCore.SignalInstance = Signal(int) # the mode value def __init__(self, record: EquipmentRecord, **kwargs) -> None: """SuperK Fianium laser from NKT Photonics. Args: record: The equipment record. **kwargs: Keyword arguments. Can be specified as attributes of an XML element in a configuration file (with the tag of the element equal to the alias of `record`). """ super().__init__(record, **kwargs) # suppress the warning that the following attributes cannot be made # available when starting the BaseEquipment as a Service self.ignore_attributes( 'level_changed', 'emission_changed', 'mode_changed', 'signaler', 'DEVICE_ID', 'MODULE_TYPE_0x60', 'MODULE_TYPE_0x88') serial = self.connection.device_get_module_serial_number_str(SuperK.DEVICE_ID) if serial and serial != record.serial: self.raise_exception(f'SuperK serial number mismatch, ' f'{serial} != {record.serial}') # different SuperK's have different mainboard registry values self.MODULE_TYPE = self.connection.device_get_type(SuperK.DEVICE_ID) if self.MODULE_TYPE == SuperK.MODULE_TYPE_0x60: self.ID = ID60 self.MODES = { 'Constant current': OperatingModes.CONSTANT_CURRENT, 'Current modulation': OperatingModes.MODULATED_CURRENT, 'Power lock': OperatingModes.POWER_LOCK, } elif self.MODULE_TYPE == SuperK.MODULE_TYPE_0x88: self.ID = ID88 self.MODES = { 'Constant current': OperatingModes.CONSTANT_CURRENT, 'Power lock': OperatingModes.POWER_LOCK, } else: self.raise_exception(f'Unsupported module type 0x{self.MODULE_TYPE:x}') status = self.connection.get_port_status() if status != NKT.PortStatusTypes.PortReady: self.raise_exception(f'{self.alias!r} port status is {status!r}') self.ensure_interlock_ok() if record.connection.properties.get('lock_front_panel', False) or \ kwargs.get('lock_front_panel', False): self.lock_front_panel(True) if not SuperK._callbacks_registered: self.signaler = register_callbacks(self) SuperK._callbacks_registered = True self.set_user_text(kwargs.get('user_text', f'SuperK {record.serial}'))
[docs] def emission(self, enable: bool) -> None: """Turn the laser emission on or off. Args: enable: Whether to turn the laser emission on or off. """ state, text = (3, 'on') if enable else (0, 'off') self.logger.info(f'turn {self.alias!r} emission {text}') try: self.connection.register_write_u8(SuperK.DEVICE_ID, self.ID.EMISSION, state) except OSError as e: error = str(e) else: self.emission_changed.emit(enable) self.maybe_emit_notification(emission=enable) return self.raise_exception(f'Cannot turn the {self.alias!r} emission {text}\n{error}')
[docs] def enable_constant_current_mode(self) -> None: """Set the laser to be in constant current mode.""" self.set_operating_mode(OperatingModes.CONSTANT_CURRENT)
[docs] def enable_constant_power_mode(self) -> None: """Set the laser to be in constant power mode.""" self.set_operating_mode(OperatingModes.CONSTANT_POWER)
[docs] def enable_modulated_current_mode(self) -> None: """Set the laser to be in modulated current mode.""" self.set_operating_mode(OperatingModes.MODULATED_CURRENT)
[docs] def enable_modulated_power_mode(self) -> None: """Set the laser to be in modulated power mode.""" self.set_operating_mode(OperatingModes.MODULATED_POWER)
[docs] def enable_power_lock_mode(self) -> None: """Set the laser to be power lock (external feedback) mode.""" self.set_operating_mode(OperatingModes.POWER_LOCK)
[docs] def ensure_interlock_ok(self) -> bool: """Make sure that the interlock is okay. Raises an exception if it is not okay, and it cannot be reset. """ status = self.connection.register_read_u16(SuperK.DEVICE_ID, self.ID.INTERLOCK) if status == 2: self.logger.info(f'{self.alias!r} interlock is okay') return True if status == 1: # then requires an interlock reset self.logger.info(f'resetting the {self.alias!r} interlock... ') status = self.connection.register_write_read_u16(SuperK.DEVICE_ID, self.ID.INTERLOCK, 1) if status == 2: self.logger.info(f'{self.alias!r} interlock is okay') return True self.raise_exception( f'Invalid {self.alias!r} interlock status code {status}. ' f'Is the key in the off position?' )
[docs] def get_current_level(self) -> float: """Returns the constant/modulated current level of the laser.""" # the documentation indicates that there is a scaling factor of 0.1 return self.connection.register_read_u16(SuperK.DEVICE_ID, self.ID.CURRENT_LEVEL) * 0.1
[docs] def get_feedback_level(self) -> float: """Get the power lock (external feedback) level of the laser.""" return self.get_current_level()
[docs] def get_operating_mode(self) -> OperatingModes: """Returns the operating mode of the laser.""" if self.MODULE_TYPE == SuperK.MODULE_TYPE_0x60: read = self.connection.register_read_u16 else: read = self.connection.register_read_u8 return OperatingModes(read(SuperK.DEVICE_ID, self.ID.MODE))
[docs] def get_operating_modes(self) -> dict[str, OperatingModes]: """Get all supported operating modes of the laser.""" return self.MODES
[docs] def get_power_level(self) -> float: """Returns the constant/modulated power level of the laser.""" if self.MODULE_TYPE == SuperK.MODULE_TYPE_0x88: self.logger.warning(f'the {self.alias!r} does not ' f'support power-level mode') return nan # the documentation indicates that there is a scaling factor of 0.1 return 0.1 * self.connection.register_read_u16( SuperK.DEVICE_ID, self.ID.POWER_LEVEL # noqa: Unresolved attribute reference 'POWER_LEVEL' for class 'ID88' )
[docs] def get_temperature(self) -> float: """Returns the temperature of the laser.""" # the documentation indicates that there is a scaling factor of 0.1 return 0.1 * self.connection.register_read_s16( SuperK.DEVICE_ID, self.ID.INLET_TEMPERATURE)
[docs] def get_user_text(self) -> str: """Returns the custom user-text value.""" return self.connection.register_read_ascii(SuperK.DEVICE_ID, self.ID.USER_TEXT)
[docs] def is_constant_current_mode(self) -> bool: """Whether the laser in constant current mode.""" return self.get_operating_mode() == OperatingModes.CONSTANT_CURRENT
[docs] def is_constant_power_mode(self) -> bool: """Whether the laser in constant power mode.""" return self.get_operating_mode() == OperatingModes.CONSTANT_POWER
[docs] def is_emission_on(self) -> bool: """Check if the laser emission is on or off.""" return bool(self.connection.register_read_u8(SuperK.DEVICE_ID, self.ID.EMISSION))
[docs] def is_modulated_current_mode(self) -> bool: """Whether the laser in modulated current mode.""" return self.get_operating_mode() == OperatingModes.MODULATED_CURRENT
[docs] def is_modulated_power_mode(self) -> bool: """Whether the laser in modulated power mode.""" return self.get_operating_mode() == OperatingModes.MODULATED_POWER
[docs] def is_power_lock_mode(self) -> bool: """Whether the laser in power lock (external feedback) mode.""" return self.get_operating_mode() == OperatingModes.POWER_LOCK
[docs] def lock_front_panel(self, lock: bool) -> bool: """Lock the front panel so that the level cannot be changed manually. Args: lock: Whether to lock (True) or unlock (False) the front panel. Returns: Whether the request to (un)lock the front panel was successful. A laser with a module type 0x88 does not permit the front panel to be (un)locked and therefore this method will always return False for this laser. """ text = 'lock' if lock else 'unlock' if self.MODULE_TYPE == SuperK.MODULE_TYPE_0x88: self.logger.warning(f'the {self.alias!r} does not support {text}ing the front panel') return False try: self.connection.register_write_u8(SuperK.FRONT_PANEL_ID, ID61.PANEL_LOCK, int(lock)) except OSError as e: self.logger.error(f'Cannot {text} the front panel of the {self.alias!r}, ' f'{e.__class__.__name__}: {e}') return False self.maybe_emit_notification(locked=bool(lock)) self.logger.info(f'{text}ed the front panel of the {self.alias!r}') return True
[docs] def set_current_level(self, percentage: float) -> float: """Set the constant/modulated current level of the laser. Args: percentage: The current level as a percentage 0 - 100 (resolution 0.1). Returns: The actual current level that the laser is at. """ self.logger.info(f'set {self.alias!r} current level to {percentage}%') return self._set_current_level(percentage)
[docs] def set_feedback_level(self, percentage: float) -> float: """Set the power-lock (external feedback) level of the laser. Args: percentage: The power-lock level as a percentage 0 - 100 (resolution 0.1). Returns: The power-lock level that the laser is at. """ self.logger.info(f'set {self.alias!r} power-lock level to {percentage}%') return self._set_current_level(percentage)
[docs] def set_operating_mode(self, mode: int | str | OperatingModes) -> None: """Set the operating mode of the laser. Args: mode: The operating mode. Can be an :class:`OperatingModes` value or member name. """ m = self.convert_to_enum(mode, OperatingModes, to_upper=True) self.emission(False) if self.connection.register_write_read_u16(SuperK.DEVICE_ID, self.ID.MODE, m) != m: self.raise_exception(f'Cannot set {self.alias!r} to {m!r}') self.mode_changed.emit(m) self.maybe_emit_notification(mode=m) self.logger.info(f'set {self.alias!r} to {m!r}') # the value of the level can change when the mode changes # it can take some time for the get_*_level() function to return the correct value sleep(0.2) if m == OperatingModes.CONSTANT_POWER or m == OperatingModes.MODULATED_POWER: level = self.get_power_level() else: level = self.get_current_level() # valid for POWER_LOCK mode as well self.level_changed.emit(level) self.maybe_emit_notification(level=level)
[docs] def set_power_level(self, percentage: float) -> float: """Set the constant/modulated power level of the laser. Args: percentage: The power level as a percentage 0 - 100 (resolution 0.1). Returns: The actual power level that the laser is at. """ if percentage < 0 or percentage > 100: self.raise_exception( f'Invalid {self.alias!r} power level of {percentage}. ' f'Must be in range [0, 100].' ) if self.MODULE_TYPE == SuperK.MODULE_TYPE_0x88: self.logger.error(f'the {self.alias!r} does not support power-level mode') return nan # the documentation indicates that there is a scaling factor of 0.1 self.logger.info(f'set {self.alias!r} power level to {percentage}%') val = self.connection.register_write_read_u16( SuperK.DEVICE_ID, self.ID.POWER_LEVEL, # noqa: Unresolved attribute reference 'POWER_LEVEL' for class 'ID88' int(percentage * 10) ) actual = float(val) * 0.1 self.level_changed.emit(actual) self.maybe_emit_notification(level=actual) return actual
[docs] def set_user_text(self, text: str) -> str: """Set the custom user-text value. Args: text: The text to write to the laser's firmware. Only ASCII characters are allowed. The maximum number of characters is 20 for the laser with module type 0x60 and 240 characters for module type 0x88. The laser with module type 0x60 can display the text on the front panel (if selected from the menu option). Returns: The text that was actually stored in the laser's firmware. """ if not text and self.MODULE_TYPE == SuperK.MODULE_TYPE_0x88: # module type 0x88 requires at least 1 character to be written text = ' ' self.logger.info(f'set the {self.alias!r} front-panel text to {text!r}') return self.connection.register_write_read_ascii(SuperK.DEVICE_ID, self.ID.USER_TEXT, text, False)
[docs] def disconnect_equipment(self): """Unlock the front panel, set the user text to an empty string and close the port.""" self.lock_front_panel(False) self.set_user_text('') super().disconnect_equipment()
def _set_current_level(self, percentage: float) -> float: if percentage < 0 or percentage > 100: self.raise_exception( f'Invalid {self.alias!r} current level of {percentage}. ' f'Must be in the range [0, 100].' ) # the documentation indicates that there is a scaling factor of 0.1 val = self.connection.register_write_read_u16(SuperK.DEVICE_ID, self.ID.CURRENT_LEVEL, int(percentage * 10)) actual = float(val) * 0.1 self.level_changed.emit(actual) self.maybe_emit_notification(level=actual) return actual
[docs] class Signaler(QtCore.QObject): """Qt Signaler for callbacks that are received from the DLL.""" # {'port': bytes, 'dev_id': int, 'status': int, 'data': bytes} device_status_changed: QtCore.SignalInstance = Signal(dict) # {'port': bytes, 'dev_id': int, 'reg_id': int, # 'reg_status': int, 'reg_type': int, 'data': bytes} register_status_changed: QtCore.SignalInstance = Signal(dict) # {'port': bytes, 'status': int, 'cur_scan': int, 'max_scan': int, 'device': int} port_status_changed: QtCore.SignalInstance = Signal(dict) def __init__(self, device: SuperK) -> None: super().__init__() self.device = device
[docs] def maybe_emit_notification(self, *args, **kwargs) -> None: """Notify all linked Clients.""" self.device.maybe_emit_notification(*args, **kwargs)
[docs] def register_callbacks(superk: SuperK) -> Signaler: """Register the callbacks from the DLL.""" def get_data(length: int, address: int) -> bytes: try: return bytes((c_ubyte * length).from_address(address)) # noqa: Array[c_ubyte] is iterable except ValueError: return b'' @NKT.DeviceStatusCallback def device_status_callback(port: bytes, dev_id: int, status: int, length: int, address: int) -> None: d = { 'port': port.decode(), 'dev_id': dev_id, 'status': NKT.DeviceStatusTypes(status), 'data': int(hexlify(get_data(length, address))) } logger.debug('SuperK device_status_callback: %s', d) signaler.device_status_changed.emit(d) signaler.maybe_emit_notification(**d) @NKT.RegisterStatusCallback def register_status_callback(port: bytes, dev_id: int, reg_id: int, reg_status: int, reg_type: int, length: int, address: int) -> None: d = { 'port': port.decode(), 'dev_id': dev_id, 'reg_id': reg_id, 'reg_status': NKT.RegisterStatusTypes(reg_status), 'reg_type': NKT.RegisterDataTypes(reg_type), 'data': get_data(length, address) } logger.debug('SuperK register_status_callback: %s', d) signaler.register_status_changed.emit(d) signaler.maybe_emit_notification(**d) @NKT.PortStatusCallback def port_status_callback(port: bytes, status: int, cur_scan: int, max_scan: int, device: int) -> None: d = { 'port': port.decode(), 'status': NKT.PortStatusTypes(status), 'cur_scan': cur_scan, 'max_scan': max_scan, 'device': device } logger.debug('SuperK port_status_callback: %s', d) signaler.port_status_changed.emit(d) signaler.maybe_emit_notification(**d) signaler = Signaler(superk) signaler.device_status_callback = device_status_callback signaler.register_status_callback = register_status_callback signaler.port_status_callback = port_status_callback superk.connection.device_create(SuperK.FRONT_PANEL_ID, True) # register the callbacks NKT.set_callback_device_status(signaler.device_status_callback) NKT.set_callback_register_status(signaler.register_status_callback) NKT.set_callback_port_status(signaler.port_status_callback) return signaler