merge until "support write_ method on readonly param and more"

from gerrit

Change-Id: I8d2ad2a381d3a37947d8afc5e17be0428d94df36
This commit is contained in:
zolliker 2022-03-21 14:00:12 +01:00
parent 7c9296fe2e
commit b1ddc01fbb
24 changed files with 604 additions and 436 deletions

View File

@ -162,7 +162,7 @@ max-line-length=132
no-space-check=trailing-comma,dict-separator no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module # 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 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab). # tab).

View File

@ -15,10 +15,8 @@ branches:
- work: current working version, usually in use on /home/l_samenv/frappy (and on neutron instruments) - 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 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 - 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 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: changes are done, eventually a sync step should happen:
1) ideally, this is done when work and wip match 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 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 5) create branch new_work from master
6) go through commits in wip and sort out: 6) go through commits in wip and sort out:
- core commits already pushed through gerrit are skipped - core commits already pushed through gerrit are skipped
- all other commits are to be cherry-picked - all other commits are to be cherry-picked
7) when arrived at the point where the new working version should be, 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 .' copy new_wip branch to work with 'git checkout -B work'.
(note the dot!) and then commit this. Not sure if this works, as work is to be pushed to git.psi.ch.
8) continue with (6) if wip and work should differ We might first remove the remote branch with 'git push origin --delete work'.
9) do like (7), but for wip branch And then create again (git push origin work)?
10) delete new_wip branch, push master, wip and work branches 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 ## 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_psi /Volumes/PPMSData/zolliker/frappy/secop_psi
cp -r secop /Volumes/PPMSData/zolliker/frappy/secop 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 ...

5
cfg/generalConfig.cfg Normal file
View File

@ -0,0 +1,5 @@
[FRAPPY]
# general config for running in git repo
logdir = ./log
piddir = ./pid
confdir = ./cfg

View File

@ -1,24 +1,23 @@
[node LscSIM.psi.ch] [NODE]
id = LscSIM.psi.ch
description = Lsc Simulation at PSI description = Lsc Simulation at PSI
[interface tcp] [INTERFACE]
type = tcp uri = tcp://5000
bindto = 0.0.0.0
bindport = 5000
[module res] [res]
class = secop_psi.ls370res.ResChannel class = secop_psi.ls370res.ResChannel
.channel = 3 channel = 3
.description = resistivity description = resistivity
.main = lsmain main = lsmain
.iodev = lscom io = lscom
[module lsmain] [lsmain]
class = secop_psi.ls370res.Main class = secop_psi.ls370res.Main
.description = main control of Lsc controller description = main control of Lsc controller
.iodev = lscom io = lscom
[module lscom] [lscom]
class = secop_psi.ls370sim.Ls370Sim class = secop_psi.ls370sim.Ls370Sim
.description = simulated serial communicator to a LS 370 description = simulated serial communicator to a LS 370
.visibility = 3 visibility = 3

View File

@ -1,24 +1,20 @@
[NODE] [node LscSIM.psi.ch]
id = ls370res.psi.ch
description = Lsc370 Test description = Lsc370 Test
[INTERFACE] [interface tcp]
uri = tcp://5000 type = tcp
bindto = 0.0.0.0
bindport = 5000
[lsmain_iodev] [module lsmain]
description = the communication device
class = secop_psi.ls370res.StringIO
uri = localhost:4567
[lsmain]
class = secop_psi.ls370res.Main class = secop_psi.ls370res.Main
description = main control of Lsc controller description = main control of Lsc controller
iodev = lsmain_iodev uri = localhost:4567
[res] [module res]
class = secop_psi.ls370res.ResChannel class = secop_psi.ls370res.ResChannel
iexc = '1mA' vexc = '2mV'
channel = 5 channel = 3
description = resistivity description = resistivity
main = lsmain main = lsmain
# the auto created iodev from lsmain: # the auto created iodev from lsmain:

View File

@ -8,117 +8,117 @@ uri = tcp://5000
[tt] [tt]
class = secop_psi.ppms.Temp class = secop_psi.ppms.Temp
description = main temperature description = main temperature
iodev = ppms io = ppms
[mf] [mf]
class = secop_psi.ppms.Field class = secop_psi.ppms.Field
target.min = -9 target.min = -9
target.max = 9 target.max = 9
.description = magnetic field description = magnetic field
.iodev = ppms io = ppms
[pos] [pos]
class = secop_psi.ppms.Position class = secop_psi.ppms.Position
.description = sample rotator description = sample rotator
.iodev = ppms io = ppms
[lev] [lev]
class = secop_psi.ppms.Level class = secop_psi.ppms.Level
.description = helium level description = helium level
.iodev = ppms io = ppms
[chamber] [chamber]
class = secop_psi.ppms.Chamber class = secop_psi.ppms.Chamber
.description = chamber state description = chamber state
.iodev = ppms io = ppms
[r1] [r1]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 1 description = resistivity channel 1
.no = 1 no = 1
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r2] [r2]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 2 description = resistivity channel 2
.no = 2 no = 2
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r3] [r3]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 3 description = resistivity channel 3
.no = 3 no = 3
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[r4] [r4]
class = secop_psi.ppms.BridgeChannel class = secop_psi.ppms.BridgeChannel
.description = resistivity channel 4 description = resistivity channel 4
.no = 4 no = 4
value.unit = Ohm value.unit = Ohm
.iodev = ppms io = ppms
[i1] [i1]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 1 description = current channel 1
.no = 1 no = 1
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i2] [i2]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 2 description = current channel 2
.no = 2 no = 2
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i3] [i3]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 3 description = current channel 3
.no = 3 no = 3
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[i4] [i4]
class = secop_psi.ppms.Channel class = secop_psi.ppms.Channel
.description = current channel 4 description = current channel 4
.no = 4 no = 4
value.unit = uA value.unit = uA
.iodev = ppms io = ppms
[v1] [v1]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 1 description = voltage channel 1
.no = 1 no = 1
value.unit = V value.unit = V
.iodev = ppms io = ppms
[v2] [v2]
class = secop_psi.ppms.DriverChannel class = secop_psi.ppms.DriverChannel
.description = voltage channel 2 description = voltage channel 2
.no = 2 no = 2
value.unit = V value.unit = V
.iodev = ppms io = ppms
[tv] [tv]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = VTI temperature description = VTI temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms io = ppms
[ts] [ts]
class = secop_psi.ppms.UserChannel class = secop_psi.ppms.UserChannel
.description = sample temperature description = sample temperature
enabled = 1 enabled = 1
value.unit = K value.unit = K
.iodev = ppms io = ppms
[ppms] [ppms]
class = secop_psi.ppms.Main class = secop_psi.ppms.Main
.description = the main and poller module description = the main and poller module
.class_id = QD.MULTIVU.PPMS.1 class_id = QD.MULTIVU.PPMS.1
.visibility = 3 visibility = 3
pollinterval = 2 pollinterval = 2

