diff --git a/.pylintrc b/.pylintrc index daa9688..21527bf 100644 --- a/.pylintrc +++ b/.pylintrc @@ -162,7 +162,7 @@ max-line-length=132 no-space-check=trailing-comma,dict-separator # Maximum number of lines in a module -max-module-lines=1200 +max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). diff --git a/README.md b/README.md index f59ef37..19aae1f 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,8 @@ branches: - work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments) this should be a copy of an earlier state of the wip branch - wip: current test version, usually in use on /home/l_samenv/frappy_wip -- archive_*: archived branches - does not contain useful work not already in wip - -IMPORTANT: make commits containing either only files to be pushed to Gerrit or only -PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message. + IMPORTANT: make commits containing either only files to be pushed to Gerrit or only + PSI internal files, not mixed. Mark local commits with '[PSI]' in the commit message. master --> mlz # these branches match after a sync step, but they might have a different history @@ -36,19 +34,20 @@ where commits may be cherry picked for input to Gerrit. As generally in the revi changes are done, eventually a sync step should happen: 1) ideally, this is done when work and wip match -2) make sure branches mlz, master, wip and work are in sync with remote, push/pull otherwise +2) make sure branches mlz, master, wip and work are in syns with remote, push/pull otherwise 3) cherry-pick commits from mlz to master -4) make sure master and mlz branches match (git diff --name-only master..mlz should only return README.md) +4) make sure master and mlz branches match (git diff --name-only master..wip should only return README.md) 5) create branch new_work from master 6) go through commits in wip and sort out: - core commits already pushed through gerrit are skipped - all other commits are to be cherry-picked 7) when arrived at the point where the new working version should be, - copy new_wip branch to work with 'git checkout work;git checkout new_wip .' - (note the dot!) and then commit this. -8) continue with (6) if wip and work should differ -9) do like (7), but for wip branch -10) delete new_wip branch, push master, wip and work branches + copy new_wip branch to work with 'git checkout -B work'. + Not sure if this works, as work is to be pushed to git.psi.ch. + We might first remove the remote branch with 'git push origin --delete work'. + And then create again (git push origin work)? +8) continue with (6) if wip and work should differ, and do like (7) for wip branch +9) delete new_wip branch, push master, wip and work branches ## Procedure to update PPMS @@ -60,5 +59,5 @@ changes are done, eventually a sync step should happen: cp -r secop_psi /Volumes/PPMSData/zolliker/frappy/secop_psi cp -r secop /Volumes/PPMSData/zolliker/frappy/secop - it may be that we need to copy additional folders ... + it may be that additional folder have to copied ... diff --git a/cfg/generalConfig.cfg b/cfg/generalConfig.cfg new file mode 100644 index 0000000..887135e --- /dev/null +++ b/cfg/generalConfig.cfg @@ -0,0 +1,5 @@ +[FRAPPY] +# general config for running in git repo +logdir = ./log +piddir = ./pid +confdir = ./cfg diff --git a/cfg/ls370sim.cfg b/cfg/ls370sim.cfg index 66ce0ca..e78e366 100644 --- a/cfg/ls370sim.cfg +++ b/cfg/ls370sim.cfg @@ -1,24 +1,23 @@ -[node LscSIM.psi.ch] +[NODE] +id = LscSIM.psi.ch description = Lsc Simulation at PSI -[interface tcp] -type = tcp -bindto = 0.0.0.0 -bindport = 5000 +[INTERFACE] +uri = tcp://5000 -[module res] +[res] class = secop_psi.ls370res.ResChannel -.channel = 3 -.description = resistivity -.main = lsmain -.iodev = lscom +channel = 3 +description = resistivity +main = lsmain +io = lscom -[module lsmain] +[lsmain] class = secop_psi.ls370res.Main -.description = main control of Lsc controller -.iodev = lscom +description = main control of Lsc controller +io = lscom -[module lscom] +[lscom] class = secop_psi.ls370sim.Ls370Sim -.description = simulated serial communicator to a LS 370 -.visibility = 3 +description = simulated serial communicator to a LS 370 +visibility = 3 diff --git a/cfg/ls370test.cfg b/cfg/ls370test.cfg index a011eb5..6ade7f7 100644 --- a/cfg/ls370test.cfg +++ b/cfg/ls370test.cfg @@ -1,24 +1,20 @@ -[NODE] -id = ls370res.psi.ch +[node LscSIM.psi.ch] description = Lsc370 Test -[INTERFACE] -uri = tcp://5000 +[interface tcp] +type = tcp +bindto = 0.0.0.0 +bindport = 5000 -[lsmain_iodev] -description = the communication device -class = secop_psi.ls370res.StringIO -uri = localhost:4567 - -[lsmain] +[module lsmain] class = secop_psi.ls370res.Main description = main control of Lsc controller -iodev = lsmain_iodev +uri = localhost:4567 -[res] +[module res] class = secop_psi.ls370res.ResChannel -iexc = '1mA' -channel = 5 +vexc = '2mV' +channel = 3 description = resistivity main = lsmain # the auto created iodev from lsmain: diff --git a/cfg/ppms.cfg b/cfg/ppms.cfg index 8f8a927..82cc65e 100644 --- a/cfg/ppms.cfg +++ b/cfg/ppms.cfg @@ -8,117 +8,117 @@ uri = tcp://5000 [tt] class = secop_psi.ppms.Temp description = main temperature -iodev = ppms +io = ppms [mf] class = secop_psi.ppms.Field target.min = -9 target.max = 9 -.description = magnetic field -.iodev = ppms +description = magnetic field +io = ppms [pos] class = secop_psi.ppms.Position -.description = sample rotator -.iodev = ppms +description = sample rotator +io = ppms [lev] class = secop_psi.ppms.Level -.description = helium level -.iodev = ppms +description = helium level +io = ppms [chamber] class = secop_psi.ppms.Chamber -.description = chamber state -.iodev = ppms +description = chamber state +io = ppms [r1] class = secop_psi.ppms.BridgeChannel -.description = resistivity channel 1 -.no = 1 +description = resistivity channel 1 +no = 1 value.unit = Ohm -.iodev = ppms +io = ppms [r2] class = secop_psi.ppms.BridgeChannel -.description = resistivity channel 2 -.no = 2 +description = resistivity channel 2 +no = 2 value.unit = Ohm -.iodev = ppms +io = ppms [r3] class = secop_psi.ppms.BridgeChannel -.description = resistivity channel 3 -.no = 3 +description = resistivity channel 3 +no = 3 value.unit = Ohm -.iodev = ppms +io = ppms [r4] class = secop_psi.ppms.BridgeChannel -.description = resistivity channel 4 -.no = 4 +description = resistivity channel 4 +no = 4 value.unit = Ohm -.iodev = ppms +io = ppms [i1] class = secop_psi.ppms.Channel -.description = current channel 1 -.no = 1 +description = current channel 1 +no = 1 value.unit = uA -.iodev = ppms +io = ppms [i2] class = secop_psi.ppms.Channel -.description = current channel 2 -.no = 2 +description = current channel 2 +no = 2 value.unit = uA -.iodev = ppms +io = ppms [i3] class = secop_psi.ppms.Channel -.description = current channel 3 -.no = 3 +description = current channel 3 +no = 3 value.unit = uA -.iodev = ppms +io = ppms [i4] class = secop_psi.ppms.Channel -.description = current channel 4 -.no = 4 +description = current channel 4 +no = 4 value.unit = uA -.iodev = ppms +io = ppms [v1] class = secop_psi.ppms.DriverChannel -.description = voltage channel 1 -.no = 1 +description = voltage channel 1 +no = 1 value.unit = V -.iodev = ppms +io = ppms [v2] class = secop_psi.ppms.DriverChannel -.description = voltage channel 2 -.no = 2 +description = voltage channel 2 +no = 2 value.unit = V -.iodev = ppms +io = ppms [tv] class = secop_psi.ppms.UserChannel -.description = VTI temperature +description = VTI temperature enabled = 1 value.unit = K -.iodev = ppms +io = ppms [ts] class = secop_psi.ppms.UserChannel -.description = sample temperature +description = sample temperature enabled = 1 value.unit = K -.iodev = ppms +io = ppms [ppms] class = secop_psi.ppms.Main -.description = the main and poller module -.class_id = QD.MULTIVU.PPMS.1 -.visibility = 3 +description = the main and poller module +class_id = QD.MULTIVU.PPMS.1 +visibility = 3 pollinterval = 2 diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 9326ba2..77491e6 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -7,7 +7,7 @@ Module Base Classes .. autodata:: secop.modules.Done .. autoclass:: secop.modules.Module - :members: earlyInit, initModule, startModule, pollerClass + :members: earlyInit, initModule, startModule .. autoclass:: secop.modules.Readable :members: Status diff --git a/requirements-dev.txt b/requirements-dev.txt index 8866ae9..4298010 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,7 @@ # doc sphinx_rtd_theme Sphinx>=1.2.1 +# for generating docu markdown>=2.6 # test suite pytest diff --git a/requirements.txt b/requirements.txt index b1b4fad..3bf325a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ python-daemon >=2.0 # for zmq interface #pyzmq>=13.1.0 #for ppms on windows -# don't forget to run +# don't forget to run # 'python Scripts/pywin32_postinstall.py -install' # from elevated prompt after install #pywin32 diff --git a/secop/core.py b/secop/core.py index be3d338..1da4370 100644 --- a/secop/core.py +++ b/secop/core.py @@ -31,7 +31,7 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \ from secop.iohandler import IOHandler, IOHandlerBase from secop.lib.enum import Enum from secop.modules import Attached, Communicator, \ - Done, Drivable, Module, Readable, Writable + Done, Drivable, Module, Readable, Writable, HasAccessibles from secop.params import Command, Parameter from secop.properties import Property from secop.proxy import Proxy, SecNode, proxy_class @@ -39,3 +39,8 @@ from secop.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev from secop.persistent import PersistentMixin, PersistentParam from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \ CommonWriteHandler, nopoll + +ERROR = Drivable.Status.ERROR +WARN = Drivable.Status.WARN +BUSY = Drivable.Status.BUSY +IDLE = Drivable.Status.IDLE diff --git a/secop/datatypes.py b/secop/datatypes.py index 12ce4ec..f5c31c1 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -22,7 +22,7 @@ # ***************************************************************************** """Define validated data types.""" -# pylint: disable=abstract-method +# pylint: disable=abstract-method, too-many-lines import sys @@ -454,7 +454,10 @@ class EnumType(DataType): super().__init__() if members is not None: kwds.update(members) - self._enum = Enum(enum_or_name, **kwds) + if isinstance(enum_or_name, str): + self._enum = Enum(enum_or_name, kwds) # allow 'self' as name + else: + self._enum = Enum(enum_or_name, **kwds) self.default = self._enum[self._enum.members[0]] def copy(self): diff --git a/secop/lib/statemachine.py b/secop/lib/statemachine.py index f24f00e..1742ff8 100644 --- a/secop/lib/statemachine.py +++ b/secop/lib/statemachine.py @@ -178,7 +178,9 @@ class StateMachine: if self.stop_exc: raise self.stop_exc except Exception as e: - self.log.info('%r raised in state %r', e, self.status_string) + # Stop and Restart are not unusual -> no warning + log = self.log.debug if isinstance(e, Stop) else self.log.warning + log('%r raised in state %r', e, self.status_string) self.last_error = e ret = self.cleanup(self) self.log.debug('cleanup %r %r %r', self.cleanup, self.last_error, ret) diff --git a/secop/modules.py b/secop/modules.py index 5623d93..322fd0e 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -161,8 +161,8 @@ class HasAccessibles(HasProperties): new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed setattr(cls, 'read_' + pname, new_rfunc) - if not pobj.readonly: - wfunc = getattr(cls, 'write_' + pname, None) + wfunc = getattr(cls, 'write_' + pname, None) + if not pobj.readonly or wfunc: # allow write_ method even when pobj is not readonly wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated if (wfunc is None or wrapped) and pobj.handler: # ignore the handler, if a write function is present @@ -181,14 +181,14 @@ class HasAccessibles(HasProperties): self.log.debug('validate %r for %r', value, pname) # we do not need to handle errors here, we do not # want to make a parameter invalid, when a write failed - value = pobj.datatype(value) - returned_value = wfunc(self, value) - self.log.debug('write_%s(%r) returned %r', pname, value, returned_value) - if returned_value is Done: + new_value = pobj.datatype(value) + new_value = wfunc(self, new_value) + self.log.debug('write_%s(%r) returned %r', pname, value, new_value) + if new_value is Done: # setattr(self, pname, getattr(self, pname)) return getattr(self, pname) - setattr(self, pname, value) # important! trigger the setter - return value + setattr(self, pname, new_value) # important! trigger the setter + return new_value else: def new_wfunc(self, value, pname=pname): @@ -201,14 +201,14 @@ class HasAccessibles(HasProperties): setattr(cls, 'write_' + pname, new_wfunc) # check for programming errors - for attrname, attrvalue in cls.__dict__.items(): + for attrname in dir(cls): prefix, _, pname = attrname.partition('_') if not pname: continue if prefix == 'do': raise ProgrammingError('%r: old style command %r not supported anymore' % (cls.__name__, attrname)) - if prefix in ('read', 'write') and not getattr(attrvalue, 'wrapped', False): + if prefix in ('read', 'write') and not getattr(getattr(cls, attrname), 'wrapped', False): raise ProgrammingError('%s.%s defined, but %r is no parameter' % (cls.__name__, attrname, pname)) @@ -280,6 +280,7 @@ class Module(HasAccessibles): # reference to the dispatcher (used for sending async updates) DISPATCHER = None + attachedModules = None def __init__(self, name, logger, cfgdict, srv): # remember the dispatcher object (for the async callbacks) @@ -391,9 +392,8 @@ class Module(HasAccessibles): continue if pname in cfgdict: - if not pobj.readonly and pobj.initwrite is not False: + if pobj.initwrite is not False and hasattr(self, 'write_' + pname): # parameters given in cfgdict have to call write_ - # TODO: not sure about readonly (why not a parameter which can only be written from config?) try: pobj.value = pobj.datatype(cfgdict[pname]) self.writeDict[pname] = pobj.value @@ -416,10 +416,8 @@ class Module(HasAccessibles): except BadValueError as e: # this should not happen, as the default is already checked in Parameter raise ProgrammingError('bad default for %s:%s: %s' % (name, pname, e)) from None - if pobj.initwrite and not pobj.readonly: + if pobj.initwrite and hasattr(self, 'write_' + pname): # we will need to call write_ - # if this is not desired, the default must not be given - # TODO: not sure about readonly (why not a parameter which can only be written from config?) pobj.value = value self.writeDict[pname] = value else: @@ -649,11 +647,11 @@ class Module(HasAccessibles): self.log.info('recovered after %d calls to doPoll (%r)', error_count, last_error) last_error = None except Exception as e: - if type(e) != last_error: + if repr(e) != last_error: error_count = 0 self.log.error('error in doPoll: %r', e) error_count += 1 - last_error = e + last_error = repr(e) now = time.time() # find ONE due slow poll and call it loop = True @@ -803,14 +801,15 @@ class Attached(Property): assign a module name to this property in the cfg file, and the server will create an attribute with this module """ - module = None - - def __init__(self, description='attached module'): - super().__init__(description, StringType(), mandatory=False) + def __init__(self, basecls=Module, description='attached module', mandatory=True): + self.basecls = basecls + super().__init__(description, StringType(), mandatory=mandatory) def __get__(self, obj, owner): if obj is None: return self - if self.module is None: - self.module = obj.DISPATCHER.get_module(super().__get__(obj, owner)) - return self.module + if obj.attachedModules is None: + # return the name of the module (called from Server on startup) + return super().__get__(obj, owner) + # return the module (called after startup) + return obj.attachedModules.get(self.name) # return None if not given diff --git a/secop/persistent.py b/secop/persistent.py index 0360b84..63ce5a1 100644 --- a/secop/persistent.py +++ b/secop/persistent.py @@ -75,7 +75,7 @@ class PersistentMixin(HasAccessibles): self.initData = {} for pname in self.parameters: pobj = self.parameters[pname] - if not pobj.readonly and getattr(pobj, 'persistent', 0): + if hasattr(self, 'write_' + pname) and getattr(pobj, 'persistent', 0): self.initData[pname] = pobj.value if pobj.persistent == 'auto': def cb(value, m=self): diff --git a/secop/proxy.py b/secop/proxy.py index 7189fa8..2c25838 100644 --- a/secop/proxy.py +++ b/secop/proxy.py @@ -23,8 +23,7 @@ from secop.client import SecopClient, decode_msg, encode_msg_frame from secop.datatypes import StringType -from secop.errors import BadValueError, \ - CommunicationFailedError, ConfigError, make_secop_error +from secop.errors import BadValueError, CommunicationFailedError, ConfigError from secop.lib import get_class from secop.modules import Drivable, Module, Readable, Writable from secop.params import Command, Parameter @@ -47,8 +46,6 @@ class ProxyModule(HasIO, Module): if parameter not in self.parameters: return # ignore unknown parameters # should be done here: deal with clock differences - if readerror: - readerror = make_secop_error(*readerror) self.announceUpdate(parameter, value, readerror, timestamp) def initModule(self): @@ -201,7 +198,7 @@ def proxy_class(remote_class, name=None): def wfunc(self, value, pname=aname): value, _, readerror = self._secnode.setParameter(self.name, pname, value) if readerror: - raise make_secop_error(*readerror) + raise readerror return value attrs['write_' + aname] = wfunc diff --git a/secop/server.py b/secop/server.py index d6253f8..c96093a 100644 --- a/secop/server.py +++ b/secop/server.py @@ -30,10 +30,11 @@ import sys import traceback from collections import OrderedDict -from secop.errors import ConfigError +from secop.errors import ConfigError, SECoPError from secop.lib import formatException, get_class, generalConfig from secop.lib.multievent import MultiEvent from secop.params import PREDEFINED_ACCESSIBLES +from secop.modules import Attached try: from daemon import DaemonContext @@ -275,6 +276,24 @@ class Server: missing_super.add('%s was not called, probably missing super call' % modobj.earlyInit.__qualname__) + # handle attached modules + for modname, modobj in self.modules.items(): + attached_modules = {} + for propname, propobj in modobj.propertyDict.items(): + if isinstance(propobj, Attached): + try: + attname = getattr(modobj, propname) + if attname: # attached module specified in cfg file + attobj = self.dispatcher.get_module(attname) + if isinstance(attobj, propobj.basecls): + attached_modules[propname] = attobj + else: + errors.append('attached module %s=%r must inherit from %r' + % (propname, attname, propobj.basecls.__qualname__)) + except SECoPError as e: + errors.append('module %s, attached %s: %s' % (modname, propname, str(e))) + modobj.attachedModules = attached_modules + # call init on each module after registering all for modname, modobj in self.modules.items(): try: @@ -287,18 +306,17 @@ class Server: failure_traceback = traceback.format_exc() errors.append('error initializing %s: %r' % (modname, e)) - if self._testonly: - return - start_events = MultiEvent(default_timeout=30) - for modname, modobj in self.modules.items(): - # startModule must return either a timeout value or None (default 30 sec) - start_events.name = 'module %s' % modname - modobj.startModule(start_events) - if not modobj.startModuleDone: - missing_super.add('%s was not called, probably missing super call' - % modobj.startModule.__qualname__) + if not self._testonly: + start_events = MultiEvent(default_timeout=30) + for modname, modobj in self.modules.items(): + # startModule must return either a timeout value or None (default 30 sec) + start_events.name = 'module %s' % modname + modobj.startModule(start_events) + if not modobj.startModuleDone: + missing_super.add('%s was not called, probably missing super call' + % modobj.startModule.__qualname__) + errors.extend(missing_super) - errors.extend(missing_super) if errors: for errtxt in errors: for line in errtxt.split('\n'): @@ -310,6 +328,8 @@ class Server: sys.stderr.write(failure_traceback) sys.exit(1) + if self._testonly: + return self.log.info('waiting for modules being started') start_events.name = None if not start_events.wait(): diff --git a/secop_psi/ccu4.py b/secop_psi/ccu4.py index e263537..e1b4801 100644 --- a/secop_psi/ccu4.py +++ b/secop_psi/ccu4.py @@ -72,7 +72,7 @@ class HeLevel(HasIO, Readable): """ name, txtvalue = self.communicate(cmd).split('=') assert name == cmd.split('=')[0] # check that we got a reply to our command - return txtvalue # Frappy will automatically convert the string to the needed data type + return float(txtvalue) def read_value(self): return self.query('h') diff --git a/secop_psi/historywriter.py b/secop_psi/historywriter.py new file mode 100644 index 0000000..76b1bea --- /dev/null +++ b/secop_psi/historywriter.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** + +import time +import frappyhistory # pylint: disable=import-error +from secop.datatypes import get_datatype, IntRange, FloatRange, ScaledInteger,\ + EnumType, BoolType, StringType, TupleOf, StructOf + + +def make_cvt_list(dt, tail=''): + """create conversion list + + list of tuple (, , ) + tail is a postfix to be appended in case of tuples and structs + """ + if isinstance(dt, (EnumType, IntRange, BoolType)): + return[(int, tail, dict(type='NUM'))] + if isinstance(dt, (FloatRange, ScaledInteger)): + return [(dt.import_value, tail, dict(type='NUM', unit=dt.unit, period=5) if dt.unit else {})] + if isinstance(dt, StringType): + return [(lambda x: x, tail, dict(type='STR'))] + if isinstance(dt, TupleOf): + items = enumerate(dt.members) + elif isinstance(dt, StructOf): + items = dt.members.items() + else: + return [] # ArrayType, BlobType and TextType are ignored: too much data, probably not used + result = [] + for subkey, elmtype in items: + for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)): + result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts)) + return result + + +class FrappyHistoryWriter(frappyhistory.FrappyWriter): + """extend writer to be used as an internal frappy connection + + API of frappyhistory.FrappyWriter: + + :meth:`put_def`(key, opts): + + define or overwrite a new curve named with options from dict + options: + + - type: + 'NUM' (any number) or 'STR' (text) + remark: tuples and structs create multiple curves + - period: + the typical 'lifetime' of a value. + The intention is, that points in a chart may be connected by a straight line + when the distance is lower than this value. If not, the line should be drawn + horizontally from the last point to a point before the next value. + For example a setpoint should have period 0, which will lead to a stepped + line, whereas for a measured value like a temperature, period should be + slightly bigger than the poll interval. In order to make full use of this, + we would need some additional parameter property. + - show: True/False, whether this curve should be shown or not by default in + a summary chart + - label: a label for the curve in the chart + + :meth:`put`(timestamp, key, value) + + timestamp: the timestamp. must not decrease! + key: the curve name + value: the value to be stored, converted to a string. '' indicates an undefined value + + self.cache is a dict of , containing the last used value + """ + def __init__(self, directory, predefined_names, dispatcher): + super().__init__(directory) + self.predefined_names = predefined_names + self.cvt_lists = {} # dict of + self.activated = False + self.dispatcher = dispatcher + self._init_time = None + + def init(self, msg): + """initialize from the 'describing' message""" + action, _, description = msg + assert action == 'describing' + self._init_time = time.time() + + for modname, moddesc in description['modules'].items(): + for pname, pdesc in moddesc['accessibles'].items(): + ident = key = modname + ':' + pname + if pname.startswith('_') and pname[1:] not in self.predefined_names: + key = modname + ':' + pname[1:] + dt = get_datatype(pdesc['datainfo']) + cvt_list = make_cvt_list(dt, key) + for _, hkey, opts in cvt_list: + if pname == 'value': + opts['period'] = opts.get('period', 0) + opts['show'] = True + opts['label'] = modname + elif pname == 'target': + opts['period'] = 0 + opts['label'] = modname + '_target' + opts['show'] = True + self.put_def(hkey, opts) + self.cvt_lists[ident] = cvt_list + # self.put(self._init_time, 'STR', 'vars', ' '.join(vars)) + self.dispatcher.handle_activate(self, None, None) + self._init_time = None + + def send_reply(self, msg): + action, ident, value = msg + if not action.endswith('update'): + print('unknown async message %r' % msg) + return + now = self._init_time or time.time() # on initialisation, use the same timestamp for all + if action == 'update': + for fun, key, _ in self.cvt_lists[ident]: + # we only look at the value, qualifiers are ignored for now + # we do not use the timestamp here, as a potentially decreasing value might + # bring the reader software into trouble + self.put(now, key, str(fun(value[0]))) + + else: # error_update + for _, key, _ in self.cvt_lists[ident]: + old = self.cache.get(key) + if old is None: + return # ignore if this key is not yet used + self.put(now, key, '') diff --git a/secop_psi/k2601b.py b/secop_psi/k2601b.py index d1ad8bc..d82a36a 100644 --- a/secop_psi/k2601b.py +++ b/secop_psi/k2601b.py @@ -18,12 +18,18 @@ # Module authors: # Markus Zolliker # ***************************************************************************** -"""Keithley 2601B source meter +"""Keithley 2601B 4 quadrant source meter -not tested yet""" +not tested yet -from secop.core import Attached, BoolType, EnumType, FloatRange, \ - HasIodev, Module, Parameter, StringIO, Writable, Done +* switching between voltage and current happens by setting their target +* switching output off by setting the active parameter of the controlling + module to False. +* setting the active parameter to True raises an error +""" + +from secop.core import Attached, BoolType, Done, EnumType, FloatRange, \ + HasIO, Module, Parameter, Readable, StringIO, Writable class K2601bIO(StringIO): @@ -32,128 +38,162 @@ class K2601bIO(StringIO): SOURCECMDS = { 0: 'reset()' + ' smua.source.output = 0 print("ok")', + 1: 'reset()' ' smua.source.func = smua.OUTPUT_DCAMPS' ' display.smua.measure.func = display.MEASURE_VOLTS' ' smua.source.autorangei = 1' - ' smua.source.output = %d print("ok"")', - 1: 'reset()' + ' smua.source.output = 1 print("ok")', + 2: 'reset()' ' smua.source.func = smua.OUTPUT_DCVOLTS' ' display.smua.measure.func = display.MEASURE_DCAMPS' ' smua.source.autorangev = 1' - ' smua.source.output = %d print("ok"")', + ' smua.source.output = 1 print("ok")', } -class SourceMeter(HasIodev, Module): +class SourceMeter(HasIO, Module): + export = False # export for tests only + mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), + readonly=False, export=False) + ilimit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2) + vlimit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2) - resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True) - power = Parameter('readback power', FloatRange(unit='W'), poll=True) - mode = Parameter('measurement mode', EnumType(current_off=0, voltage_off=1, current_on=2, voltage_on=3), - readonly=False, poll=True) - - iodevClass = K2601bIO - - def read_resistivity(self): - return self.sendRecv('print(smua.measure.r())') - - def read_power(self): - return self.sendRecv('print(smua.measure.p())') + ioClass = K2601bIO def read_mode(self): - return float(self.sendRecv('print(smua.source.func+2*smua.source.output)')) + return float(self.communicate('print((smua.source.func+1)*smua.source.output)')) def write_mode(self, value): - assert 'ok' == self.sendRecv(SOURCECMDS[value % 2] % (value >= 2)) + if value == 'current': + self.write_vlimit(self.vlimit) + elif value == 'voltage': + self.write_ilimit(self.ilimit) + assert self.communicate(SOURCECMDS[value]) == 'ok' return self.read_mode() + def read_ilimit(self): + if self.mode == 'current': + return self.ilimit + return float(self.communicate('print(smua.source.limiti)')) -class Current(HasIodev, Writable): - sourcemeter = Attached() + def write_ilimit(self, value): + if self.mode == 'current': + return self.ilimit + return float(self.communicate('smua.source.limiti = %g print(smua.source.limiti)' % value)) - value = Parameter('measured current', FloatRange(unit='A'), poll=True) - target = Parameter('set current', FloatRange(unit='A'), poll=True) - active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter - limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True) + def read_vlimit(self): + if self.mode == 'voltage': + return self.ilimit + return float(self.communicate('print(smua.source.limitv)')) - def initModule(self): - self._sourcemeter.registerCallbacks(self) + def write_vlimit(self, value): + if self.mode == 'voltage': + return self.ilimit + return float(self.communicate('smua.source.limitv = %g print(smua.source.limitv)' % value)) + + +class Power(HasIO, Readable): + value = Parameter('readback power', FloatRange(unit='W')) + ioClass = K2601bIO def read_value(self): - return self.sendRecv('print(smua.measure.i())') + return float(self.communicate('print(smua.measure.p())')) + + +class Resistivity(HasIO, Readable): + value = Parameter('readback resistivity', FloatRange(unit='Ohm')) + ioClass = K2601bIO + + def read_value(self): + return float(self.communicate('print(smua.measure.r())')) + + +class Current(HasIO, Writable): + sourcemeter = Attached() + + value = Parameter('measured current', FloatRange(unit='A')) + target = Parameter('set current', FloatRange(unit='A')) + active = Parameter('current is controlled', BoolType(), default=False) + limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2) + + def initModule(self): + self.sourcemeter.registerCallbacks(self) + + def read_value(self): + return float(self.communicate('print(smua.measure.i())')) def read_target(self): - return self.sendRecv('print(smua.source.leveli)') + return float(self.communicate('print(smua.source.leveli)')) def write_target(self, value): - if not self.active: - raise ValueError('current source is disabled') - if value > self.limit: + if value > self.sourcemeter.ilimit: raise ValueError('current exceeds limit') - return self.sendRecv('smua.source.leveli = %g print(smua.source.leveli)' % value) + value = float(self.communicate('smua.source.leveli = %g print(smua.source.leveli)' % value)) + if not self.active: + self.sourcemeter.write_mode('current') # triggers update_mode -> set active to True + return value def read_limit(self): - if self.active: - return self.limit - return self.sendRecv('print(smua.source.limiti)') + return self.sourcemeter.read_ilimit() def write_limit(self, value): - if self.active: - return value - return self.sendRecv('smua.source.limiti = %g print(smua.source.limiti)' % value) - - def write_active(self, value): - if value: - self._sourcemeter.write_mode('current_on') - elif self._sourcemeter.mode == 'current_on': - self._sourcemeter.write_mode('current_off') - return self.active + return self.sourcemeter.write_ilimit(value) def update_mode(self, mode): # will be called whenever the attached sourcemeters mode changes - self.active = mode == 'current_on' + self.active = mode == 'current' + + def write_active(self, value): + self.sourcemeter.read_mode() + if value == self.value: + return Done + if value: + raise ValueError('activate only by setting target') + self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False + return Done -class Voltage(HasIodev, Writable): +class Voltage(HasIO, Writable): sourcemeter = Attached() - value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) - target = Parameter('set voltage', FloatRange(unit='V'), poll=True) - active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter - limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) + value = Parameter('measured voltage', FloatRange(unit='V')) + target = Parameter('set voltage', FloatRange(unit='V')) + active = Parameter('voltage is controlled', BoolType()) + limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2) def initModule(self): - self._sourcemeter.registerCallbacks(self) + self.sourcemeter.registerCallbacks(self) def read_value(self): - return self.sendRecv('print(smua.measure.v())') + return float(self.communicate('print(smua.measure.v())')) def read_target(self): - return self.sendRecv('print(smua.source.levelv)') + return float(self.communicate('print(smua.source.levelv)')) def write_target(self, value): - if not self.active: - raise ValueError('voltage source is disabled') - if value > self.limit: + if value > self.sourcemeter.vlimit: raise ValueError('voltage exceeds limit') - return self.sendRecv('smua.source.levelv = %g print(smua.source.levelv)' % value) + value = float(self.communicate('smua.source.levelv = %g print(smua.source.levelv)' % value)) + if not self.active: + self.sourcemeter.write_mode('voltage') # triggers update_mode -> set active to True + return value def read_limit(self): - if self.active: - return self.limit - return self.sendRecv('print(smua.source.limitv)') + return self.sourcemeter.read_vlimit() def write_limit(self, value): - if self.active: - return value - return self.sendRecv('smua.source.limitv = %g print(smua.source.limitv)' % value) - - def write_active(self, value): - if value: - self._sourcemeter.write_mode('voltage_on') - elif self._sourcemeter.mode == 'voltage_on': - self._sourcemeter.write_mode('voltage_off') - return self.active + return self.sourcemeter.write_vlimit(value) def update_mode(self, mode): # will be called whenever the attached sourcemeters mode changes - self.active = mode == 'voltage_on' + self.active = mode == 'voltage' + + def write_active(self, value): + self.sourcemeter.read_mode() + if value == self.value: + return Done + if value: + raise ValueError('activate only by setting target') + self.sourcemeter.write_mode('off') # triggers update_mode -> set active to False + return Done diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py index ca10cec..026ac01 100644 --- a/secop_psi/ls370res.py +++ b/secop_psi/ls370res.py @@ -27,8 +27,7 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange from secop.lib import formatStatusBits from secop.modules import Attached, Done, \ Drivable, Parameter, Property, Readable -from secop.poller import REGULAR, Poller -from secop.io import HasIodev +from secop.io import HasIO Status = Drivable.Status @@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO): wait_before = 0.05 -class Main(HasIodev, Drivable): +class Main(HasIO, Drivable): - value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17)) + value = Parameter('the current channel', datatype=IntRange(0, 17)) target = Parameter('channel to select', datatype=IntRange(0, 17)) autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) pollinterval = Parameter(default=1, export=False) - pollerClass = Poller - iodevClass = StringIO + ioClass = StringIO _channel_changed = 0 # time of last channel change _channels = None # dict of def earlyInit(self): + super().earlyInit() self._channels = {} def register_channel(self, modobj): self._channels[modobj.channel] = modobj - def startModule(self, started_callback): - started_callback() + def startModule(self, start_events): + super().startModule(start_events) for ch in range(1, 16): if ch not in self._channels: - self.sendRecv('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch)) + self.communicate('INSET %d,0,0,0,0,0;INSET?%d' % (ch, ch)) def read_value(self): channel, auto = scan.send_command(self) @@ -113,7 +112,7 @@ class Main(HasIodev, Drivable): def write_target(self, channel): scan.send_change(self, channel, self.autoscan) - # self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan)) + # self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan)) if channel != self.value: self.value = 0 self._channel_changed = time.time() @@ -122,11 +121,11 @@ class Main(HasIodev, Drivable): def write_autoscan(self, value): scan.send_change(self, self.value, value) - # self.sendRecv('SCAN %d,%d;SCAN?' % (channel, self.autoscan)) + # self.communicate('SCAN %d,%d;SCAN?' % (channel, self.autoscan)) return value -class ResChannel(HasIodev, Readable): +class ResChannel(HasIO, Readable): """temperature channel on Lakeshore 336""" RES_RANGE = {key: i+1 for i, key in list( @@ -140,8 +139,7 @@ class ResChannel(HasIodev, Readable): enumerate(mag % val for mag in ['%guV', '%gmV'] for val in [2, 6.32, 20, 63.2, 200, 632]))} - pollerClass = Poller - iodevClass = StringIO + ioClass = StringIO _main = None # main module _last_range_change = 0 # time of last range change @@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable): _trigger_read = False def initModule(self): + super().initModule() self._main = self.DISPATCHER.get_module(self.main) self._main.register_channel(self) @@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable): return Done # we got here, when we missed the idle state of self._main self._trigger_read = False - result = self.sendRecv('RDGR?%d' % self.channel) + result = self.communicate('RDGR?%d' % self.channel) result = float(result) if self.autorange == 'soft': now = time.time() @@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable): def read_status(self): if not self.enabled: return [self.Status.DISABLED, 'disabled'] - if self.channel != self._main.value: + if self.channel != self.main.value: return Done - result = int(self.sendRecv('RDGST?%d' % self.channel)) + result = int(self.communicate('RDGST?%d' % self.channel)) result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities) statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS)) if statustext: @@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable): if autorange: result['autorange'] = 'hard' # else: do not change autorange - # self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange)) + self.log.debug('%s range %r %r %r' % (self.name, rng, autorange, self.autorange)) if excoff: result.update(iexc=0, vexc=0) elif iscur: @@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable): def write_enabled(self, value): inset.write(self, 'enabled', value) if value: - self._main.write_target(self.channel) + self.main.write_target(self.channel) return Done diff --git a/secop_psi/trinamic.py b/secop_psi/trinamic.py index 102bcde..9b3edad 100644 --- a/secop_psi/trinamic.py +++ b/secop_psi/trinamic.py @@ -24,13 +24,12 @@ import time import struct -from math import log10 from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \ - HasIodev, Parameter, Property, Drivable, PersistentMixin, PersistentParam + HasIO, Parameter, Property, Drivable, PersistentMixin, PersistentParam, Done from secop.io import BytesIO from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError - +from secop.rwhandler import ReadHandler, WriteHandler MOTOR_STOP = 3 MOVE = 4 @@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE MAX_ACCEL = 2047 * ACCEL_SCALE CURRENT_SCALE = 2.8/250 -ENCODER_RESOLUTION = 0.4 # 360 / 1024, rounded up +ENCODER_RESOLUTION = 360 / 1024 + +HW_ARGS = { + # : (address, scale factor) + 'encoder_tolerance': (212, ANGLE_SCALE), + 'speed': (4, SPEED_SCALE), + 'minspeed': (130, SPEED_SCALE), + 'currentspeed': (3, SPEED_SCALE), + 'maxcurrent': (6, CURRENT_SCALE), + 'standby_current': (7, CURRENT_SCALE,), + 'acceleration': (5, ACCEL_SCALE), + 'target_reached': (8, 1), + 'move_status': (207, 1), + 'error_bits': (208, 1), + 'free_wheeling': (204, 0.01), + 'power_down_delay': (214, 0.01), +} + +# special handling (adjust zero): +ENCODER_ADR = 209 +STEPPOS_ADR = 1 -class HwParam(PersistentParam): - adr = Property('parameter address', IntRange(0, 255), export=False) - scale = Property('scale factor (physical value / unit)', FloatRange(), export=False) - - def __init__(self, description, datatype, adr, scale=1, poll=True, - readonly=True, persistent=None, **kwds): - """hardware parameter""" - if persistent is None: - persistent = not readonly - if isinstance(datatype, FloatRange) and datatype.fmtstr == '%g': - datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale) + 0.01)) - super().__init__(description, datatype, poll=poll, adr=adr, scale=scale, - persistent=persistent, readonly=readonly, **kwds) - - def copy(self): - res = HwParam(self.description, self.datatype.copy(), self.adr) - res.name = self.name - res.init(self.propertyValues) - return res +def writable(*args, **kwds): + """convenience function to create writable hardware parameters""" + return PersistentParam(*args, readonly=False, initwrite=True, **kwds) -class Motor(PersistentMixin, HasIodev, Drivable): +class Motor(PersistentMixin, HasIO, Drivable): address = Property('module address', IntRange(0, 255), default=1) - value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'), poll=False, default=0) # polling by read_status + value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f')) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0) - encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), - 209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True) - steppos = HwParam('position from motor steps', FloatRange(unit='$'), - 1, ANGLE_SCALE, readonly=True, initwrite=False) - target = Parameter('_', FloatRange(unit='$'), default=0) + encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), + readonly=True, initwrite=False) + steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'), + readonly=True, initwrite=False) + target = Parameter('', FloatRange(unit='$'), default=0) - move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'), - readonly=False, default=5, group='more') - safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'), - readonly=False, default=0.2, group='more') + move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'), + readonly=False, default=360, group='more') tolerance = Parameter('positioning tolerance', FloatRange(unit='$'), readonly=False, default=0.9) - encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance', - FloatRange(0, 360., unit='$'), - 212, ANGLE_SCALE, readonly=False, group='more') - speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), - 4, SPEED_SCALE, readonly=False, group='motorparam') - minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), - 130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam') - currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'), - 3, SPEED_SCALE, readonly=True, group='motorparam') - maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'), - 6, CURRENT_SCALE, readonly=False, group='motorparam') - standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'), - 7, CURRENT_SCALE, readonly=False, group='motorparam') - acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'), - 5, ACCEL_SCALE, readonly=False, group='motorparam') - target_reached = HwParam('_', BoolType(), 8, group='hwstatus') - move_status = HwParam('_', IntRange(0, 3), - 207, readonly=True, group='hwstatus') - error_bits = HwParam('_', IntRange(0, 255), - 208, readonly=True, group='hwstatus') - # the doc says msec, but I believe the scale is 10 msec - free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'), - 204, 0.01, default=0.1, readonly=False, group='motorparam') - power_down_delay = HwParam('_', FloatRange(0, 60., unit='sec'), - 214, 0.01, default=0.1, readonly=False, group='motorparam') - baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), - readonly=False, default=0, poll=True, visibility=3, group='more') + encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance', + FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more') + speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40) + minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), + default=SPEED_SCALE, group='motorparam') + currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), + group='motorparam') + maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'), + default=1.4, group='motorparam') + standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'), + default=0.1, group='motorparam') + acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'), + default=150., group='motorparam') + target_reached = Parameter('', BoolType(), group='hwstatus') + move_status = Parameter('', IntRange(0, 3), group='hwstatus') + error_bits = Parameter('', IntRange(0, 255), group='hwstatus') + free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), + default=0.1, group='motorparam') + power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'), + default=0.1, group='motorparam') + baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), + readonly=False, default=0, visibility=3, group='more') pollinterval = Parameter(group='more') - - iodevClass = BytesIO - # fast_pollfactor = 0.001 # poll as fast as possible when busy - fast_pollfactor = 0.05 + ioClass = BytesIO + fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running _started = 0 - _calc_timeout = True + _calcTimeout = True _need_reset = None - _last_change = 0 def comm(self, cmd, adr, value=0, bank=0): """set or get a parameter @@ -138,21 +131,23 @@ class Motor(PersistentMixin, HasIodev, Drivable): :param value: if given, the parameter is written, else it is returned :return: the returned value """ - if self._calc_timeout: - self._calc_timeout = False - baudrate = getattr(self._iodev._conn.connection, 'baudrate', None) + if self._calcTimeout and self.io._conn: + self._calcTimeout = False + baudrate = getattr(self.io._conn.connection, 'baudrate', None) if baudrate: if baudrate not in BAUDRATES: raise CommunicationFailedError('unsupported baud rate: %d' % baudrate) - self._iodev.timeout = 0.03 + 200 / baudrate + self.io.timeout = 0.03 + 200 / baudrate exc = None + byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value)) + byt += bytes([sum(byt) & 0xff]) for itry in range(3,0,-1): - byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value)) try: - reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9) + reply = self.communicate(byt, 9) if sum(reply[:-1]) & 0xff != reply[-1]: raise CommunicationFailedError('checksum error') + # will try again except Exception as e: if itry == 1: raise @@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable): raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr)) return result - def get(self, pname, **kwds): - """get parameter""" - pobj = self.parameters[pname] - value = self.comm(GET_AXIS_PAR, pobj.adr, **kwds) - # do not apply scale when 1 (datatype might not be float) - return value if pobj.scale == 1 else value * pobj.scale + def startModule(self, start_events): + super().startModule(start_events) - def set(self, pname, value, check=True, **kwds): - """set parameter and check result""" - pobj = self.parameters[pname] - scale = pobj.scale - rawvalue = round(value / scale) - for itry in range(2): - self.comm(SET_AXIS_PAR, pobj.adr, rawvalue, **kwds) - if check: - result = self.comm(GET_AXIS_PAR, pobj.adr, **kwds) - if result != rawvalue: - self.log.warning('result for %s does not match %d != %d, try again', pname, result, rawvalue) - continue - value = result * scale - return value - else: - raise HardwareError('result for %s does not match %d != %d' % (pname, result, rawvalue)) + def fix_encoder(self=self): + try: + # get encoder value from motor. at this stage self.encoder contains the persistent value + encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero + self.fix_encoder(encoder) + except Exception as e: + self.log.error('fix_encoder failed with %r', e) - def startModule(self, started_callback): - # get encoder value from motor. at this stage self.encoder contains the persistent value - encoder = self.get('encoder') - encoder += self.zero - self.fix_encoder(encoder) - super().startModule(started_callback) + start_events.queue(fix_encoder) def fix_encoder(self, encoder_from_hw): """fix encoder value @@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable): # calculate nearest, most probable value adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360 if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance: - # encoder module0 360 has changed + # encoder modulo 360 has changed self.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)', self.encoder, encoder_from_hw, adjusted_encoder) if adjusted_encoder != encoder_from_hw: self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder) self._need_reset = True self.status = self.Status.ERROR, 'saved encoder value does not match reading' - self.set('encoder', adjusted_encoder - self.zero, check=False) + self._write_axispar(adjusted_encoder - self.zero, ENCODER_ADR, ANGLE_SCALE, readback=False) + + def _read_axispar(self, adr, scale=1): + value = self.comm(GET_AXIS_PAR, adr) + # do not apply scale when 1 (datatype might not be float) + return value if scale == 1 else value * scale + + def _write_axispar(self, value, adr, scale=1, readback=True): + rawvalue = round(value / scale) + self.comm(SET_AXIS_PAR, adr, rawvalue) + if readback: + result = self.comm(GET_AXIS_PAR, adr) + if result != rawvalue: + raise HardwareError('result for adr=%d scale=%g does not match %g != %g' + % (adr, scale, result * scale, value)) + return result * scale + return rawvalue * scale + + @ReadHandler(HW_ARGS) + def read_hwparam(self, pname): + """handle read for HwParam""" + args = HW_ARGS[pname] + reply = self._read_axispar(*args) + try: + value = getattr(self, pname) + except Exception: + return reply + if reply != value: + if not self.parameters[pname].readonly: + # this should not happen + self.log.warning('hw parameter %s has changed from %r to %r, write again', pname, value, reply) + self._write_axispar(value, *args, readback=False) + reply = self._read_axispar(*args) + return reply + + @WriteHandler(HW_ARGS) + def write_hwparam(self, pname, value): + """handler write for HwParam""" + return self._write_axispar(value, *HW_ARGS[pname]) def read_value(self): encoder = self.read_encoder() @@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable): self._need_reset = True self.status = self.Status.ERROR, 'power loss' # or should we just fix instead of error status? - # self.set('steppos', self.steppos - self.zero, check=False) + # self._write_axispar(self.steppos - self.zero, readback=False) self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag self._started = 0 @@ -256,13 +271,10 @@ class Motor(PersistentMixin, HasIodev, Drivable): self.log.error('encoder (%.2f) does not match internal pos (%.2f)', self.encoder, self.steppos) return self.Status.ERROR, 'encoder does not match internal pos' return self.status - now = self.parameters['steppos'].timestamp - if self.steppos != oldpos: - self._last_change = now - return self.Status.BUSY, 'moving' - if now < self._last_change + 0.2 and not (self.read_target_reached() or self.read_move_status() - or self.read_error_bits()): + if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status() + or self.read_error_bits()): return self.Status.BUSY, 'moving' + # TODO: handle the different errors from move_status and error_bits diff = self.target - self.encoder if abs(diff) <= self.tolerance: self._started = 0 @@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable): def write_target(self, target): self.read_value() # make sure encoder and steppos are fresh - if self.maxcurrent >= self.safe_current + CURRENT_SCALE and ( - abs(target - self.encoder) > self.move_limit + self.tolerance): - raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target)) + if abs(target - self.encoder) > self.move_limit: + raise BadValueError('can not move more than %s deg' % self.move_limit) diff = self.encoder - self.steppos if self._need_reset: raise HardwareError('need reset (%s)' % self.status[1]) @@ -284,91 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable): self._need_reset = True self.status = self.Status.ERROR, 'encoder does not match internal pos' raise HardwareError('need reset (encoder does not match internal pos)') - self.set('steppos', self.encoder - self.zero, check=False) + self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE) self._started = time.time() - self.log.debug('move to %.1f', target) + self.log.info('move to %.1f', target) self.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE) self.status = self.Status.BUSY, 'changed target' return target def write_zero(self, value): - diff = value - self.zero - self.encoder += diff - self.steppos += diff - self.value += diff - return value + self.zero = value + self.read_value() # apply zero to encoder, steppos and value + return Done def read_encoder(self): - return self.get('encoder') + self.zero + return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero def read_steppos(self): - return self.get('steppos') + self.zero - - def read_encoder_tolerance(self): - return self.get('encoder_tolerance') - - def write_encoder_tolerance(self, value): - return self.set('encoder_tolerance', value) - - def read_target_reached(self): - return self.get('target_reached') - - def read_speed(self): - return self.get('speed') - - def write_speed(self, value): - return self.set('speed', value) - - def read_minspeed(self): - return self.get('minspeed') - - def write_minspeed(self, value): - return self.set('minspeed', value) - - def read_currentspeed(self): - return self.get('currentspeed') - - def read_acceleration(self): - return self.get('acceleration') - - def write_acceleration(self, value): - return self.set('acceleration', value) - - def read_maxcurrent(self): - return self.get('maxcurrent') - - def write_maxcurrent(self, value): - return self.set('maxcurrent', value) - - def read_standby_current(self): - return self.get('standby_current') - - def write_standby_current(self, value): - return self.set('standby_current', value) - - def read_free_wheeling(self): - return self.get('free_wheeling') - - def write_free_wheeling(self, value): - return self.set('free_wheeling', value) - - def read_power_down_delay(self): - return self.get('power_down_delay') - - def write_power_down_delay(self, value): - return self.set('power_down_delay', value) - - def read_move_status(self): - return self.get('move_status') - - def read_error_bits(self): - return self.get('error_bits') + return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + self.zero @Command(FloatRange()) def set_zero(self, value): - """adapt zero to make current position equal to given value""" - raw = self.read_value() - self.zero - self.write_zero(value - raw) + """adjust zero""" + self.write_zero(value - self.read_value()) def read_baudrate(self): return self.comm(GET_GLOB_PAR, 65) @@ -381,14 +329,14 @@ class Motor(PersistentMixin, HasIodev, Drivable): """set steppos to encoder value, if not within tolerance""" if self._started: raise IsBusyError('can not reset while moving') - tol = ENCODER_RESOLUTION + tol = ENCODER_RESOLUTION * 1.1 for itry in range(10): diff = self.read_encoder() - self.read_steppos() if abs(diff) <= tol: self._need_reset = False self.status = self.Status.IDLE, 'ok' return - self.set('steppos', self.encoder - self.zero, check=False) + self._write_axispar(self.encoder - self.zero, STEPPOS_ADR, ANGLE_SCALE, readback=False) self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE) time.sleep(0.1) if itry > 5: @@ -398,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable): @Command() def stop(self): + """stop motor immediately""" self.comm(MOTOR_STOP, 0) self.status = self.Status.IDLE, 'stopped' - self.target = self.value # indicate to customers that this was stopped self._started = 0 - #@Command() - #def step(self): - # self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE) + @Command() + def step_forward(self): + """move one full step forwards - #@Command() - #def back(self): - # self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE) + for quick tests + """ + self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE) - #@Command(IntRange(), result=IntRange()) - #def get_axis_par(self, adr): - # return self.comm(GET_AXIS_PAR, adr) + @Command() + def step_back(self): + """move one full step backwards - #@Command((IntRange(), FloatRange()), result=IntRange()) - #def set_axis_par(self, adr, value): - # return self.comm(SET_AXIS_PAR, adr, value) + for quick tests + """ + self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE) + + @Command(IntRange(), result=IntRange()) + def get_axis_par(self, adr): + """get arbitrary motor parameter""" + return self.comm(GET_AXIS_PAR, adr) + + @Command((IntRange(), IntRange()), result=IntRange()) + def set_axis_par(self, adr, value): + """set arbitrary motor parameter""" + return self.comm(SET_AXIS_PAR, adr, value) diff --git a/test/test_attach.py b/test/test_attach.py index 9c6c69c..76aa4b8 100644 --- a/test/test_attach.py +++ b/test/test_attach.py @@ -75,4 +75,6 @@ def test_attach(): assert m.propertyValues['att'] == 'a' srv.dispatcher.register_module(a, 'a') srv.dispatcher.register_module(m, 'm') + assert m.att == 'a' + m.attachedModules = {'att': a} assert m.att == a diff --git a/test/test_lib_enum.py b/test/test_lib_enum.py index 78016fa..97228e1 100644 --- a/test/test_lib_enum.py +++ b/test/test_lib_enum.py @@ -64,6 +64,7 @@ def test_EnumMember(): assert a != 3 assert a == 1 + def test_Enum(): e1 = Enum('e1') e2 = Enum('e2', e1, a=1, b=3) @@ -75,3 +76,4 @@ def test_Enum(): assert e2.b > e3.a assert e3.c >= e2.a assert e3.b <= e2.b + assert Enum({'self': 0, 'other': 1})('self') == 0 diff --git a/test/test_modules.py b/test/test_modules.py index 178d222..7760404 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -149,7 +149,7 @@ def test_ModuleMagic(): a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) # remark: it might be a programming error to override the datatype # and not overriding the read_* method. This is not checked! - b2 = Parameter('', datatype=BoolType(), default=True, + b2 = Parameter('', datatype=StringType(), default='empty', readonly=False, initwrite=True) def write_a1(self, value): @@ -157,6 +157,7 @@ def test_ModuleMagic(): return value def write_b2(self, value): + value = value.upper() self._b2_written = value return value @@ -211,11 +212,11 @@ def test_ModuleMagic(): o2.startModule(event) event.wait() # value has changed type, b2 and a1 are written - expectedAfterStart.update(value=0, b2=True, a1=True) + expectedAfterStart.update(value=0, b2='EMPTY', a1=True) # ramerk: a1=True: this behaviour is a Porgamming error assert updates.pop('o2') == expectedAfterStart assert o2._a1_written == 2.7 - assert o2._b2_written is True + assert o2._b2_written == 'EMPTY' assert not updates