Driver Programming: Difference between revisions

From SweepMe! Wiki
Jump to navigation Jump to search
(→‎Stop a measurement: Now using exceptions)
 
(34 intermediate revisions by 2 users not shown)
Line 1: Line 1:
[[Drivers]] are small python based code snippets that tell SweepMe! how to interface with a certain instrument.
[[Drivers]] are small Python-based code snippets that define how SweepMe! should interact with different instruments.
Driver programming in SweepMe! refers to the process of creating new drivers or enhancing the capabilities of existing ones to expand or customize the functionalities of SweepMe! measurement procedures. The drivers are open-source and can be found on the instrument-drivers [https://github.com/SweepMe/instrument-drivers GitHub repository].


=== Basic idea ===
== Core Principles ==


SweepMe! creates a Device object based on a selected Driver. For this Device object, several semantic functions are called that are the identical across all modules and instruments. For example, these pre-defined semantic functions are named "connect", "initialize", "configure", "start", "measure", "call", "unconfigure", "deinitialize", "disconnect" and so on. There are many more such functions and you only need to add those to your Driver that you need. When these functions are called during the program run is described here: [[Sequencer procedure]]. As a Driver developer, you have to make sure that your instrument is doing right things at these functions, e.g. after "initialize" is called, your instrument should be initialized, but it remains your choice what is exactly done. Once, these semantic functions are perfectly adjusted, you can use your Driver in combination with all other SweepMe! modules and drivers.
==== Conceptualization of Instruments ====


In SweepMe!'s instrument drivers you have no direct access to the main program or the modules, but rather SweepMe! calls the semantic standard functions and sets a number of variables to control the program flow.
SweepMe! abstracts physical instruments into functional units, focusing on the features needed for specific tasks. This approach simplifies user interaction with the software and enhances its versatility. For example, a multifunction device can be divided into separate drivers for each of its capabilities, such as signal generation and signal measurement, allowing users to access and control different functionalities independently.


Conceptually, instruments are not implemented into SweepMe! as physical units with all their features, but rather instruments are implemented as functional units. Some instruments have multiple functions and for all these functions a driver can be created. For example, an instrument that can be used as signal generator and as oscilloscope can be implemented by a driver for [[Signal]] and by a driver for [[Scope]]. This way, functional units of the instrument can be controlled and accessed independently.
==== The EmptyDevice Class ====


=== Minimal working example ===
The foundation of driver programming in SweepMe! is the EmptyDevice class. This class outlines the basic structure and sequence of operations required for a device to function within the software. It includes methods such as <code>[[connect()]]</code>, <code>[[initialize()]]</code>, and <code>[[measure()]]</code>, into which developers insert the specific Python commands needed to control their hardware. The [[Sequencer procedure]] then dictates the order in which these methods are called by SweepMe!, ensuring a smooth workflow.


A [[Drivers|Driver]] is a '''main.py''' file in which a python class-Object is inherited from a parent class called EmptyDeviceClass.
== Step-by-Step Guide ==


The following four lines of codes are always needed:
=== Choosing Your Starting Point ===


{{syntaxhighlight|lang=python|code=
There are three paths you can take to start developing your driver:
from pysweepme.EmptyDeviceClass import EmptyDevice  # Loading the EmptyDevice Class
 
* '''Recommended for completely new drivers:''' Copy the code from the [https://github.com/SweepMe/instrument-drivers/blob/main/src/Logger-DeviceClass_template-minimal/main.py minimal logger driver template] and modify/extend to your needs. If you are new to driver development, you may want to start with one of the below approaches, which gives more guidance on what functions are required.
 
* '''When a similar driver already exists:''' Begin by finding a driver in the [[Version manager]] or in the [https://github.com/SweepMe/instrument-drivers/tree/main/src GitHub driver repository] that closely matches your device's functionality. [[Drivers#Create_a_new_one_using_the_version_manager|Create a custom version and rename it]]. This approach gives you a solid foundation to modify and adapt the driver to your specific needs.
 
* For a more comprehensive template, use the [https://github.com/SweepMe/instrument-drivers/tree/main/src/Logger-DeviceClass_template full Device Class Template]. It's a blueprint that guides you through almost all components of a driver.


class Device(EmptyDevice):            # Creating a new Device Class by inheriting from EmptyDevice
=== Development Steps ===
    def __init__(self):              # The python class object need to be initialized
      EmptyDevice.__init__(self)    # Finally, the initialization of EmptyDevice has to be done
}}


Copying these four files into an empty main.py results in a working Driver that is basically doing nothing. You can then add further code as needed.
# Import Modules: Begin by importing the necessary Python modules for your driver.
# Create the Device Class: Follow the example to define your Device class. This class is where you'll implement the functionality of your driver.
# Define Static Variables: Add static variables such as "description" to provide information about your driver.
# Initialization Functions:
## Implement the <code>__init__</code> function for initializing your class and define [[Driver_Programming#Defining_return_variables| return variables]].
## If your device requires port discovery, add the <code>find_Ports</code> function or use the [[Port manager]].
## Include <code>[[Driver_Programming#GUI_interaction|set_GUIparameter]]</code> and <code>get_GUIparameter</code> to interact with the GUI elements.
# Implement Standard Functions: Add functions like <code>[[connect()]]</code>, <code>[[disconnect()]]</code>, <code>[[initialize()]]</code>, [[deinitialize()]]</code>, <code>[[configure()]]</code>, <code>[[unconfigure()]]</code>, <code>[[signin()]]</code>, and <code>[[signout()]]</code>. Group these functions together for clarity.
# Measurement Point Functions: Incorporate standard functions called at each measurement point, such as <code>[[start()]]</code>, <code>[[apply()]]</code>, <code>[[measure()]]</code>, and <code>[[call()]]</code>. Maintain the sequence to ensure logical flow.
# Optional: Setter and Getter Functions: Create functions to simplify access to the instrument's properties (e.g., <code>set_voltage</code>, <code>get_voltage</code>). These can be used within standard semantic functions or when Drivers are used in independent python programs, e.g. using [[pysweepme]].
# Convenience Functions: At the end, add any helper functions that facilitate driver programming but don't directly interact with device properties.


=== Examples ===
== Programming style guide ==


All Drivers that are available via the [[version manager]] are also examples. Download them and see the source code, e.g. using the "Open/Modify" button that each module has. Copy Drivers to the public folder "CustomDevices" using the version manager and start using them as a template for your own Drivers.
All points are recommendations that might help you to create a Driver but feel free to do it your way. Also, rudimentary Drivers that do not support all features of an instrument or a certain programming style can be very helpful to other users to get started.  


* A Driver should be as convenient as possible. If you can make a decision for the user by doing some extra checks or by getting the information from a config file try to include it.


=== Programming style guide ===
* If a user could enter values that are not supported by the instrument, use the function [[initialize()]] to do an initial check and stop the measurement if a value is not supported. Inform the user what was wrong and which values are supported.


All points are recommendation that might help you to create a Driver, but feel free to do it your way. Also rudimentary Drivers that do not support all features of an instrument or a certain programming style can be very helpful to other users to use get started.  
* Whenever you get a value e.g. "self.value" during apply or any parameter during "get_GUIparameter" transform them immediately into the type you need. The type of a parameter can change (e.g. from integer to string) at some point, when a value is handed over from a different module or if the user interface of a module is changed in the future. By redefining the type of the parameter, you can ensure that your Driver will not break.


* A Driver should be as convenient as possible. If you can take a decision for the user by doing some extra checks or by getting the information from a config file try to include it.
* Sometimes the user interface of a module does not support all options of your instrument. In that case, contact us to discuss how we can improve the module. Sometimes it is also possible to 'stretch' the user interface to your needs. For example: A module has the option "Input" (represented by a drop-down menu), but your instrument has two inputs that can be selected/deselected, then you can add possible choices like "None", "1", "2", and "1 & 2". That way, you do not need a second user interface option like "Input 2" to support the second input.


* If a user could enter values that are not supported by the instrument, use the function [[initialize]] to do an initial check and stop the measurement if a value is not supported. Inform the user what was wrong and which values are supported.
* Variables should start with capital letters and use no underscore or camel case programming style. Whitespaces are possible.


* Whenever you get a value e.g. "self.value" during apply or any parameter during "get_GUIparameter" transform them immediately into the type you need. The type of a parameter can change (e.g. from integer to string) at some point, when a value is handed over from a different module or if the user interface of a module is changed in future. By redefining the type of the parameter, you can ensure that your Driver will not break.
* GUi parameters that are created using set_GUIparameter in modules [[Logger]], [[Switch]], or [[Robot]] should start with a capital letter, can use white spaces, and should be user-friendly (e.g. no underscore, no camel case)


* Sometimes the user interface of a module does not support all options of your instrument. In that case contact us to discuss how we can improve the module. Sometimes it is also possible to 'stretch' the user interface to your needs. For example: A module has the option "Input" (represented by drop-down menu), but your instrument has two inputs that can be selected/deselected, then you can add possible choices like "None", "1", "2", and "1 & 2". That way, you do not need a second user interface option like "Input 2" to support the second input.
* pysweepme examples should be in the folder "pysweepme" that is in the driver folder.


* To make it easy to read a Driver, we recommend to use a certain structure:
== File and Directory Structure ==
# start with the import of modules
# create the class 'Device' according to the minimal working example
# add static variables such as "description"
# add the function "__init__"
# add the function "find_Ports" if your Driver takes care about finding ports
# add the function "set_GUIparameter" to set the default values of the modules user interface
# add the function "get_GUIparameter" to retrieve the users selection from the modules user interface
# now add the standard functions such as [[connect]], [[initialize]], [[configure]],
# functions like connect/disconnect, initialize/deinitialize, configure/unconfigure, signin/signout can be added close together so that one can easily see what is done at the beginning and at the end.
# then add standard functions that are called at each measurement points, such as [[start]], [[apply]], [[measure]], [[call]], ... Try to keep the sequence in which they are called.
# after all standard semantic functions, that you need, are overloaded, you can add and create new functions that simplify the access to some properties of the instrument ("setter and getter functions",  e.g. "set_voltage", "get_voltage", "set_filter", "get_filter", etc. These functions can be used within the standard semantic functions, but they could also be used when Drivers are used in independent python programs, e.g. using [[pysweepme]].
# at the bottom, you can add your own convenience functions that help you to do the programming, but which are no standard functions and that do not set/get any property directly.
# Variables should start with capital letter and use no underscore or camel case programming style. Whitespaces are possible.
# GUi parameters that are created using set_GUIparameter in modules [[Logger]], [[Switch]], or [[Robot]] should start with capital letter, can use white spaces and should be user-friendly (e.g. no underscore, no camel case)
# pysweepme examples should be in a folder "pysweepme" that is in the driver folder.


=== Documentation ===
A SweepMe! driver has a name of the following format, which is used as the directory name for the driver:


If a user of your Driver needs further instruction, we recomment to upload the driver to our server and add a description on the webpage each driver gets on this webpage: [[https://sweep-me.net/devices/]]
'''<Type of the Module>-<Name of the manufacturer>_<Name of the instrument model>'''


If your Driver is not publicly available, add a descriptive comment to the code of the main.py file. In case you develop a Driver for the generic modules [[Logger]] and [[Switch]], you can add a description to the Driver by inserting a text for the static variable 'description'
Examples:
* SMU-Keithley_2400
* LCRmeter-HP_4284A
* Logger-PC_Mouse


In genereal, we recommend to rather add more comments than less. It will help other users to understand the code and to learn developing own drivers.  
The <Type of the Module> must be related to the [[Modules]] provided by SweepMe! and every Module provides different functionality to control a certain type of equipment.


=== Instantiation & destruction ===
Following common files and directories exist for a SweepMe! driver:


A instance of the Driver is instantiated and destroyed very often:
{| class="wikitable"
* for each measurement run
|+ Common files and directories of a SweepMe! driver
* whenever the Module needs to know the 'variables', 'units' or the shortname
|-
! File / Directory !! Explanation
|-
| <code>main.py</code> || Python code which provides the actual driver.
|-
| <code>license.txt</code> || License for the driver. This license usually only covers the SweepMe! specific driver without any third party libraries.
|-
| <code>libs/</code> || Only for SweepMe! 1.5.5 compatibility. Contains third party libraries required by the driver.
|-
| <code>libraries/</code> || Contains third party libraries required by the driver, if those are not included in SweepMe!.
|-
| <code>libraries/requirements.txt</code> || The third party packages required by the driver in the [https://pip.pypa.io/en/stable/reference/requirements-file-format/ pip requirements.txt format].
|-
| <code>libraries/libs_39_32/</code> || Third party libraries for 32bit Version and Python 3.9. This directory is created by the [[External_libraries_and_dependencies#Library_Builder|LibraryBuilder]] from the <code>libraries/requirements.txt</code> file.
|-
| <code>libraries/libs_39_64/</code> || Third party libraries for 64bit Version and Python 3.9. This directory is created by the [[External_libraries_and_dependencies#Library_Builder|LibraryBuilder]] from the <code>libraries/requirements.txt</code> file.
|-
| <code>libraries/libs_common/</code> || Libraries required by the driver that are not architecture specific. These can be third-party libraries or custom python modules that are separated from the <code>main.py</code> for a clearer structure.
|-
| <code>info.ini</code> || Metadata of the driver used by the SweepMe! version manager. This file only exists for official versions and it should be deleted when writing a custom driver.
|-
| <code>config.ini</code> || Configuration that allows to specify architecture compatibility of the driver, e.g. if the driver only works with a particular python version or bitness. The following example specifies a compatibility with 32bit-Python 3.6 and 64bit-Python 3.9:
<syntaxhighlight lang="ini">
[config]
architecture = "3.6-32, 3.9-64"
</syntaxhighlight>
|-
| <code>run.ini</code> || Created by SweepMe! and only required for uploading drivers.
|-
| <code><name>.ini</code>|| An ini file with the name identical to the driver (i.e. ''<Type of the Module>-<Name of the manufacturer>_<Name of the instrument model>'' can be used when the driver needs a system specific configuration that has to be adjusted by the user before the driver can be used. The user can simply copy the file to their custom files directory and use this file as a template.
|}


Thus, the __init__ should be a light-weight function as it is called often. It also means that you cannot store parameters in a Driver to use them later again. Every parameter of the Driver instance exists only as long as the Driver lives and after a run, for example, the Driver instance is destroyed. This further means that dll files or other files should not be loaded in the __init__ function, but rather during [[connect]] or [[initialize]].


To handover parameters and objects to future Driver instances, use the functions 'store_parameter' and 'restore_parameter' as explained [[Device_Class_Programming#Exchange_parameters_between_device_class_instances|here]].
== Initialization ==


=== Importing python packages ===
=== Importing Python packages ===


All packages which come along with SweepMe! can be imported as usual at the beginning of the file.
All packages that come along with SweepMe! can be imported as usual at the beginning of the file.
If you need to import packages that do not come with the SweepMe! installation, you can use the [[LibraryBuilder]] to ship an extra python package with your DeviceClass.
If you need to import packages that do not come with the SweepMe! installation, you can use the [[LibraryBuilder]] to ship an extra python package with your DeviceClass.


=== __init__ ===
=== __init__ ===


This function must be part of any Driver, and according to the minimal working example the function "__init__" of the base class "EmptyDevice" must be called first. Then, you can define variables, units, plottype, and savetype as described above. Furthermore, you can set the variable self.shortname to a string that will be shown in the sequencer to help the user to quickly identify which instrument is used. Besides that, you can use the __init__ function to define important variables that are frequently needed in all other functions.
This function must be part of any Driver, and according to the minimal working example the function "__init__" of the base class "EmptyDevice" must be called first. Then, you can define variables, units, plottype, and savetype as described above. Furthermore, you can set the variable self.shortname to a string that will be shown in the sequencer to help the user to identify which instrument is used quickly. Besides that, you can use the __init__ function to define important variables that are frequently needed in all other functions.
 
=== Instantiation & destruction ===
 
An instance of the Driver is instantiated and destroyed very often:
* for each measurement run
* whenever the Module needs to know the 'variables', 'units', or the shortname
 
Thus, the __init__ should be a lightweight function as it is called often. It also means that you cannot store parameters in a Driver to use them later again. Every parameter of the Driver instance exists only as long as the Driver lives and after a run, for example, the Driver instance is destroyed. This further means that dll files or other files should not be loaded in the __init__ function, but rather during [[connect()]] or [[initialize()]].
 
To handover parameters and objects to future Driver instances, use the functions 'store_parameter' and 'restore_parameter' as explained [[Device_Class_Programming#Exchange_parameters_between_device_class_instances|here]].


=== Defining return variables ===
=== Defining return variables ===
Line 90: Line 131:
Each Driver can return an arbitrary number of variables that are subsequently available for plotting, displaying them in a monitor widget, or saving them to the measurement data file.
Each Driver can return an arbitrary number of variables that are subsequently available for plotting, displaying them in a monitor widget, or saving them to the measurement data file.


In the function "__init__" or in "get_GUIparameter" of your Driver you have to define following objects:
In the function "__init__" or in "get_GUIparameter" of your Driver you have to define the following objects:


{{syntaxhighlight|lang=python|code=
{{syntaxhighlight|lang=python|code=
Line 106: Line 147:
Additionally, you can define 'self.plottype' and 'self.savetype' which again need to have the same length as 'self.variables'. If you do not define them, they will be always True for each variable. If the plottype of a variable is True, you can select this variable in the plot. If the savetype of a variable is True, the data of this variable is saved to the measurement file.
Additionally, you can define 'self.plottype' and 'self.savetype' which again need to have the same length as 'self.variables'. If you do not define them, they will be always True for each variable. If the plottype of a variable is True, you can select this variable in the plot. If the savetype of a variable is True, the data of this variable is saved to the measurement file.


In order to return your measured data to SweepMe! use the [[call]] function and return as many values as you have defined variables.
To return your measured data to SweepMe! use the [[call()]] function and return as many values as you have defined variables.


Returned data types can be: int, float, str, bool, list, np.array
Returned data types can be: int, float, str, bool, list, np.array


=== GUI interaction ===  
=== Ports ===


The interaction with grahical user interface (GUI) is realized with the two function get_GUIparameter and set_GUIparameter.
If you use standardized communications interfaces like GPIB, COM, or USBTMC, you make use of SweepMe!'s [[port manager]] that manages everything and you can use the write and read function to communicate with your instrument. Otherwise, you have to handle the creation and destruction of a port object yourself using the functions [[connect()]] and [[disconnect()]].
 
==== Finding & selecting ports ====
 
If your Driver makes use of the [[port manager]], available ports will be automatically added to the field "Port". Otherwise, you can add the function "find_Ports" and return a list of strings that identify possible ports.
In both cases, you can retrieve the port selected by the user via the function "get_GUIparameter" using the key "Port".
 
Starting from SweepMe! 1.5.5, you can use the function "find_ports" which is recommended.
 
==== Multichannel Support ====
 
Some measurement equipment has two or more channels but only one communications port, e.g. some Source-Measuring-Units or Parameter Analyzers have multiple channels to independently source voltages or currents, but everything is controlled via one port.
Use the key "Channel" with the functions "set_GUIparameter" and "get_GUIparameter" to list available ports in a Drop-down box.
 
 
== GUI interaction ==
 
The interaction with the graphical user interface (GUI) is realized with the two functions get_GUIparameter and set_GUIparameter.
The function get_GUIparameter receives a dictionary with all parameters of the GUI.
The function get_GUIparameter receives a dictionary with all parameters of the GUI.
The function set_GUIparameter has to return a dictionary that tells SweepMe! which GUI elements should be enabled (active) and which options or default values should be displayed.
The function set_GUIparameter has to return a dictionary that tells SweepMe! which GUI elements should be enabled (active) and which options or default values should be displayed.
Line 118: Line 176:
=== Getting GUI parameter ===
=== Getting GUI parameter ===


In order to figure out which parameters are selected by the user, the function 'get_GUIparameter' has to be used. It has one argument 'parameter' that is used to handover a dictionary that consists of keys that are related to the GUI elements of the Module and the selected values. Overwrite this function in your Driver to make use of it. For example:
To figure out which parameters are selected by the user, the function 'get_GUIparameter' has to be used. It has one argument 'parameter' that is used to hand over a dictionary that consists of keys that are related to the GUI elements of the Module and the selected values. Overwrite this function in your Driver to make use of it. For example:


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
Line 127: Line 185:
Here, the print command can be used to see the dictionary in the debug widget and learn which keys are accessible. These keys of the dictionary can vary between each Module, but there are some which are common for all, like "Label", "Device", "Port".
Here, the print command can be used to see the dictionary in the debug widget and learn which keys are accessible. These keys of the dictionary can vary between each Module, but there are some which are common for all, like "Label", "Device", "Port".


In order to load a single parameter, for example the Sweep mode or the selected port, use:
To load a single parameter, for example, the Sweep mode or the selected port, use:


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
Line 135: Line 193:
}}
}}


The variables 'self.sweepmode' and 'self.port_string' contain 'self.' which makes them an attribute of the entire Driver so that you can use them in all other functions of your Drivers that have 'self' as first argument.
The variables 'self.sweepmode' and 'self.port_string' contain 'self.' which makes them an attribute of the entire Driver so that you can use them in all other functions of your Drivers that have 'self' as the first argument.


It is good practice to immediately change the data type to whatever you need for further processing. Should, the type of the data, that is handed over, changes, your code will not break.
It is good practice to immediately change the data type to whatever you need for further processing. Should, the type of data, that is handed over, change, your code will not break.


Typical functions:
Typical functions:
Line 167: Line 225:
Here, "SweepMode" represents the ComboBox of the Module that presents the possible modes to vary set values. To get all possible keys that can be used with set_GUIparameter, you can use the function get_GUIparameter.
Here, "SweepMode" represents the ComboBox of the Module that presents the possible modes to vary set values. To get all possible keys that can be used with set_GUIparameter, you can use the function get_GUIparameter.


For modules with a fixed user interface, you cannot change the type of the widget that is used for the given key. Accordingly, you have use a certain format to set the value for a key, for example:
For modules with a fixed user interface, you cannot change the type of the widget that is used for the given key. Accordingly, you have to use a certain format to set the value for a key, for example:


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
Line 180: Line 238:




==== Creating individual parameters ====
=== Creating individual parameters ===
The Modules [[Logger]], [[Switch]], and [[Robot]] provide the possibility to generate GUI items dynamically. Just add your own keys to the dictionary and based on the type of the default value, the corresponding GUI element will be created for you.
The Modules [[Logger]], [[Switch]], and [[Robot]] provide the possibility to generate GUI items dynamically. Just add your own keys to the dictionary and based on the type of the default value, the corresponding GUI element will be created for you.


Line 200: Line 258:
The keys you use will be returned then by get_GUIparameter with the selected values of the user. Please make sure that you do not use keys that are provided by the Module. See the description of get_GUIparameter to see which keys already exist.
The keys you use will be returned then by get_GUIparameter with the selected values of the user. Please make sure that you do not use keys that are provided by the Module. See the description of get_GUIparameter to see which keys already exist.


==== Pre-defined GUI keys ====
=== Pre-defined GUI keys ===


There are some keys that should not be used to define your own GUI keys when creating Drivers for [[Logger]], [[Switch]], and [[Robot]].
Some keys should not be used to define your own GUI keys when creating Drivers for [[Logger]], [[Switch]], and [[Robot]].


* '''"Label":''' The field where the user can change the label of the module
* '''"Label":''' The field where the user can change the label of the module
Line 209: Line 267:
* '''"Calibration":''' the field where the user selects the calibration
* '''"Calibration":''' the field where the user selects the calibration
* '''"Channel":''' the field where the user selects the channel
* '''"Channel":''' the field where the user selects the channel
* '''"Description":''' the field where the user can read an info about the Driver
* '''"Description":''' the field where the user can read info about the Driver
* '''"SweepMode":''' the field where the user selects the sweep mode
* '''"SweepMode":''' the field where the user selects the sweep mode
* '''"SweepValue":''' the field where the user selects the sweep value
* '''"SweepValue":''' the field where the user selects the sweep value


=== Description ===
=== Reloading GUI fields ===
 
Drivers that are programmed for the modules [[Logger]], [[Switch]], or [[Robot]] can have a description that is displayed in the description box when the corresponding Driver is selected.
 
To enable this feature, add a static variable 'description' to your class, e.g.
 
{{syntaxhighlight|lang=python|code=
class Device(EmptyDevice):
    description = "here you describe how your Driver should be used" # a static variable


    def __init__(self):
If you have made changes to the GUI in your driver and you want to update the GUI of the module, just click on the Device/Driver selection dropdown box which will renew the GUI. If you have changed the default value of a GUI or the options of a drowdown box, you can either delete and recreate the module in the Sequencer or select the new option and after a refresh of the driver, the old options are gone.
      ...
}}


The string will be interpreted like html, so that headings, enumerations, etc. are possible.
== Parameter variation ==
 
=== Parameter variation ===


It's oftentimes the case that the user wants to vary a parameter over the driver instrument and acquire data. In SweepMe!, parameters can be varied by two approaches, which is explained below.
It's oftentimes the case that the user wants to vary a parameter over the driver instrument and acquire data. In SweepMe!, parameters can be varied by two approaches, which is explained below.


==== Sweep ====
=== Sweep ===
The main approach to do a parameter variation is through sweep. In a SweepMe! driver, the developer can define different parameter variations in a list in the GUIparameter dictionary with the key ''"SweepMode"'' under ''set_GUIparameter()'' method (See [[Driver Programming#Setting GUI parameter|Setting GUI parameter]] and [[Driver Programming#Getting GUI parameter|Getting GUI parameter]] sections). The "Sweep Mode" value can be used in other methods to configure the instrument. In addition to "Sweep Mode", there is a build-in "Sweep Value" GUI combobox element, where the user can select the source of the sweep from SweepMe!'s parameter space. This source value can be retrieved as ''self.value'' within the driver. Whenever the source value is changed, the [[apply()]] method of the driver will be called by SweepMe!.
The main approach to do a parameter variation is through sweep. In a SweepMe! driver, the developer can define different parameter variations in a list in the GUIparameter dictionary with the key ''"SweepMode"'' under ''set_GUIparameter()'' method (See [[Driver Programming#Setting GUI parameter|Setting GUI parameter]] and [[Driver Programming#Getting GUI parameter|Getting GUI parameter]] sections). The "Sweep Mode" value can be used in other methods to configure the instrument. In addition to "Sweep Mode", there is a built-in "Sweep Value" GUI combobox element, where the user can select the source of the sweep from SweepMe!'s parameter space. This source value can be retrieved as ''self.value'' within the driver. Whenever the source value is changed, the [[apply()]] method of the driver will be called by SweepMe!.


==== Reconfigure ====
=== Reconfigure ===
For module types like [[Logger]], [[Switch]], or [[Robot]], the alternative approach to vary a parameter is to pass the parameter as a string with following structure: ''{ModuleName_VariableName_Unit}''. A list of all parameters can be found under [[Parameters]] widget. For each parameter, the string can be simply copied by right-clicking on the parameter. Normally, SweepMe! takes the value of parameters only once at the beginning of the measurement. When a the value of a parameter changes, However, this updated parameter and its value will be available within method ''reconfigure()'', as shown below:
For module types like [[Logger]], [[Switch]], or [[Robot]], the alternative approach to varying a parameter is to pass the parameter as a string with the following structure: ''{ModuleName_VariableName_Unit}''. A list of all parameters can be found under the [[Parameters]] widget. For each parameter, the string can be simply copied by right-clicking on the parameter. Normally, SweepMe! takes the value of parameters only once at the beginning of the measurement. When a value of a parameter changes, however, this updated parameter and its value will be available within the method ''reconfigure()'', as shown below:


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
Line 250: Line 296:
Please note that at the beginning of the measurement, the reconfigure method is called within [[signin()]], i.e. before [[configure()]] and the parameter has its default value in all methods except reconfigure.
Please note that at the beginning of the measurement, the reconfigure method is called within [[signin()]], i.e. before [[configure()]] and the parameter has its default value in all methods except reconfigure.


=== Ports ===
=== Parameter store ===
(available from SweepMe! 1.5.5)
 
The parameter store can be used to exchange parameters or objects between Driver instances during an entire SweepMe! session.


If you use standardized communications interfaces like GPIB, COM, or USBTMC, you make use of SweepMe!'s [[port manager]] that manages everything and you can use write and read function to communicate with your instrument. Otherwise, you have to handle the creation and destruction of a port object yourself using the functions [[connect]] and [[disconnect]].
As Drivers are often instantiated and destroyed, it might be necessary to forward parameters or information to the next Driver instance during a SweepMe! session. There are two possibilities:
* parameters are stored in a configuration file
* parameters and stored in a parameter store that is provided by SweepMe!


==== Finding & selecting ports ====


If your Driver makes use of the [[port manager]], available ports will be automatically added to the field "Port". Otherwise you can add the function "find_Ports" and return a list of strings that identify possible ports.
To store a parameter, use
In both cases, you can retrieve the port selected by the user via the function "get_GUIparameter" using the key "Port".
{{syntaxhighlight|lang=python|code=
self.store_parameter(key, value)
}}
where 'key' is a string and 'value' can be any Python object. The key-value pair is stored in a dictionary that is available to all Drivers and which exists during the entire SweepMe! session.


Starting from SweepMe! 1.5.5, you can use the function "find_ports" which is recommended.
To restore a parameter, use
{{syntaxhighlight|lang=python|code=
value = self.restore_parameter(key)
}}
where 'key' is a string that was previously used during 'store_parameter' and 'value' is the object that was stored. If the key is unknown, 'None' is returned.


=== Calibrations  ===


''SweepMe! version: >= 1.5.5.28''
To erase a parameter, just set it back to None
{{syntaxhighlight|lang=python|code=
self.store_parameter(key, None)
}}


Some modules like [[Spectrometer]] or [[NetworkAnalyzer]] allow the user to choose a calibration file.
=== Communication between driver instances ===


To keep calibration files even if a SweepMe! version is changed or a new driver version is loaded, we recommend to put calibration files into the public SweepMe! folder "CalibrationFiles". You can find the calibration folder using the following line
Sometimes multiple instances of a driver are created for a measurement and they need to share some information, e.g. a certain object to handle the communication. For this purpose, a dictionary called "self.device_communication" is available in all drivers being the same in all driver instances. This dictionary is always emptied right at the start of a run.
You can simply add a key-value pair and all other drivers can access the entries as well and vice versa. The dictionary is available starting with a measurement and can be accessed during all functions that are called during the measurement. To avoid interference, we recommend using very unique key-strings that e.g. include the name of the drivers or even the port identifier to differentiate between multiple units of the same instrument.
For example, a Driver that opens a dll-file to communicate with a spectrometer will have exclusive access to that port. To allow multiple spectrometer modules and their drivers to access the same port object it must be made available to all.


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
calibration_folder = self.get_folder("CALIBRATIONS")
def connect(self):
   
    if "my_spectrometer_port" in self.device_communication:         
        # it means that another instance of this device class already opened the port object
        self.port = self.device_communication["my_spectrometer_port"] 
    else:                                                           
        # the port object is not available yet, so we have to create it and make it available to other instances of the same device class
        self.port = create_some_port_object()
        self.device_communication["my_spectrometer_port"] = self.port
}}
}}


Modules that support calibrations, have a button "Find calibrations" which triggers a couple of ways to find calibrations:
The same can be done when disconnecting and closing the port object:


# A Driver can define a list of strings for the key "Calibration" during the method "set_GUIparameter".
{{syntaxhighlight|lang=python|code=
# A Driver can have a function 'get_calibrationfile_properties(port="")' that must return two lists: The first list is a list of strings being the file extensions that the calibration files must have. The second list is a list of strings being characters that the calibration files must contain, e.g. a serial number. The function 'get_calibrationfile_properties(port = "")' must have one argument that is the selected port which might be needed to find the correct calibrations files. All files are searched for in the public SweepMe! folder "Calibration Files". You can create subfolders to organize your calibration files for different instruments or dates you did a calibrations. The subfolder name is automatically prepended to the calibrations file name.
def disconnect(self):
# A Driver can have a function 'find_calibrations()' that must return a list of strings. The function is called together with connect/disconnect, so that you are able to communicate with an instrument to ask for possible calibration options. This might be the case for network analyzers where calibrations are stored on the instrument sometimes.
   
    if "my_spectrometer_port" in self.device_communication:
        # if the key is still available, the first instance of the Driver will close the port and remove the key
        self.port.close() # close your port or disconnect
        del self.device_communication["my_spectrometer_port"] # remove the key-value pair from the dictionary
   
}}
Here, is important that that port is only closed once but not for all modules that access the same instrument. Once the key is removed from self.device_communication, further driver instances will not try to close the port object anymore.


The selected calibration file name can be accessed via 'get_GUIparameter' via the key "Calibration". To create the full path to the selected calibration file, you have to prepend the path to the public SweepMe! folder "CalibrationFiles". How you handle the calibration file is up to you. Define in your Driver, whether the file is e.g. loaded or just handed over to a third party library.


We recommend to take Drivers for the modules [[Spectrometer]] or [[NetworkAnalyzer]] as an example.
The dictionary "self.device_communication" is emptied before each measurement so that values are not available during the next measurement.


=== Multichannel support ===
Some measurement equipment has two or more channels but only one communications port, e.g. some Source-Measuring-Units or Paramter Analyzers have multiple channels to independently source voltages or currents, but everything is controlled via one port.
Use the key "Channel" with the functions "set_GUIparameter" and "get_GUIparameter" to list available ports in a Drop-down box.


== Error Handling ==
=== Stop a measurement ===
=== Stop a measurement ===


The measurement stops whenever a standard function such as 'connect', 'initialize', ... , raises an exception. This can be used to you would like to abort the measurement whenever something goes wrong.
The measurement stops whenever a standard function such as 'connect', 'initialize', ..., raises an exception. This can be used if you would like to abort the measurement whenever something goes wrong.
{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
msg = "text-to-be-displayed-in-a-message-box-to-inform-user"
msg = "text-to-be-displayed-in-a-message-box-to-inform-user"
Line 297: Line 369:
The error message will be displayed in a message box to the user.
The error message will be displayed in a message box to the user.


=== Communication with the user ===
== Communication with the user ==


If you like to display a message in the info box of the "Measurement" tab, you can use:
If you like to display a message in the info box of the "Measurement" tab, you can use:
Line 319: Line 391:




=== Parameter store ===
== Find and get folders ==
(available from SweepMe! 1.5.5)


The parameter store can be used to exchange parameters or objects between Driver instances during an entire SweepMe! session.
Often you need to access some of the standard pre-defined folders. For that purpose, you can use the function 'get_Folder' that each Driver can use.


As Drivers are often instantiated and destroyed, it might be necessary to forward parameters or infomation to the next Driver instance during a SweepMe! session. There are two possibilities:
{{syntaxhighlight|lang=python|code=
* parameters are stored in a configuration file
self.get_Folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.
* parameters and stored in a parameter store that is provided by SweepMe!
}}


Starting from SweepMe! 1.5.5, you can use 'get_folder', which is recommended:


To store a parameter, use
{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
self.store_parameter(key, value)
self.get_folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.
}}  
}}
where 'key' is a string and 'value' can be any python object. The key-value pair is stored in dictionary that is available to all Drivers and which exists during the entire SweepMe! session.
 
=== Often used keys ===
 
* "SELF": the folder of the Driver script "main.py" (introduced with SweepMe! 1.5.5. -> please update to 1.5.5.44 or later because of a bug)
* "TEMP": the temporary folder in which all measurement data is stored before the user saves it
* "CUSTOMFILES" (>= 1.5.5) or "CUSTOM" (1.5.4): the folder "CustomFiles" of the public SweepMe! folder in which setup-specific files might be stored
* "EXTLIBS": the folder "ExternalLibraries" of the public SweepMe! folder in which external dll files can be stored
* "CALIBRATIONS": the folder "Calibrations" of the public SweepMe! folder in which calibrations files can be stored
* "PUBLIC": the public SweepMe! folder
 


To restore a parameter, use
== Documentation ==
{{syntaxhighlight|lang=python|code=  
value = self.restore_parameter(key)
}}
where 'key' is a string that was previously used during 'store_parameter' and 'value' is the object that was stored. If the key is unknown, 'None' is returned.


If a user of your Driver needs further instruction, we recommend uploading the driver to our server and adding a description on the webpage each driver gets on this webpage: [[https://sweep-me.net/devices/]]


To erase a parameter, just set it back to None
If your Driver is not publicly available, add a descriptive comment to the code of the main.py file. In case you develop a Driver for the generic modules [[Logger]] and [[Switch]], you can add a description to the Driver by inserting a text for the static variable 'description'
{{syntaxhighlight|lang=python|code=
self.store_parameter(key, None)
}}


=== Communication between driver instances ===
In general, we recommend to rather add more comments than less. It will help other users understand the code and to learn developing own drivers.


Sometimes multiple instances of a driver are created for a measurement and they need to share some information, e.g. a certain object to handle the communication. For this purpose a dictionary called "self.device_communication" is available in all drivers being exactly the same in all driver instances. This dictionary is always emptied right at the start of a run.
=== Description ===
You can simply add a key-value pair and all other drivers can access the entries as well and vice versa. The dictionary is available starting with a measurement and can be accessed during all functions that are called during the measurement. To avoid interference, we recommend to use very unique key-strings that e.g. include the name of the drivers or even the port identifier in order to differentiate between multiple units of the same instrument.
For example, a Driver which opens a dll-file to communicate with a spectrometer will have exclusive access to that port. In order to allow multiple spectrometer modules and their drivers to access the same port object it must be made available to all.


{{syntaxhighlight|lang=python|code=
Drivers that are programmed for the modules [[Logger]], [[Switch]], or [[Robot]] can have a description that is displayed in the description box when the corresponding Driver is selected.
def connect(self):
   
    if "my_spectrometer_port" in self.device_communication:         
        # it means that another instance of this device class already opened the port object
        self.port = self.device_communication["my_spectrometer_port"]  
    else:                                                           
        # the port object is not available yet, so we have to create it and make it available to other instances of the same device class
        self.port = create_some_port_object()
        self.device_communication["my_spectrometer_port"] = self.port
}}


The same can be done, when disconnecting and closing the port object:
To enable this feature, add a static variable 'description' to your class, e.g.


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
def disconnect(self):
class Device(EmptyDevice):
      
     description = "here you describe how your Driver should be used" # a static variable
     if "my_spectrometer_port" in self.device_communication:
 
        # if the key is still available, the first instance of the Driver will close the port and remove the key
     def __init__(self):
        self.port.close() # close your port or disconnect
      ...
        del self.device_communication["my_spectrometer_port"] # remove the key-value pair from the dictionary
   
}}
}}
Here, is important that that port is only closed once but not for all modules that access the same instrument. Once the key is removed from self.device_communication, further driver instances will not try to close the port object anymore.


The dictionary "self.device_communication" is emptied before each measurement so that values are not available during the next measurement.
The string will be interpreted like HTML, so that headings, enumerations, etc. are possible.


=== Module specific functions ===
== Module-specific functions ==


Several modules provide additional functionality, e.g. the latest version of the monochromator module has a button to tell the instrument to go to a home position. These module specific functions are described on the page of each [[Modules|Module]].
Several modules provide additional functionality, e.g. the latest version of the monochromator module has a button to tell the instrument to go to a home position. These module-specific functions are described on the page of each [[Modules|Module]].




=== Configuration file ===
=== [[Configuration File]] ===  


Sometimes an instrument has setup-specific properties that are fixed but which must be once set for each instrument. These properties are typically not supported by the user interface of the module. Then, it would be more appropriate to save these configurations in a file that is stored in the public SweepMe! folder. This ensures that the configuration is not lost if a Driver is updated.
Sometimes an instrument has setup-specific properties that are fixed but which must be once set for each instrument. These properties are typically not supported by the user interface of the module. Then, it would be more appropriate to save these configurations in a file that is stored in the public SweepMe! folder. This ensures that the configuration is not lost if a Driver is updated.
Line 409: Line 467:
}}
}}


A Driver should come with a template configuration file. Starting from version 1.5.5, the user can copy the template configuration file to the "CustomFiles" using the version manager. Inform the user of your Driver about the possibility to customize the handling via a configuration file by adding some documentation.
A Driver should come with a template configuration file. Starting from version 1.5.5, the user can copy the template configuration file to the "CustomFiles" using the version manager. Inform the user of your Driver about the possibility of customizing the handling via a configuration file by adding some documentation.
 
=== Calibrations  ===


=== Find and get folders ===
''SweepMe! version: >= 1.5.5.28''


Often you need to access some of the standard pre-defined folders. For that purpose you can use the function 'get_Folder' that each Driver can use.
Some modules like [[Spectrometer]] or [[NetworkAnalyzer]] allow the user to choose a calibration file.
 
To keep calibration files even if a SweepMe! version is changed or a new driver version is loaded, we recommend putting calibration files into the public SweepMe! folder "CalibrationFiles". You can find the calibration folder using the following line


{{syntaxhighlight|lang=python|code=  
{{syntaxhighlight|lang=python|code=  
self.get_Folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.
calibration_folder = self.get_folder("CALIBRATIONS")
}}
}}


Starting from SweepMe! 1.5.5, you can use 'get_folder', which is recommended:
Modules that support calibrations, have a button "Find calibrations" which triggers a couple of ways to find calibrations:


{{syntaxhighlight|lang=python|code=
# A Driver can define a list of strings for the key "Calibration" during the method "set_GUIparameter".
self.get_folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.
# A Driver can have a function 'get_calibrationfile_properties(port="")' that must return two lists: The first list is a list of strings being the file extensions that the calibration files must have. The second list is a list of strings being characters that the calibration files must contain, e.g. a serial number. The function 'get_calibrationfile_properties(port = "")' must have one argument which is the selected port that might be needed to find the correct calibrations files. All files are searched for in the public SweepMe! folder "Calibration Files". You can create subfolders to organize your calibration files for different instruments or dates you did a calibration. The subfolder name is automatically prepended to the calibrations file name.
}}
# A Driver can have a function 'find_calibrations()' that must return a list of strings. The function is called together with connect/disconnect so that you can communicate with an instrument to ask for possible calibration options. This might be the case for network analyzers where calibrations are stored on the instrument sometimes. 


==== Often used keys ====
The selected calibration file name can be accessed via 'get_GUIparameter' via the key "Calibration". To create the full path to the selected calibration file, you have to prepend the path to the public SweepMe! folder "CalibrationFiles". How you handle the calibration file is up to you. Define in your Driver, whether the file is e.g. loaded or just handed over to a third-party library.


* "SELF": the folder of the Driver script "main.py" (introduced with SweepMe! 1.5.5. -> please update to 1.5.5.44 or later because of a bug)
We recommend to take Drivers for the modules [[Spectrometer]] or [[NetworkAnalyzer]] as an example.
* "TEMP": the temporary folder in which all measurement data is stored before the user saves it
* "CUSTOMFILES" (>= 1.5.5) or "CUSTOM" (1.5.4): the folder "CustomFiles" of the public SweepMe! folder in which setup-specific files might be stored
* "EXTLIBS": the folder "ExternalLibraries" of the public SweepMe! folder in which external dll files can be stored
* "CALIBRATIONS": the folder "Calibrations" of the public SweepMe! folder in which calibrations files can be stored
* "PUBLIC": the public SweepMe! folder

Latest revision as of 12:31, 7 October 2024

Drivers are small Python-based code snippets that define how SweepMe! should interact with different instruments. Driver programming in SweepMe! refers to the process of creating new drivers or enhancing the capabilities of existing ones to expand or customize the functionalities of SweepMe! measurement procedures. The drivers are open-source and can be found on the instrument-drivers GitHub repository.

Core Principles

Conceptualization of Instruments

SweepMe! abstracts physical instruments into functional units, focusing on the features needed for specific tasks. This approach simplifies user interaction with the software and enhances its versatility. For example, a multifunction device can be divided into separate drivers for each of its capabilities, such as signal generation and signal measurement, allowing users to access and control different functionalities independently.

The EmptyDevice Class

The foundation of driver programming in SweepMe! is the EmptyDevice class. This class outlines the basic structure and sequence of operations required for a device to function within the software. It includes methods such as connect(), initialize(), and measure(), into which developers insert the specific Python commands needed to control their hardware. The Sequencer procedure then dictates the order in which these methods are called by SweepMe!, ensuring a smooth workflow.

Step-by-Step Guide

Choosing Your Starting Point

There are three paths you can take to start developing your driver:

  • Recommended for completely new drivers: Copy the code from the minimal logger driver template and modify/extend to your needs. If you are new to driver development, you may want to start with one of the below approaches, which gives more guidance on what functions are required.
  • For a more comprehensive template, use the full Device Class Template. It's a blueprint that guides you through almost all components of a driver.

Development Steps

  1. Import Modules: Begin by importing the necessary Python modules for your driver.
  2. Create the Device Class: Follow the example to define your Device class. This class is where you'll implement the functionality of your driver.
  3. Define Static Variables: Add static variables such as "description" to provide information about your driver.
  4. Initialization Functions:
    1. Implement the __init__ function for initializing your class and define return variables.
    2. If your device requires port discovery, add the find_Ports function or use the Port manager.
    3. Include set_GUIparameter and get_GUIparameter to interact with the GUI elements.
  5. Implement Standard Functions: Add functions like connect(), disconnect(), initialize(), deinitialize(), configure(), unconfigure(), signin(), and signout(). Group these functions together for clarity.
  6. Measurement Point Functions: Incorporate standard functions called at each measurement point, such as start(), apply(), measure(), and call(). Maintain the sequence to ensure logical flow.
  7. Optional: Setter and Getter Functions: Create functions to simplify access to the instrument's properties (e.g., set_voltage, get_voltage). These can be used within standard semantic functions or when Drivers are used in independent python programs, e.g. using pysweepme.
  8. Convenience Functions: At the end, add any helper functions that facilitate driver programming but don't directly interact with device properties.

Programming style guide

All points are recommendations that might help you to create a Driver but feel free to do it your way. Also, rudimentary Drivers that do not support all features of an instrument or a certain programming style can be very helpful to other users to get started.

  • A Driver should be as convenient as possible. If you can make a decision for the user by doing some extra checks or by getting the information from a config file try to include it.
  • If a user could enter values that are not supported by the instrument, use the function initialize() to do an initial check and stop the measurement if a value is not supported. Inform the user what was wrong and which values are supported.
  • Whenever you get a value e.g. "self.value" during apply or any parameter during "get_GUIparameter" transform them immediately into the type you need. The type of a parameter can change (e.g. from integer to string) at some point, when a value is handed over from a different module or if the user interface of a module is changed in the future. By redefining the type of the parameter, you can ensure that your Driver will not break.
  • Sometimes the user interface of a module does not support all options of your instrument. In that case, contact us to discuss how we can improve the module. Sometimes it is also possible to 'stretch' the user interface to your needs. For example: A module has the option "Input" (represented by a drop-down menu), but your instrument has two inputs that can be selected/deselected, then you can add possible choices like "None", "1", "2", and "1 & 2". That way, you do not need a second user interface option like "Input 2" to support the second input.
  • Variables should start with capital letters and use no underscore or camel case programming style. Whitespaces are possible.
  • GUi parameters that are created using set_GUIparameter in modules Logger, Switch, or Robot should start with a capital letter, can use white spaces, and should be user-friendly (e.g. no underscore, no camel case)
  • pysweepme examples should be in the folder "pysweepme" that is in the driver folder.

File and Directory Structure

A SweepMe! driver has a name of the following format, which is used as the directory name for the driver:

<Type of the Module>-<Name of the manufacturer>_<Name of the instrument model>

Examples:

  • SMU-Keithley_2400
  • LCRmeter-HP_4284A
  • Logger-PC_Mouse

The <Type of the Module> must be related to the Modules provided by SweepMe! and every Module provides different functionality to control a certain type of equipment.

Following common files and directories exist for a SweepMe! driver:

Common files and directories of a SweepMe! driver
File / Directory Explanation
main.py Python code which provides the actual driver.
license.txt License for the driver. This license usually only covers the SweepMe! specific driver without any third party libraries.
libs/ Only for SweepMe! 1.5.5 compatibility. Contains third party libraries required by the driver.
libraries/ Contains third party libraries required by the driver, if those are not included in SweepMe!.
libraries/requirements.txt The third party packages required by the driver in the pip requirements.txt format.
libraries/libs_39_32/ Third party libraries for 32bit Version and Python 3.9. This directory is created by the LibraryBuilder from the libraries/requirements.txt file.
libraries/libs_39_64/ Third party libraries for 64bit Version and Python 3.9. This directory is created by the LibraryBuilder from the libraries/requirements.txt file.
libraries/libs_common/ Libraries required by the driver that are not architecture specific. These can be third-party libraries or custom python modules that are separated from the main.py for a clearer structure.
info.ini Metadata of the driver used by the SweepMe! version manager. This file only exists for official versions and it should be deleted when writing a custom driver.
config.ini Configuration that allows to specify architecture compatibility of the driver, e.g. if the driver only works with a particular python version or bitness. The following example specifies a compatibility with 32bit-Python 3.6 and 64bit-Python 3.9:
[config]
architecture = "3.6-32, 3.9-64"
run.ini Created by SweepMe! and only required for uploading drivers.
<name>.ini An ini file with the name identical to the driver (i.e. <Type of the Module>-<Name of the manufacturer>_<Name of the instrument model> can be used when the driver needs a system specific configuration that has to be adjusted by the user before the driver can be used. The user can simply copy the file to their custom files directory and use this file as a template.


Initialization

Importing Python packages

All packages that come along with SweepMe! can be imported as usual at the beginning of the file. If you need to import packages that do not come with the SweepMe! installation, you can use the LibraryBuilder to ship an extra python package with your DeviceClass.

__init__

This function must be part of any Driver, and according to the minimal working example the function "__init__" of the base class "EmptyDevice" must be called first. Then, you can define variables, units, plottype, and savetype as described above. Furthermore, you can set the variable self.shortname to a string that will be shown in the sequencer to help the user to identify which instrument is used quickly. Besides that, you can use the __init__ function to define important variables that are frequently needed in all other functions.

Instantiation & destruction

An instance of the Driver is instantiated and destroyed very often:

  • for each measurement run
  • whenever the Module needs to know the 'variables', 'units', or the shortname

Thus, the __init__ should be a lightweight function as it is called often. It also means that you cannot store parameters in a Driver to use them later again. Every parameter of the Driver instance exists only as long as the Driver lives and after a run, for example, the Driver instance is destroyed. This further means that dll files or other files should not be loaded in the __init__ function, but rather during connect() or initialize().

To handover parameters and objects to future Driver instances, use the functions 'store_parameter' and 'restore_parameter' as explained here.

Defining return variables

Each Driver can return an arbitrary number of variables that are subsequently available for plotting, displaying them in a monitor widget, or saving them to the measurement data file.

In the function "__init__" or in "get_GUIparameter" of your Driver you have to define the following objects:

self.variables = ["Variable1", "Variable2", "Variable3"]
self.units = ["s", "m", ""]

In this example, three variables are defined, but you can also add more. Please make sure that you have the same number of units which are defined as seconds, meters, and no unit (empty string).

self.plottype = [True, True, False]
self.savetype = [True, False, False]

Additionally, you can define 'self.plottype' and 'self.savetype' which again need to have the same length as 'self.variables'. If you do not define them, they will be always True for each variable. If the plottype of a variable is True, you can select this variable in the plot. If the savetype of a variable is True, the data of this variable is saved to the measurement file.

To return your measured data to SweepMe! use the call() function and return as many values as you have defined variables.

Returned data types can be: int, float, str, bool, list, np.array

Ports

If you use standardized communications interfaces like GPIB, COM, or USBTMC, you make use of SweepMe!'s port manager that manages everything and you can use the write and read function to communicate with your instrument. Otherwise, you have to handle the creation and destruction of a port object yourself using the functions connect() and disconnect().

Finding & selecting ports

If your Driver makes use of the port manager, available ports will be automatically added to the field "Port". Otherwise, you can add the function "find_Ports" and return a list of strings that identify possible ports. In both cases, you can retrieve the port selected by the user via the function "get_GUIparameter" using the key "Port".

Starting from SweepMe! 1.5.5, you can use the function "find_ports" which is recommended.

Multichannel Support

Some measurement equipment has two or more channels but only one communications port, e.g. some Source-Measuring-Units or Parameter Analyzers have multiple channels to independently source voltages or currents, but everything is controlled via one port. Use the key "Channel" with the functions "set_GUIparameter" and "get_GUIparameter" to list available ports in a Drop-down box.


GUI interaction

The interaction with the graphical user interface (GUI) is realized with the two functions get_GUIparameter and set_GUIparameter. The function get_GUIparameter receives a dictionary with all parameters of the GUI. The function set_GUIparameter has to return a dictionary that tells SweepMe! which GUI elements should be enabled (active) and which options or default values should be displayed.

Getting GUI parameter

To figure out which parameters are selected by the user, the function 'get_GUIparameter' has to be used. It has one argument 'parameter' that is used to hand over a dictionary that consists of keys that are related to the GUI elements of the Module and the selected values. Overwrite this function in your Driver to make use of it. For example:

def get_GUIparameter(self, parameter):
    print(parameter)

Here, the print command can be used to see the dictionary in the debug widget and learn which keys are accessible. These keys of the dictionary can vary between each Module, but there are some which are common for all, like "Label", "Device", "Port".

To load a single parameter, for example, the Sweep mode or the selected port, use:

def get_GUIparameter(self, parameter):
    self.sweepmode = str(parameter["SweepMode"])
    self.port_string = str(parameter["Port"])

The variables 'self.sweepmode' and 'self.port_string' contain 'self.' which makes them an attribute of the entire Driver so that you can use them in all other functions of your Drivers that have 'self' as the first argument.

It is good practice to immediately change the data type to whatever you need for further processing. Should, the type of data, that is handed over, change, your code will not break.

Typical functions:

  • int(): change to an integer number
  • float(): change to a float number
  • str(): change to a string

Attention: Do not overwrite the parameters within the dictionary that was handed over in get_GUIparameter as this dictionary is still a global object and changes can influence the program behavior. This will be fixed with the release of 1.5.5.

Setting GUI parameter

GUI elements of the Module for which you implement your Driver must be activated. For that reason, put the following function into your DeviceClass

def set_GUIparameter(self):
    GUIparameter = {}
    return GUIparameter

Now, you can fill the dictionary GUIparameter with keys and values. Each key represents a certain GUI item of the Module.

GUIparameter = {
                "SweepMode" : ["Current in A", "Voltage in V"],  # define a list 
               }

Here, "SweepMode" represents the ComboBox of the Module that presents the possible modes to vary set values. To get all possible keys that can be used with set_GUIparameter, you can use the function get_GUIparameter.

For modules with a fixed user interface, you cannot change the type of the widget that is used for the given key. Accordingly, you have to use a certain format to set the value for a key, for example:

GUIparameter = {
                "KeyInteger"    : 1,                       # use an integer for a field that requires an integer such as a SpinBox
                "KeyFloat"      : 1.23,                    # use a float for a field that requires an float such as DoubleSpinBox
                "KeyString "    : "SomeText",              # use a string for a field that requires a string such as a LineEdit
                "KeyComboBox"   : ["Choice1", "Choice2"],  # use a list of strings for a field that requires a selection such as a ComboBox
                "KeyCheckBox"   : True,                    # use a bool for a field that requires a check such as a CheckBox
               }


Creating individual parameters

The Modules Logger, Switch, and Robot provide the possibility to generate GUI items dynamically. Just add your own keys to the dictionary and based on the type of the default value, the corresponding GUI element will be created for you.

GUIparameter = {
                "MyOwnKeyForInteger"   : 1,                       # define a GUI element to enter an integer
                "MyOwnKeyForFloat"     : 1.23,                    # define a GUI element to enter a float
                "MyOwnKeyForString"    : "SomeText",              # define a GUI element to enter a string
                "MyOwnKeyForSelection" : ["Choice1", "Choice2"],  # define a GUI element to select from a ComboBox
                "MyOwnKeyForBool"      : True,                    # define a GUI element to use CheckBox
                "MyOwnKeyForSeparator" : None,                    # define a GUI element that just displays the key with bold font
                ""                     : None,                    # an empty line
                "MyOwnKeyForFolder"    : pathlib.Path(<folder>),  # define a GUI element that displays a button to select a folder
                "MyOwnKeyForFile"      : pathlib.Path(<file>),    # define a GUI element that displays a button to select a file
                " "                    : None,                    # another empty line with a different key from the first empty line
               }

The keys you use will be returned then by get_GUIparameter with the selected values of the user. Please make sure that you do not use keys that are provided by the Module. See the description of get_GUIparameter to see which keys already exist.

Pre-defined GUI keys

Some keys should not be used to define your own GUI keys when creating Drivers for Logger, Switch, and Robot.

  • "Label": The field where the user can change the label of the module
  • "Device": The field where the user selects the Driver
  • "Port": the field where the user selects the port
  • "Calibration": the field where the user selects the calibration
  • "Channel": the field where the user selects the channel
  • "Description": the field where the user can read info about the Driver
  • "SweepMode": the field where the user selects the sweep mode
  • "SweepValue": the field where the user selects the sweep value

Reloading GUI fields

If you have made changes to the GUI in your driver and you want to update the GUI of the module, just click on the Device/Driver selection dropdown box which will renew the GUI. If you have changed the default value of a GUI or the options of a drowdown box, you can either delete and recreate the module in the Sequencer or select the new option and after a refresh of the driver, the old options are gone.

Parameter variation

It's oftentimes the case that the user wants to vary a parameter over the driver instrument and acquire data. In SweepMe!, parameters can be varied by two approaches, which is explained below.

Sweep

The main approach to do a parameter variation is through sweep. In a SweepMe! driver, the developer can define different parameter variations in a list in the GUIparameter dictionary with the key "SweepMode" under set_GUIparameter() method (See Setting GUI parameter and Getting GUI parameter sections). The "Sweep Mode" value can be used in other methods to configure the instrument. In addition to "Sweep Mode", there is a built-in "Sweep Value" GUI combobox element, where the user can select the source of the sweep from SweepMe!'s parameter space. This source value can be retrieved as self.value within the driver. Whenever the source value is changed, the apply() method of the driver will be called by SweepMe!.

Reconfigure

For module types like Logger, Switch, or Robot, the alternative approach to varying a parameter is to pass the parameter as a string with the following structure: {ModuleName_VariableName_Unit}. A list of all parameters can be found under the Parameters widget. For each parameter, the string can be simply copied by right-clicking on the parameter. Normally, SweepMe! takes the value of parameters only once at the beginning of the measurement. When a value of a parameter changes, however, this updated parameter and its value will be available within the method reconfigure(), as shown below:

def reconfigure(self, parameters, keys):
        print(keys) # A list of updated parameters
        print(parameters) # A dictionary of all parameters
        if "ParameterKey" in keys:
            updated_value = parameters["ParameterKey"]
            print(updated_value)

Please note that at the beginning of the measurement, the reconfigure method is called within signin(), i.e. before configure() and the parameter has its default value in all methods except reconfigure.

Parameter store

(available from SweepMe! 1.5.5)

The parameter store can be used to exchange parameters or objects between Driver instances during an entire SweepMe! session.

As Drivers are often instantiated and destroyed, it might be necessary to forward parameters or information to the next Driver instance during a SweepMe! session. There are two possibilities:

  • parameters are stored in a configuration file
  • parameters and stored in a parameter store that is provided by SweepMe!


To store a parameter, use

self.store_parameter(key, value)

where 'key' is a string and 'value' can be any Python object. The key-value pair is stored in a dictionary that is available to all Drivers and which exists during the entire SweepMe! session.

To restore a parameter, use

value = self.restore_parameter(key)

where 'key' is a string that was previously used during 'store_parameter' and 'value' is the object that was stored. If the key is unknown, 'None' is returned.


To erase a parameter, just set it back to None

self.store_parameter(key, None)

Communication between driver instances

Sometimes multiple instances of a driver are created for a measurement and they need to share some information, e.g. a certain object to handle the communication. For this purpose, a dictionary called "self.device_communication" is available in all drivers being the same in all driver instances. This dictionary is always emptied right at the start of a run. You can simply add a key-value pair and all other drivers can access the entries as well and vice versa. The dictionary is available starting with a measurement and can be accessed during all functions that are called during the measurement. To avoid interference, we recommend using very unique key-strings that e.g. include the name of the drivers or even the port identifier to differentiate between multiple units of the same instrument. For example, a Driver that opens a dll-file to communicate with a spectrometer will have exclusive access to that port. To allow multiple spectrometer modules and their drivers to access the same port object it must be made available to all.

def connect(self):
    
    if "my_spectrometer_port" in self.device_communication:           
        # it means that another instance of this device class already opened the port object
        self.port = self.device_communication["my_spectrometer_port"]   
    else:                                                             
        # the port object is not available yet, so we have to create it and make it available to other instances of the same device class
        self.port = create_some_port_object()
        self.device_communication["my_spectrometer_port"] = self.port

The same can be done when disconnecting and closing the port object:

def disconnect(self):
    
    if "my_spectrometer_port" in self.device_communication:
        # if the key is still available, the first instance of the Driver will close the port and remove the key
        self.port.close() # close your port or disconnect
        del self.device_communication["my_spectrometer_port"] # remove the key-value pair from the dictionary

Here, is important that that port is only closed once but not for all modules that access the same instrument. Once the key is removed from self.device_communication, further driver instances will not try to close the port object anymore.


The dictionary "self.device_communication" is emptied before each measurement so that values are not available during the next measurement.


Error Handling

Stop a measurement

The measurement stops whenever a standard function such as 'connect', 'initialize', ..., raises an exception. This can be used if you would like to abort the measurement whenever something goes wrong.

msg = "text-to-be-displayed-in-a-message-box-to-inform-user"
raise Exception(msg)

The error message will be displayed in a message box to the user.

Communication with the user

If you like to display a message in the info box of the "Measurement" tab, you can use:

self.message_Info("text-to-be-displayed-in-the-info-box")

If you like to inform the user with a message box, use

self.message_Box("text-to-be-displayed-in-the-message-box")

Please note that the message box is non-blocking and the measurement will continue, even if the the message is not confirmed by the user.

Log messages to a file

You can easily write messages to a file using

self.write_Log("message-to-be-saved")

The message will be saved in the file temp_logbook.txt within the temp-folder. When saving data the leading "temp" of the file will be automatically renamed by the given file name.


Find and get folders

Often you need to access some of the standard pre-defined folders. For that purpose, you can use the function 'get_Folder' that each Driver can use.

self.get_Folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.

Starting from SweepMe! 1.5.5, you can use 'get_folder', which is recommended:

self.get_folder("<KEY>") # returns an absolute path to the folder for a given <KEY>.

Often used keys

  • "SELF": the folder of the Driver script "main.py" (introduced with SweepMe! 1.5.5. -> please update to 1.5.5.44 or later because of a bug)
  • "TEMP": the temporary folder in which all measurement data is stored before the user saves it
  • "CUSTOMFILES" (>= 1.5.5) or "CUSTOM" (1.5.4): the folder "CustomFiles" of the public SweepMe! folder in which setup-specific files might be stored
  • "EXTLIBS": the folder "ExternalLibraries" of the public SweepMe! folder in which external dll files can be stored
  • "CALIBRATIONS": the folder "Calibrations" of the public SweepMe! folder in which calibrations files can be stored
  • "PUBLIC": the public SweepMe! folder


Documentation

If a user of your Driver needs further instruction, we recommend uploading the driver to our server and adding a description on the webpage each driver gets on this webpage: [[1]]

If your Driver is not publicly available, add a descriptive comment to the code of the main.py file. In case you develop a Driver for the generic modules Logger and Switch, you can add a description to the Driver by inserting a text for the static variable 'description'

In general, we recommend to rather add more comments than less. It will help other users understand the code and to learn developing own drivers.

Description

Drivers that are programmed for the modules Logger, Switch, or Robot can have a description that is displayed in the description box when the corresponding Driver is selected.

To enable this feature, add a static variable 'description' to your class, e.g.

class Device(EmptyDevice):
    description = "here you describe how your Driver should be used" # a static variable

    def __init__(self):
       ...

The string will be interpreted like HTML, so that headings, enumerations, etc. are possible.

Module-specific functions

Several modules provide additional functionality, e.g. the latest version of the monochromator module has a button to tell the instrument to go to a home position. These module-specific functions are described on the page of each Module.


Configuration File

Sometimes an instrument has setup-specific properties that are fixed but which must be once set for each instrument. These properties are typically not supported by the user interface of the module. Then, it would be more appropriate to save these configurations in a file that is stored in the public SweepMe! folder. This ensures that the configuration is not lost if a Driver is updated.

There exist convenience functions to load such a configuration file that can be used within a Driver. These functions expect to find a file in the folder "CustomFiles" of the public SweepMe! folder. The file must have the name of the Driver and the extension ".ini". The formatting should be like in any INI file [[2]]


self.isConfigFile() # returns True if the file exists, else False
self.getConfigSections() # returns a list of strings that represent all found sections
self.getConfigOptions(section) # returns a dictionary of key-value pairs of the given section string
self.getConfig() # returns a dictionary with dictionaries of the options for all sections, i.e. the entire config file

A Driver should come with a template configuration file. Starting from version 1.5.5, the user can copy the template configuration file to the "CustomFiles" using the version manager. Inform the user of your Driver about the possibility of customizing the handling via a configuration file by adding some documentation.

Calibrations

SweepMe! version: >= 1.5.5.28

Some modules like Spectrometer or NetworkAnalyzer allow the user to choose a calibration file.

To keep calibration files even if a SweepMe! version is changed or a new driver version is loaded, we recommend putting calibration files into the public SweepMe! folder "CalibrationFiles". You can find the calibration folder using the following line

calibration_folder = self.get_folder("CALIBRATIONS")

Modules that support calibrations, have a button "Find calibrations" which triggers a couple of ways to find calibrations:

  1. A Driver can define a list of strings for the key "Calibration" during the method "set_GUIparameter".
  2. A Driver can have a function 'get_calibrationfile_properties(port="")' that must return two lists: The first list is a list of strings being the file extensions that the calibration files must have. The second list is a list of strings being characters that the calibration files must contain, e.g. a serial number. The function 'get_calibrationfile_properties(port = "")' must have one argument which is the selected port that might be needed to find the correct calibrations files. All files are searched for in the public SweepMe! folder "Calibration Files". You can create subfolders to organize your calibration files for different instruments or dates you did a calibration. The subfolder name is automatically prepended to the calibrations file name.
  3. A Driver can have a function 'find_calibrations()' that must return a list of strings. The function is called together with connect/disconnect so that you can communicate with an instrument to ask for possible calibration options. This might be the case for network analyzers where calibrations are stored on the instrument sometimes.

The selected calibration file name can be accessed via 'get_GUIparameter' via the key "Calibration". To create the full path to the selected calibration file, you have to prepend the path to the public SweepMe! folder "CalibrationFiles". How you handle the calibration file is up to you. Define in your Driver, whether the file is e.g. loaded or just handed over to a third-party library.

We recommend to take Drivers for the modules Spectrometer or NetworkAnalyzer as an example.