diff --git a/doc/source/conf.py b/doc/source/conf.py index 2235369..0fdb17f 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -58,7 +58,7 @@ master_doc = 'index' # General information about the project. project = 'Frappy' -copyright = '2017-2021, Enrico Faulhaber, Markus Zolliker,' +copyright = '2017-2023, Enrico Faulhaber, Markus Zolliker,' #copyright = '2017, SECoP Committee' author = 'Enrico Faulhaber, Markus Zolliker' @@ -211,7 +211,7 @@ epub_exclude_files = ['search.html'] # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'https://docs.python.org/3/': None} +# intersphinx_mapping = {'https://docs.python.org/3/': None} from frappy.lib.classdoc import class_doc_handler diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst new file mode 100644 index 0000000..a23e063 --- /dev/null +++ b/doc/source/configuration.rst @@ -0,0 +1,58 @@ +Configuration File +.................. + +.. _node configuration: + +:Node(equipment_id, description, interface, \*\*kwds): + + Specify the SEC-node properties. + + The arguments are SECoP node properties and additional internal node configurations + + :Parameters: + + - **equipment_id** - a globally unique string identifying the SEC node + - **description** - a human readable description of the SEC node + - **interface** - an uri style string indication the address for the server + - **kwds** - other SEC node properties + +.. _mod configuration: + +:Mod(name, cls, description, \*\*kwds): + + Create a SECoP module. + Keyworded argument matching a parameter name are used to configure + the initial value of a parameter. For configuring the parameter properties + the value must be an instance of **Param**, using the keyworded arguments + for modifying the default values of the parameter properties. In this case, + the initial value may be given as the first positional argument. + In case command properties are to be modified **Command** has to be used. + + :Parameters: + + - **name** - the module name + - **cls** - a qualified class name or the python class of a module + - **description** - a human readable description of the module + - **kwds** - parameter, property or command configurations + +.. _param configuration: + +:Param(value=, \*\*kwds): + + Configure a parameter + + :Parameters: + + - **value** - if given, the initial value of the parameter + - **kwds** - parameter or datatype SECoP properties (see :class:`frappy.param.Parameter` + and :class:`frappy.datatypes.Datatypes`) + +.. _command configuration: + +:Command(\*\*kwds): + + Configure a command + + :Parameters: + + - **kwds** - command SECoP properties (see :class:`frappy.param.Commands`) diff --git a/doc/source/frappy_demo.rst b/doc/source/frappy_demo.rst index 02202ec..bcaef19 100644 --- a/doc/source/frappy_demo.rst +++ b/doc/source/frappy_demo.rst @@ -8,3 +8,7 @@ Demo .. automodule:: frappy_demo.test :show-inheritance: :members: + +.. automodule:: frappy_demo.lakeshore + :show-inheritance: + :members: diff --git a/doc/source/index.rst b/doc/source/index.rst index 93742c3..d0d0733 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -5,6 +5,10 @@ Frappy Programming Guide :maxdepth: 2 introduction + structure + programming + magic + server tutorial reference frappy_psi diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index 34a841f..0ff6d5c 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -57,14 +57,30 @@ to provide to the user. Programming a Driver -------------------- -Programming a driver means extending one of the base classes like :class:`frappy.modules.Readable` -or :class:`frappy.modules.Drivable`. The parameters are defined in the dict :py:attr:`parameters`, as a -class attribute of the extended class, using the :class:`frappy.params.Parameter` constructor, or in case -of altering the properties of an inherited parameter, :class:`frappy.params.Override`. +:ref:`Programming a driver ` means: + +- selecting a base class to be extended (e.g. :class:`frappy.modules.Readable` + or :class:`frappy.modules.Drivable`). +- defining the parameters +- coding the methods to retrieve and access these parameters + + +Support for Communication with the Hardware +------------------------------------------- + +Often the access to the hardware has to be done over a serial communication over LAN, +RS232 or USB. The mixin :class:`frappy.io.HasIO` and the classes :class:`frappy.io.StringIO` +and :class:`frappy.io.BytesIO` have all the functionality needed for this. + +Some hardware also requires calls to libraries offered by the manufacturers, certainly this +is also possible. In case there is no python package for this, but a C/C++ API, you might +use one of the following: + +- ``Ctypes (A foreign function library for Python) `` +- ``CFFI (C Foreign Function Interface for Python) `` +- ``Extending Python with C or C++ `` + + +.. TODO: shift this to an extra section -Parameters usually need a method :meth:`read_()` -implementing the code to retrieve their value from the hardware. Writeable parameters -(with the argument ``readonly=False``) usually need a method :meth:`write_()` -implementing how they are written to the hardware. Above methods may be omitted, when -there is no interaction with the hardware involved. diff --git a/doc/source/magic.rst b/doc/source/magic.rst new file mode 100644 index 0000000..4a5f347 --- /dev/null +++ b/doc/source/magic.rst @@ -0,0 +1,61 @@ +Frappy Internals +---------------- + +Frappy is a powerful framework, which does everything behind the +scenes you need for getting a SEC node to work. This section describes +what the framwork does for you. + +Startup +....... + +TODO: describe startup: init methods, first polls + +.. _polling: + +Polling +....... + +By default, a module inheriting from :class:`Readable ` is +polled every :attr:`pollinterval` seconds. More exactly, the :meth:`doPoll` +method is called, which by default calls :meth:`read_value` and :meth:`read_status`. + +The programmer might override the behaviour of :meth:`doPoll`, often it is wise +to super call the inherited method. + +:Note: + + Even for modules not inheriting from :class:`Readable `, + :meth:`doPoll` is called regularly. Its default implementation is doing nothing, + but may be overridden to do customized polling. + +In addition, the :meth:`read_` method is called every :attr:`slowinterval` +seconds for all parameters, in case the value was not updated since :attr:`pollinterval` +seconds. + +The decorator :func:`nopoll ` might be used on a :meth:`read_` +method in order to indicate, that the value is not polled by the slow poll mechanism. + + +.. _client notification: + +Client Notification +................... + +Whenever a parameter is changed by assigning a value to the attribute or by +means of the access method, an ``update`` message is sent to all activated clients. +Frappy implements the extended version of the ``activate`` message, where single modules +and parameters might be activated. + + +.. _type check: + +Type check and type conversion +.............................. + +Assigning a parameter to a value by setting the attribute via ``self. = `` +or ``. = `` involves a type check and possible a type conversion, +but not a range check for numeric types. The range check is only done on a ``change`` +message. + + +TODO: error handling, logging diff --git a/doc/source/programming.rst b/doc/source/programming.rst new file mode 100644 index 0000000..e3f03c9 --- /dev/null +++ b/doc/source/programming.rst @@ -0,0 +1,105 @@ +Coding +====== + +.. _class_coding: + +Coding a Class for a SECoP Module +--------------------------------- + +A SECoP module is represented as an instance of a python class. +For programming such a class, typically you create a +subclass of one of the base classes :class:`Readable `, +:class:`Writable ` or :class:`Drivable `. +It is also quite common to inherit from classes created for similar modules, +and or to inherit from a mixin class like :class:`HasIO `. + +For creating the :ref:`parameters `, +class attributes are used, using the name of +the parameter as the attribute name and an instantiation of :class:`frappy.params.Parameter` +for defining the parameter. If a parameter is already given by an inherited class, +the parameter declaration might be omitted, or just its altered properties +have to be given. + +In addition, you might need one or several configurable items +(see :ref:`properties `), declared in the same way, with +`` =`` :class:`frappy.params.Property` ``(...)``. + +For each of the parameters, the behaviour has to be programmed with the +following access methods: + +def read\_\ **\ (self): + Called on a ``read`` SECoP message and whenever the internal poll mechanism + of Frappy tries to get a new value. The return value should be the + retrieved value. + In special cases :data:`Done ` might be returned instead, + when the internal code has already updated the parameter, or + when the value has not changed and no updates should be emitted. + This method might also be called internally, in case a fresh value of + the parameter is needed. + +.. admonition:: polling + + The Frappy framework has a built in :ref:`polling ` mechanism, + which calls above method regularely. Each time ``read_`` is + called, the Frappy framework ensures then that the value of the parameter + is updated and the activated clients will be notified by means of an + ``update`` message. + +def write\_\ **\ (self, value): + Called on a ``change`` SECoP message. The ``value`` argument is the value + given by the change message, and the method should implement the change, + typically by handing it over to the hardware. On success, the method must + return the accepted value. If the value may be read back + from the hardware, the readback value should be returned, which might be + slighly altered for example by rounding. The idea is, that the returned + value would be the same, as if it would be done by the ``read_`` + method. Often the easiest implementation is just returning the result of + a call to the ``read_`` method. + Also, :ref:`Done ` might be returned in special + cases, e.g. when the code was written in a way, when self. is + assigned already before returning from the method. + +.. admonition:: behind the scenes + + Assigning a parameter to a value by setting the attribute via + ``self. = `` or ``. = `` includes + a :ref:`type check `, some type conversion and ensures that + a :ref:`notification ` with an + ``update`` message is sent to all activated clients. + +Example code: + +.. code:: python + + from frappy.core import HasIO, Drivable, Property, Parameter, StringType + + class TemperatureLoop(HasIO, Drivable): + """a temperature sensor with loop""" + # internal property to configure the channel + channel = Property('the Lakeshore channel', datatype=StringType()) + # modifying a property of inherited parameters (unit is propagated to the FloatRange datatype) + value = Parameter(unit='K') + target = Parameter(unit='K') + + def read_value(self): + # using the inherited HasIO.communicate method to send a command and get the reply + reply = self.communicate(f'KRDG?{self.channel}') + return float(reply) + + def read_status(self): + ... determine the status from the hardware and return it ... + return status_code, status_text + + def read_target(self): + ... read back the target value ... + return target + + def write_target(self, target): + ... write here the target to the hardware ... + # important: make sure that the status is changed to BUSY within this method: + self.status = BUSY, 'target changed' + return self.read_target() # return the read back value + + + +.. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies diff --git a/doc/source/reference.rst b/doc/source/reference.rst index f6d3527..f144949 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -1,9 +1,18 @@ Reference --------- +Core +.... + +For convenience everything documented on this page may also be +imported from the frappy.core module. + + Module Base Classes ................... +.. _done unique: + .. autodata:: frappy.modules.Done .. autoclass:: frappy.modules.Module @@ -27,21 +36,61 @@ Parameters, Commands and Properties .. autoclass:: frappy.modules.Attached :show-inheritance: +Access method decorators +........................ + +.. autofunction:: frappy.rwhandler.nopoll + + +.. _datatypes: Datatypes ......... .. autoclass:: frappy.datatypes.FloatRange -.. autoclass:: frappy.datatypes.IntRange -.. autoclass:: frappy.datatypes.BoolType -.. autoclass:: frappy.datatypes.ScaledInteger -.. autoclass:: frappy.datatypes.EnumType -.. autoclass:: frappy.datatypes.StringType -.. autoclass:: frappy.datatypes.TupleOf -.. autoclass:: frappy.datatypes.ArrayOf -.. autoclass:: frappy.datatypes.StructOf -.. autoclass:: frappy.datatypes.BLOBType + :members: __call__ +.. autoclass:: frappy.datatypes.IntRange + :members: __call__ + +.. autoclass:: frappy.datatypes.BoolType + :members: __call__ + +.. autoclass:: frappy.datatypes.ScaledInteger + :members: __call__ + +.. autoclass:: frappy.datatypes.EnumType + :members: __call__ + +.. autoclass:: frappy.datatypes.StringType + :members: __call__ + +.. autoclass:: frappy.datatypes.TupleOf + :members: __call__ + +.. autoclass:: frappy.datatypes.ArrayOf + :members: __call__ + +.. autoclass:: frappy.datatypes.StructOf + :members: __call__ + +.. autoclass:: frappy.datatypes.BLOBType + :members: __call__ + +.. autoclass:: frappy.datatypes.DataTypeType + :members: __call__ + +.. autoclass:: frappy.datatypes.ValueType + :members: __call__ + +.. autoclass:: frappy.datatypes.NoneOr + :members: __call__ + +.. autoclass:: frappy.datatypes.OrType + :members: __call__ + +.. autoclass:: frappy.datatypes.LimitsType + :members: __call__ Communication @@ -51,6 +100,9 @@ Communication :show-inheritance: :members: communicate +.. autoclass:: frappy.io.IOBase + :show-inheritance: + .. autoclass:: frappy.io.StringIO :show-inheritance: :members: communicate, multicomm @@ -62,6 +114,12 @@ Communication .. autoclass:: frappy.io.HasIO :show-inheritance: +.. autoclass:: frappy.lib.asynconn.AsynTcp + :show-inheritance: + +.. autoclass:: frappy.lib.asynconn.AsynSerial + :show-inheritance: + .. autoclass:: frappy.rwhandler.ReadHandler :show-inheritance: :members: @@ -85,5 +143,4 @@ Exception classes .. automodule:: frappy.errors :members: -.. include:: server.rst - +.. include:: configuration.rst \ No newline at end of file diff --git a/doc/source/server.rst b/doc/source/server.rst index d5890e2..1ace342 100644 --- a/doc/source/server.rst +++ b/doc/source/server.rst @@ -1,47 +1,42 @@ +Server +------ + Configuration ............. -The configuration consists of a **NODE** section, an **INTERFACE** section and one -section per SECoP module. +The configuration code consists of a :ref:`Node() ` section, and one +:ref:`Mod() ` section per SECoP module. -The **NODE** section contains a description of the SEC node and a globally unique ID of -the SEC node. Example: +The **Node** section contains a globally unique ID of the SEC node, +a description of the SEC node and the server interface uri. Example: -.. code:: +.. code:: python - [NODE] - description = a description of the SEC node - id = globally.valid.identifier + Node('globally.valid.identifier', + 'a description of the SEC node', + interface = 'tcp://5000') -The **INTERFACE** section defines the server interface. Currently only tcp is supported. -When the TCP port is given as an argument of the server start script, this section is not -needed or ignored. The main information is the port number, in this example 5000: +For the interface scheme currently only tcp is supported. +When the TCP port is given as an argument of the server start script, **interface** is not +needed or ignored. The main information is the port number, in this example 5000. -.. code:: - - [INTERFACE] - uri = tcp://5000 - - -All other sections define the SECoP modules. The section name itself is the module name, -mandatory fields are **class** and **description**. **class** is a path to the Python class -from there the module is instantiated, separated with dots. In the following example the class +All other :ref:`Mod() ` sections define the SECoP modules. +Mandatory fields are **name**, **cls** and **description**. **cls** is a path to the Python class +from where the module is instantiated, separated with dots. In the following example the class **HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory frappy_psi in the python module file ccu4.py: -.. code:: +.. code:: python - [helevel] - class = frappy_psi.ccu4.HeLevel - description = this is the He level sensor of the main reservoir - empty = 380 - empty.export = False - full = 0 - full.export = False + Mod('helevel', + 'frappy_psi.ccu4.HeLevel', + 'this is the He level sensor of the main reservoir', + empty_length = Param(380, export=False), + full = Param(0, export=False)) It is highly recommended to use all lower case for the module name, as SECoP names have to be unique despite of casing. In addition, parameters, properties and parameter properties might -be initialized in this section. In the above example **empty** and **full** are parameters, +be initialized in this section. In the above example **empty_length** and **full_length** are parameters, the resistivity of the He Level sensor at the end of the ranges. In addition, we alter the default property **export** of theses parameters, as we do not want to expose these parameters to the SECoP interface. @@ -54,12 +49,12 @@ The Frappy server can be started via the **bin/frappy-server** script. .. parsed-literal:: - usage: frappy-server [-h] [-v | -q] [-d] name + usage: bin/frappy-server [-h] [-v | -q] [-d] [-t] [-p port] [-c cfgfiles] name Manage a Frappy server positional arguments: - name name of the instance. Uses etc/name.cfg for configuration + name name of the instance. Uses /name_cfg.py for configuration optional arguments: -c, --cfgfiles config files to be used. Comma separated list. diff --git a/doc/source/structure.rst b/doc/source/structure.rst new file mode 100644 index 0000000..dbc685c --- /dev/null +++ b/doc/source/structure.rst @@ -0,0 +1,83 @@ +Structure +--------- + +Node Structure +.............. + +Before starting to write the code for drivers, you have to think about +the node structure. What are the modules I want to create? What is to +be represented as a SECoP module, what as a parameter? At this point +you should not look what the hardware offers (e.g. channels A and B of +a temperature controller), but on what you need for doing an +experiment. Typically, each quantity you measure or control, has to +be represented by a module. You need module parameters for influencing +how you achieve or control the quantity. And you will need configurable +internal properties to configure the access to the hardware. + + +Examples: + +- A temperature sensor, without an attached control loop, should inherit + from :class:`Readable ` + +- A temperature sensor with a control loop should inherit from + :class:`Drivable `. You will need to implement a criterion for + deciding when the temperature is reached (e.g. tolerance and time window) + +- If the heater power is a quantity of interest, it should be its own + module inheriting from :class:`Writable `. + +- If it is a helium cryostat, you may want to implement a helium level + reading module inheriting from :class:`Readable ` + + +.. _module structure parameters: + +Module Structure: Parameters +............................ + +The next step is to determine which parameters we need in addition to +the standard ones given by the inherited class. As a temperature sensor +inherits from :class:`Readable `, it has already a ``value`` +parameter representing the measured temperature. It has also a +``status`` parameter, indicating whether the measured temperature is +valid (``IDLE``), invalid (``ERROR``) or there might be a less +critical issue (``WARN``). In addition you might want additional +parameters, like an alarm threshold. + +For the controlled temperature, in addition to above, inherited from +:class:`Drivable ` it has a writable ``target`` parameter. +In addition we might need control parameters or a changeable target limits. + +For the heater you might want to have a changeable power limit or power range. + + +.. _module structure properties: + +Module Structure: Properties +............................ + +For the access to the hardware, we will need internal properties for +configuring the hardware access. This might the IP address of a +LAN connection or the path of an internal serial device. +In Frappy, when inheriting from the mixin :class:`HasIO `, +either the property ``io`` referring to an explicitly configured +communicator or the ``uri`` property, generating a communicator with +the given uri can be used for this. + +In addition, depending on the hardware probably you need a property to +configure the channel number or name assigned to the module. + +For the heater output, you might need to configure the heater resistance. + + +Parameter Structure +................... + +A parameter also has properties, which have to be set when declaring +the parameter. Even for the inherited parameters, often the properties +have to be overriden. For example, the ``unit`` property of the ``value`` +parameter on the temperature sensor will be set to 'K', and the ``max`` +property of the ``target`` parameter should be set to the maximum possible +value for the hardware. This value may then probably get more restricted +by an entry in the configuration file. \ No newline at end of file diff --git a/doc/source/tutorial.rst b/doc/source/tutorial.rst index c349f65..e993c64 100644 --- a/doc/source/tutorial.rst +++ b/doc/source/tutorial.rst @@ -5,3 +5,4 @@ Tutorial :maxdepth: 2 tutorial_helevel + tutorial_t_control diff --git a/doc/source/tutorial_t_control.rst b/doc/source/tutorial_t_control.rst new file mode 100644 index 0000000..420d294 --- /dev/null +++ b/doc/source/tutorial_t_control.rst @@ -0,0 +1,406 @@ +A Simple Temperature Controller +=============================== + +The Use Case +------------ + +Let us assume we have simple cryostat or furnace with one temperature sensor +and a heater. We want first to implement reading the temperature and then +add the control loop. Assume also we have a LakeShore temperature controller +to access the hardware. + + +Coding the Sensor Module +------------------------ + +A temperature sensor without control loop is to be implemented as a subclass +of :class:`Readable `. You create this example to be used in your +facility, so you add it to the subdirectory of your facility. You might need +to create it, if it is not already there. In this example, you may +replace *frappy_psi* by *frappy_*. The name the python file +is chosen from the type of temperature controller *lakeshore.py*. + +We assume that the temperature controller is already configured with input ``A`` +being used, and the proper calibration curve assigned. In productive code +this configuration may also be done by Frappy, but this would extend the scope +of this tutorial too much. + +So we define a class and define the parameter properties for the value: + +``frappy_psi/lakeshore.py``: + +.. code:: python + + # the most common Frappy classes can be imported from frappy.core + from frappy.core import Readable, Parameter, FloatRange + + class TemperatureSensor(Readable): + """a temperature sensor (generic for different models)""" + # 1500 is the maximum T allowed for most of the lakeshore models + # this should be further restricted in the configuration (see below) + value = Parameter(datatype=FloatRange(0, 1500, unit='K')) + + +For the next step, we have to code how to retrieve the temperature +from the controller. For this we add the method ``read_value``. +In addition, we have to define a communicator class, and make +``TemperatureSensor`` inherit from :class:`HasIO ` +in order to add the :meth:`communicate` method to the class. + +See :ref:`lsc_manual_extract` for details of the needed commands. + + +.. code:: python + + from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType + + class LakeshoreIO(StringIO): + wait_before = 0.05 # Lakeshore requires a wait time of 50 ms between commands + # '*IDN?' is sent on connect, and the reply is checked to match the regexp 'LSCI,.*' + identification = [('*IDN?', 'LSCI,.*')] + + class TemperatureSensor(HasIO, Readable): + """a temperature sensor (generic for different models)""" + # internal property to configure the channel + # see below for the difference of 'Property' and 'Parameter' + channel = Property('the Lakeshore channel', datatype=StringType()) + # 0, 1500 is the allowed range by the LakeShore controller + # this should be further restricted in the configuration (see below) + value = Parameter(datatype=FloatRange(0, 1500, unit='K')) + + def read_value(self): + # the communicate method sends a command and returns the reply + reply = self.communicate(f'KRDG?{self.channel}') + # convert to float + return float(reply) + + +This is the code to run a minimalistic SEC Node, which does just read a temperature +and nothing else. + +.. Note:: + + A :class:`Property ` is used instead of a + :class:`Parameter `, for a configurable item not changing + on run time. A ``Property`` is typically only internal needed and by default not + visible by SECoP. + + +Before we start the frappy server for the first time, we have to create a configuration file. +The directory tree of the Frappy framework contains the code for all drivers but the +configuration file determines, which code will be loaded when a server is started. +We choose the name *example_cryo* and create therefore a configuration file +*example_cryo_cfg.py* in the *cfg* subdirectory: + +``cfg/example_cryo_cfg.py``: + +.. code:: python + + Node('example_cryo.psi.ch', # a globally unique identification + 'this is an example cryostat for the Frappy tutorial', # describes the node + interface='tcp://10767') # you might choose any port number > 1024 + Mod('io', # the name of the module + 'frappy_psi.lakeshore.LakeshoreIO', # the class used for communication + 'communication to main controller', # a description + # the serial connection, including serial settings (see frappy.io.IOBase): + uri='serial://COM6:?baudrate=57600+parity=odd+bytesize=7', + ) + Mod('T', + 'frappy_psi.lakeshore.TemperatureSensor', + 'Sample Temperature', + io='io', # refers to above defined module 'io' + channel='A', # the channel on the LakeShore for this module + value=Param(max=470), # alter the maximum expected T + ) + +The first section in the configuration file configures the common settings for the server. +:ref:`Node ` describes the main properties of the SEC Node: an identifier, +which should be globally unique, a description of the node, and an interface defining the server address. +Usually the only important value in the server address is the TCP port under which the +server will be accessible. Currently only the tcp scheme is supported. + +Then for each module a :ref:`Mod ` section follows. +We have to create the ``io`` module for communication first, with +the ``uri`` as its most important argument. +In case of a serial connection the prefix is ``serial://``. On a Windows machine, the full +uri is something like ``serial://COM6:?baudrate=9600`` on a linux system it might be +``serial:///dev/ttyUSB0?baudrate=9600``. In case of a LAN connection, the uri should +be something like ``tcp://129.129.138.78:7777`` or ``tcp://mydevice.psi.ch:7777``, where +7777 is the tcp port the LakeShore is listening to. + +Now, we are ready to start our first server. In the main frappy directory, we +start it with: + +.. code:: + + python bin/frappy-server example_cryo + +If error messages appear, you have first to try to fix the errors. +Else you might open an other console or terminal, in order to start +a frappy client, for example the GUI client. The argument is +compose by the machine running the server and the server port chosen +in the configuration file: + +.. code:: + + python bin/frappy-gui localhost:10767 + + +A ``Readable`` SECoP module also has a status parameter. Until now, we completely +ignored it. As you may see, the value of status parameter is always ``(IDLE, '')``. +However, we should implement the status parameter to give information about the +validity of the sensor reading. The controller has a query command ``RDGST?`` +returning a code describing error states. We implement this by adding a the +``read_status`` method to the class: + +.. code:: python + + from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,\ + IDLE, ERROR + + ... + + class TemperatureSensor(HasIO, Readable): + + ... + + def read_status(self): + code = int(self.communicate(f'RDGST?{self.channel}')) + if code >= 128: + text = 'units overrange' + elif code >= 64: + text = 'units zero' + elif code >= 32: + text = 'temperature overrange' + elif code >= 16: + text = 'temperature underrange' + elif code % 2: + # ignore 'old reading', as this may happen in normal operation + text = 'invalid reading' + else: + return IDLE, '' + return ERROR, text + +After a restart of the server and the client, the status should change to +``ERROR, ''`` when the sensor is unplugged. + + +Extend the Class to a Temperature Loop +-------------------------------------- + +As we want to implement also temperature control, we have extend the class more. +Instead of adding just more methods to the ``TemperatureSensor`` class, we +create a new class ``TemperatureLoop`` inheriting from Temperature sensor. +This way, we would for example be able to create a node with a controlled +temperature on one channel, and a sensor module without control on an other channel. + +Temperature control is represented by a subclass of :class:`Drivable `. +So our new class will be based on ``TemperatureSensor`` where we have already +implemented the readable stuff. We need to define some properties of the ``target`` +parameter and add a property ``loop`` indicating, which control loop and +heater output we use. + +In addition, we have to implement the methods ``write_target`` and ``read_target``: + +.. code:: python + + from frappy.core import Readable, Parameter, FloatRange, HasIO, StringIO, Property, StringType,\ + IDLE, BUSY, WARN, ERROR, Drivable, IntRange + + ... + + class TemperatureLoop(TemperatureSensor, Drivable): + # lakeshore loop number to be used for this module + loop = Property('lakeshore loop', IntRange(1, 2), default=1) + target = Parameter(datatype=FloatRange(unit='K', min=0, max=1500)) + + def write_target(self, target): + # we always use a request / reply scheme + reply = self.communicate(f'SETP {self.loop},{target};SETP?{self.loop}') + return float(reply) + + def read_target(self): + return float(self.communicate(f'SETP?{self.loop}')) + +In order to test this, we will need to change the entry module ``T`` in the +configuration file: + +.. code:: python + + Mod('T', + 'frappy_psi.lakeshore.TemperatureLoop', + 'Sample Temperature', + io='io', + channel='A', # the channel on the LakeShore for this module + loop=1, # the loop to be used + value=Param(max=470), # set the maximum expected T + target=Param(max=420), # set the maximum allowed target T + ) + +To test that this step worked, just restart the server and the client. +If the temperature controller is not yet configured for controlling the +temperature on channel A with loop 1, this has to be done first. +Especially the heater has to be switched on, setting the maximum heater +range. + +There are two things still missing: + +- We want to switch on the heater automatically, when the target is changed. + A property ``heater_range`` is added for this. +- We want to handle the status code correctly: set to ``BUSY`` when the + target is changed, and back to ``IDLE`` when the target temperature is reached. + The parameter ``tolerance`` is used for this. For the tutorial we use here + a rather simple mechanism. In reality, often over- or undershoot happens. + A better algorithm would not switch to IDLE before the temperature was within + tolerance for some given time. + + +.. code:: python + + from frappy.core import Readable, Drivable, Parameter, FloatRange, \ + HasIO, StringIO, IDLE, BUSY, WARN, ERROR + + ... + + class TemperatureLoop(TemperatureSensor, Drivable): + ... + heater_range = Property('heater power range', IntRange(0, 5)) # max. 3 on LakeShore 336 + tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly=False) + _driving = False + ... + + def write_target(self, target): + # reactivate heater in case it was switched off + self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}') + reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}') + self._driving = True + # Setting the status attribute triggers an update message for the SECoP status + # parameter. This has to be done before returning from this method! + self.status = BUSY, 'target changed' + return float(reply) + + ... + + def read_status(self): + code = int(self.communicate(f'RDGST?{self.channel}')) + if code >= 128: + text = 'units overrange' + elif code >= 64: + text = 'units zero' + elif code >= 32: + text = 'temperature overrange' + elif code >= 16: + text = 'temperature underrange' + elif code % 2: + # ignore 'old reading', as this may happen in normal operation + text = 'invalid reading' + elif abs(self.target - self.value) > self.tolerance: + if self._driving: + return BUSY, 'approaching setpoint' + return WARN, 'temperature out of tolerance' + else: # within tolerance: simple convergence criterion + self._driving = False + return IDLE, '' + return ERROR, text + + +Finally, the config file would be: + +``cfg/example_cryo_cfg.py``: + +.. code:: python + + Node('example_cryo.psi.ch', # a globally unique identification + 'this is an example cryostat for the Frappy tutorial', # describes the node + interface='tcp://10767') # you might choose any port number > 1024 + Mod('io', # the name of the module + 'frappy_psi.lakeshore.LakeshoreIO', # the class used for communication + 'communication to main controller', # a description + uri='serial://COM6:?baudrate=57600+parity=odd+bytesize=7', # the serial connection + ) + Mod('T', + 'frappy_psi.lakeshore.TemperatureLoop', + 'Sample Temperature', + io='io', + channel='A', # the channel on the LakeShore for this module + loop=1, # the loop to be used + value=Param(max=470), # set the maximum expected T + target=Param(max=420), # set the maximum allowed target T + heater_range=3, # 5 for model 350 + ) + + +Now, you should try again restarting the server and the client, if it works, you have done a good job! +If not, you might need to fix the code first ... + + +More Complex Configurations +........................... + +Without coding any more class, much more complex situations might be realized just by +extending the configuration. Using a single LakeShore controller, you might add more +temperature sensors or (in the case of Model 336 or 350) even a second temperature loop, +just by adding more ``Mod(`` sections to the configuration file. In case more than 4 channels +are needed, an other module ``io2`` has to be added for the second controller and so on. + + +Appendix 1: The Solution +------------------------ + +You will find the full solution code via the ``[source]`` link in the automatic +created documentation of the class :class:`frappy_demo.lakeshore.TemperatureLoop`. + + + +.. _lsc_manual_extract: + +Appendix 2: Extract from the LakeShore Manual +--------------------------------------------- + +.. table:: commands used in this tutorial + + ====================== ======================= + **Query Identification** + ---------------------------------------------- + Command \*IDN? *term* + Reply ,,/