View File

@ -7,7 +7,7 @@ Module Base Classes
.. autodata:: secop.modules.Done .. autodata:: secop.modules.Done
.. autoclass:: secop.modules.Module .. autoclass:: secop.modules.Module
:members: earlyInit, initModule, startModule, pollerClass :members: earlyInit, initModule, startModule
.. autoclass:: secop.modules.Readable .. autoclass:: secop.modules.Readable
:members: Status :members: Status

View File

@ -1,6 +1,7 @@
# doc # doc
sphinx_rtd_theme sphinx_rtd_theme
Sphinx>=1.2.1 Sphinx>=1.2.1
# for generating docu
markdown>=2.6 markdown>=2.6
# test suite # test suite
pytest pytest

View File

@ -31,7 +31,7 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
from secop.iohandler import IOHandler, IOHandlerBase from secop.iohandler import IOHandler, IOHandlerBase
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.modules import Attached, Communicator, \ 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.params import Command, Parameter
from secop.properties import Property from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class 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.persistent import PersistentMixin, PersistentParam
from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \ from secop.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
CommonWriteHandler, nopoll CommonWriteHandler, nopoll
ERROR = Drivable.Status.ERROR
WARN = Drivable.Status.WARN
BUSY = Drivable.Status.BUSY
IDLE = Drivable.Status.IDLE

View File

@ -22,7 +22,7 @@
# ***************************************************************************** # *****************************************************************************
"""Define validated data types.""" """Define validated data types."""
# pylint: disable=abstract-method # pylint: disable=abstract-method, too-many-lines
import sys import sys
@ -454,7 +454,10 @@ class EnumType(DataType):
super().__init__() super().__init__()
if members is not None: if members is not None:
kwds.update(members) 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]] self.default = self._enum[self._enum.members[0]]
def copy(self): def copy(self):

View File

@ -178,7 +178,9 @@ class StateMachine:
if self.stop_exc: if self.stop_exc:
raise self.stop_exc raise self.stop_exc
except Exception as e: 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 self.last_error = e
ret = self.cleanup(self) ret = self.cleanup(self)
self.log.debug('cleanup %r %r %r', self.cleanup, self.last_error, ret) self.log.debug('cleanup %r %r %r', self.cleanup, self.last_error, ret)

View File

