Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Thorlabs KLS1550 laser driver #236

Merged
merged 11 commits into from
Aug 23, 2023
184 changes: 184 additions & 0 deletions docs/examples/Thorlabs_KLS1550.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "fifteen-voluntary",
"metadata": {},
"source": [
"# QCoDeS example with Thorlabs KLS1550 laser source\n",
"(same .dll as for KLS101, KLSnnn)"
]
},
{
"cell_type": "markdown",
"id": "overall-velvet",
"metadata": {},
"source": [
"## Initialisation\n",
"Import all required libraries for driving the devices."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "failing-acquisition",
"metadata": {},
"outputs": [],
"source": [
"import qcodes as qc\n",
"from qcodes_contrib_drivers.drivers.Thorlabs.Kinesis import Thorlabs_Kinesis\n",
"from qcodes_contrib_drivers.drivers.Thorlabs.KLS1550 import Thorlabs_KLS1550"
]
},
{
"cell_type": "markdown",
"id": "premium-batman",
"metadata": {},
"source": [
"Create an instance of `Thorlabs_Kinesis` which is a wrapper for the device dll (passed as an argument to the object) that starts up the Kinesis server. The DLL needs be installed from https://www.thorlabs.com/software_pages/viewsoftwarepage.cfm?code=Motion_Control under \"Kinesis software\"."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "executive-percentage",
"metadata": {},
"outputs": [],
"source": [
"kinesis = Thorlabs_Kinesis(\"Thorlabs.MotionControl.KCube.LaserSource.dll\", sim=False)\n",
"print(kinesis.device_list())"
]
},
{
"cell_type": "markdown",
"id": "israeli-functionality",
"metadata": {},
"source": [
"Create an instance of `Thorlabs_KLS1550` which is the device driver object, opening the device and starting polling (requesting device information) at 200 milisecods loops (must know the serial number shown on the device)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "modern-capital",
"metadata": {},
"outputs": [],
"source": [
"qc.Instrument.close_all()\n",
"laser = Thorlabs_KLS1550(\n",
" name=\"laser\",\n",
" serial_number=\"...\",\n",
" polling_speed=200, \n",
" kinesis=kinesis)"
]
},
{
"cell_type": "markdown",
"id": "periodic-democracy",
"metadata": {},
"source": [
"## Turning the laser output on/off\n",
"Set the parameter `output_enabled` to True for \"laser on\" and False for \"laser off\". Note that the laser will only turn on if the safety switch is also on."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "manual-traveler",
"metadata": {},
"outputs": [],
"source": [
"laser.output_enabled.set(True) # laser turns on"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "round-there",
"metadata": {},
"outputs": [],
"source": [
"laser.output_enabled.set(False) # laser turns off"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "brutal-screw",
"metadata": {},
"outputs": [],
"source": [
"laser.output_enabled.get() # Check laser output status"
]
},
{
"cell_type": "markdown",
"id": "favorite-sitting",
"metadata": {},
"source": [
"## Setting the laser output power\n",
"Set the laser output power in Watts and get the current power value from the device (also in Watts)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "cardiac-lobby",
"metadata": {},
"outputs": [],
"source": [
"laser.power.set(1e-3) # Set power to 1 mW"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "technical-grounds",
"metadata": {},
"outputs": [],
"source": [
"laser.power.get() # Gets laser power reading"
]
},
{
"cell_type": "markdown",
"id": "promotional-address",
"metadata": {},
"source": [
"## Disconnecting\n",
"Disconnect the device from the driver (but not the server, it still shows under `device_list`), stopping polling and closing the device (both for Kinesis server and qcodes)."
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "accepted-introduction",
"metadata": {},
"outputs": [],
"source": [
"laser.close()"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (qcodes)",
"language": "python",
"name": "qcodes"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.4"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
81 changes: 81 additions & 0 deletions qcodes_contrib_drivers/drivers/Thorlabs/KLS1550.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from qcodes import Instrument
import qcodes.utils.validators as vals

from .Kinesis import Thorlabs_Kinesis


class Thorlabs_KLS1550(Instrument):
def __init__(
self,
name: str,
serial_number: str,
polling_speed_ms: int,
kinesis: Thorlabs_Kinesis,
**kwargs,
):
super().__init__(name, **kwargs)
# Save Kinesis server reference
self.kinesis = kinesis

# Initialization
self.serial_number = serial_number
self.polling_speed_ms = polling_speed_ms
self.kinesis.open_laser(self.serial_number)
self.kinesis.start_polling(self.serial_number, self.polling_speed_ms)
self.info = self.kinesis.laser_info(self.serial_number)
self.model = self.info[0].value.decode("utf-8")
self.version = self.info[4].value

# Parameters
self.add_parameter(
"output_enabled",
get_cmd=self._get_output_enabled,
set_cmd=self._set_output_enabled,
vals=vals.Bool(),
unit="",
label="Laser output on/off",
docstring="Turn laser output on/off. Note that laser key switch must be on to turn laser output on.",
)

self.add_parameter(
"power",
get_cmd=self._get_power,
set_cmd=self._set_power,
vals=vals.Numbers(0, 0.007), # [ATTENTION] max power for simulator is 10mW
unit="W",
label="Power output",
)

self.connect_message()

def get_idn(self):
return {
"vendor": "Thorlabs",
"model": self.model,
"firmware": self.version,
"serial": self.serial_number,
}

def _get_output_enabled(self):
# First status bit represents 'output enabled'
return bool(self.kinesis.laser_status_bits(self.serial_number) & 1)

def _set_output_enabled(self, value: bool):
if value:
self.kinesis.laser_enable_output(self.serial_number)
else:
self.kinesis.laser_disable_output(self.serial_number)

def _get_power(self):
return self.kinesis.get_laser_power(self.serial_number)

def _set_power(self, power_W: float):
self.kinesis.set_laser_power(self.serial_number, power_W)

def disconnect(self):
self.kinesis.stop_polling(self.serial_number)
self.kinesis.close_laser(self.serial_number)

def close(self):
self.disconnect()
super().close()
132 changes: 132 additions & 0 deletions qcodes_contrib_drivers/drivers/Thorlabs/Kinesis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import os
import ctypes
from ctypes import wintypes

class Thorlabs_KinesisException(Exception):
pass

class Thorlabs_Kinesis():

def __init__(self, dll_path: str, sim: bool) -> None:

os.add_dll_directory(r"C:\Program Files\Thorlabs\Kinesis")
self.dll = ctypes.cdll.LoadLibrary(dll_path)

if sim:
self.enable_simulation()
else:
pass

# See Thorlabs Kinesis C API documentation for error description
def error_check(self, r: int, source:str) -> int:
if r != 0:
raise Thorlabs_KinesisException("Kinesis: [{}]: Error code {}".format(source, r))
else:
pass

return r

# Connects to simulator (which must be already running but
# without GUI open, only one port)
def enable_simulation(self) -> None:
r = self.dll.TLI_InitializeSimulations()
# Throws error message even if simulation was successfull,
# check the log in the Kinesis simulator for confirmation instead
# self.error_check(r, 'Enable simulation')

def disable_simulation(self) -> None:
r = self.dll.TLI_UninitializeSimulations()
self.error_check(r, 'Disable simulation')

# Returns list with all the available (and closed?) devices
def device_list(self) -> list:
r = self.dll.TLI_BuildDeviceList()
if r == 0 or r == 16:
device_list = ctypes.create_string_buffer(250)
r2 = self.dll.TLI_GetDeviceListExt(ctypes.byref(device_list), 250)
self.error_check(r2, 'Get device list')
else:
self.error_check(r, 'Build device list')

return device_list.value.decode("utf-8").rstrip(',').split(",")

# Helper convertion function
def to_char_p(self, s: str) -> ctypes.c_char_p:
return ctypes.c_char_p(s.encode('ascii'))

# Functions for starting and stopping connection to the laser source
def open_laser(self, serial_number: str) -> None:
r = self.dll.LS_Open(self.to_char_p(serial_number))
self.error_check(r, 'Opening device')

def start_polling(self, serial_number: str, polling_speed: int) -> None:
# Note: this function returns a boolean (for some reason)
r = self.dll.LS_StartPolling(self.to_char_p(serial_number), ctypes.c_int(polling_speed))
if r != 1:
# 21 = FT_SpecificFunctionFail - The function failed to complete succesfully.
# If unsuccessful, then r seems to be -1749177856 but this is not explained in the documentation
self.error_check(21, 'Start polling')

def close_laser(self, serial_number: str) -> None:
self.dll.LS_Close(self.to_char_p(serial_number))

def stop_polling(self, serial_number: str) -> None:
self.dll.LS_StopPolling(self.to_char_p(serial_number))

# Gets device information from a serial number
def laser_info(self, serial_number: str) -> list:
# Defining variables for information storing
model = ctypes.create_string_buffer(8)
model_size = ctypes.wintypes.DWORD(8)
type_num = ctypes.wintypes.WORD()
channel_num = ctypes.wintypes.WORD()
notes = ctypes.create_string_buffer(48)
notes_size = ctypes.c_ulong(48)
firmware_version = ctypes.wintypes.DWORD()
hardwware_version = ctypes.wintypes.WORD()
modification_state = ctypes.wintypes.WORD()

r = self.dll.LS_GetHardwareInfo(self.to_char_p(serial_number), ctypes.byref(model), model_size,
ctypes.byref(type_num), ctypes.byref(channel_num),
ctypes.byref(notes), notes_size, ctypes.byref(firmware_version),
ctypes.byref(hardwware_version), ctypes.byref(modification_state))
self.error_check(r, 'Get hardware info')

return [model, type_num, channel_num, notes, firmware_version, hardwware_version, modification_state]

# Returns a string with the status in binary (see doc. for bit meaning)
def laser_status_bits(self, serial_number: str) -> int:
# Note: status bits updated at polling interval, hence no LS_RequestStatusBits
integer = self.dll.LS_GetStatusBits(self.to_char_p(serial_number))
if integer == 0x40000000:
self.error_check(integer, 'Get status bits')
else:
return integer

# Turnng the laser on/off (on only if safety key is on)
def laser_enable_output(self, serial_number: str) -> None:
r = self.dll.LS_EnableOutput(self.to_char_p(serial_number))
self.error_check(r, 'Enable laser output')

def laser_disable_output(self, serial_number: str) -> None:
r = self.dll.LS_DisableOutput(self.to_char_p(serial_number))
self.error_check(r, 'Disable laser output')

# Reading laser power
def get_laser_power(self, serial_number: str) -> float:
max_num = 32767
max_power_W = 0.007
num = self.dll.LS_GetPowerReading(self.to_char_p(serial_number))

return num/max_num * max_power_W

# [ATTENTION] Setting laser power
def set_laser_power(self, serial_number: str, power_W: float) -> None:
# Maximum integer level for laser power
max_num = 32767
max_power_W = 0.007
# [ATTENTION] Maximum power is 7mW for actual device but 10mW for the simulator (somehow)
percentage = power_W/max_power_W
r = self.dll.LS_SetPower(self.to_char_p(serial_number), int(percentage*max_num))
self.error_check(r, 'Set laser power')