Source code for pyrolab.drivers.lasers.ppcl55x

# Copyright © PyroLab Project Contributors
# Licensed under the terms of the GNU GPLv3+ License
# (see pyrolab/__init__.py for details)

"""
Pure Photonics Tunable Laser
============================

Driver for Pure Photonic tunable lasers.

.. note::

   Pure Photonic offers GUI controllers, a command line interface, and
   DLL/driver downloads on their `website
   <https://www.pure-photonics.com/downloads1>`_. These are not required by
   PyroLab, however I've found that installing the VCP drivers from
   https://ftdichip.com/drivers/vcp-drivers/ make the driver more consistent.

.. admonition:: Dependencies
   :class: note

   pyserial
"""

import array
import logging
import threading
import time
from typing import List

import serial
import numpy as np
from scipy.constants import speed_of_light as C_SPEED

from pyrolab.drivers.lasers import Laser
from pyrolab.api import expose, behavior


log = logging.getLogger(__name__)


ITLA_NOERROR = 0x00
ITLA_EXERROR = 0x01
ITLA_AEERROR = 0x02
ITLA_CPERROR = 0x03
ITLA_NRERROR = 0x04
ITLA_CSERROR = 0x05
ITLA_ERROR_SERPORT = 0x01
ITLA_ERROR_SERBAUD = 0x02

REG_Nop = 0x00
REG_Mfgr = 0x02
REG_Model = 0x03
REG_Serial = 0x04
REG_Release = 0x06
REG_Gencfg = 0x08
REG_AeaEar = 0x0B
REG_Iocap = 0x0D
REG_Ear = 0x10
REG_Dlconfig = 0x14
REG_Dlstatus = 0x15
REG_Channel = 0x30
REG_Power = 0x31
REG_Resena = 0x32
REG_Grid = 0x34
REG_Fcf1 = 0x35
REG_Fcf2 = 0x36
REG_Lfo1 = 0x40
REG_Lfo2 = 0x41
REG_Oop = 0x42
REG_Opsl = 0x50
REG_Opsh = 0x51
REG_Lfl1 = 0x52
REG_Lfl2 = 0x53
REG_Lfh1 = 0x54
REG_Lfh2 = 0x55
REG_Currents = 0x57
REG_Temps = 0x58
REG_Ftf = 0x62
REG_Mode = 0x90
REG_PW = 0xE0
REG_Csweepsena = 0xE5
REG_Csweepamp = 0xE4
REG_Cscanamp = 0xE4
REG_Cscanon = 0xE5
REG_Csweepon = 0xE5
REG_Csweepoffset = 0xE6
REG_Cscanoffset = 0xE6
REG_Cscansled = 0xF0
REG_Cscanf1 = 0xF1
REG_Cscanf2 = 0xF2
REG_CjumpTHz = 0xEA
REG_CjumpGHz = 0xEB
REG_CjumpSled = 0xEC
REG_Cjumpon = 0xED
REG_Cjumpoffset = 0xE6

WRITE_ONLY = 0
WRITE_READ = 1