@ -161,8 +161,8 @@ class HasAccessibles(HasProperties):
new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
setattr(cls, 'read_' + pname, new_rfunc) 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 wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated
if (wfunc is None or wrapped) and pobj.handler: if (wfunc is None or wrapped) and pobj.handler:
# ignore the handler, if a write function is present # 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) self.log.debug('validate %r for %r', value, pname)
# we do not need to handle errors here, we do not # we do not need to handle errors here, we do not
# want to make a parameter invalid, when a write failed # want to make a parameter invalid, when a write failed
value = pobj.datatype(value) new_value = pobj.datatype(value)
returned_value = wfunc(self, value) new_value = wfunc(self, new_value)
self.log.debug('write_%s(%r) returned %r', pname, value, returned_value) self.log.debug('write_%s(%r) returned %r', pname, value, new_value)
if returned_value is Done: if new_value is Done:
# setattr(self, pname, getattr(self, pname)) # setattr(self, pname, getattr(self, pname))
return getattr(self, pname) return getattr(self, pname)
setattr(self, pname, value) # important! trigger the setter setattr(self, pname, new_value) # important! trigger the setter
return value return new_value
else: else:
def new_wfunc(self, value, pname=pname): def new_wfunc(self, value, pname=pname):
@ -201,14 +201,14 @@ class HasAccessibles(HasProperties):
setattr(cls, 'write_' + pname, new_wfunc) setattr(cls, 'write_' + pname, new_wfunc)
# check for programming errors # check for programming errors
for attrname, attrvalue in cls.__dict__.items(): for attrname in dir(cls):
prefix, _, pname = attrname.partition('_') prefix, _, pname = attrname.partition('_')
if not pname: if not pname:
continue continue
if prefix == 'do': if prefix == 'do':
raise ProgrammingError('%r: old style command %r not supported anymore' raise ProgrammingError('%r: old style command %r not supported anymore'
% (cls.__name__, attrname)) % (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' raise ProgrammingError('%s.%s defined, but %r is no parameter'
% (cls.__name__, attrname, pname)) % (cls.__name__, attrname, pname))
@ -280,6 +280,7 @@ class Module(HasAccessibles):
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
attachedModules = None
def __init__(self, name, logger, cfgdict, srv): def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks) # remember the dispatcher object (for the async callbacks)
@ -391,9 +392,8 @@ class Module(HasAccessibles):
continue continue
if pname in cfgdict: 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_<pname> # parameters given in cfgdict have to call write_<pname>
# TODO: not sure about readonly (why not a parameter which can only be written from config?)
try: try:
pobj.value = pobj.datatype(cfgdict[pname]) pobj.value = pobj.datatype(cfgdict[pname])
self.writeDict[pname] = pobj.value self.writeDict[pname] = pobj.value
@ -416,10 +416,8 @@ class Module(HasAccessibles):
except BadValueError as e: except BadValueError as e:
# this should not happen, as the default is already checked in Parameter # 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 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_<pname> # we will need to call write_<pname>
# 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 pobj.value = value
self.writeDict[pname] = value self.writeDict[pname] = value
else: else:
@ -649,11 +647,11 @@ class Module(HasAccessibles):
self.log.info('recovered after %d calls to doPoll (%r)', error_count, last_error) self.log.info('recovered after %d calls to doPoll (%r)', error_count, last_error)
last_error = None last_error = None
except Exception as e: except Exception as e:
if type(e) != last_error: if repr(e) != last_error:
error_count = 0 error_count = 0
self.log.error('error in doPoll: %r', e) self.log.error('error in doPoll: %r', e)
error_count += 1 error_count += 1
last_error = e last_error = repr(e)
now = time.time() now = time.time()
# find ONE due slow poll and call it # find ONE due slow poll and call it
loop = True loop = True
@ -803,14 +801,15 @@ class Attached(Property):
assign a module name to this property in the cfg file, assign a module name to this property in the cfg file,
and the server will create an attribute with this module and the server will create an attribute with this module
""" """
module = None def __init__(self, basecls=Module, description='attached module', mandatory=True):
self.basecls = basecls
def __init__(self, description='attached module'): super().__init__(description, StringType(), mandatory=mandatory)
super().__init__(description, StringType(), mandatory=False)
def __get__(self, obj, owner): def __get__(self, obj, owner):
if obj is None: if obj is None:
return self return self
if self.module is None: if obj.attachedModules is None:
self.module = obj.DISPATCHER.get_module(super().__get__(obj, owner)) # return the name of the module (called from Server on startup)
return self.module return super().__get__(obj, owner)
# return the module (called after startup)
return obj.attachedModules.get(self.name) # return None if not given

View File

@ -75,7 +75,7 @@ class PersistentMixin(HasAccessibles):
self.initData = {} self.initData = {}
for pname in self.parameters: for pname in self.parameters:
pobj = self.parameters[pname] 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 self.initData[pname] = pobj.value
if pobj.persistent == 'auto': if pobj.persistent == 'auto':
def cb(value, m=self): def cb(value, m=self):

View File

@ -23,8 +23,7 @@
from secop.client import SecopClient, decode_msg, encode_msg_frame from secop.client import SecopClient, decode_msg, encode_msg_frame
from secop.datatypes import StringType from secop.datatypes import StringType
from secop.errors import BadValueError, \ from secop.errors import BadValueError, CommunicationFailedError, ConfigError
CommunicationFailedError, ConfigError, make_secop_error
from secop.lib import get_class from secop.lib import get_class
from secop.modules import Drivable, Module, Readable, Writable from secop.modules import Drivable, Module, Readable, Writable
from secop.params import Command, Parameter from secop.params import Command, Parameter
@ -47,8 +46,6 @@ class ProxyModule(HasIO, Module):
if parameter not in self.parameters: if parameter not in self.parameters:
return # ignore unknown parameters return # ignore unknown parameters
# should be done here: deal with clock differences # should be done here: deal with clock differences
if readerror:
readerror = make_secop_error(*readerror)
self.announceUpdate(parameter, value, readerror, timestamp) self.announceUpdate(parameter, value, readerror, timestamp)
def initModule(self): def initModule(self):
@ -201,7 +198,7 @@ def proxy_class(remote_class, name=None):
def wfunc(self, value, pname=aname): def wfunc(self, value, pname=aname):
value, _, readerror = self._secnode.setParameter(self.name, pname, value) value, _, readerror = self._secnode.setParameter(self.name, pname, value)
if readerror: if readerror:
raise make_secop_error(*readerror) raise readerror
return value return value
attrs['write_' + aname] = wfunc attrs['write_' + aname] = wfunc

View File

