Skip to content

Commit

Permalink
Merge pull request #2 from asvela/dev/v0.2.0
Browse files Browse the repository at this point in the history
v0.2.0: Automatic discovery of wavelength/temperature control, improved docs
  • Loading branch information
asvela authored Nov 30, 2021
2 parents 9e53e2d + d841fec commit eba98c1
Show file tree
Hide file tree
Showing 4 changed files with 469 additions and 212 deletions.
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

0 comments on commit eba98c1

Please sign in to comment.