[docs] @behavior(instance_mode="single") @expose class PPCL55xBase(Laser): """ Base class for a generic Pure Photonics Tunable Laser. Do not instantiate. The laser must already be physically powered and connected to a USB port of some host computer, whether that be a local machine or one hosted by a PyroLab server. Methods such as :py:func:`on` and :py:func:`off` will simply turn the laser diode on and off, not the laser itself. """ @property def MINIMUM_FREQUENCY(self): return C_SPEED / self.MAXIMUM_WAVELENGTH @property def MAXIMUM_FREQUENCY(self): return C_SPEED / self.MINIMUM_WAVELENGTH @property def MINIMUM_POWER_MW(self): return 10 ** (self.MINIMUM_POWER_DBM / 10) @property def MAXIMUM_POWER_MW(self): return 10 ** (self.MAXIMUM_POWER_DBM / 10)
[docs] def connect(self, port: str = "", baudrate: int = 9600) -> None: """ Connects to and initializes the laser. All parameters are keyword arguments. Parameters ---------- port : str COM port the laser is connected to (default "") baudrate : int baudrate of the serial connection (default 9600) """ log.debug("Entering connect()") self.latest_register = 0 self.queue = [] self.max_row_ticket = 0 if hasattr(self, "device") and self.device.is_open: log.debug("Already connected") return True try: self.device = serial.Serial( port, baudrate, timeout=1, parity=serial.PARITY_NONE ) except serial.SerialException as e: raise ConnectionError( f"Could not connect to laser on port {port} with baudrate {baudrate}." )
[docs] def close(self) -> None: """ Disconnect from the laser """ self.device.close()
[docs] def power_mW(self, power: float = None) -> float: """ Set the power on the laser. Parameters ---------- power : float Power that the laser will be set to in mW Returns ------- int Integer representing error message, 0 if no error. """ if power is not None: if power < self.MINIMUM_POWER_MW or power > self.MAXIMUM_POWER_MW: raise ValueError( "Inputed power not in acceptable range " + str(self.MINIMUM_POWER_MW) + " to " + str(self.MAXIMUM_POWER_MW) ) sendPower = int(np.log10(power) * 1000) # scale the power inputed # on the REG_Power register, send the power self._communicate(REG_Power, sendPower, 1) else: message = self._communicate(REG_Oop, 0, 0) power = ( np.round( np.power(10.0, (256.0 * message[2] + message[3]) / 1000.0) * 100.0 ) / 100.0 ) if power > self.MAXIMUM_POWER_MW * 1.1: power = 0 return power
[docs] def power_dBm(self, power: float = None) -> float: """ Set the power on the laser. Parameters ---------- power : float Power that the laser will be set to in dBm Returns ------- int Integer representing error message, 0 if no error. """ if power is not None: if power < self.MINIMUM_POWER_DBM or power > self.MAXIMUM_POWER_DBM: raise ValueError( "Inputed power not in acceptable range " + str(self.MINIMUM_POWER_DBM) + " to " + str(self.MAXIMUM_POWER_DBM) ) sendPower = int(power * 100) # scale the power inputed # on the REG_Power register, send the power self._communicate(REG_Power, sendPower, 1) else: message = self._communicate(REG_Oop, 0, 0) power = (256.0 * message[2] + message[3]) / 100.0 if power > self.MAXIMUM_POWER_DBM * 1.1: power = 0 return power
[docs] def set_channel(self, channel: int) -> int: """ Set the channel (should always be 1) Parameters ---------- channel : int channel that the laser is on Returns ------- int Integer representing error message, 0 if no error. """ # on the REG_Channel register, send the channel response = self._communicate(REG_Channel, channel, 1) return response
[docs] def set_mode(self, mode: int) -> int: """ Set the mode of operation for the laser Parameters ---------- mode : int | The mode corresponds to the following modes of the laser: | 0: Regular mode | 1: No dither mode | 2: Clean mode Returns ------- int Integer representing error message, 0 if no error. """ # on the REG_Mode register, send the mode response = self._communicate(REG_Mode, mode, 1) return response
[docs] def frequency(self, frequency: float = None) -> float: """ Set the frequncy of the laser. .. important:: Laser must be off in order to set the frequency. Therefore, if the laser is not off this function will turn it off, set the frequency, then turn it back on. Parameters ---------- frequency : float Frequency of the laser to be set in THz Returns ------- int Integer representing error message, 0 if no error. """ if frequency is not None: # if the wavelength is not in the allowed range if frequency < self.MINIMUM_FREQUENCY or frequency > self.MAXIMUM_FREQUENCY: raise ValueError( "Inputed frequency not in acceptable range " + str(self.MINIMUM_FREQUENCY) + " to " + str(self.MAXIMUM_FREQUENCY) ) frequency = np.round(frequency * 10.0) / 10.0 freq_t = int(frequency / 1000) # convert the wavelength to frequency for each register freq_g = int(frequency * 10) - freq_t * 10000 if self.power_dBm() > 0: # if the laser is currently on self.off() self._communicate(REG_Fcf1, freq_t, 1) self._communicate(REG_Fcf2, freq_g, 1) while self.frequency() != frequency: time.sleep(0.01) self.on() else: self._communicate(REG_Fcf1, freq_t, 1) self._communicate(REG_Fcf2, freq_g, 1) else: t_message = self._communicate(REG_Lfo1, 0, 0) g_message = self._communicate(REG_Lfo2, 0, 0) freq_t = 256.0 * t_message[2] + t_message[3] freq_g = 256.0 * g_message[2] + g_message[3] frequency = freq_t * 1000.0 + freq_g / 10.0 return frequency
[docs] def wavelength(self, wavelength: float = None) -> float: """ Set the wavelength of the laser. .. important:: Laser must be off in order to set the wavelength. Therefore, if the laser is not off this function will turn it off, set the wavelength, then turn it back on. Parameters ---------- wavelength : float Wavelength of the laser to be set in nm Returns ------- int Integer representing error message, 0 if no error. """ if wavelength is not None: # if the wavelength is not in the allowed range if ( wavelength < self.MINIMUM_WAVELENGTH or wavelength > self.MAXIMUM_WAVELENGTH ): raise ValueError( "Inputed wavelength not in acceptable range " + str(self.MINIMUM_WAVELENGTH) + " to " + str(self.MAXIMUM_WAVELENGTH) ) frequency = np.round(C_SPEED / wavelength * 10.0) / 10.0 freq_t = int(frequency / 1000) # convert the wavelength to frequency for each register freq_g = int(frequency * 10) - freq_t * 10000 if self.power_dBm() > 0: # if the laser is currently on self.off() self._communicate(REG_Fcf1, freq_t, 1) self._communicate(REG_Fcf2, freq_g, 1) while self.frequency() != frequency: time.sleep(0.01) self.on() else: self._communicate(REG_Fcf1, freq_t, 1) self._communicate(REG_Fcf2, freq_g, 1) else: t_message = self._communicate(REG_Lfo1, 0, 0) g_message = self._communicate(REG_Lfo2, 0, 0) freq_t = 256.0 * t_message[2] + t_message[3] freq_g = 256.0 * g_message[2] + g_message[3] frequency = freq_t * 1000.0 + freq_g / 10.0 wavelength = np.round(C_SPEED / frequency * 1000.0) / 1000.0 return wavelength
[docs] def on(self) -> int: """ Turn on the laser Returns ------- int Integer representing error message, 0 if no error. """ # start communication by sending 8 to REG_Resena register response = self._communicate(REG_Resena, 8, 1) for i in range(10): # send 0 to REG_Nop to allow time for laser diode to turn on response = self._communicate(REG_Nop, 0, 0) self.is_on = True return response
[docs] def off(self) -> int: """ Turn off the laser Returns ------- int Integer representing error message, 0 if no error. """ # stop communication by sending 0 to REG_Resena register response = self._communicate(REG_Resena, 0, 1) for i in range(10): # send 0 to REG_Nop to allow time for laser diode to turn on response = self._communicate(REG_Nop, 0, 0) self.is_on = False return response
def _communicate(self, register: int, data: int, write_read: int) -> int: """ Function that implements the commmunication with the laser. It will first send a message then receive a response if wanted. Parameters ---------- register : int Register to which will be written to on the laser data : int User-specific data that will be sent to the laser write_read : int | Defines if the communication is only write or write-read | 0: Write only | 1: Write then read Returns ------- int Integer representing error message, 0 if no error. """ lock = threading.Lock() lock.acquire() row_ticket = self.max_row_ticket + 1 self.max_row_ticket = self.max_row_ticket + 1 self.queue.append(row_ticket) lock.release() while self.queue[0] != row_ticket: row_ticket = row_ticket data_byte_0 = int(data / 256) data_byte_1 = int(data - data_byte_0 * 256) self.latest_register = register # modify bytes for sending message = [write_read, register, data_byte_0, data_byte_1] self._send(message) # send the message received_message = self._receive() # receive the response from the laser lock.acquire() self.queue.pop(0) lock.release() # error_message = int(received_message[0] & 0x03) return received_message """ Function sends message of four bytes to the laser. """ def _send(self, message: List[int]) -> None: """ Sends message of four bytes to the laser after calculating the checksum Parameters ---------- message : List[int] Message that will be sent to the laser """ message[0] = ( message[0] | int(self._checksum(message)) * 16 ) # calculate checksum log.debug(f"sending message: {message}") self.device.flush() # construct the bytes from the inputed message send_bytes = array.array("B", message).tobytes() self.device.write(send_bytes) # write the bytes on the serial connection def _receive(self) -> List[int]: """ Receives and verifies message from the laser with checksum Returns ------- List[int] Bytes of message received. Raises ------ PyroLabException.CommunicationException """ reference_time = time.time() while self.device.inWaiting() < 4: # wait until 4 bytes are received if ( time.time() > reference_time + 0.5 ): # if it takes longer than 0.5 seconds break return (0xFF, 0xFF, 0xFF, 0xFF) try: num = self.device.inWaiting() # get number of bytes for debugging purposes message = [] bytes_read = self.device.read(num) # read the four bytes from serial for b in bytes_read: message.append(b) # construct array of bytes from the message message = message[0:4] except: raise CommunicationError("No response from laser") if self._checksum(message) == message[0] >> 4: # ensure the checksum is correct log.debug(f"message received: {message[2]} {message[3]}") return message # return the message received else: # if the checksum is wrong, log a CS error raise CommunicationError("Incorrect checksum returned") def _checksum(self, message: List[int]) -> int: # calculate checksum """ Calculate the checksum of a message Parameters ---------- message : List[int] Four-byte message that will be used to produce a checksum Returns ------- int Calculated checksum """ bip_8 = (message[0] & 0x0F) ^ message[1] ^ message[2] ^ message[3] bip_4 = ((bip_8 & 0xF0) >> 4) ^ (bip_8 & 0x0F) return bip_4
[docs] @expose class PPCL550(PPCL55xBase): """ Driver for a Pure Photonic PPCL550 series laser. The laser must already be physically powered and connected to a USB port of some host computer, whether that be a local machine or one hosted by a PyroLab server. Methods such as :py:func:`on` and :py:func:`off` will simply turn the laser diode on and off, not the laser itself. Attributes ---------- MINIMUM_WAVELENGTH : float The minimum wavelength of the laser in nanometers (value 1515). MAXIMUM_WAVELENGTH : float The maximum wavelength of the laser in nanometers (value 1570). MINIMUM_POWER_DBM : float The minimum power of the laser in dBm (value 6). MAXIMUM_POWER_DBM : float The maximum power of the laser in dBm (value 13.5). Examples -------- .. code-block:: python import time from pyrolab.drivers.lasers.ppcl550 import PPCL550 # choose the correct COM port laser = PPCL55x(port="COM6") # set the power wavelength and channel laser.setPower(13.5) laser.setWavelength(1550) laser.setChannel(1) # turn the laser on laser.on() time.sleep(10) # adjust the power to 10 dBm laser.setPower(10) time.sleep(10) # change the wavelength to 1570 nm laser.setWavelength(1570) time.sleep(10) laser.off() laser.close() """ # The 550 laser has a range of 1515-1570 nm, power range of 6-13.5 dBm MINIMUM_WAVELENGTH = 1515 MAXIMUM_WAVELENGTH = 1570 MINIMUM_POWER_DBM = 6 MAXIMUM_POWER_DBM = 13.5
[docs] @expose class PPCL551(PPCL55xBase): """ Driver for a Pure Photonic PPCL551 series laser. The laser must already be physically powered and connected to a USB port of some host computer, whether that be a local machine or one hosted by a PyroLab server. Methods such as :py:func:`on` and :py:func:`off` will simply turn the laser diode on and off, not the laser itself. Attributes ---------- MINIMUM_WAVELENGTH : float The minimum wavelength of the laser in nanometers (value 1569). MAXIMUM_WAVELENGTH : float The maximum wavelength of the laser in nanometers (value 1625). MINIMUM_POWER_DBM : float The minimum power of the laser in dBm (value 6). MAXIMUM_POWER_DBM : float The maximum power of the laser in dBm (value 13.5). Examples -------- .. code-block:: python import time from pyrolab.drivers.lasers.ppcl550 import PPCL551 # choose the correct COM port laser = PPCL551(minWL=1569, maxWL=1625, port="COM5") # set the power wavelength and channel laser.setPower(13.5) laser.setWavelength(1570) laser.setChannel(1) # turn the laser on laser.on() time.sleep(10) # adjust the power to 10 dBm laser.setPower(10) time.sleep(10) # change the wavelength to 1600 nm laser.setWavelength(1600) time.sleep(10) laser.off() laser.close() """ # The 551 laser has a range of 1569-1625 nm, power range of 6-13.5 dBm MINIMUM_WAVELENGTH = 1569 MAXIMUM_WAVELENGTH = 1625 MINIMUM_POWER_DBM = 6 MAXIMUM_POWER_DBM = 13.5