@ -30,10 +30,11 @@ import sys
import traceback import traceback
from collections import OrderedDict 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 import formatException, get_class, generalConfig
from secop.lib.multievent import MultiEvent from secop.lib.multievent import MultiEvent
from secop.params import PREDEFINED_ACCESSIBLES from secop.params import PREDEFINED_ACCESSIBLES
from secop.modules import Attached
try: try:
from daemon import DaemonContext from daemon import DaemonContext
@ -275,6 +276,24 @@ class Server:
missing_super.add('%s was not called, probably missing super call' missing_super.add('%s was not called, probably missing super call'
% modobj.earlyInit.__qualname__) % 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 # call init on each module after registering all
for modname, modobj in self.modules.items(): for modname, modobj in self.modules.items():
try: try:
@ -287,18 +306,17 @@ class Server:
failure_traceback = traceback.format_exc() failure_traceback = traceback.format_exc()
errors.append('error initializing %s: %r' % (modname, e)) errors.append('error initializing %s: %r' % (modname, e))
if self._testonly: if not self._testonly:
return start_events = MultiEvent(default_timeout=30)
start_events = MultiEvent(default_timeout=30) for modname, modobj in self.modules.items():
for modname, modobj in self.modules.items(): # startModule must return either a timeout value or None (default 30 sec)
# startModule must return either a timeout value or None (default 30 sec) start_events.name = 'module %s' % modname
start_events.name = 'module %s' % modname modobj.startModule(start_events)
modobj.startModule(start_events) if not modobj.startModuleDone:
if not modobj.startModuleDone: missing_super.add('%s was not called, probably missing super call'
missing_super.add('%s was not called, probably missing super call' % modobj.startModule.__qualname__)
% modobj.startModule.__qualname__) errors.extend(missing_super)
errors.extend(missing_super)
if errors: if errors:
for errtxt in errors: for errtxt in errors:
for line in errtxt.split('\n'): for line in errtxt.split('\n'):
@ -310,6 +328,8 @@ class Server:
sys.stderr.write(failure_traceback) sys.stderr.write(failure_traceback)
sys.exit(1) sys.exit(1)
if self._testonly:
return
self.log.info('waiting for modules being started') self.log.info('waiting for modules being started')
start_events.name = None start_events.name = None
if not start_events.wait(): if not start_events.wait():

View File

@ -72,7 +72,7 @@ class HeLevel(HasIO, Readable):
""" """
name, txtvalue = self.communicate(cmd).split('=') name, txtvalue = self.communicate(cmd).split('=')
assert name == cmd.split('=')[0] # check that we got a reply to our command 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): def read_value(self):
return self.query('h') return self.query('h')

140
secop_psi/historywriter.py Normal file
View File

@ -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 <markus.zolliker@psi.ch>
# *****************************************************************************
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 (<conversion function>, <tail>, <curve options>)
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 <key> with options from dict <opts>
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 <period> 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 <key> of <value as string>, containing the last used value
"""
def __init__(self, directory, predefined_names, dispatcher):
super().__init__(directory)
self.predefined_names = predefined_names
self.cvt_lists = {} # dict <mod:param> of <conversion list>
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, '')

View File

@ -18,12 +18,18 @@
# Module authors: # Module authors:
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# ***************************************************************************** # *****************************************************************************
"""Keithley 2601B source meter """Keithley 2601B 4 quadrant source meter
not tested yet""" not tested yet
from secop.core import Attached, BoolType, EnumType, FloatRange, \ * switching between voltage and current happens by setting their target
HasIodev, Module, Parameter, StringIO, Writable, Done * 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): class K2601bIO(StringIO):
@ -32,128 +38,162 @@ class K2601bIO(StringIO):
SOURCECMDS = { SOURCECMDS = {
0: 'reset()' 0: 'reset()'
' smua.source.output = 0 print("ok")',
1: 'reset()'
' smua.source.func = smua.OUTPUT_DCAMPS' ' smua.source.func = smua.OUTPUT_DCAMPS'
' display.smua.measure.func = display.MEASURE_VOLTS' ' display.smua.measure.func = display.MEASURE_VOLTS'
' smua.source.autorangei = 1' ' smua.source.autorangei = 1'
' smua.source.output = %d print("ok"")', ' smua.source.output = 1 print("ok")',
1: 'reset()' 2: 'reset()'
' smua.source.func = smua.OUTPUT_DCVOLTS' ' smua.source.func = smua.OUTPUT_DCVOLTS'
' display.smua.measure.func = display.MEASURE_DCAMPS' ' display.smua.measure.func = display.MEASURE_DCAMPS'
' smua.source.autorangev = 1' ' 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) ioClass = K2601bIO
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())')
def read_mode(self): 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): 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() 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): def write_ilimit(self, value):
sourcemeter = Attached() 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) def read_vlimit(self):
target = Parameter('set current', FloatRange(unit='A'), poll=True) if self.mode == 'voltage':
active = Parameter('current is controlled', BoolType(), default=False) # polled by SourceMeter return self.ilimit
limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True) return float(self.communicate('print(smua.source.limitv)'))
def initModule(self): def write_vlimit(self, value):
self._sourcemeter.registerCallbacks(self) 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): 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): def read_target(self):
return self.sendRecv('print(smua.source.leveli)') return float(self.communicate('print(smua.source.leveli)'))
def write_target(self, value): def write_target(self, value):
if not self.active: if value > self.sourcemeter.ilimit:
raise ValueError('current source is disabled')
if value > self.limit:
raise ValueError('current exceeds limit') 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): def read_limit(self):
if self.active: return self.sourcemeter.read_ilimit()
return self.limit
return self.sendRecv('print(smua.source.limiti)')
def write_limit(self, value): def write_limit(self, value):
if self.active: return self.sourcemeter.write_ilimit(value)
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
def update_mode(self, mode): def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes # 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() sourcemeter = Attached()
value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) value = Parameter('measured voltage', FloatRange(unit='V'))
target = Parameter('set voltage', FloatRange(unit='V'), poll=True) target = Parameter('set voltage', FloatRange(unit='V'))
active = Parameter('voltage is controlled', BoolType(), default=False) # polled by SourceMeter active = Parameter('voltage is controlled', BoolType())
limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) limit = Parameter('voltage limit', FloatRange(0, 2.0, unit='V'), default=2)
def initModule(self): def initModule(self):
self._sourcemeter.registerCallbacks(self) self.sourcemeter.registerCallbacks(self)
def read_value(self): def read_value(self):
return self.sendRecv('print(smua.measure.v())') return float(self.communicate('print(smua.measure.v())'))
def read_target(self): def read_target(self):
return self.sendRecv('print(smua.source.levelv)') return float(self.communicate('print(smua.source.levelv)'))
def write_target(self, value): def write_target(self, value):
if not self.active: if value > self.sourcemeter.vlimit:
raise ValueError('voltage source is disabled')
if value > self.limit:
raise ValueError('voltage exceeds limit') 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): def read_limit(self):
if self.active: return self.sourcemeter.read_vlimit()
return self.limit
return self.sendRecv('print(smua.source.limitv)')
def write_limit(self, value): def write_limit(self, value):
if self.active: return self.sourcemeter.write_vlimit(value)
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
def update_mode(self, mode): def update_mode(self, mode):
# will be called whenever the attached sourcemeters mode changes # 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

