Source code for FreiCtrl_laser.luxx_communication

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
This module uses **pyserial** module for communication with OMICRON LuxX
laser. The class **Laser** provides convenient methods for control of one
or several lasers, connected to the computer.

Example
-------
laser1 = Laser()
laser1.smart_ask("GSI")
laser1.smart_ask("GOM")
laser1.start()
laser1.set_power(24)
laser1.stop()
del laser1

Author
------
Roman Kiselev, January 2014
Artur Schneider, November 2023
- adopted to python3

License
-------
GNU GPL - use it for any purpose
"""


import serial
import sys
import re
from threading import Thread
from time import time, sleep
from enum import IntEnum
import logging

[docs] class Laser(): """ This class represents OMICRON LuxX laser. It opens the communication port upon object creation and asks the device for model and maximum power. Functions --------- * __init__(port="auto", baudrate=500000) constructor. Open port, get model name and power * __del__() destructor. Close the port upon completion * write(command) send command to device * read() read all data from port buffer * ask(command) send command and get only the relevant answer, as well as time * smart_ask(command) ask, which prints HEX answers in a table * print_HEX(HEXstring) print a nice table representing bits in a HEX number * start() start emission (takes 3 seconds) * stop() stop emission immediately * setPower(power_value) change the emitted power * getPower() ask for the current power (seting point, not actual emitted power) * setMode set an operating mode: standby, current control, power control or analog modulation. * getMode determine current operating mode * setAutostart if set, the laser will start emission on key turn event or on powerup. Otherwise, it can be started only from software with *LOn* command (**start** function). * getAutostart determine if autostart is active Laser control ------------- The device itself contains FTDI microchip, which emulates serial port via USB. In Linux it appears as **\dev\ttyUSB#** file, where **#** is a number. In Windows, it leads to appearance of an additional COM port (**COM##**). If the laser is connected via USB cable, the baudrate is 500000; it can also be connected via RS-232 interface with a special cable, then the baudrate should be set to 57600. The laser is controlled with short commands (register matters!). The full desciption of commands can be found in the file *PhoxX_LuxX_BrixX_command_list V1.0.pdf*. Here is a list of commands that are relevant to the LuxX model: * RsC Reset Controller * GFw Get Firmware * GSN Get Serial Number * GSI Get Spec Info * GMP Get Maximum Power * GWH Get Working Hours * ROM Recall Operating Mode (Standby, CW-ACC, CW-APC, Analog) * GOM Get Operating Mode (**ASCII HEX**) * SOM Set Operating Mode (**ASCII HEX**) * SAS Set Auto Start (Laser will emit after startup) * SAP Set Auto Powerup * LOn Laser On - start emission * LOf Laser Off * POn Power On * POf Power Off * GAS Get Actual Status * GFB Get Failure Byte (**ASCII HEX**) * GLF Get Last Failurebyte (**ASCII HEX**) * MDP Measure Diode Power * MTD Measure Temperature Diode * MTA Measure Temperature Ambient * CLD Calibrate Laser Diode * SLP Set Level Power - set the emitted power (**ASCII HEX up to 0xFFF**) * GLP Get Level Power (**ASCII HEX**) """ def __init__(self, port="auto", baudrate=500000): """ Open port (*auto* stands for **/dev/ttyUSB0** in Linux or **COM17** in Windows, because it is what I use); then get device model; finally, get maximum output power and store it in **pmax** variable. """ self.parameters = {} self.status = {} self.port = None try: if port == "auto": if sys.platform.find("win") >= 0: # for computer in the lab port = "COM17" elif sys.platform.find("linux") >= 0: port = "/dev/ttyUSB0" else: print(f"Sorry, unsupported operating system: '{sys.platform}'") port = None self.port = serial.Serial(port, baudrate, timeout=0.3) self.firmware = self.ask("GFw") if self.firmware.find("LuxX") < 0 & \ self.firmware.find("BrixX") < 0 & \ self.firmware.find("PhoxX") < 0: print("The LuxX | BrixX | PhoxX laser is not connected. " + \ "The received answer for '?GFw\\r' command is:\n" + \ self.firmware) raise serial.SerialException # From this point we know, that the right laser is connected self.wavelength = float(self.ask("GSI").split()[0]) self.serial = self.ask("GSN") self.hours = self.ask("GWH") self.pmax = float(self.ask("GMP")) self.laser_name = f"{self.firmware}_{self.serial}_{self.wavelength}" self.log = logging.getLogger(self.laser_name) #self.laser_name_mine = # give some more desriptive names self.get_parameters_mine() self.get_errors_mine() self.get_status_mine() except serial.SerialException: raise OSError('Port "%s" is unavailable.\n' % port + \ 'May be the laser is not connected, the wrong' + \ ' port is specified or the port is already opened') def __del__(self): """Close the port before exit.""" try: if self.port: self.port.close() print("Port closed") except serial.SerialException: print('could not close the port') def __repr__(self): return f"{self.laser_name}"
[docs] def write(self, command): """Send *command* to device. Preceed it with "?" und end with CR.""" #self.port.write("?" + command + "\r") self.port.write(f"?{command}\r".encode('utf-8'))
[docs] def read(self): """Read all information from the port and return it as string.""" answer = self.port.readall() try: answer = answer.replace(b"\xa7", b" | ").decode() except UnicodeDecodeError: print(f'got some weird unicode characters{answer}') return None return answer.replace("\r", "\n")
[docs] def ask(self, command): """Write, then read. However, return only the relevant info.""" self.write(command) try: response = self.read() except TypeError: # happens sometimes if serial was closed return if response == "" and command == "GFw": #happens if not a laser raise serial.SerialException if re.findall("!UK\n", response): print(f"Command '{command}' is unknown for this device") else: response = re.findall(f"!{command[:3]}(.+)\n", response)[-1] if response[0] == "x": print("Laser responded with error to command '%s'" % command) return response
[docs] def smart_ask(self, command): """ Several commands return information coded in ASCII HEX numbers. The relevant are bits in the registers. For convenient representation, we will print these bytes in tables. This is relevant for the following commands: * GOM * GFB * GLF * GLP For all other commands the behavior is identical to **ask** function """ if command in ["GOM", "GFB", "GLF", "GLP"]: return print_hex(self.ask(command)) else: return self.ask(command)
def turn_power_on(self): res = self.ask("POn") self.log.debug('power on') return res def turn_power_off(self): res = self.ask("POf") self.log.debug('power off') return res
[docs] def start(self): """Start the emission (takes about 3 seconds)""" #self.write("LOn") self.log.info('Laser on') return self.ask("LOn") """ > ok x error """
[docs] def stop(self): """Stop the emission immediately""" #self.write("LOf") r = self.ask("LOf") self.log.debug('Laser off') return r
[docs] def set_power(self, power): """Set the desired power in mW""" # Calculate the corresponding HEX code and transmit it if power > self.pmax: print(f"Laser provides %imW only. The maximum power is set {self.pmax} ") self.write("SLPFFF") else: code = hex(int(4095*power/self.pmax))[2:].upper().zfill(3) self.ask("SLP%s" % code)
[docs] def get_power(self): """Get the current power value in mW""" code = self.ask("GLP") return int(code, 16)*self.pmax/4095.
[docs] def set_mode(self, mode): """ The device is able to work in the following modes: * Standby Laser is ready, but no emission is produced. However, if we it is turned on (e.g. with **start** function), then change to other mode will result in immediate emission, i.e. without 3 seconds delay. * CW-ACC constant wave, automatic current control * CW-APC constant wave, automatic power control * Analog the output power is dependent on the analog input; however, it cannot exceed the specified with **set_power** value. """ if mode == "Standby" or mode == 0: mode = 0 elif mode == "CW-ACC" or mode == 1: mode = 1 elif mode == "CW-APC" or mode == 2: mode = 2 elif mode == "Analog" or mode == 3: mode = 3 elif mode == "Analog+Digital" or mode == 4: mode = 4 else: print("**mode** must be one of 'Standby', 'CW-ACC', " + \ "'CW-APC', 'Analog' 'Analog+Digital', or number 0-4. Nothing changed.") return return self.ask("ROM%i" % mode)
[docs] def get_mode(self): """ The device is able to work in the following modes: * Standby turned off * CW-ACC constant wave, automatic current control * CW-APC constant wave, automatic power control * Analog the output power is dependent on the analog input; however, it cannot exceed the specified with **set_power** value. """ mode = self.ask("ROM") if mode == "0": return "Standby" elif mode == "1": return "CW-ACC" elif mode == "2": return "CW-APC" elif mode == "3": return "Analog" elif mode == "4": return "Analog+Digital" else: return mode
[docs] def set_autostart(self, state): """Decide if light is emitted on powerup.""" if state: self.ask("SAS1") else: self.ask("SAS0")
[docs] def get_autostart(self): """Check if light is emitted on powerup.""" return self.ask("SAS")
[docs] def get_emitted_power(self) -> float: """ get current power measured by internal diode :return: """ try: return float(self.ask('MDP')) except TypeError: return 0
[docs] def get_parameters(self): """Print a table showing laser status.""" self.smart_ask("GOM") print("""Bit description: 15 Auto PowerUP if ONE 14 Autostart (emission at powerup) if ONE 13 Adhoc USB - Laser sends info messages from time to time if ONE 8 Power control (APC) mode if ONE; current control (ACC) if ZERO 7 External analog input enabled if ONE 4 Mod level: ONE - active; ZERO - not active 3 Bias level: ONE - active; ZERO - not active Bits 0, 1, 2, 5, 6, 9, 10, 11, 12 are reserved """)
def get_status_mine(self): response = self.ask("GAS") scale = 16 ## equals to hexadecimal num_of_bits = 16 try: bit_representation = bin(int(response, scale))[2:].zfill(num_of_bits) except TypeError: return bit_representation = bit_representation[::-1] self.status["interlock"] = True if int(bit_representation[0]) else False self.status["on state"] = True if int(bit_representation[1]) else False self.status["preheat"] = True if int(bit_representation[2]) else False self.status["laser_enable"] = True if int(bit_representation[6]) else False # this is self.status["key"] = True if int(bit_representation[7]) else False self.status["powered"] = True if int(bit_representation[9]) else False def get_parameters_mine(self): response = self.ask("GOM") scale = 16 ## equals to hexadecimal num_of_bits = 16 bit_representation = bin(int(response, scale))[2:].zfill(num_of_bits) bit_representation = bit_representation[::-1] self.parameters['AutoPowerup'] = True if int(bit_representation[15]) else False self.parameters['Autostart'] = True if int(bit_representation[14]) else False self.parameters['Adhoc USB'] = True if int(bit_representation[13]) else False self.parameters['Power control'] = "APC" if int(bit_representation[8]) else "ACC" self.parameters['External analog'] = True if int(bit_representation[7]) else False self.parameters['ext TTL'] = True if int(bit_representation[5]) else False self.parameters['Mod level'] = True if int(bit_representation[4]) else False self.parameters['Bias level'] = True if int(bit_representation[3]) else False
[docs] def get_errors(self): """Print contents of failure byte""" self.smart_ask("GFB") print("""Bit description: 15 Diode power exceeded maximum value 14 An internal error occured 12 The temperature at the diode exceeded the valid temperature range 11 The ambient temperature exceeded the minimum or maximum value 10 The current through the diode exceeded the maximum allowed value 9 The interlock loop is not closed. Please close the interlockt loop 8 Overvoltage or Undervoltage lockout occured. Bring supply voltage to a valid range 4 If CDRH-Bit is set and no CDRH-Kit is connected or CDRH-Bit is not set but a CDRH-Kit is connected CDRH-Kit is a box with a key, a LED and an interlock 0 Soft interlock: If an interlock error occurs, this bit is set. It can only be reset by resetting the whole system, even if the interlock error is not present anymore. Bits 1, 2, 3, 5, 6, 7, 13 are reserved """)
def get_errors_mine(self): response = self.ask("GFB") errors = check_errors(response) print(f"{self.laser_name}:") for error in errors: print(error.description) if len(errors) == 1 and errors[0] == LaserErrors.no_errors: self.error_state = False else: self.error_state = True def prepare(self, power=-1): self.set_mode(3) # set to analog mode self.log.debug('Analog Mode') #if power == -1: # power = self.pmax # #set to max power #self.set_power(power) #self.log.debug(f'Set power to {power}') response = self.start() if response == '>': return True else: return False def power_up(self): self.ask('POn') self.log.debug('Power On') self.set_mode(3) # set to analog mode self.log.debug('Analog Mode') def test_apc(self): self.ask('POn') self.log.debug('Power On') self.set_mode(2) # set to APC mode self.log.debug('APC Mode') response = self.start() sleep(3) self.set_mode(3) # set to analog mode self.stop() self.ask('POf') self.log.debug('Power Off') def start_test(self): self.test_thread = Thread(target=self.test_apc) self.test_thread.start()
[docs] def stopwatch(func, *func_args, **func_kwargs): """Call **func** and print elapsed time""" start_time = time() result = func(*func_args, **func_kwargs) print("Time elapsed: %5.2f ms" % ((time() - start_time)*1000.0)) return result
def check_errors(hex_code) -> list: scale = 16 ## equals to hexadecimal num_of_bits = 16 bit_representation = bin(int(hex_code, scale))[2:].zfill(num_of_bits) bit_representation = bit_representation[::-1] on_bits = [b.start() for b in re.finditer('1', bit_representation)] if len(on_bits) == 0: return [LaserErrors(-1)] else: return [LaserErrors(on_bit) for on_bit in on_bits]
[docs] class LaserErrors(IntEnum): no_errors = (-1, "No errors") soft_inter = (0, "Soft interlock: If an interlock error occurs, this bit is set. It can only be reset by resetting " "the whole system, even if the interlock error is not present anymore.") max_power = (15, "Diode power exceeded maximum value") int_error = (14, "An internal error occured") temp_error = (12, "The temperature at the diode exceeded the valid temperature range") atemp_error = (11, "The ambient temperature exceeded the minimum or maximum value") curr_error = (10, "The current through the diode exceeded the maximum allowed value") interloop_error = (9, "The interlock loop is not closed. Please close the interlockt loop") voltage_error = (8, "Overvoltage or Undervoltage lockout occured. Bring supply voltage to a valid range") bit_error = (4, "If CDRH-Bit is set and no CDRH-Kit is connected or CDRH-Bit is not set but a CDRH-Kit is connected" " CDRH-Kit is a box with a key, a LED and an interlock") def __new__(cls, value, alias): member = int.__new__(cls, value) member._value_ = value member.description = alias return member
from PyQt6 import QtSerialPort import serial.tools.list_ports class LaserModule: def __init__(self): self.lasers = [] self.log = logging.getLogger('LaserModule') for port in serial.tools.list_ports.comports(): if "/dev/ttyUSB" not in port.device or "LuxX" not in port.description: # usually lasers called like this #skip arduino connected as USB0, find a way to identify? continue try: pot_laser = Laser(port=port.device) # try to connect to laser self.log.info(f"Found {pot_laser.laser_name} at {port.device} with {pot_laser.wavelength}nm wavelength and {pot_laser.pmax}mW max power") self.lasers.append(pot_laser) except OSError: self.log.error(f'{port.device} is not a valid laser') if len(self.lasers) == 0: self.log.warning('Could not find any lasers!') def __del__(self): """Close the port before exit.""" try: for laser in self.lasers: laser.port.close() print("Port closed") except serial.SerialException: print('could not close the port') def turn_off_lasers(self): for laser in self.lasers: laser.stop() laser.turn_power_off() def turn_on_lasers(self, spec_lasers): for laser in self.lasers: if laser.wavelength in spec_lasers: # TODO think about power ? ! laser.prepare() #set to standby #set to correct mode def prepare_lasers(self, wavelengths: list): self.log.debug('Starting lasers') for laser in self.lasers: if laser.wavelength in wavelengths: laser.prepare() def powerup_lasers(self, wavelengths: list): self.log.debug('Poweringup lasers') for laser in self.lasers: if laser.wavelength in wavelengths: laser.power_up() def put_lasers_standby(self): self.log.debug('Setting lasers in standby') for laser in self.lasers: laser.stop() laser.set_mode(0) def test_lasers(self, wavelengths: list): self.log.debug('Testing lasers') for laser in self.lasers: if laser.wavelength in wavelengths: laser.start_test()
[docs] class LaserColor (IntEnum): m = 405 b = 473 g = 560 y = 594 r = 647
[docs] class TriggerEnum(IntEnum): IntTrigger = 0 ExtTrigger0 = 1 ExtTrigger1 = 2 ExtTrigger2 = 3 ExtTrigger3 = 4 IntTrigger2 = 5
if __name__ == '__main__': las_module = LaserModule() print('hi') # to trigger a laser # power up (auto) # set to mode 4 # start the laser # trigger # do a gui ? user a queu to send commands ? or asynch processing to not send commands while waiting for response ?