diff --git a/doc/source/client/index.rst b/doc/source/client/index.rst deleted file mode 100644 index 313ab1a..0000000 --- a/doc/source/client/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Client documentation -==================== - -.. toctree:: - :maxdepth: 2 - diff --git a/doc/source/conf.py b/doc/source/conf.py index 2832a16..273fc13 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- # -# SECoP documentation build configuration file, created by +# Frappy documentation build configuration file, created by # sphinx-quickstart on Mon Sep 11 10:58:28 2017. # # This file is execfile()d with the current directory set to its @@ -57,9 +57,9 @@ source_suffix = ['.rst', '.md'] master_doc = 'index' # General information about the project. -project = 'SECoP' -#copyright = '2017, Enrico Faulhaber, Markus Zolliker' -copyright = '2017, SECoP Committee' +project = 'Frappy' +copyright = '2017-2021, Enrico Faulhaber, Markus Zolliker,' +#copyright = '2017, SECoP Committee' author = 'Enrico Faulhaber, Markus Zolliker' # The version info for the project you're documenting, acts as replacement for @@ -89,6 +89,9 @@ pygments_style = 'sphinx' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True +# sort by source instead of alphabetic +autodoc_member_order = 'bysource' + default_role = 'any' # -- Options for HTML output ---------------------------------------------- @@ -136,7 +139,7 @@ html_sidebars = { # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'SECoPdoc' +htmlhelp_basename = 'Frappydoc' # -- Options for LaTeX output --------------------------------------------- @@ -163,7 +166,7 @@ latex_elements = { # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'SECoP.tex', 'SECoP source documentation', + (master_doc, 'Frappy.tex', 'Frappy source documentation', 'Enrico Faulhaber, Markus Zolliker', 'manual'), ] @@ -173,7 +176,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'secop', 'SECoP source documentation', + (master_doc, 'frappy', 'Frappy source documentation', [author], 1) ] @@ -184,8 +187,8 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'SECoP', 'SECoP source documentation', - author, 'SECoP', 'One line description of project.', + (master_doc, 'Frappy', 'Frappy source documentation', + author, 'Frappy', 'One line description of project.', 'Miscellaneous'), ] diff --git a/doc/source/facility/index.rst b/doc/source/facility/index.rst index c1cd03f..0b83d17 100644 --- a/doc/source/facility/index.rst +++ b/doc/source/facility/index.rst @@ -7,3 +7,4 @@ Facility specific functionalities demo/index mlz/index ess/index + psi/index diff --git a/doc/source/facility/psi/index.rst b/doc/source/facility/psi/index.rst new file mode 100644 index 0000000..36b46e1 --- /dev/null +++ b/doc/source/facility/psi/index.rst @@ -0,0 +1,10 @@ +PSI +=== + +.. toctree:: + :maxdepth: 3 + + ppms + ls370res + + diff --git a/doc/source/facility/psi/ls370res.rst b/doc/source/facility/psi/ls370res.rst new file mode 100644 index 0000000..81c7e32 --- /dev/null +++ b/doc/source/facility/psi/ls370res.rst @@ -0,0 +1,7 @@ +LakeShore 370 resistivity +========================= + +.. automodule:: secop_psi.ls370res + :members: + + diff --git a/doc/source/facility/psi/ppms.rst b/doc/source/facility/psi/ppms.rst new file mode 100644 index 0000000..a2e9fac --- /dev/null +++ b/doc/source/facility/psi/ppms.rst @@ -0,0 +1,7 @@ +PPMS +==== + +.. automodule:: secop_psi.ppms + :members: + + diff --git a/doc/source/framework.rst b/doc/source/framework.rst new file mode 100644 index 0000000..4f5b307 --- /dev/null +++ b/doc/source/framework.rst @@ -0,0 +1,67 @@ +Framework documentation +======================= + + +Module Base Classes +------------------- + +.. autoclass:: secop.core.Module + :members: startModule + +.. autoclass:: secop.core.Readable + :members: pollerClass, Status + +.. autoclass:: secop.core.Writable + +.. autoclass:: secop.core.Drivable + :members: Status, isBusy, isDriving, do_stop + + +Parameters, Commands and Properties +----------------------------------- + +.. autoclass:: secop.core.Parameter +.. autoclass:: secop.core.Command +.. autoclass:: secop.core.Override +.. autoclass:: secop.core.Property +.. autoclass:: secop.core.Attached + + +Datatypes +--------- + +.. autoclass:: secop.core.FloatRange +.. autoclass:: secop.core.IntRange +.. autoclass:: secop.core.BoolType +.. autoclass:: secop.core.ScaledInteger +.. autoclass:: secop.core.EnumType +.. autoclass:: secop.core.StringType +.. autoclass:: secop.core.TupleOf +.. autoclass:: secop.core.ArrayOf +.. autoclass:: secop.core.StructOf +.. autoclass:: secop.core.BLOBType + + +Communication +------------- + +.. autoclass:: secop.core.Communicator + :members: do_communicate + +.. autoclass:: secop.core.StringIO + :members: do_communicate, do_multicomm + +.. autoclass:: secop.core.HasIodev + +.. autoclass:: secop.core.IOHandlerBase + :members: + +.. autoclass:: secop.core.IOHandler + :members: + + +Exception classes +----------------- + +.. automodule:: secop.errors + :members: diff --git a/doc/source/framework/datatypes.rst b/doc/source/framework/datatypes.rst deleted file mode 100644 index 7829321..0000000 --- a/doc/source/framework/datatypes.rst +++ /dev/null @@ -1,6 +0,0 @@ -Datatypes -========= - -.. automodule:: secop.datatypes - :members: - diff --git a/doc/source/framework/errors.rst b/doc/source/framework/errors.rst deleted file mode 100644 index 374fa48..0000000 --- a/doc/source/framework/errors.rst +++ /dev/null @@ -1,6 +0,0 @@ -Exception classes -================= - -.. automodule:: secop.errors - :members: - diff --git a/doc/source/framework/index.rst b/doc/source/framework/index.rst deleted file mode 100644 index fe99d05..0000000 --- a/doc/source/framework/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Framework documentation -======================= - -.. toctree:: - :maxdepth: 2 - - datatypes - errors - diff --git a/doc/source/gui/index.rst b/doc/source/gui/index.rst deleted file mode 100644 index 0b1d2f8..0000000 --- a/doc/source/gui/index.rst +++ /dev/null @@ -1,6 +0,0 @@ -Graphical user interface documentation -====================================== - -.. toctree:: - :maxdepth: 2 - diff --git a/doc/source/index.rst b/doc/source/index.rst index 46a1f63..970728d 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -1,13 +1,12 @@ -Welcome to FRAPPY documentation! -================================ +Welcome to the FRAPPY documentation! +==================================== .. toctree:: :maxdepth: 2 - server/index - client/index - framework/index - gui/index + tutorial/tutorial + server + framework facility/index diff --git a/doc/source/server.rst b/doc/source/server.rst new file mode 100644 index 0000000..2e7abb3 --- /dev/null +++ b/doc/source/server.rst @@ -0,0 +1,77 @@ +Configuring and Starting +======================== + +Configuration +------------- + +The configuration consists of a **NODE** section, an **INTERFACE** section and one +section per SECoP module. + +The **NODE** section contains a description of the SEC node and a globally unique ID of +the SEC node. Example: + +.. code:: + + [NODE] + description = a description of the SEC node + id = globally.valid.identifier + +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: + +.. 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 +**HeLevel** used by the **helevel** module can be found in the PSI facility subdirectory +secop_psi in the python module file ccu4.py: + +.. code:: + + [helevel] + class = secop_psi.ccu4.HeLevel + description = this is the He level sensor of the main reservoir + empty = 380 + empty.export = False + full = 0 + full.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, +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. + + +Starting +-------- + +The Frappy server can be started via the **bin/secop-server** script. + +.. parsed-literal:: + + usage: secop-server [-h] [-v | -q] [-d] name + + Manage a Frappy server + + positional arguments: + name name of the instance. Uses etc/name.cfg for configuration + + optional arguments: + -c, --cfgfiles config files to be used. Comma separated list. + defaults to when omitted + -p, --port server port (default: take from cfg file) + -h, --help show this help message and exit + -v, --verbose output lots of diagnostic information + -q, --quiet suppress non-error messages + -d, --daemonize run as daemon + -t, --test check cfg files only + + diff --git a/doc/source/server/configuration.rst b/doc/source/server/configuration.rst deleted file mode 100644 index ddb2814..0000000 --- a/doc/source/server/configuration.rst +++ /dev/null @@ -1,3 +0,0 @@ -Configuration -============= - diff --git a/doc/source/server/index.rst b/doc/source/server/index.rst deleted file mode 100644 index f30bf7b..0000000 --- a/doc/source/server/index.rst +++ /dev/null @@ -1,11 +0,0 @@ -Server documentation -==================== - -.. toctree:: - :maxdepth: 3 - - starting - configuration - modules - protocol/index - diff --git a/doc/source/server/modules.rst b/doc/source/server/modules.rst deleted file mode 100644 index 3368a98..0000000 --- a/doc/source/server/modules.rst +++ /dev/null @@ -1,6 +0,0 @@ -Module base classes -=================== - -.. automodule:: secop.modules - :members: - diff --git a/doc/source/server/protocol/index.rst b/doc/source/server/protocol/index.rst deleted file mode 100644 index 2b01466..0000000 --- a/doc/source/server/protocol/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -protocol stack -============== - -.. toctree:: - :maxdepth: 3 - - interface/index - diff --git a/doc/source/server/protocol/interface/index.rst b/doc/source/server/protocol/interface/index.rst deleted file mode 100644 index 2a3f5cf..0000000 --- a/doc/source/server/protocol/interface/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Interfaces -========== - -.. toctree:: - :maxdepth: 3 - - tcp - zmq - diff --git a/doc/source/server/protocol/interface/tcp.rst b/doc/source/server/protocol/interface/tcp.rst deleted file mode 100644 index 7bc09da..0000000 --- a/doc/source/server/protocol/interface/tcp.rst +++ /dev/null @@ -1,6 +0,0 @@ -TCP -=== - -.. automodule:: secop.protocol.interface.tcp - :members: - diff --git a/doc/source/server/protocol/interface/zmq.rst b/doc/source/server/protocol/interface/zmq.rst deleted file mode 100644 index d184207..0000000 --- a/doc/source/server/protocol/interface/zmq.rst +++ /dev/null @@ -1,6 +0,0 @@ -ZMQ -=== - -.. automodule:: secop.protocol.interface.zmq - :members: - diff --git a/doc/source/server/starting.rst b/doc/source/server/starting.rst deleted file mode 100644 index 620ff94..0000000 --- a/doc/source/server/starting.rst +++ /dev/null @@ -1,21 +0,0 @@ -Starting -======== - -The SECoP server can be started via the ``bin/secop-server`` script. - -.. parsed-literal:: - - usage: secop-server [-h] [-v | -q] [-d] name - - Manage a SECoP server - - positional arguments: - name Name of the instance. Uses etc/name.cfg for configuration - - optional arguments: - -h, --help show this help message and exit - -v, --verbose Output lots of diagnostic information - -q, --quiet suppress non-error messages - -d, --daemonize Run as daemon - - diff --git a/doc/source/tutorial/tutorial.rst b/doc/source/tutorial/tutorial.rst new file mode 100644 index 0000000..d56ad4d --- /dev/null +++ b/doc/source/tutorial/tutorial.rst @@ -0,0 +1,228 @@ +Frappy Programming Guide +======================== + +Introduction +------------ +*Frappy* is a Python framework for creating Sample Environment Control Nodes (SEC Node) with a SECoP interface. A *SEC Node* is a service, running usually a computer or microcomputer, which accesses the hardware over the interfaces given by the manufacturer of the used electronic devices. It provides access to the data in an abstracted form over the SECoP interface. [*SECoP*](https://github.com/SampleEnvironment/SECoP/tree/master/protocol) is a protocol for communicating with Sample Environment and other mobile devices, specified by a committee of the [ISSE](https://sampleenvironment.org). The Frappy framework deals with all the details of the SECoP protocol, so the programmer can concentrate on the details of accessing the hardware with support for different types of interfaces (TCP or Serial, ASCII or binary). However, the programmer should be aware of the basic principle of the SECoP protocol: the hardware abstraction. + +Hardware Abstraction +-------------------- + +The idea of hardware abstraction is to hide the details of hardware access from the SECoP interface. +A SECoP module is a logical component of an abstract view of the sample environment. +It is one independent value of measurement like a temperature or physical output like a current or voltage. +This corresponds roughly to an EPICS channel or a NICOS device. On the hardware side we may have devices +with several channels, like a typical temperature controller, which will be represented individual SECoP modules. +On the other hand a SECoP channel might be linked with several hardware devices, for example if you imagine +a superconducting magnet controller built of seperate electronic devices like a power supply, switch heater +and coil temperature monitor. The latter case does not mean that we have to hide complete the details in the +SECoP interface. For an expert it might be useful to give at least read access to hardware specific data +by providing them as seperate SECoP modules. But the magnet module should be usable without knowledge of +all the inner details. + +A SECoP module has: + +* **properties**: static information describing the module, for example a human readable *description* of + the module or information about the intended *visibiliy*. +* **parameters**: changing information about the state of a module (for example the *status* containing + information about the state of the module )or modifiable information influencing the measurement + (for example a "ramp" rate) +* **commands**: actions, for example *stop* + +A SECoP module belongs to an interface class, mainly *Readable* or *Drivable*. A *Readable* has at least the +parameters *value* and *status*, a *Drivable* in addition *target*. *value* is the main value of the module +and is read only. *status* is a tuple (status code, status text), and *target* is the target value. +When the *target* parameter value of a *Drivable* changes, the status code changes normally to a busy code. +As soon as the target value is reached, the status code changes back to an idle code, if no error occurs. + +**Programmers Hint:** before starting to code, choose carefully the main SECoP modules you have to provide +to the user. + +Tutorial Example +---------------- +For this tutorial we choose as an example a cryostat with a LakeShore 336 temperature controller, a level +meter and a motorized needle value. Let us start with the level meter, as this is the simplest module. + +Configuration +------------- +Before we start coding, we create a configuration file. The frappy framework usually has all present code +in the directory tree, and the server is started with the configuration as an argument, determining which +modules are to be configured, ans which code is effectively to be used. We choose the name *example_cryo* +and create therefore a configuration file *example_cryo.cfg* in the *cfg* subdirectory. + +Let us start with a simple configuration for the level meter only: + +``cfg/example_cryo.cfg``: + +.. code:: ini + + [NODE] + description = this is an example cryostat for the Frappy tutorial + id = example_cryo.sampleenvironment.org + + [INTERFACE] + uri = tcp://5000 + + [helev] + description = He level of the cryostat He reservoir + class = secop_psi.ccu4.HeLevel + uri = linse-moxa-4.psi.ch:3001 + + # TO BE MOVED + [tmain] + description = main (heat exchange) temperature + class = secop_psi.ls336.ControlledChannel + iodev = lsio + +The configuration file contains several section starting with a line in rectangular brackets. + +The *NODE* section describes the main properties of the SEC Node: a description of the node and +an id, which should be globally unique. + +The *INTERFACE* section defines the address of the server, usually the only important value here +is the TCP port under which the server will be accessible. Currently only tcp is supported. + +All the other sections define the SECoP modules to be used. A module section at least contain a +human readable *description*, and the Python *class* used. Other properties or parameter values may +follow, in this case the *uri* for the communication with the He level monitor. + +Code the Python Class for the Module +------------------------------------ +As mentioned in the introduction, we have to code the access to the hardware (driver), and the Frappy +framework will deal with the SECoP interface. The code for the driver is located in a subdirectory +named after the facility or institute programming the driver in our case *secop_psi*. +We create a file named from the electronic device CCU4 we use here for the He level reading. +CCU4 luckily has a very simple and logical protocol: + +* ``=\n`` sets the parameter named ```` to the value ```` +* ``\n`` reads the parameter named ```` +* in both cases, the reply is ``=\n`` + +``secop_psi/ccu4.py``: + +.. code:: python + + # the most common classes can be imported from secop.core + from secop.core import Readable, Parameter, Override, FloatRange, BoolType, \ + StringIO, HasIodev + + # inheriting HasIodev mixin creates us the things needed for talking + # with a device by means of the sendRecv method + # Readable as a base class defines the value and status parameters + + class HeLevel(HasIodev, Readable): + """He Level channel of CCU4""" + + # define or alter the parameters + parameters = { + # we are changing the 'unit' parameter property of the inherited 'value' + # parameter, therefore 'Override' + 'value': Override(unit='%'), + } + # tells us how to communicate. StringIO is using \n as line end, which fits + iodevClass = StringIO + + def read_value(self): + # method for reading the main value + reply = self.sendRecv('h') # send 'h\n' and get the reply 'h=\n' + name, txtvalue = reply.split('=') + assert name == 'h' # check that we got a reply to our command + return txtvalue # the framework will automatically convert the string to a float + +This is already a very simple working He Level meter driver. For a next step, we want to improve it: + +* We should tell the client, when there is an error. That is what the *status* parameter is for. + We do not need to declare the status parameter, as it is inherited from *Readable*. +* We want to be able to configure the He Level sensor and we want to be able to switch the + Level Monitor to fast reading before we start to fill. + +Let us start to code these additions: + +.. code:: python + + ... + # define or alter the parameters + parameters = { + + ... + + # the first two arguments to Parameter are 'description' and 'datatype' + # it is highly recommended to define always the physical unit + 'empty': Parameter('warm length when empty', FloatRange(0, 2000), + readonly=False, unit='mm'), + 'full': Parameter('warm length when full', FloatRange(0, 2000), + readonly=False, unit='mm'), + 'fast': Parameter('fast reading', BoolType(), + readonly=False), + } + + ... + + Status = Readable.Status + + STATUS_MAP = { + 0: (Status.IDLE, 'sensor ok'), + 1: (Status.ERROR, 'sensor warm'), + 2: (Status.ERROR, 'no sensor'), + 3: (Status.ERROR, 'timeout'), + 4: (Status.ERROR, 'not yet read'), + 5: (Status.DISABLED, 'disabled'), + } + + def read_status(self): + name, txtvalue = self.sendRecv('hsf').split('=') + assert name == 'hsf' + return self.STATUS_MAP(int(txtvalue)) + + def read_emtpy(self): + name, txtvalue = self.sendRecv('hem').split('=') + assert name == 'hem' + return txtvalue + + def write_empty(self, value): + name, txtvalue = self.sendRecv('hem=%g' % value).split('=') + assert name == 'hem' + return txtvalue + + ... + +We realize now, that we will repeat similar code for other parameters, which means it might be +worth to create our own *_sendRecv* method, and then the *read_* and *write_* methods +will become shorter: + +.. code:: python + + ... + + def _sendRecv(self, cmd): + # method may be used for reading and writing parameters + name, txtvalue = self.sendRecv(cmd).split('=') + assert name == cmd.split('=')[0] # check that we got a reply to our command + return txtvalue # the framework will automatically convert the string to a float + + def read_value(self): + return self._sendRecv('h') + + ... + + def read_status(self): + return self.STATUS_MAP(int(self._sendRecv('hsf'))) + + def read_empty(self): + return self._sendRecv('hem') + + def write_empty(self, value): + return self._sendRecv('hem=%g' % value) + + def read_full(self): + return self._sendRecv('hfu') + + def write_full(self, value): + return self._sendRecv('hfu=%g' % value) + + def read_fast(self): + return self._sendRecv('hf') + + def write_fast(self, value): + return self._sendRecv('hf=%s' % value) + diff --git a/secop/core.py b/secop/core.py index d792c4c..8ea1cd4 100644 --- a/secop/core.py +++ b/secop/core.py @@ -36,3 +36,4 @@ from secop.metaclass import Done from secop.iohandler import IOHandler, IOHandlerBase from secop.stringio import StringIO, HasIodev from secop.proxy import SecNode, Proxy, proxy_class +from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC diff --git a/secop/datatypes.py b/secop/datatypes.py index 9ff6b92..b34a611 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -53,8 +53,8 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for Parser = Parser() -# base class for all DataTypes class DataType(HasProperties): + """base class for all data types""" IS_COMMAND = False unit = '' default = None @@ -157,8 +157,14 @@ class Stub(DataType): # SECoP types: + class FloatRange(DataType): - """Restricted float type""" + """(restricted) float type + + :param minval: (property **min**) + :param maxval: (property **max**) + """ + properties = { 'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max), 'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max), @@ -170,11 +176,11 @@ class FloatRange(DataType): extname='relative_resolution', default=1.2e-7), } - def __init__(self, minval=None, maxval=None, **kwds): + def __init__(self, minval=None, maxval=None, **properties): super().__init__() - kwds['min'] = minval if minval is not None else -sys.float_info.max - kwds['max'] = maxval if maxval is not None else sys.float_info.max - self.set_properties(**kwds) + properties['min'] = minval if minval is not None else -sys.float_info.max + properties['max'] = maxval if maxval is not None else sys.float_info.max + self.set_properties(**properties) def checkProperties(self): self.default = 0 if self.min <= 0 <= self.max else self.min @@ -236,7 +242,11 @@ class FloatRange(DataType): class IntRange(DataType): - """Restricted int type""" + """restricted int type + + :param minval: (property **min**) + :param maxval: (property **max**) + """ properties = { 'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True), 'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True), @@ -296,10 +306,14 @@ class IntRange(DataType): class ScaledInteger(DataType): - """Scaled integer int type + """scaled integer (= fixed resolution float) type - note: limits are for the scaled value (i.e. the internal value) - the scale is only used for calculating to/from transport serialisation""" + :param minval: (property **min**) + :param maxval: (property **max**) + + note: limits are for the scaled float value + the scale is only used for calculating to/from transport serialisation + """ properties = { 'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True), 'min': Property('low limit', FloatRange(), extname='min', mandatory=True), @@ -312,7 +326,7 @@ class ScaledInteger(DataType): extname='relative_resolution', default=1.2e-7), } - def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds): + def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **properties): super().__init__() scale = float(scale) if absolute_resolution is None: @@ -321,7 +335,7 @@ class ScaledInteger(DataType): min=DEFAULT_MIN_INT * scale if minval is None else float(minval), max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval), absolute_resolution=absolute_resolution, - **kwds) + **properties) def checkProperties(self): self.default = 0 if self.min <= 0 <= self.max else self.min @@ -401,14 +415,20 @@ class ScaledInteger(DataType): class EnumType(DataType): + """enumeration - def __init__(self, enum_or_name='', **kwds): + :param enum_or_name: the name of the Enum or an Enum to inherit from + :param members: members= + + other keywords: (additional) members + """ + def __init__(self, enum_or_name='', **members): super().__init__() - if 'members' in kwds: - kwds = dict(kwds) - kwds.update(kwds['members']) - kwds.pop('members') - self._enum = Enum(enum_or_name, **kwds) + if 'members' in members: + members = dict(members) + members.update(members['members']) + members.pop('members') + self._enum = Enum(enum_or_name, **members) self.default = self._enum[self._enum.members[0]] def copy(self): @@ -448,6 +468,10 @@ class EnumType(DataType): class BLOBType(DataType): + """binary large object + + internally treated as bytes + """ properties = { 'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes', default=0), @@ -511,6 +535,9 @@ class BLOBType(DataType): class StringType(DataType): + """string + + """ properties = { 'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED), extname='minchars', default=0), @@ -520,11 +547,11 @@ class StringType(DataType): Stub('BoolType'), extname='isUTF8', default=False), } - def __init__(self, minchars=0, maxchars=None, **kwds): + def __init__(self, minchars=0, maxchars=None, **properties): super().__init__() if maxchars is None: maxchars = minchars or UNLIMITED - self.set_properties(minchars=minchars, maxchars=maxchars, **kwds) + self.set_properties(minchars=minchars, maxchars=maxchars, **properties) def checkProperties(self): self.default = ' ' * self.minchars @@ -602,6 +629,9 @@ class TextType(StringType): class BoolType(DataType): + """boolean + + """ default = False def export_datatype(self): @@ -646,6 +676,9 @@ Stub.fix_datatypes() class ArrayOf(DataType): + """data structure with fields of homogeneous type + + """ properties = { 'minlen': Property('minimum number of elements', IntRange(0), extname='minlen', default=0), @@ -743,6 +776,9 @@ class ArrayOf(DataType): class TupleOf(DataType): + """data structure with fields of inhomogeneous type + + """ def __init__(self, *members): super().__init__() @@ -813,7 +849,9 @@ class ImmutableDict(dict): class StructOf(DataType): + """data structure with named fields + """ def __init__(self, optional=None, **members): super().__init__() self.members = members @@ -890,6 +928,10 @@ class StructOf(DataType): class CommandType(DataType): + """command + + a pseudo datatype for commands with arguments and return values + """ IS_COMMAND = True def __init__(self, argument=None, result=None): @@ -948,8 +990,8 @@ class CommandType(DataType): raise BadValueError('incompatible datatypes') - # internally used datatypes (i.e. only for programming the SEC-node) + class DataTypeType(DataType): def __call__(self, value): """check if given value (a python obj) is a valid datatype @@ -1111,7 +1153,10 @@ def get_datatype(json, pname=''): """returns a DataType object from description inverse of .export_datatype() - the pname argument, if given, is used to name EnumTypes from the parameter name + + :param json: the datainfo object as returned from json.loads + :param pname: if given, used to name EnumTypes from the parameter name + :return: the datatype (instance of DataType) """ if json is None: return json diff --git a/secop/iohandler.py b/secop/iohandler.py index eb6908e..1c18532 100644 --- a/secop/iohandler.py +++ b/secop/iohandler.py @@ -197,20 +197,18 @@ class IOHandler(IOHandlerBase): the same format as the arguments for the change command. Examples: devices from LakeShore, PPMS - implementing classes may override the following class variables - """ - CMDARGS = [] # list of properties or parameters to be used for building some of the the query and change commands - CMDSEPARATOR = None # if not None, it is possible to join a command and a query with the given separator + :param group: the handler group (used for analyze_ and change_) + :param querycmd: the command for a query, may contain named formats for cmdargs + :param replyfmt: the format for reading the reply with some scanf like behaviour + :param changecmd: the first part of the change command (without values), may be + omitted if no write happens + + """ + CMDARGS = [] #: list of properties or parameters to be used for building some of the the query and change commands + CMDSEPARATOR = None #: if not None, it is possible to join a command and a query with the given separator def __init__(self, group, querycmd, replyfmt, changecmd=None): - """initialize the IO handler - - group: the handler group (used for analyze_ and change_) - querycmd: the command for a query, may contain named formats for cmdargs - replyfmt: the format for reading the reply with some scanf like behaviour - changecmd: the first part of the change command (without values), may be - omitted if no write happens - """ + """initialize the IO handler""" self.group = group self.parameters = set() self._module_class = None @@ -269,7 +267,7 @@ class IOHandler(IOHandlerBase): return self.read def read(self, module): - """write values from module""" + # read values from module assert module.__class__ == self._module_class try: # do a read of the current hw values @@ -293,7 +291,8 @@ class IOHandler(IOHandlerBase): def get_write_func(self, pname): """returns the write function passed to the metaclass - If pre_wfunc is given, it is to be called before change_. + :param pname: the parameter name + May be overriden to return None, if not used """ @@ -304,7 +303,7 @@ class IOHandler(IOHandlerBase): return wfunc def write(self, module, pname, value): - """write value to the module""" + # write value to parameter pname of the module assert module.__class__ == self._module_class force_read = False valuedict = {pname: value} diff --git a/secop/lib/asynconn.py b/secop/lib/asynconn.py index 97be5c4..829b45f 100644 --- a/secop/lib/asynconn.py +++ b/secop/lib/asynconn.py @@ -126,7 +126,7 @@ class AsynConn: self._rxbuffer += data def readbytes(self, nbytes, timeout=None): - """read one line + """read a fixed number of bytes return either bytes or None if not enough data available within 1 sec (self.timeout) if a non-zero timeout is given, a timeout error is raised instead of returning None diff --git a/secop/lib/enum.py b/secop/lib/enum.py index 591dd13..73f76ed 100644 --- a/secop/lib/enum.py +++ b/secop/lib/enum.py @@ -270,7 +270,10 @@ class Enum(dict): self.name = name def __getattr__(self, key): - return self[key] + try: + return self[key] + except KeyError as e: + raise AttributeError(str(e)) def __setattr__(self, key, value): if self.name and key != 'name': @@ -286,7 +289,8 @@ class Enum(dict): raise TypeError('Enum %r can not be changed!' % self.name) def __repr__(self): - return '' % (self.name, len(self)//2) + return 'Enum(%r, %s)' % (self.name, ', '.join('%s=%d' % (m.name, m.value) for m in self.members)) + # return '' % (self.name, len(self)//2) def __call__(self, key): return self[key] diff --git a/secop/metaclass.py b/secop/metaclass.py index becdb8d..8244162 100644 --- a/secop/metaclass.py +++ b/secop/metaclass.py @@ -28,7 +28,7 @@ from collections import OrderedDict from secop.errors import ProgrammingError, BadValueError from secop.params import Command, Override, Parameter from secop.datatypes import EnumType -from secop.properties import PropertyMeta +from secop.properties import PropertyMeta, add_extra_doc class Done: @@ -206,6 +206,10 @@ class ModuleMeta(PropertyMeta): raise ProgrammingError('%r: command %r has to be specified ' 'explicitly!' % (name, attrname[3:])) + add_extra_doc(newtype, '**parameters**', + {k: p for k, p in accessibles.items() if isinstance(p, Parameter)}) + add_extra_doc(newtype, '**commands**', + {k: p for k, p in accessibles.items() if isinstance(p, Command)}) attrs['__constructed__'] = True return newtype diff --git a/secop/modules.py b/secop/modules.py index 9ccde80..97a0f85 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -46,19 +46,21 @@ from secop.poller import Poller, BasicPoller class Module(HasProperties, metaclass=ModuleMeta): - """Basic Module + """basic module - ALL secop Modules derive from this + all SECoP modules derive from this. - note: within Modules, parameters should only be addressed as self. - i.e. self.value, self.target etc... + note: within modules, parameters should only be addressed as ``self.`` + i.e. ``self.value``, ``self.target`` etc... these are accessing the cached version. - they can also be written to (which auto-calls self.write_ and - generate an async update) + they can also be written to, generating an async update - if you want to 'update from the hardware', call self.read_() instead + if you want to 'update from the hardware', call ``self.read_()`` instead the return value of this method will be used as the new cached value and be an async update sent automatically. + + if you want to 'update the hardware' call ``self.write_()``. + The return value of this method will also update the cache. """ # static properties, definitions in derived classes should overwrite earlier ones. # note: properties don't change after startup and are usually filled @@ -78,7 +80,6 @@ class Module(HasProperties, metaclass=ModuleMeta): extname='implementation'), 'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()), extname='interface_classes'), - # what else? } # properties, parameters and commands are auto-merged upon subclassing @@ -88,7 +89,7 @@ class Module(HasProperties, metaclass=ModuleMeta): # reference to the dispatcher (used for sending async updates) DISPATCHER = None - pollerClass = Poller + pollerClass = Poller #: default poller used def __init__(self, name, logger, cfgdict, srv): # remember the dispatcher object (for the async callbacks) @@ -401,12 +402,7 @@ class Module(HasProperties, metaclass=ModuleMeta): class Readable(Module): - """Basic readable Module - - providing the readonly parameter 'value' and 'status' - - Also allow configurable polling per 'pollinterval' parameter. - """ + """basic readable module""" # pylint: disable=invalid-name Status = Enum('Status', IDLE = 100, @@ -415,7 +411,7 @@ class Readable(Module): ERROR = 400, DISABLED = 0, UNKNOWN = 401, - ) + ) #: status codes parameters = { 'value': Parameter('current value of the Module', readonly=True, datatype=FloatRange(), @@ -478,10 +474,7 @@ class Readable(Module): class Writable(Readable): - """Basic Writable Module - - providing a settable 'target' parameter to those of a Readable - """ + """basic writable module""" parameters = { 'target': Parameter('target value of the Module', default=0, readonly=False, datatype=FloatRange(), @@ -490,13 +483,9 @@ class Writable(Readable): class Drivable(Writable): - """Basic Drivable Module + """basic drivable module""" - provides a stop command to interrupt actions. - Also status gets extended with a BUSY state indicating a running action. - """ - - Status = Enum(Readable.Status, BUSY=300) + Status = Enum(Readable.Status, BUSY=300) #: Status codes commands = { 'stop': Command( @@ -511,11 +500,18 @@ class Drivable(Writable): } def isBusy(self, status=None): - """helper function for treating substates of BUSY correctly""" + """check for busy, treating substates correctly + + returns True when busy (also when finalizing) + """ return 300 <= (status or self.status)[0] < 400 def isDriving(self, status=None): - """helper function (finalize is busy, not driving)""" + """check for driving, treating status substates correctly + + returns True when busy, but not finalizing + """ + """""" return 300 <= (status or self.status)[0] < 390 # improved polling: may poll faster if module is BUSY @@ -537,13 +533,13 @@ class Drivable(Writable): return fastpoll def do_stop(self): - """default implementation of the stop command - - by default does nothing.""" + # default implementation of the stop command + # by default does nothing + pass class Communicator(Module): - """Basic communication Module + """basic communication module providing no parameters, but a 'communicate' command. """ @@ -555,8 +551,24 @@ class Communicator(Module): ), } + def do_communicate(self, command): + """communicate command + + :param command: the command to be sent + :return: the reply + """ + raise NotImplementedError() + class Attached(Property): + """a special property, defining an attached modle + + assign a module name to this property in the cfg file, + and the server will create an attribute with this module + + :param attrname: the name of the to be created attribute. if not given + the attribute name is the property name prepended by an underscore. + """ # we can not put this to properties.py, as it needs datatypes def __init__(self, attrname=None): self.attrname = attrname diff --git a/secop/params.py b/secop/params.py index 310a195..76ddb41 100644 --- a/secop/params.py +++ b/secop/params.py @@ -68,50 +68,41 @@ class Accessible(HasProperties, CountedObj): class Parameter(Accessible): - """storage for Parameter settings + value + qualifiers - - if readonly is False, the value can be changed (by code, or remote) - if no default is given, the parameter MUST be specified in the configfile - during startup, value is initialized with the default value or - from the config file if specified there - - poll can be: - - None: will be converted to True/False if handler is/is not None - - False or 0 (never poll this parameter) - - True or > 0 (poll this parameter) - - the exact meaning depends on the used poller - meaning for secop.poller.Poller: - - 1 or True (AUTO), converted to SLOW (readonly=False), DYNAMIC('status' and 'value') or REGULAR(else) - - 2 (SLOW), polled with lower priority and a multiple of pollperiod - - 3 (REGULAR), polled with pollperiod - - 4 (DYNAMIC), polled with pollperiod, if not BUSY, else with a fraction of pollperiod - meaning for the basicPoller: - - True or 1 (poll this every pollinterval) - - positive int (poll every N(th) pollinterval) - - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval) - note: Drivable (and derived classes) poll with 10 fold frequency if module is busy.... - """ + """storage for parameter settings + value + qualifiers""" + # poll: meaning for the basicPoller: + # - True or 1 (poll this every pollinterval) + # - positive int (poll every N(th) pollinterval) + # - negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval) + # note: Drivable (and derived classes) poll with 10 fold frequency if module is busy.... properties = { - 'description': Property('Description of the Parameter', TextType(), + 'description': Property('mandatory description of the parameter', TextType(), extname='description', mandatory=True), - 'datatype': Property('Datatype of the Parameter', DataTypeType(), + 'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(), extname='datainfo', mandatory=True), - 'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(), + 'readonly': Property('not changeable via SECoP (default True)', BoolType(), extname='readonly', mandatory=True), - 'group': Property('Optional parameter group this parameter belongs to', StringType(), + 'group': Property('optional parameter group this parameter belongs to', StringType(), extname='group', default=''), - 'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='visibility', default=1), - 'constant': Property('Optional constant value for constant parameters', ValueType(), + 'constant': Property('optional constant value for constant parameters', ValueType(), extname='constant', default=None, mandatory=False), - 'default': Property('Default (startup) value of this parameter if it can not be read from the hardware.', + 'default': Property('default (startup) value of this parameter if it can not be read from the hardware.', ValueType(), export=False, default=None, mandatory=False), - 'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)', + 'export': Property('[internal] is this parameter accessible via SECoP? (vs. internal parameter)', OrType(BoolType(), StringType()), export=False, default=True), - 'poll': Property('Polling indicator', NoneOr(IntRange()), export=False, default=None), - 'needscfg': Property('needs value in config', NoneOr(BoolType()), export=False, default=None), - 'optional': Property('[Internal] is this parameter optional?', BoolType(), export=False, + 'poll': Property('[internal] polling indicator, may be:\n' + '\n '.join(['', + '* None (omitted): will be converted to True/False if handler is/is not None', + '* False or 0 (never poll this parameter)', + '* True or 1 (AUTO), converted to SLOW (readonly=False), ' + 'DYNAMIC (*status* and *value*) or REGULAR (else)', + '* 2 (SLOW), polled with lower priority and a multiple of pollinterval', + '* 3 (REGULAR), polled with pollperiod', + '* 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, else polled with pollperiod']), + NoneOr(IntRange()), export=False, default=None), + 'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None), + 'optional': Property('[internal] is this parameter optional?', BoolType(), export=False, settable=False, default=False), 'handler': Property('[internal] overload the standard read and write functions', ValueType(), export=False, default=None, mandatory=False, settable=False), @@ -225,7 +216,7 @@ class Override(CountedObj): """Stores the overrides to be applied to a Parameter note: overrides are applied by the metaclass during class creating - reorder= True: use position of Override instead of inherited for the order + reorder=True: use position of Override instead of inherited for the order """ def __init__(self, description="", datatype=None, *, reorder=False, **kwds): super(Override, self).__init__() @@ -273,21 +264,21 @@ class Command(Accessible): """storage for Commands settings (description + call signature...) """ properties = { - 'description': Property('Description of the Command', TextType(), + 'description': Property('description of the command', TextType(), extname='description', export=True, mandatory=True), - 'group': Property('Optional command group of the command.', StringType(), + 'group': Property('optional command group of the command.', StringType(), extname='group', export=True, default=''), - 'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), extname='visibility', export=True, default=1), - 'export': Property('[internal] Flag: is the command accessible via SECoP? (vs. pure internal use)', + 'export': Property('[internal] flag: is the command accessible via SECoP? (vs. pure internal use)', OrType(BoolType(), StringType()), export=False, default=True), 'optional': Property('[internal] is the command optional to implement? (vs. mandatory)', BoolType(), export=False, default=False, settable=False), 'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'', DataTypeType(), extname='datainfo', mandatory=True), - 'argument': Property('Datatype of the argument to the command, or None.', + 'argument': Property('datatype of the argument to the command, or None.', NoneOr(DataTypeType()), export=False, mandatory=True), - 'result': Property('Datatype of the result from the command, or None.', + 'result': Property('datatype of the result from the command, or None.', NoneOr(DataTypeType()), export=False, mandatory=True), } diff --git a/secop/poller.py b/secop/poller.py index 852803c..0f29151 100644 --- a/secop/poller.py +++ b/secop/poller.py @@ -40,10 +40,10 @@ from secop.lib import mkthread from secop.errors import ProgrammingError # poll types: -AUTO = 1 # equivalent to True, converted to REGULAR, SLOW or DYNAMIC -SLOW = 2 -REGULAR = 3 -DYNAMIC = 4 +AUTO = 1 #: equivalent to True, converted to REGULAR, SLOW or DYNAMIC +SLOW = 2 #: polling with low priority and increased poll interval (used by default when readonly=False) +REGULAR = 3 #: polling with standard interval (used by default for read only parameters except status and value) +DYNAMIC = 4 #: polling with shorter poll interval when BUSY (used by default for status and value) class PollerBase: diff --git a/secop/properties.py b/secop/properties.py index bd048b1..1be6916 100644 --- a/secop/properties.py +++ b/secop/properties.py @@ -30,13 +30,19 @@ from secop.errors import ProgrammingError, ConfigError, BadValueError # storage for 'properties of a property' class Property: - '''base class holding info about a property + """base class holding info about a property - properties are only sent to the ECS if export is True, or an extname is set - if mandatory is True, they MUST have a value in the cfg file assigned to them. - otherwise, this is optional in which case the default value is applied. - All values MUST pass the datatype. - ''' + :param description: mandatory + :param datatype: the datatype to be accepted. not only to the SECoP datatypes are allowed! + also for example ``ValueType()`` (any type!), ``NoneOr(...)``, etc. + :param default: a default value. SECoP properties are normally not sent to the ECS, + when they match the default + :param extname: external name + :param export: sent to the ECS when True. defaults to True, when ``extname`` is given + :param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value + assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute) + :param settable: settable from the cfg file + """ # note: this is intended to be used on base classes. # the VALUES of the properties are on the instances! def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True): @@ -79,6 +85,17 @@ class Properties(OrderedDict): raise ProgrammingError('deleting Properties is not supported!') +def add_extra_doc(cls, title, items): + """add bulleted list to doc string + + using names and description of items + """ + bulletlist = ['\n - **%s** - %s' % (k, p.description) for k, p in items.items()] + if bulletlist: + doctext = '%s\n\n%s' % (title, ''.join(bulletlist)) + cls.__doc__ = (cls.__doc__ or '') + '\n\n %s\n' % doctext + + class PropertyMeta(type): """Metaclass for HasProperties @@ -124,6 +141,8 @@ class PropertyMeta(type): raise ProgrammingError('%r: property %r can not be set to %r' % (newtype, k, attrs[k])) setattr(newtype, k, property(getter)) + + add_extra_doc(newtype, '**properties**', attrs.get('properties', {})) # only new properties return newtype diff --git a/secop/stringio.py b/secop/stringio.py index 9ef68ed..5f535d5 100644 --- a/secop/stringio.py +++ b/secop/stringio.py @@ -49,8 +49,10 @@ class StringIO(Communicator): Property('used encoding', datatype=StringType(), default='ascii', settable=True), 'identification': - Property('a list of tuples with commands and expected responses as regexp', - datatype=ArrayOf(TupleOf(StringType(),StringType())), default=[], export=False), + Property('identification\n\n' + 'a list of tuples with commands and expected responses as regexp, ' + 'to be sent on connect', + datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False), } parameters = { 'timeout': @@ -65,7 +67,7 @@ class StringIO(Communicator): commands = { 'multicomm': Command('execute multiple commands in one go', - argument=ArrayOf(StringType()), result= ArrayOf(StringType())) + argument=ArrayOf(StringType()), result=ArrayOf(StringType())) } _reconnectCallbacks = None @@ -221,7 +223,7 @@ class HasIodev(Module): """ properties = { 'iodev': Attached(), - 'uri': Property('uri for auto creation of iodev', StringType(), default=''), + 'uri': Property('uri for automatic creation of the attached communication module', StringType(), default=''), } iodevDict = {} diff --git a/secop_demo/cryo.py b/secop_demo/cryo.py index 7efc466..64665ce 100644 --- a/secop_demo/cryo.py +++ b/secop_demo/cryo.py @@ -181,7 +181,10 @@ class Cryostat(CryoBase): return (self.p, self.i, self.d) def do_stop(self): - # stop the ramp by setting current setpoint as target + """"stop the ramp + + by setting current setpoint as target + """ # XXX: discussion: take setpoint or current value ??? self.write_target(self.setpoint) diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 76342dc..a935a86 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -54,7 +54,15 @@ except ImportError: class IOHandler(secop.iohandler.IOHandler): - CMDARGS = ['no'] + """IO handler for PPMS commands + + deals with typical format: + + - query command: ``?`` + - reply: ``,, ..`` + - change command: `` ,,...`` + """ + CMDARGS = ['no'] # the channel number is needed in channel commands CMDSEPARATOR = None # no command chaining def __init__(self, name, querycmd, replyfmt): @@ -63,7 +71,7 @@ class IOHandler(secop.iohandler.IOHandler): class Main(Communicator): - """general ppms dummy module""" + """ppms communicator module""" parameters = { 'pollinterval': Parameter('poll interval', readonly=False, @@ -126,6 +134,8 @@ class Main(Communicator): class PpmsMixin(HasIodev, Module): + """common methods for ppms modules""" + properties = { 'iodev': Attached(), } @@ -139,29 +149,24 @@ class PpmsMixin(HasIodev, Module): self._iodev.register(self) def startModule(self, started_callback): + """""" # no polls except on main module started_callback() def read_value(self): - """polling is done by the main module - - and PPMS does not deliver really more fresh values when polled more often - """ + # polling is done by the main module + # and PPMS does not deliver really more fresh values when polled more often return Done def read_status(self): - """polling is done by the main module - - and PPMS does not deliver really fresh status values anyway: the status is not - changed immediately after a target change! - """ + # polling is done by the main module + # and PPMS does not deliver really fresh status values anyway: + # e.g. the status is not changed immediately after a target change! return Done def update_value_status(self, value, packed_status): - """update value and status - - to be reimplemented for modules looking at packed_status - """ + # update value and status + # to be reimplemented for modules looking at packed_status if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') return @@ -173,6 +178,8 @@ class PpmsMixin(HasIodev, Module): class Channel(PpmsMixin, Readable): + """channel base class""" + parameters = { 'value': Override('main value of channels', poll=True), @@ -201,6 +208,8 @@ class Channel(PpmsMixin, Readable): class UserChannel(Channel): + """user channel""" + parameters = { 'pollinterval': Override(visibility=3), @@ -223,6 +232,8 @@ class UserChannel(Channel): class DriverChannel(Channel): + """driver channel""" + drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') parameters = { @@ -247,6 +258,8 @@ class DriverChannel(Channel): class BridgeChannel(Channel): + """bridge channel""" + bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') # pylint: disable=invalid-name ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) @@ -306,11 +319,10 @@ class Level(PpmsMixin, Readable): channel = 'level' def update_value_status(self, value, packed_status): - """must be a no-op - - when called from Main.read_data, value is always None - value and status is polled via settings - """ + # must be a no-op + # when called from Main.read_data, value is always None + # value and status is polled via settings + pass def analyze_level(self, level, status): # ignore 'old reading' state of the flag, as this happens only for a short time @@ -377,7 +389,6 @@ class Chamber(PpmsMixin, Drivable): channel = 'chamber' def update_value_status(self, value, packed_status): - """update value and status""" status_code = (packed_status >> 8) & 0xf if status_code in self.STATUS_MAP: self.value = status_code @@ -390,10 +401,6 @@ class Chamber(PpmsMixin, Drivable): return dict(target=target) def change_chamber(self, change): - """write settings, combining = and current attributes - - and request updated settings - """ if change.target == self.Operation.noop: return None return (change.target,) @@ -474,7 +481,6 @@ class Temp(PpmsMixin, Drivable): _ramp_at_limit = False def update_value_status(self, value, packed_status): - """update value and status""" if value is None: self.status = (self.Status.ERROR, 'invalid value') return @@ -649,7 +655,6 @@ class Field(PpmsMixin, Drivable): _last_change = 0 # means no target change is pending def update_value_status(self, value, packed_status): - """update value and status""" if value is None: self.status = (self.Status.ERROR, 'invalid value') return @@ -776,7 +781,6 @@ class Position(PpmsMixin, Drivable): _within_target = 0 # time since we are within target def update_value_status(self, value, packed_status): - """update value and status""" if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') return