View File

@ -27,8 +27,7 @@ from secop.datatypes import BoolType, EnumType, FloatRange, IntRange
from secop.lib import formatStatusBits from secop.lib import formatStatusBits
from secop.modules import Attached, Done, \ from secop.modules import Attached, Done, \
Drivable, Parameter, Property, Readable Drivable, Parameter, Property, Readable
from secop.poller import REGULAR, Poller from secop.io import HasIO
from secop.io import HasIodev
Status = Drivable.Status Status = Drivable.Status
@ -58,29 +57,29 @@ class StringIO(secop.io.StringIO):
wait_before = 0.05 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)) target = Parameter('channel to select', datatype=IntRange(0, 17))
autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False)
pollinterval = Parameter(default=1, export=False) pollinterval = Parameter(default=1, export=False)
pollerClass = Poller ioClass = StringIO
iodevClass = StringIO
_channel_changed = 0 # time of last channel change _channel_changed = 0 # time of last channel change
_channels = None # dict <channel no> of <module object> _channels = None # dict <channel no> of <module object>
def earlyInit(self): def earlyInit(self):
super().earlyInit()
self._channels = {} self._channels = {}
def register_channel(self, modobj): def register_channel(self, modobj):
self._channels[modobj.channel] = modobj self._channels[modobj.channel] = modobj
def startModule(self, started_callback): def startModule(self, start_events):
started_callback() super().startModule(start_events)
for ch in range(1, 16): for ch in range(1, 16):
if ch not in self._channels: 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): def read_value(self):
channel, auto = scan.send_command(self) channel, auto = scan.send_command(self)
@ -113,7 +112,7 @@ class Main(HasIodev, Drivable):
def write_target(self, channel): def write_target(self, channel):
scan.send_change(self, channel, self.autoscan) 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: if channel != self.value:
self.value = 0 self.value = 0
self._channel_changed = time.time() self._channel_changed = time.time()
@ -122,11 +121,11 @@ class Main(HasIodev, Drivable):
def write_autoscan(self, value): def write_autoscan(self, value):
scan.send_change(self, self.value, 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 return value
class ResChannel(HasIodev, Readable): class ResChannel(HasIO, Readable):
"""temperature channel on Lakeshore 336""" """temperature channel on Lakeshore 336"""
RES_RANGE = {key: i+1 for i, key in list( 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'] enumerate(mag % val for mag in ['%guV', '%gmV']
for val in [2, 6.32, 20, 63.2, 200, 632]))} for val in [2, 6.32, 20, 63.2, 200, 632]))}
pollerClass = Poller ioClass = StringIO
iodevClass = StringIO
_main = None # main module _main = None # main module
_last_range_change = 0 # time of last range change _last_range_change = 0 # time of last range change
@ -166,6 +164,7 @@ class ResChannel(HasIodev, Readable):
_trigger_read = False _trigger_read = False
def initModule(self): def initModule(self):
super().initModule()
self._main = self.DISPATCHER.get_module(self.main) self._main = self.DISPATCHER.get_module(self.main)
self._main.register_channel(self) self._main.register_channel(self)
@ -181,7 +180,7 @@ class ResChannel(HasIodev, Readable):
return Done return Done
# we got here, when we missed the idle state of self._main # we got here, when we missed the idle state of self._main
self._trigger_read = False self._trigger_read = False
result = self.sendRecv('RDGR?%d' % self.channel) result = self.communicate('RDGR?%d' % self.channel)
result = float(result) result = float(result)
if self.autorange == 'soft': if self.autorange == 'soft':
now = time.time() now = time.time()
@ -214,9 +213,9 @@ class ResChannel(HasIodev, Readable):
def read_status(self): def read_status(self):
if not self.enabled: if not self.enabled:
return [self.Status.DISABLED, 'disabled'] return [self.Status.DISABLED, 'disabled']
if self.channel != self._main.value: if self.channel != self.main.value:
return Done 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) result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities)
statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS)) statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS))
if statustext: if statustext:
@ -228,7 +227,7 @@ class ResChannel(HasIodev, Readable):
if autorange: if autorange:
result['autorange'] = 'hard' result['autorange'] = 'hard'
# else: do not change autorange # 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: if excoff:
result.update(iexc=0, vexc=0) result.update(iexc=0, vexc=0)
elif iscur: elif iscur:
@ -281,5 +280,5 @@ class ResChannel(HasIodev, Readable):
def write_enabled(self, value): def write_enabled(self, value):
inset.write(self, 'enabled', value) inset.write(self, 'enabled', value)
if value: if value:
self._main.write_target(self.channel) self.main.write_target(self.channel)
return Done return Done

