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

v0.2.0: Automatic discovery of wavelength/temperature control, improved docs #2

Merged
merged 3 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 73 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
[![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/asvela/dlc-control?style=flat-square)](https://www.codefactor.io/repository/github/asvela/dlc-control)
[![MIT License](https://img.shields.io/github/license/asvela/dlc-control?style=flat-square)](https://github.com/asvela/dlc-control/blob/main/LICENSE)

Convenience wrapper of Toptica Laser SDK for controlling a Toptica CTL with a DCLpro
Convenience wrapper of Toptica Laser SDK for controlling a Toptica CTL with a DLCpro

*Word of caution: This module controls potentially Class 4 lasers.*
*Use is entirely on your own risk.*

API documentation available [here](https://asvela.github.io/dlc-control/).
Docs can be built with ``python3 -m pdoc --html -o ./docs dlccontrol.py``
Docs can be built with ``python3 -m pdoc --html -o ./docs dlccontrol.py`` (needs
pdoc3 to be installed).

The ``DLCcontrol`` class can read and control:

Expand All @@ -26,6 +27,8 @@ The ``DLCcontrol`` class can read and control:
- scan end
- scan offset
- scan amplitude
* user level (normal, maintenance, service)
* any other setting using the ``DLCcontrol.client`` attribute


The class will check that the wavelength/temperature setpoint and internal scan
Expand Down Expand Up @@ -69,7 +72,7 @@ temperatures:
```


### Examples
### Comparison to the Topica laser SDK

The module uses properties extensively (listed as `Instance variables` in the
docs), which means class attributes have setter and getter functions,
Expand All @@ -78,47 +81,81 @@ which can be used like this:
```python
import dlccontrol as ctrl

with ctrl.DLCcontrol("xx.xx.xx.xx", wl_setting_present=True) as dlc:
dlc.wavelength_setpoint = 1550
actual_wl = dlc.wavelength_actual
# Set up a the analogue remote control sweeping the current with the
# on input Fine1
dlc.remote_select = "CC"
dlc.remote_signal = "Fine1"
dlc.remote_factor = 10
dlc.remote_enable = True
# Use the internal voltage scan and gradually increase the scan amplitude
dlc.scan_output_channel = "PC"
initial_amplitude = dlc.scan_amplitude
dlc.scan_frequency = 20
for i in range(10):
dlc.scan_amplitude = i
dlc.scan_amplitude = initial_amplitude
with ctrl.DLCcontrol("xx.xx.xx.xx") as dlc:
# Change wavelength or laser diode temperature depending on how the unit is
# controlled
if dlc.wl_setting_present:
dlc.wavelength_setpoint = 1550
actual_wl = dlc.wavelength_actual
if dlc.temp_setting_present:
dlc.temp_setpoint = 20
actual_temp = dlc.temp_actual
# Set up a the analogue remote control sweeping the current with the
# on input Fine1
dlc.remote_select = "CC"
dlc.remote_signal = "Fine1"
dlc.remote_factor = 10
dlc.remote_enable = True
# Use the internal voltage scan and gradually increase the scan amplitude
dlc.scan_output_channel = "PC"
initial_amplitude = dlc.scan_amplitude
dlc.scan_frequency = 20
for i in range(10):
dlc.scan_amplitude = i
dlc.scan_amplitude = initial_amplitude
```

Doing the same with the Toptica SDK would look like this (and this module
is providing a lot of other features in addition to simplifying the syntax)
is providing other features in addition to simplifying the syntax)

```python
import toptica.lasersdk.dlcpro.v2_4_0 as toptica
import toptica.lasersdk.decop as decop

with toptica.DLCpro(toptica.NetworkConnection("xx.xx.xx.xx")) as dlc:
try:
dlc.laser1.ctl.wavelength_set.set(float(1550))
actual_wl = dlc.laser1.ctl.wavelength_act.get()
# Set up a the analogue remote control sweeping the current with the
# on input Fine1
dlc.laser1.dl.cc.external_input.signal.set(0)
dlc.laser1.dl.cc.external_input.factor.set(10)
dlc.laser1.dl.cc.external_input.enable.set(True)
# Use the internal voltage scan and gradually increase the scan amplitude
dlc.laser1.scan.output_channel.set(50)
initial_amplitude = dlc.laser1.scan.amplitude.get()
dlc.laser1.scan.frequency.set(20)
for i in range(10):
dlc.laser1.scan.amplitude.set(float(i))
dlc.laser1.scan.amplitude.set(initial_amplitude)
except decop.DecopError:
pass
try:
dlc.laser1.dl.tc.temp_set(float(20))
actual_temp = dlc.laser1.dl.tc.temp_act.get()
except decop.DecopError:
pass
# Set up a the analogue remote control sweeping the current with the
# on input Fine1
dlc.laser1.dl.cc.external_input.signal.set(0)
dlc.laser1.dl.cc.external_input.factor.set(10)
dlc.laser1.dl.cc.external_input.enable.set(True)
# Use the internal voltage scan and gradually increase the scan amplitude
dlc.laser1.scan.output_channel.set(50)
initial_amplitude = dlc.laser1.scan.amplitude.get()
dlc.laser1.scan.frequency.set(20)
for i in range(10):
dlc.laser1.scan.amplitude.set(float(i))
dlc.laser1.scan.amplitude.set(initial_amplitude)
```

### Access any setting with `client`

If you want to access other settings than what the wrapper conveniently offers,
the ``DLCcontrol.client`` attribute is useful as it can give you access to any other setting:

```python
import dlccontrol as ctrl
with ctrl.DLCcontrol("xx.xx.xx.xx") as dlc:
print(dlc.client.get("serial-number"))
# Need higher privilige to access the following commands
dlc.set_user_level(1, "password from manual")
dlc.client.set("laser1:dl:cc:current-clip", 250)
dlc.client.set("laser1:dl:factory-settings:cc:current-clip", 250)
dlc.client.exec("laser-common:store-all")
```

Note also the ``DLCcontrol.set_user_level()`` function to elevate the connection for access
to protected settings.

More examples are in the `examples.py` module.


Expand All @@ -142,10 +179,11 @@ please report issues there. Contributions are also welcome.
The source code is licensed under the MIT license.


### Change log
### Changelog

* v0.1.1 Nov 2021:
- Added support for temperature tuned lasers
* v0.2.0 Nov 2021:
- Added support for temperature tuned lasers, automatic discovery of whether
the laser is wavelength or temperature controlled
- Adding a `client` attribute to the `DLCcontrol` class to access any laser
attribute
- Methods for setting and getting the user level for enabling change of
Expand Down
125 changes: 89 additions & 36 deletions dlccontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,19 +97,22 @@ class InputChannel(int, enum.Enum):


class DLCcontrol:
"""Control a Toptica DLC over an Ethernet connection
"""Control a Toptica DLCpro over an Ethernet connection

Parameters
----------
ip : str, default the module constant ``IP``
Ip address of the DLC unit
ip : str, default is the class attribute _ip (which defaults to the module constant ``IP``)
IP address of the DLC unit
open_on_init : bool, default ``True``
Decide if ``open()`` should be called during the initialisation of
the class object
wl_setting_present : bool, default ``False``
Use ``True`` if the wavelength of the laser can be set
temp_setting_present : bool, default ``False``
Use ``True`` if the temperature of the laser diode can be set
discover_wl_or_temp_control : bool, default ``True``
Let the object automatically check if the laser is controlled by setting the
wavelength or diode temperature
force_wl_control_available : bool, default ``False``
Force the object to assume the wavelength of the laser can be set
force_temp_control_available : bool, default ``False``
Force the object to assume the temperature of the laser diode can be set
"""

_ip = IP
Expand All @@ -123,23 +126,30 @@ class DLCcontrol:
"""MHz/mA or MHz/V calibration for the internal scan. Set by calling the
``freq_per_sec_internal_scan()`` method. After being set, the calibration
will be kept in memory for future calls"""
wl_setting_present = False
"""Tells the wrapper whether the laser is controlled with a wavelength setpoint"""
temp_setting_present = False
"""Tells the wrapper whether the laser is controlled with a temperature setpoint"""
wl_control_available = False
"""Tells the object whether the laser is controlled with a wavelength setpoint"""
temp_control_available = False
"""Tells the object whether the laser is controlled with a temperature setpoint"""
client = None
"""After opening the connection the client can be used to control any setting
for the DLCpro, for instance `self.client.set("laser1:dl:cc:current-act", 10)`
to set the laser diode current to 10mA"""

def __init__(
self,
ip=None,
open_on_init=True,
wl_setting_present=None,
temp_setting_present=None,
**kwargs,
ip: Union[str, None] = None,
open_on_init: bool = True,
discover_wl_or_temp_control: bool = True,
force_wl_control_available: bool = False,
force_temp_control_available: bool = False,
):
if wl_setting_present is not None:
self.wl_setting_present = wl_setting_present
if temp_setting_present is not None:
self.temp_setting_present = temp_setting_present
self.discover_wl_or_temp_control = discover_wl_or_temp_control
if force_wl_control_available:
self.wl_control_available = True
self.discover_wl_or_temp_control = False
if force_temp_control_available:
self.temp_control_available = True
self.discover_wl_or_temp_control = False
if ip is not None:
self._ip = ip
self.connection = dlcsdk.NetworkConnection(self._ip)
Expand All @@ -159,7 +169,9 @@ def open(self):
laser required to use the class"""
self.dlc.open()
self._is_open = True
# Make sure all class sttributes are up to date
# Make sure all class attributes are up to date
if self.discover_wl_or_temp_control:
self._discover_control()
self.get_limits_from_dlc()
self.get_scan_parameters()
self.get_remote_parameters()
Expand All @@ -170,9 +182,21 @@ def close(self):
if self._is_open:
self.dlc.close()

def set_user_level(self, level: int, password: str = "default", verbose=True):
def set_user_level(
self, level: int, password: str = "default", verbose: bool = True
):
"""Sets the user level privileges of the client *connection*, does not change
the user level on the DLCpro console"""
the user level on the DLCpro console

Parameters
----------
level : int
User level where 3 is normal, 2 is maintenance, 1 is service
password : str
Password for accessing this level. Using `default` will select the correct
passoword for level 3 and 2. For level 1, the password is unique to the
DLCpro and can be found in the datasheet.
"""
if password == "default":
if level == 1:
print(
Expand All @@ -197,7 +221,36 @@ def get_user_level(self) -> decop.UserLevel:

# Limits and settings ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ##

def get_limits_from_dlc(self, verbose=False) -> dict:
def _discover_control(self, verbose: bool = False):
# Check for wavelength control
try:
self.dlc.laser1.ctl.wavelength_min.get()
self.wl_control_available = True
except decop.DecopError as e:
errormsg = e.args[0]
if not ("-20" in errormsg or "unavailable" in errormsg):
print(
f"Unknown error '{e}' when discovering if wavelength control is available"
)
self.temp_control_available = False
# Check for laser diode temperature control
try:
self.dlc.laser1.dl.tc.temp_set_min.get()
self.temp_control_available = True
except decop.DecopError as e:
errormsg = e.args[0]
if not ("-20" in errormsg or "unavailable" in errormsg):
print(
f"Unknown error '{e}' when discovering if laser diode temperature control is available"
)
self.temp_control_available = False
if verbose:
print(f"The laser has wavelength control: {self.wl_control_available}")
print(
f"The laser has diode temperature control: {self.temp_control_available}"
)

def get_limits_from_dlc(self, verbose: bool = False) -> dict:
"""Query the laser for the wavelength, piezo voltage, current and
scan frequency limits, and populate the ``_lims`` dict attribute

Expand All @@ -218,14 +271,14 @@ def get_limits_from_dlc(self, verbose=False) -> dict:
"wlmin": None,
"wlmax": None,
}
if self.wl_setting_present:
if self.wl_control_available:
self._lims.update(
{
"wlmin": self.dlc.laser1.ctl.wavelength_min.get(),
"wlmax": self.dlc.laser1.ctl.wavelength_max.get(),
}
)
if self.temp_setting_present:
if self.temp_control_available:
self._lims.update(
{
"tmin": self.dlc.laser1.dl.tc.temp_set_min.get(),
Expand Down Expand Up @@ -427,22 +480,22 @@ def current_enabled(self, val: bool):
@property
def wavelength_actual(self) -> float:
"""The actual wavelength of the laser (read only)"""
if not self.wl_setting_present:
if not self.wl_control_available:
return None
return self.dlc.laser1.ctl.wavelength_act.get()

@property
def wavelength_setpoint(self) -> float:
"""The setpont of the laser wavelength"""
if not self.wl_setting_present:
if not self.wl_control_available:
return None
return self.dlc.laser1.ctl.wavelength_set.get()

@wavelength_setpoint.setter
def wavelength_setpoint(self, val: float):
if not self.wl_setting_present:
if not self.wl_control_available:
raise RuntimeError(
"Cannot set wavelength when `wl_setting_present` is False"
"Cannot set wavelength when `wl_control_available` is False"
)
if val is None:
return
Expand All @@ -456,23 +509,23 @@ def wavelength_setpoint(self, val: float):

@property
def temp_actual(self) -> float:
"""The actual wavelength of the laser (read only)"""
if not self.temp_setting_present:
"""The actual temperature of the laser diode (read only)"""
if not self.temp_control_available:
return None
return self.dlc.laser1.dl.tc.temp_act.get()

@property
def temp_setpoint(self) -> float:
"""The setpont of the laser wavelength"""
if not self.temp_setting_present:
"""The setpoint of the laser diode temperature"""
if not self.temp_control_available:
return None
return self.dlc.laser1.dl.tc.temp_set.get()

@temp_setpoint.setter
def temp_setpoint(self, val: float):
if not self.temp_setting_present:
if not self.temp_control_available:
raise RuntimeError(
"Cannot set diode temperature `temp_setting_present` is False"
"Cannot set diode temperature `temp_control_available` is False"
)
if val is None:
return
Expand Down
Loading