View File

@ -24,13 +24,12 @@
import time import time
import struct import struct
from math import log10
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \ 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.io import BytesIO
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError
from secop.rwhandler import ReadHandler, WriteHandler
MOTOR_STOP = 3 MOTOR_STOP = 3
MOVE = 4 MOVE = 4
@ -50,84 +49,78 @@ MAX_SPEED = 2047 * SPEED_SCALE
ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE ACCEL_SCALE = 1E12 / 2 ** 31 * ANGLE_SCALE
MAX_ACCEL = 2047 * ACCEL_SCALE MAX_ACCEL = 2047 * ACCEL_SCALE
CURRENT_SCALE = 2.8/250 CURRENT_SCALE = 2.8/250
ENCODER_RESOLUTION = 0.4 # 360 / 1024, rounded up ENCODER_RESOLUTION = 360 / 1024
HW_ARGS = {
# <parameter name>: (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): def writable(*args, **kwds):
adr = Property('parameter address', IntRange(0, 255), export=False) """convenience function to create writable hardware parameters"""
scale = Property('scale factor (physical value / unit)', FloatRange(), export=False) return PersistentParam(*args, readonly=False, initwrite=True, **kwds)
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
class Motor(PersistentMixin, HasIodev, Drivable): class Motor(PersistentMixin, HasIO, Drivable):
address = Property('module address', IntRange(0, 255), default=1) 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) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), encoder = PersistentParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True) readonly=True, initwrite=False)
steppos = HwParam('position from motor steps', FloatRange(unit='$'), steppos = PersistentParam('position from motor steps', FloatRange(unit='$', fmtstr='%.3f'),
1, ANGLE_SCALE, readonly=True, initwrite=False) readonly=True, initwrite=False)
target = Parameter('_', FloatRange(unit='$'), default=0) target = Parameter('', FloatRange(unit='$'), default=0)
move_limit = Parameter('max. angle to drive in one go when current above safe_current', FloatRange(unit='$'), move_limit = Parameter('max. angle to drive in one go', FloatRange(unit='$'),
readonly=False, default=5, group='more') readonly=False, default=360, group='more')
safe_current = Parameter('motor current allowed for big steps', FloatRange(unit='A'),
readonly=False, default=0.2, group='more')
tolerance = Parameter('positioning tolerance', FloatRange(unit='$'), tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
readonly=False, default=0.9) readonly=False, default=0.9)
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance', encoder_tolerance = writable('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
FloatRange(0, 360., unit='$'), FloatRange(0, 360., unit='$', fmtstr='%.3f'), group='more')
212, ANGLE_SCALE, readonly=False, group='more') speed = writable('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'), default=40)
speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), minspeed = writable('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
4, SPEED_SCALE, readonly=False, group='motorparam') default=SPEED_SCALE, group='motorparam')
minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), currentspeed = Parameter('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec', fmtstr='%.1f'),
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam') group='motorparam')
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'), maxcurrent = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
3, SPEED_SCALE, readonly=True, group='motorparam') default=1.4, group='motorparam')
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'), standby_current = writable('', FloatRange(0, 2.8, unit='A', fmtstr='%.2f'),
6, CURRENT_SCALE, readonly=False, group='motorparam') default=0.1, group='motorparam')
standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'), acceleration = writable('', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2', fmtstr='%.1f'),
7, CURRENT_SCALE, readonly=False, group='motorparam') default=150., group='motorparam')
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'), target_reached = Parameter('', BoolType(), group='hwstatus')
5, ACCEL_SCALE, readonly=False, group='motorparam') move_status = Parameter('', IntRange(0, 3), group='hwstatus')
target_reached = HwParam('_', BoolType(), 8, group='hwstatus') error_bits = Parameter('', IntRange(0, 255), group='hwstatus')
move_status = HwParam('_', IntRange(0, 3), free_wheeling = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
207, readonly=True, group='hwstatus') default=0.1, group='motorparam')
error_bits = HwParam('_', IntRange(0, 255), power_down_delay = writable('', FloatRange(0, 60., unit='sec', fmtstr='%.2f'),
208, readonly=True, group='hwstatus') default=0.1, group='motorparam')
# the doc says msec, but I believe the scale is 10 msec baudrate = Parameter('', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'), readonly=False, default=0, visibility=3, group='more')
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')
pollinterval = Parameter(group='more') pollinterval = Parameter(group='more')
ioClass = BytesIO
iodevClass = BytesIO fast_pollfactor = 0.001 # not used any more, TODO: use a statemachine for running
# fast_pollfactor = 0.001 # poll as fast as possible when busy
fast_pollfactor = 0.05
_started = 0 _started = 0
_calc_timeout = True _calcTimeout = True
_need_reset = None _need_reset = None
_last_change = 0
def comm(self, cmd, adr, value=0, bank=0): def comm(self, cmd, adr, value=0, bank=0):
"""set or get a parameter """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 :param value: if given, the parameter is written, else it is returned
:return: the returned value :return: the returned value
""" """
if self._calc_timeout: if self._calcTimeout and self.io._conn:
self._calc_timeout = False self._calcTimeout = False
baudrate = getattr(self._iodev._conn.connection, 'baudrate', None) baudrate = getattr(self.io._conn.connection, 'baudrate', None)
if baudrate: if baudrate:
if baudrate not in BAUDRATES: if baudrate not in BAUDRATES:
raise CommunicationFailedError('unsupported baud rate: %d' % baudrate) raise CommunicationFailedError('unsupported baud rate: %d' % baudrate)
self._iodev.timeout = 0.03 + 200 / baudrate self.io.timeout = 0.03 + 200 / baudrate
exc = None 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): for itry in range(3,0,-1):
byt = struct.pack('>BBBBi', self.address, cmd, adr, bank, round(value))
try: try:
reply = self._iodev.communicate(byt + bytes([sum(byt) & 0xff]), 9) reply = self.communicate(byt, 9)
if sum(reply[:-1]) & 0xff != reply[-1]: if sum(reply[:-1]) & 0xff != reply[-1]:
raise CommunicationFailedError('checksum error') raise CommunicationFailedError('checksum error')
# will try again
except Exception as e: except Exception as e:
if itry == 1: if itry == 1:
raise raise
@ -168,36 +163,18 @@ class Motor(PersistentMixin, HasIodev, Drivable):
raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr)) raise CommunicationFailedError('bad reply %r to command %s %d' % (reply, cmd, adr))
return result return result
def get(self, pname, **kwds): def startModule(self, start_events):
"""get parameter""" super().startModule(start_events)
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 set(self, pname, value, check=True, **kwds): def fix_encoder(self=self):
"""set parameter and check result""" try:
pobj = self.parameters[pname] # get encoder value from motor. at this stage self.encoder contains the persistent value
scale = pobj.scale encoder = self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
rawvalue = round(value / scale) self.fix_encoder(encoder)
for itry in range(2): except Exception as e:
self.comm(SET_AXIS_PAR, pobj.adr, rawvalue, **kwds) self.log.error('fix_encoder failed with %r', e)
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 startModule(self, started_callback): start_events.queue(fix_encoder)
# 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)
def fix_encoder(self, encoder_from_hw): def fix_encoder(self, encoder_from_hw):
"""fix encoder value """fix encoder value
@ -211,14 +188,52 @@ class Motor(PersistentMixin, HasIodev, Drivable):
# calculate nearest, most probable value # calculate nearest, most probable value
adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360 adjusted_encoder = encoder_from_hw + round((self.encoder - encoder_from_hw) / 360.) * 360
if abs(self.encoder - adjusted_encoder) >= self.encoder_tolerance: 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.log.error('saved encoder value (%.2f) does not match reading (%.2f %.2f)',
self.encoder, encoder_from_hw, adjusted_encoder) self.encoder, encoder_from_hw, adjusted_encoder)
if adjusted_encoder != encoder_from_hw: if adjusted_encoder != encoder_from_hw:
self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder) self.log.info('take next closest encoder value (%.2f)' % adjusted_encoder)
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'saved encoder value does not match reading' 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): def read_value(self):
encoder = self.read_encoder() encoder = self.read_encoder()
@ -240,7 +255,7 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'power loss' self.status = self.Status.ERROR, 'power loss'
# or should we just fix instead of error status? # 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.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
self._started = 0 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) 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.ERROR, 'encoder does not match internal pos'
return self.status return self.status
now = self.parameters['steppos'].timestamp if oldpos != self.steppos or not (self.read_target_reached() or self.read_move_status()
if self.steppos != oldpos: or self.read_error_bits()):
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()):
return self.Status.BUSY, 'moving' return self.Status.BUSY, 'moving'
# TODO: handle the different errors from move_status and error_bits
diff = self.target - self.encoder diff = self.target - self.encoder
if abs(diff) <= self.tolerance: if abs(diff) <= self.tolerance:
self._started = 0 self._started = 0
@ -273,9 +285,8 @@ class Motor(PersistentMixin, HasIodev, Drivable):
def write_target(self, target): def write_target(self, target):
self.read_value() # make sure encoder and steppos are fresh self.read_value() # make sure encoder and steppos are fresh
if self.maxcurrent >= self.safe_current + CURRENT_SCALE and ( if abs(target - self.encoder) > self.move_limit:
abs(target - self.encoder) > self.move_limit + self.tolerance): raise BadValueError('can not move more than %s deg' % self.move_limit)
raise BadValueError('can not move more than %s deg %g %g' % (self.move_limit, self.encoder, target))
diff = self.encoder - self.steppos diff = self.encoder - self.steppos
if self._need_reset: if self._need_reset:
raise HardwareError('need reset (%s)' % self.status[1]) raise HardwareError('need reset (%s)' % self.status[1])
@ -284,91 +295,28 @@ class Motor(PersistentMixin, HasIodev, Drivable):
self._need_reset = True self._need_reset = True
self.status = self.Status.ERROR, 'encoder does not match internal pos' self.status = self.Status.ERROR, 'encoder does not match internal pos'
raise HardwareError('need reset (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._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.comm(MOVE, 0, (target - self.zero) / ANGLE_SCALE)
self.status = self.Status.BUSY, 'changed target' self.status = self.Status.BUSY, 'changed target'
return target return target
def write_zero(self, value): def write_zero(self, value):
diff = value - self.zero self.zero = value
self.encoder += diff self.read_value() # apply zero to encoder, steppos and value
self.steppos += diff return Done
self.value += diff
return value
def read_encoder(self): def read_encoder(self):
return self.get('encoder') + self.zero return self._read_axispar(ENCODER_ADR, ANGLE_SCALE) + self.zero
def read_steppos(self): def read_steppos(self):
return self.get('steppos') + self.zero return self._read_axispar(STEPPOS_ADR, ANGLE_SCALE) + 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')
@Command(FloatRange()) @Command(FloatRange())
def set_zero(self, value): def set_zero(self, value):
"""adapt zero to make current position equal to given value""" """adjust zero"""
raw = self.read_value() - self.zero self.write_zero(value - self.read_value())
self.write_zero(value - raw)
def read_baudrate(self): def read_baudrate(self):
return self.comm(GET_GLOB_PAR, 65) 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""" """set steppos to encoder value, if not within tolerance"""
if self._started: if self._started:
raise IsBusyError('can not reset while moving') raise IsBusyError('can not reset while moving')
tol = ENCODER_RESOLUTION tol = ENCODER_RESOLUTION * 1.1
for itry in range(10): for itry in range(10):
diff = self.read_encoder() - self.read_steppos() diff = self.read_encoder() - self.read_steppos()
if abs(diff) <= tol: if abs(diff) <= tol:
self._need_reset = False self._need_reset = False
self.status = self.Status.IDLE, 'ok' self.status = self.Status.IDLE, 'ok'
return 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) self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
time.sleep(0.1) time.sleep(0.1)
if itry > 5: if itry > 5:
@ -398,23 +346,33 @@ class Motor(PersistentMixin, HasIodev, Drivable):
@Command() @Command()
def stop(self): def stop(self):
"""stop motor immediately"""
self.comm(MOTOR_STOP, 0) self.comm(MOTOR_STOP, 0)
self.status = self.Status.IDLE, 'stopped' self.status = self.Status.IDLE, 'stopped'
self.target = self.value # indicate to customers that this was stopped
self._started = 0 self._started = 0
#@Command() @Command()
#def step(self): def step_forward(self):
# self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE) """move one full step forwards
#@Command() for quick tests
#def back(self): """
# self.comm(MOVE, 1, - FULL_STEP / ANGLE_SCALE) self.comm(MOVE, 1, FULL_STEP / ANGLE_SCALE)
#@Command(IntRange(), result=IntRange()) @Command()
#def get_axis_par(self, adr): def step_back(self):
# return self.comm(GET_AXIS_PAR, adr) """move one full step backwards
#@Command((IntRange(), FloatRange()), result=IntRange()) for quick tests
#def set_axis_par(self, adr, value): """
# return self.comm(SET_AXIS_PAR, adr, value) 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)

View File

@ -75,4 +75,6 @@ def test_attach():
assert m.propertyValues['att'] == 'a' assert m.propertyValues['att'] == 'a'
srv.dispatcher.register_module(a, 'a') srv.dispatcher.register_module(a, 'a')
srv.dispatcher.register_module(m, 'm') srv.dispatcher.register_module(m, 'm')
assert m.att == 'a'
m.attachedModules = {'att': a}
assert m.att == a assert m.att == a

View File

@ -64,6 +64,7 @@ def test_EnumMember():
assert a != 3 assert a != 3
assert a == 1 assert a == 1
def test_Enum(): def test_Enum():
e1 = Enum('e1') e1 = Enum('e1')
e2 = Enum('e2', e1, a=1, b=3) e2 = Enum('e2', e1, a=1, b=3)
@ -75,3 +76,4 @@ def test_Enum():
assert e2.b > e3.a assert e2.b > e3.a
assert e3.c >= e2.a assert e3.c >= e2.a
assert e3.b <= e2.b assert e3.b <= e2.b
assert Enum({'self': 0, 'other': 1})('self') == 0

View File

@ -149,7 +149,7 @@ def test_ModuleMagic():
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
# remark: it might be a programming error to override the datatype # remark: it might be a programming error to override the datatype
# and not overriding the read_* method. This is not checked! # and not overriding the read_* method. This is not checked!
b2 = Parameter('<b2>', datatype=BoolType(), default=True, b2 = Parameter('<b2>', datatype=StringType(), default='empty',
readonly=False, initwrite=True) readonly=False, initwrite=True)
def write_a1(self, value): def write_a1(self, value):
@ -157,6 +157,7 @@ def test_ModuleMagic():
return value return value
def write_b2(self, value): def write_b2(self, value):
value = value.upper()
self._b2_written = value self._b2_written = value
return value return value
@ -211,11 +212,11 @@ def test_ModuleMagic():
o2.startModule(event) o2.startModule(event)
event.wait() event.wait()
# value has changed type, b2 and a1 are written # 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 # ramerk: a1=True: this behaviour is a Porgamming error
assert updates.pop('o2') == expectedAfterStart assert updates.pop('o2') == expectedAfterStart
assert o2._a1_written == 2.7 assert o2._a1_written == 2.7
assert o2._b2_written is True assert o2._b2_written == 'EMPTY'
assert not updates assert not updates