20 Commits

Author SHA1 Message Date
a7fd90cd6d flowsas project as of 2025-04-14 2025-04-14 11:40:12 +02:00
adfb561308 add cetoni pump for flowsas project 2024-04-03 11:31:38 +02:00
70a31b5cae attocube: remove seperate pyanc350 python module
- implement positioner as an io class
- proper shut down behaviour

Change-Id: If04176f779809fd5b08f586556cac668cf188479
2024-03-28 10:12:18 +01:00
8ee97ade63 adjust cfg foe attocube to current example 2024-03-27 17:13:58 +01:00
1715f95dd4 frappy_psi.attocube: add lock protection to hw access
in order to avoid sporadic timeout problems

Change-Id: I36f67ae72b65e9c1f3179cae942b0a7d94584e55
2024-03-27 17:08:24 +01:00
db29776dd5 reworked attocube
- step_mode: soft closed loop, stepwise, reading encoder after a delay
- calib_steps command to determine step size

Change-Id: I27bdffb4d564ac9c55a6473704ac2de6ad92bac8
2024-03-25 16:47:13 +01:00
a2905d9fbc improve attocube driver
- driving in an extra thread, hoping not to miss end of travel
  status bits (does not work always)
- maxtry parameter for trying several times

TODO: move by step (in an other thread)
Change-Id: I89b51d1f6926f2fd26ec22d43aede377b5231583
2024-03-22 14:38:23 +01:00
16b826394f fixes for attocube
Change-Id: Id5eeb749ba010fec59d1c2f8a3258ee34a47e246
2024-03-20 16:59:04 +01:00
ea8570d422 wip: fix attocube 2024-03-20 16:12:03 +01:00
1169e0cd09 improve sea interface
Change-Id: I58fb4b10ef9466f90e4cd58b6c67bcfb11c493e3
2024-03-08 15:59:16 +01:00
7d02498b3d improve async behaviour of parmod.Driv
Change-Id: I3889614a0deaba4ef13b86c6600b6f96bc502a39
2024-03-08 15:58:17 +01:00
694b121c01 fix more stop doc strings
Change-Id: Id7ea0a6d0c959e980beee8fbea73932c701977d7
2024-03-08 15:38:34 +01:00
0f50de9a7f fix command doc string handling and change default stop doc string
- fix inheritance of command description
- when no stop method is given, then the description should indicate
  that stop is a no-op -> add missing doc strings to stop methods
- add test to make sure stop command doc strings are given
  when implemented

Change-Id: If891359350e8dcdec39a706841d61d4f8ec8926f
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33266
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-03-08 15:34:08 +01:00
b454f47a12 fix docstring in frappy.error.OutOfRangeError
Change-Id: I006c061a5d88ac7c97808efd56faece927916e78
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33183
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-03-08 15:34:02 +01:00
6e7be6b4c7 simplify callbacks
on Module, use one single callback list 'paramsCallback' instead of
'valueCallbacks', 'errorCallbacks'. Redesign the mechanism to
avoid most of the closures.

Change-Id: Ie7f68f6bf97ab3f3cd961faa20b0e77730e5b37d
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33118
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
2024-03-08 15:33:52 +01:00
af28511403 fixes for proxy modules
- for the case when the remote module name does not match,
  'read', 'change' and 'do' does not work
- a proxy to an IO class has enablePoll == False, but it needs
  a triggerPoll for modules relying on it to work
- a proxy on a communicator module has a status even when the
  remote does not - this needs 2 fixes

Change-Id: Icd44da4c2984f27ce7147dec633739f9176012ec
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33168
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-03-08 15:33:45 +01:00
9d9d31693b bugfix in automatic creation if attached io
srv.modules does no longer exist

Change-Id: Ibc52fe35f27ad110e60947702d97ee40f359b7c5
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33167
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-03-08 15:33:39 +01:00
3a7fff713d move StructParam to frappy/extparams.py
+ typos and fixes in doc strings

Change-Id: Ib3e9add84ce2a6fb5c33770cae7f2da3f5655506
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/33033
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-02-26 13:43:10 +01:00
2acab33faa add FloatEnumParam
for the use case where a parameter is a selection of discrete
float parameters

declaring a parameter as FloatEnumParam will create effectively
two parameters, one with datatype FloatRange and another with
datatype Enum, influencing each other automatically.

in a later change StructParam should be moved from
frappy/structparam.py to frappy/extparams.py

Change-Id: Ica3fd8dcaf6e9439e8178390f220dec15e52cc86
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32975
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-02-26 13:43:10 +01:00
8c589cc138 simulation: extra_params might be a list
- still accept comma separated string
- remove legacy naming '.extra_params'

Change-Id: I497cf7722d0b39dd31c516383449a4cc4e7dcb7d
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32968
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
2024-02-26 13:43:10 +01:00
33 changed files with 2100 additions and 651 deletions

19
cfg/attocube_cfg.py Normal file
View File

@ -0,0 +1,19 @@
Node('attocube_test.psi.ch',
'a single attocube axis',
interface='tcp://5000',
)
Mod('r',
'frappy_psi.attocube.Axis',
'ANRv220-F3-02882',
axis = 1,
value = Param(unit='deg'),
tolerance = 0.1,
target_min = 0,
target_max = 360,
steps_fwd = 45,
steps_bwd = 85,
step_mode = True,
# gear = 1.2,
)

60
cfg/flowsas_cfg.py Normal file
View File

@ -0,0 +1,60 @@
Node('flowsas.psi.ch',
'flowsas test motors',
'tcp://3000',
)
#Mod('mot_io',
# 'frappy_psi.phytron.PhytronIO',
# 'io for motor control',
# uri = 'serial:///dev/ttyUSB0',
# )
#Mod('hmot',
# 'frappy_psi.phytron.Motor',
# 'horizontal axis',
# axis = 'X',
# io = 'mot_io',
# encoder_mode = 'NO',
# )
#Mod('vmot',
# 'frappy_psi.phytron.Motor',
# 'vertical axis',
# axis = 'Y',
# io = 'mot_io',
# encoder_mode= 'NO',
# )
Mod('syr_io',
'frappy_psi.cetoni_pump.LabCannBus',
'Module for bus',
deviceconfig = "/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/conti_flow",
)
Mod('syr1',
'frappy_psi.cetoni_pump.SyringePump',
'First syringe pump',
io='syr_io',
pump_name = "Nemesys_S_1_Pump",
valve_name = "Nemesys_S_1_Valve",
inner_diameter_set = 14.5673,
piston_stroke_set = 60,
)
Mod('syr2',
'frappy_psi.cetoni_pump.SyringePump',
'Second syringe pump',
io='syr_io',
pump_name = "Nemesys_S_2_Pump",
valve_name = "Nemesys_S_2_Valve",
inner_diameter_set = 14.5673,
piston_stroke_set = 60,
)
Mod('contiflow',
'frappy_psi.cetoni_pump.ContiFlowPump',
'Continuous flow pump',
io='syr_io',
inner_diameter_set = 14.5673,
piston_stroke_set = 60,
)

View File

@ -0,0 +1,12 @@
Node('flowsas.psi.ch',
'peristaltic pump',
'tcp://3000',
)
Mod('peripump',
'frappy_psi.gilsonpump.PeristalticPump',
'Peristaltic pump',
addr_AO = 'ao1',
addr_dir_relay = 'o1',
addr_run_relay = 'o2',
)

13
cfg/pressureTest_cfg.py Normal file
View File

@ -0,0 +1,13 @@
Node('vf.psi.ch',
'small vacuum furnace',
'tcp://5000',
)
Mod('p',
'frappy_psi.ionopimax.VoltageInput',
'Vacuum pressure',
addr = 'av2',
rawrange = (0, 10),
valuerange = (0, 10),
value = Param(unit='V'),
)

11
cfg/rheotrigger_cfg.py Normal file
View File

@ -0,0 +1,11 @@
Node('flowsas.psi.ch',
'rheometer triggering',
'tcp://3000',
)
Mod('rheo',
'frappy_psi.rheo_trigger.RheoTrigger',
'Trigger for the rheometer',
addr='dt1',
doBeep = False,
)

View File

@ -232,7 +232,7 @@ class ReadFailedError(SECoPError):
class OutOfRangeError(SECoPError): class OutOfRangeError(SECoPError):
"""The requested parameter can not be read just now""" """The value read from the hardware is out of sensor or calibration range"""
name = 'OutOfRange' name = 'OutOfRange'

304
frappy/extparams.py Normal file
View File

@ -0,0 +1,304 @@
# *****************************************************************************
#
# 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>
#
# *****************************************************************************
"""extended parameters
special parameter classes with some automatic functionality
"""
import re
from frappy.core import Parameter, Property
from frappy.datatypes import BoolType, DataType, DataTypeType, EnumType, \
FloatRange, StringType, StructOf, ValueType
from frappy.errors import ProgrammingError
class StructParam(Parameter):
"""convenience class to create a struct Parameter together with individual params
Usage:
class Controller(Drivable):
...
ctrlpars = StructParam('ctrlpars struct', [
('pid_p', 'p', Parameter('control parameter p', FloatRange())),
('pid_i', 'i', Parameter('control parameter i', FloatRange())),
('pid_d', 'd', Parameter('control parameter d', FloatRange())),
], readonly=False)
...
then implement either read_ctrlpars and write_ctrlpars or
read_pid_p, read_pid_i, read_pid_d, write_pid_p, write_pid_i and write_pid_d
the methods not implemented will be created automatically
"""
# use properties, as simple attributes are not considered on copy()
paramdict = Property('dict <parametername> of Parameter(...)', ValueType())
hasStructRW = Property('has a read_<struct param> or write_<struct param> method',
BoolType(), default=False)
insideRW = 0 # counter for avoiding multiple superfluous updates
def __init__(self, description=None, paramdict=None, prefix_or_map='', *, datatype=None, readonly=False, **kwds):
"""create a struct parameter together with individual parameters
in addition to normal Parameter arguments:
:param paramdict: dict <member name> of Parameter(...)
:param prefix_or_map: either a prefix for the parameter name to add to the member name
or a dict <member name> or <parameter name>
"""
if isinstance(paramdict, DataType):
raise ProgrammingError('second argument must be a dict of Param')
if datatype is None and paramdict is not None: # omit the following on Parameter.copy()
if isinstance(prefix_or_map, str):
prefix_or_map = {m: prefix_or_map + m for m in paramdict}
for membername, param in paramdict.items():
param.name = prefix_or_map[membername]
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
kwds['influences'] = [p.name for p in paramdict.values()]
self.updateEnable = {}
if paramdict:
kwds['paramdict'] = paramdict
super().__init__(description, datatype, readonly=readonly, **kwds)
def __set_name__(self, owner, name):
# names of access methods of structed param (e.g. ctrlpars)
struct_read_name = f'read_{name}' # e.g. 'read_ctrlpars'
struct_write_name = f'write_{name}' # e.h. 'write_ctrlpars'
self.hasStructRW = hasattr(owner, struct_read_name) or hasattr(owner, struct_write_name)
for membername, param in self.paramdict.items():
pname = param.name
changes = {
'readonly': self.readonly,
'influences': set(param.influences) | {name},
}
param.ownProperties.update(changes)
param.init(changes)
setattr(owner, pname, param)
param.__set_name__(owner, param.name)
if self.hasStructRW:
rname = f'read_{pname}'
if not hasattr(owner, rname):
def rfunc(self, membername=membername, struct_read_name=struct_read_name):
return getattr(self, struct_read_name)()[membername]
rfunc.poll = False # read_<struct param> is polled only
setattr(owner, rname, rfunc)
if not self.readonly:
wname = f'write_{pname}'
if not hasattr(owner, wname):
def wfunc(self, value, membername=membername,
name=name, rname=rname, struct_write_name=struct_write_name):
valuedict = dict(getattr(self, name))
valuedict[membername] = value
getattr(self, struct_write_name)(valuedict)
return getattr(self, rname)()
setattr(owner, wname, wfunc)
if not self.hasStructRW:
if not hasattr(owner, struct_read_name):
def struct_read_func(self, name=name, flist=tuple(
(m, f'read_{p.name}') for m, p in self.paramdict.items())):
pobj = self.parameters[name]
# disable updates generated from the callbacks of individual params
pobj.insideRW += 1 # guarded by self.accessLock
try:
return {m: getattr(self, f)() for m, f in flist}
finally:
pobj.insideRW -= 1
setattr(owner, struct_read_name, struct_read_func)
if not (self.readonly or hasattr(owner, struct_write_name)):
def struct_write_func(self, value, name=name, funclist=tuple(
(m, f'write_{p.name}') for m, p in self.paramdict.items())):
pobj = self.parameters[name]
pobj.insideRW += 1 # guarded by self.accessLock
try:
return {m: getattr(self, f)(value[m]) for m, f in funclist}
finally:
pobj.insideRW -= 1
setattr(owner, struct_write_name, struct_write_func)
super().__set_name__(owner, name)
def finish(self, modobj=None):
"""register callbacks for consistency"""
super().finish(modobj)
if modobj:
if self.hasStructRW:
def cb(value, modobj=modobj, structparam=self):
for membername, param in structparam.paramdict.items():
setattr(modobj, param.name, value[membername])
modobj.addCallback(self.name, cb)
else:
for membername, param in self.paramdict.items():
def cb(value, modobj=modobj, structparam=self, membername=membername):
if not structparam.insideRW:
prev = dict(getattr(modobj, structparam.name))
prev[membername] = value
setattr(modobj, structparam.name, prev)
modobj.addCallback(param.name, cb)
class FloatEnumParam(Parameter):
"""combine enum and float parameter
Example Usage:
vrange = FloatEnumParam('sensor range', ['500uV', '20mV', '1V'], 'V')
The following will be created automatically:
- the parameter vrange will get a datatype FloatRange(5e-4, 1, unit='V')
- an additional parameter `vrange_idx` will be created with an enum type
{'500uV': 0, '20mV': 1, '1V': 2}
- the method `write_vrange` will be created automatically
However, the methods `write_vrange_idx` and `read_vrange_idx`, if needed,
have to implemented by the programmer.
Writing to the float parameter involves 'rounding' to the closest allowed value.
Customization:
The individual labels might be customized by defining them as a tuple
(<index>, <label>, <float value>) where either the index or the float value
may be omitted.
When the index is omitted, the element will be the previous index + 1 or
0 when it is the first element.
Omitted values will be determined from the label, assuming that they use
one of the predefined unit prefixes together with the given unit.
The name of the index parameter is by default '<name>_idx' but might be
changed with the idx_name argument.
"""
# use properties, as simple attributes are not considered on copy()
idx_name = Property('name of attached index parameter', StringType(), default='')
valuedict = Property('dict <index> of <value>', ValueType(dict))
enumtype = Property('dict <label> of <index', DataTypeType())
# TODO: factor out unit handling, at the latest when needed elsewhere
PREFIXES = {'q': -30, 'r': -27, 'y': -24, 'z': -21, 'a': -18, 'f': -15,
'p': -12, 'n': -9, 'u': -6, 'µ': -6, 'm': -3,
'': 0, 'k': 3, 'M': 6, 'G': 9, 'T': 12,
'P': 15, 'E': 18, 'Z': 21, 'Y': 24, 'R': 25, 'Q': 30}
def __init__(self, description=None, labels=None, unit='',
*, datatype=None, readonly=False, **kwds):
if labels is None:
# called on Parameter.copy()
super().__init__(description, datatype, readonly=readonly, **kwds)
return
if isinstance(labels, DataType):
raise ProgrammingError('second argument must be a list of labels, not a datatype')
nextidx = 0
try:
edict = {}
vdict = {}
for elem in labels:
if isinstance(elem, str):
idx, label = [nextidx, elem]
else:
if isinstance(elem[0], str):
elem = [nextidx] + list(elem)
idx, label, *tail = elem
if tail:
vdict[idx], = tail
edict[label] = idx
nextidx = idx + 1
except (ValueError, TypeError) as e:
raise ProgrammingError('labels must be a list of labels or tuples '
'([index], label, [value])') from e
pat = re.compile(rf'([+-]?\d*\.?\d*) *({"|".join(self.PREFIXES)}){unit}$')
try:
# determine missing values from labels
for label, idx in edict.items():
if idx not in vdict:
value, prefix = pat.match(label).groups()
vdict[idx] = float(f'{value}e{self.PREFIXES[prefix]}')
except (AttributeError, ValueError) as e:
raise ProgrammingError(f"{label!r} has not the form '<float><prefix>{unit}'") from e
try:
enumtype = EnumType(**edict)
except TypeError as e:
raise ProgrammingError(str(e)) from e
datatype = FloatRange(min(vdict.values()), max(vdict.values()), unit=unit)
super().__init__(description, datatype, enumtype=enumtype, valuedict=vdict,
readonly=readonly, **kwds)
def __set_name__(self, owner, name):
super().__set_name__(owner, name)
if not self.idx_name:
self.idx_name = name + '_idx'
iname = self.idx_name
idx_param = Parameter(f'index of {name}', self.enumtype,
readonly=self.readonly, influences={name})
idx_param.init({})
setattr(owner, iname, idx_param)
idx_param.__set_name__(owner, iname)
self.setProperty('influences', {iname})
if not hasattr(owner, f'write_{name}'):
# customization (like rounding up or down) might be
# achieved by adding write_<name>. if not, the default
# is rounding to the closest value
def wfunc(mobj, value, vdict=self.valuedict, fname=name, wfunc_iname=f'write_{iname}'):
getattr(mobj, wfunc_iname)(
min(vdict, key=lambda i: abs(vdict[i] - value)))
return getattr(mobj, fname)
setattr(owner, f'write_{name}', wfunc)
def __get__(self, instance, owner):
"""getter for value"""
if instance is None:
return self
return self.valuedict[instance.parameters[self.idx_name].value]
def trigger_setter(self, modobj, _):
# trigger update of float parameter on change of enum parameter
modobj.announceUpdate(self.name, getattr(modobj, self.name))
def finish(self, modobj=None):
"""register callbacks for consistency"""
super().finish(modobj)
if modobj:
modobj.addCallback(self.idx_name, self.trigger_setter, modobj)

View File

@ -61,7 +61,6 @@ class HasIO(Module):
ioname = opts.get('io') or f'{name}_io' ioname = opts.get('io') or f'{name}_io'
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
io.callingModule = [] io.callingModule = []
srv.modules[ioname] = io
srv.secnode.add_module(io, ioname) srv.secnode.add_module(io, ioname)
self.ioDict[self.uri] = ioname self.ioDict[self.uri] = ioname
self.io = ioname self.io = ioname

View File

@ -141,6 +141,7 @@ class SequencerMixin:
return self.Status.IDLE, '' return self.Status.IDLE, ''
def stop(self): def stop(self):
"""stop sequence"""
if self.seq_is_alive(): if self.seq_is_alive():
self._seq_stopflag = True self._seq_stopflag = True

View File

@ -83,15 +83,14 @@ class HasAccessibles(HasProperties):
override_values.pop(key, None) override_values.pop(key, None)
elif key in accessibles: elif key in accessibles:
override_values[key] = value override_values[key] = value
# remark: merged_properties contain already the properties of accessibles of cls
for aname, aobj in list(accessibles.items()): for aname, aobj in list(accessibles.items()):
if aname in override_values: if aname in override_values:
aobj = aobj.copy()
value = override_values[aname] value = override_values[aname]
if value is None: if value is None:
accessibles.pop(aname) accessibles.pop(aname)
continue continue
aobj.merge(merged_properties[aname]) aobj = aobj.create_from_value(merged_properties[aname], value)
aobj.override(value)
# replace the bare value by the created accessible # replace the bare value by the created accessible
setattr(cls, aname, aobj) setattr(cls, aname, aobj)
else: else:
@ -334,8 +333,7 @@ class Module(HasAccessibles):
self.secNode = srv.secnode self.secNode = srv.secnode
self.log = logger self.log = logger
self.name = name self.name = name
self.valueCallbacks = {} self.paramCallbacks = {}
self.errorCallbacks = {}
self.earlyInitDone = False self.earlyInitDone = False
self.initModuleDone = False self.initModuleDone = False
self.startModuleDone = False self.startModuleDone = False
@ -469,8 +467,7 @@ class Module(HasAccessibles):
apply default when no value is given (in cfg or as Parameter argument) apply default when no value is given (in cfg or as Parameter argument)
or complain, when cfg is needed or complain, when cfg is needed
""" """
self.valueCallbacks[pname] = [] self.paramCallbacks[pname] = []
self.errorCallbacks[pname] = []
if isinstance(pobj, Limit): if isinstance(pobj, Limit):
basepname = pname.rpartition('_')[0] basepname = pname.rpartition('_')[0]
baseparam = self.parameters.get(basepname) baseparam = self.parameters.get(basepname)
@ -535,68 +532,46 @@ class Module(HasAccessibles):
err.report_error = False err.report_error = False
return # no updates for repeated errors return # no updates for repeated errors
err = secop_error(err) err = secop_error(err)
elif not changed and timestamp < (pobj.timestamp or 0) + pobj.omit_unchanged_within: value_err = value, err
# no change within short time -> omit
return
pobj.timestamp = timestamp or time.time()
if err:
callbacks = self.errorCallbacks
pobj.readerror = arg = err
else: else:
callbacks = self.valueCallbacks if not changed and timestamp < (pobj.timestamp or 0) + pobj.omit_unchanged_within:
arg = value # no change within short time -> omit
pobj.readerror = None return
value_err = (value,)
pobj.timestamp = timestamp or time.time()
pobj.readerror = err
for cbfunc, cbargs in self.paramCallbacks[pname]:
try:
cbfunc(*cbargs, *value_err)
except Exception:
pass
if pobj.export: if pobj.export:
self.updateCallback(self, pobj) self.updateCallback(self, pobj)
cblist = callbacks[pname]
for cb in cblist: def addCallback(self, pname, callback_function, *args):
try: self.paramCallbacks[pname].append((callback_function, args))
cb(arg)
except Exception:
# print(formatExtendedTraceback())
pass
def registerCallbacks(self, modobj, autoupdate=()): def registerCallbacks(self, modobj, autoupdate=()):
"""register callbacks to another module <modobj> """register callbacks to another module <modobj>
- whenever a self.<param> changes: whenever a self.<param> changes or changes its error state:
<modobj>.update_<param> is called with the new value as argument. <modobj>.update_param(<value> [, <exc>]) is called,
If this method raises an exception, <modobj>.<param> gets into an error state. where <value> is the new value and <exc> is given only in case of error.
If the method does not exist and <param> is in autoupdate, if the method does not exist, and <param> is in autoupdate
<modobj>.<param> is updated to self.<param> <modobj>.announceUpdate(<pname>, <value>, <exc>) is called
- whenever <self>.<param> gets into an error state: with <exc> being None in case of no error.
<modobj>.error_update_<param> is called with the exception as argument.
If this method raises an error, <modobj>.<param> gets into an error state.
If this method does not exist, and <param> is in autoupdate,
<modobj>.<param> gets into the same error state as self.<param>
"""
for pname in self.parameters:
errfunc = getattr(modobj, 'error_update_' + pname, None)
if errfunc:
def errcb(err, p=pname, efunc=errfunc):
try:
efunc(err)
except Exception as e:
modobj.announceUpdate(p, err=e)
self.errorCallbacks[pname].append(errcb)
else:
def errcb(err, p=pname):
modobj.announceUpdate(p, err=err)
if pname in autoupdate:
self.errorCallbacks[pname].append(errcb)
updfunc = getattr(modobj, 'update_' + pname, None) Remark: when <modobj>.update_<param> does not accept the <exc> argument,
if updfunc: nothing happens (the callback is catched by try / except).
def cb(value, ufunc=updfunc, efunc=errcb): Any exceptions raised by the callback function are silently ignored.
try: """
ufunc(value) autoupdate = set(autoupdate)
except Exception as e: for pname in self.parameters:
efunc(e) cbfunc = getattr(modobj, 'update_' + pname, None)
self.valueCallbacks[pname].append(cb) if cbfunc:
self.addCallback(pname, cbfunc)
elif pname in autoupdate: elif pname in autoupdate:
def cb(value, p=pname): self.addCallback(pname, modobj.announceUpdate, pname)
modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None): def isBusy(self, status=None):
"""helper function for treating substates of BUSY correctly""" """helper function for treating substates of BUSY correctly"""
@ -614,6 +589,10 @@ class Module(HasAccessibles):
# enablePoll == False: we still need the poll thread for writing values from writeDict # enablePoll == False: we still need the poll thread for writing values from writeDict
if hasattr(self, 'io'): if hasattr(self, 'io'):
self.io.polledModules.append(self) self.io.polledModules.append(self)
if not self.io.triggerPoll:
# when self.io.enablePoll is False, triggerPoll is not
# created for self.io in the else clause below
self.triggerPoll = threading.Event()
else: else:
self.triggerPoll = threading.Event() self.triggerPoll = threading.Event()
self.polledModules.append(self) self.polledModules.append(self)
@ -713,8 +692,8 @@ class Module(HasAccessibles):
for mobj in polled_modules: for mobj in polled_modules:
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
# trigger a poll interval change when self.pollinterval changes. # trigger a poll interval change when self.pollinterval changes.
if 'pollinterval' in mobj.valueCallbacks: if 'pollinterval' in mobj.paramCallbacks:
mobj.valueCallbacks['pollinterval'].append(pinfo.update_interval) mobj.addCallback('pollinterval', pinfo.update_interval)
for pname, pobj in mobj.parameters.items(): for pname, pobj in mobj.parameters.items():
rfunc = getattr(mobj, 'read_' + pname) rfunc = getattr(mobj, 'read_' + pname)

View File

@ -36,6 +36,7 @@ from .modulebase import Module
class Readable(Module): class Readable(Module):
"""basic readable module""" """basic readable module"""
# pylint: disable=invalid-name
Status = Enum('Status', Status = Enum('Status',
IDLE=StatusType.IDLE, IDLE=StatusType.IDLE,
WARN=StatusType.WARN, WARN=StatusType.WARN,
@ -92,7 +93,7 @@ class Drivable(Writable):
@Command(None, result=None) @Command(None, result=None)
def stop(self): def stop(self):
"""cease driving, go to IDLE state""" """not implemented - this is a no-op"""
class Communicator(HasComlog, Module): class Communicator(HasComlog, Module):

View File

@ -57,13 +57,17 @@ class Accessible(HasProperties):
def as_dict(self): def as_dict(self):
return self.propertyValues return self.propertyValues
def override(self, value): def create_from_value(self, properties, value):
"""override with a bare value""" """return a clone with given value and inherited properties"""
raise NotImplementedError
def clone(self, properties, **kwds):
"""return a clone of ourselfs with inherited properties"""
raise NotImplementedError raise NotImplementedError
def copy(self): def copy(self):
"""return a (deep) copy of ourselfs""" """return a (deep) copy of ourselfs"""
raise NotImplementedError return self.clone(self.propertyValues)
def updateProperties(self, merged_properties): def updateProperties(self, merged_properties):
"""update merged_properties with our own properties""" """update merged_properties with our own properties"""
@ -234,13 +238,15 @@ class Parameter(Accessible):
# avoid export=True overrides export=<name> # avoid export=True overrides export=<name>
self.ownProperties['export'] = self.export self.ownProperties['export'] = self.export
def copy(self): def clone(self, properties, **kwds):
"""return a (deep) copy of ourselfs""" """return a clone of ourselfs with inherited properties"""
res = type(self)() res = type(self)(**kwds)
res.name = self.name res.name = self.name
res.init(self.propertyValues) res.init(properties)
res.init(res.ownProperties)
if 'datatype' in self.propertyValues: if 'datatype' in self.propertyValues:
res.datatype = res.datatype.copy() res.datatype = res.datatype.copy()
res.finish()
return res return res
def updateProperties(self, merged_properties): def updateProperties(self, merged_properties):
@ -253,9 +259,9 @@ class Parameter(Accessible):
merged_properties.pop(key) merged_properties.pop(key)
merged_properties.update(self.ownProperties) merged_properties.update(self.ownProperties)
def override(self, value): def create_from_value(self, properties, value):
"""override default""" """return a clone with given value and inherited properties"""
self.value = self.datatype(value) return self.clone(properties, value=self.datatype(value))
def merge(self, merged_properties): def merge(self, merged_properties):
"""merge with inherited properties """merge with inherited properties
@ -390,7 +396,7 @@ class Command(Accessible):
else: else:
# goodie: allow @Command instead of @Command() # goodie: allow @Command instead of @Command()
self.func = argument # this is the wrapped method! self.func = argument # this is the wrapped method!
if argument.__doc__: if argument.__doc__ is not None:
self.description = inspect.cleandoc(argument.__doc__) self.description = inspect.cleandoc(argument.__doc__)
self.name = self.func.__name__ # this is probably not needed self.name = self.func.__name__ # this is probably not needed
self._inherit = inherit # save for __set_name__ self._inherit = inherit # save for __set_name__
@ -439,38 +445,37 @@ class Command(Accessible):
f' members!: {params} != {members}') f' members!: {params} != {members}')
self.argument.optional = [p for p,v in sig.parameters.items() self.argument.optional = [p for p,v in sig.parameters.items()
if v.default is not inspect.Parameter.empty] if v.default is not inspect.Parameter.empty]
if 'description' not in self.propertyValues and func.__doc__: if 'description' not in self.ownProperties and func.__doc__ is not None:
self.description = inspect.cleandoc(func.__doc__) self.description = inspect.cleandoc(func.__doc__)
self.ownProperties['description'] = self.description self.ownProperties['description'] = self.description
self.func = func self.func = func
return self return self
def copy(self): def clone(self, properties, **kwds):
"""return a (deep) copy of ourselfs""" """return a clone of ourselfs with inherited properties"""
res = type(self)() res = type(self)(**kwds)
res.name = self.name res.name = self.name
res.func = self.func res.func = self.func
res.init(self.propertyValues) res.init(properties)
res.init(res.ownProperties)
if res.argument: if res.argument:
res.argument = res.argument.copy() res.argument = res.argument.copy()
if res.result: if res.result:
res.result = res.result.copy() res.result = res.result.copy()
self.finish() res.finish()
return res return res
def updateProperties(self, merged_properties): def updateProperties(self, merged_properties):
"""update merged_properties with our own properties""" """update merged_properties with our own properties"""
merged_properties.update(self.ownProperties) merged_properties.update(self.ownProperties)
def override(self, value): def create_from_value(self, properties, value):
"""override method """return a clone with given value and inherited properties
this is needed when the @Command is missing on a method overriding a command""" this is needed when the @Command is missing on a method overriding a command"""
if not callable(value): if not callable(value):
raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command') raise ProgrammingError(f'{self.name} = {value!r} is overriding a Command')
self.func = value return self.clone(properties)(value)
if value.__doc__:
self.description = inspect.cleandoc(value.__doc__)
def merge(self, merged_properties): def merge(self, merged_properties):
"""merge with inherited properties """merge with inherited properties

View File

@ -84,9 +84,7 @@ class PersistentMixin(Module):
flag = getattr(pobj, 'persistent', False) flag = getattr(pobj, 'persistent', False)
if flag: if flag:
if flag == 'auto': if flag == 'auto':
def cb(value, m=self): self.addCallback(pname, self.saveParameters)
m.saveParameters()
self.valueCallbacks[pname].append(cb)
self.initData[pname] = pobj.value self.initData[pname] = pobj.value
if not pobj.given: if not pobj.given:
if pname in loaded: if pname in loaded:
@ -129,16 +127,18 @@ class PersistentMixin(Module):
self.writeInitParams() self.writeInitParams()
return loaded return loaded
def saveParameters(self): def saveParameters(self, _=None):
"""save persistent parameters """save persistent parameters
- to be called regularly explicitly by the module - to be called regularly explicitly by the module
- the caller has to make sure that this is not called after - the caller has to make sure that this is not called after
a power down of the connected hardware before loadParameters a power down of the connected hardware before loadParameters
dummy argument to avoid closure for callback
""" """
if self.writeDict: if self.writeDict:
# do not save before all values are written to the hw, as potentially # do not save before all values are written to the hw, as potentially
# factory default values were read in the mean time # factory default values were read in the meantime
return return
self.__save_params() self.__save_params()

View File

@ -71,7 +71,7 @@ class ProxyModule(HasIO, Module):
pname, pobj = params.popitem() pname, pobj = params.popitem()
props = remoteparams.get(pname, None) props = remoteparams.get(pname, None)
if props is None: if props is None:
if pobj.export: if pobj.export and pname != 'status':
self.log.warning('remote parameter %s:%s does not exist', self.module, pname) self.log.warning('remote parameter %s:%s does not exist', self.module, pname)
continue continue
dt = props['datatype'] dt = props['datatype']
@ -108,17 +108,19 @@ class ProxyModule(HasIO, Module):
# for now, the error message must be enough # for now, the error message must be enough
def nodeStateChange(self, online, state): def nodeStateChange(self, online, state):
disconnected = Readable.Status.ERROR, 'disconnected'
if online: if online:
if not self._consistency_check_done: if not self._consistency_check_done:
self._check_descriptive_data() self._check_descriptive_data()
self._consistency_check_done = True self._consistency_check_done = True
if self.status == disconnected:
self.status = Readable.Status.IDLE, 'connected'
else: else:
newstatus = Readable.Status.ERROR, 'disconnected'
readerror = CommunicationFailedError('disconnected') readerror = CommunicationFailedError('disconnected')
if self.status != newstatus: if self.status != disconnected:
for pname in set(self.parameters) - set(('module', 'status')): for pname in set(self.parameters) - set(('module', 'status')):
self.announceUpdate(pname, None, readerror) self.announceUpdate(pname, None, readerror)
self.announceUpdate('status', newstatus) self.status = disconnected
def checkProperties(self): def checkProperties(self):
pass # skip pass # skip
@ -193,7 +195,7 @@ def proxy_class(remote_class, name=None):
attrs[aname] = pobj attrs[aname] = pobj
def rfunc(self, pname=aname): def rfunc(self, pname=aname):
value, _, readerror = self._secnode.getParameter(self.name, pname, True) value, _, readerror = self._secnode.getParameter(self.module, pname, True)
if readerror: if readerror:
raise readerror raise readerror
return value return value
@ -203,7 +205,7 @@ def proxy_class(remote_class, name=None):
if not pobj.readonly: if not pobj.readonly:
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.module, pname, value)
if readerror: if readerror:
raise readerror raise readerror
return value return value
@ -214,7 +216,7 @@ def proxy_class(remote_class, name=None):
cobj = aobj.copy() cobj = aobj.copy()
def cfunc(self, arg=None, cname=aname): def cfunc(self, arg=None, cname=aname):
return self._secnode.execCommand(self.name, cname, arg)[0] return self._secnode.execCommand(self.module, cname, arg)[0]
attrs[aname] = cobj(cfunc) attrs[aname] = cobj(cfunc)

View File

@ -239,6 +239,7 @@ class HasStates:
@Command @Command
def stop(self): def stop(self):
"""stop state machine"""
self.stop_machine() self.stop_machine()
def final_status(self, code=IDLE, text=''): def final_status(self, code=IDLE, text=''):

View File

@ -1,164 +0,0 @@
# *****************************************************************************
#
# 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>
#
# *****************************************************************************
"""convenience class to create a struct Parameter together with indivdual params
Usage:
class Controller(Drivable):
...
ctrlpars = StructParam('ctrlpars struct', [
('pid_p', 'p', Parameter('control parameter p', FloatRange())),
('pid_i', 'i', Parameter('control parameter i', FloatRange())),
('pid_d', 'd', Parameter('control parameter d', FloatRange())),
], readonly=False)
...
then implement either read_ctrlpars and write_ctrlpars or
read_pid_p, read_pid_i, read_pid_d, write_pid_p, write_pid_i and write_pid_d
the methods not implemented will be created automatically
"""
from frappy.core import Parameter, Property
from frappy.datatypes import BoolType, DataType, StructOf, ValueType
from frappy.errors import ProgrammingError
class StructParam(Parameter):
"""create a struct parameter together with individual parameters
in addition to normal Parameter arguments:
:param paramdict: dict <member name> of Parameter(...)
:param prefix_or_map: either a prefix for the parameter name to add to the member name
or a dict <member name> or <paramerter name>
"""
# use properties, as simple attributes are not considered on copy()
paramdict = Property('dict <parametername> of Parameter(...)', ValueType())
hasStructRW = Property('has a read_<struct param> or write_<struct param> method',
BoolType(), default=False)
insideRW = 0 # counter for avoiding multiple superfluous updates
def __init__(self, description=None, paramdict=None, prefix_or_map='', *, datatype=None, readonly=False, **kwds):
if isinstance(paramdict, DataType):
raise ProgrammingError('second argument must be a dict of Param')
if datatype is None and paramdict is not None: # omit the following on Parameter.copy()
if isinstance(prefix_or_map, str):
prefix_or_map = {m: prefix_or_map + m for m in paramdict}
for membername, param in paramdict.items():
param.name = prefix_or_map[membername]
datatype = StructOf(**{m: p.datatype for m, p in paramdict.items()})
kwds['influences'] = [p.name for p in paramdict.values()]
self.updateEnable = {}
super().__init__(description, datatype, paramdict=paramdict, readonly=readonly, **kwds)
def __set_name__(self, owner, name):
# names of access methods of structed param (e.g. ctrlpars)
struct_read_name = f'read_{name}' # e.g. 'read_ctrlpars'
struct_write_name = f'write_{name}' # e.h. 'write_ctrlpars'
self.hasStructRW = hasattr(owner, struct_read_name) or hasattr(owner, struct_write_name)
for membername, param in self.paramdict.items():
pname = param.name
changes = {
'readonly': self.readonly,
'influences': set(param.influences) | {name},
}
param.ownProperties.update(changes)
param.init(changes)
setattr(owner, pname, param)
param.__set_name__(owner, param.name)
if self.hasStructRW:
rname = f'read_{pname}'
if not hasattr(owner, rname):
def rfunc(self, membername=membername, struct_read_name=struct_read_name):
return getattr(self, struct_read_name)()[membername]
rfunc.poll = False # read_<struct param> is polled only
setattr(owner, rname, rfunc)
if not self.readonly:
wname = f'write_{pname}'
if not hasattr(owner, wname):
def wfunc(self, value, membername=membername,
name=name, rname=rname, struct_write_name=struct_write_name):
valuedict = dict(getattr(self, name))
valuedict[membername] = value
getattr(self, struct_write_name)(valuedict)
return getattr(self, rname)()
setattr(owner, wname, wfunc)
if not self.hasStructRW:
if not hasattr(owner, struct_read_name):
def struct_read_func(self, name=name, flist=tuple(
(m, f'read_{p.name}') for m, p in self.paramdict.items())):
pobj = self.parameters[name]
# disable updates generated from the callbacks of individual params
pobj.insideRW += 1 # guarded by self.accessLock
try:
return {m: getattr(self, f)() for m, f in flist}
finally:
pobj.insideRW -= 1
setattr(owner, struct_read_name, struct_read_func)
if not (self.readonly or hasattr(owner, struct_write_name)):
def struct_write_func(self, value, name=name, funclist=tuple(
(m, f'write_{p.name}') for m, p in self.paramdict.items())):
pobj = self.parameters[name]
pobj.insideRW += 1 # guarded by self.accessLock
try:
return {m: getattr(self, f)(value[m]) for m, f in funclist}
finally:
pobj.insideRW -= 1
setattr(owner, struct_write_name, struct_write_func)
super().__set_name__(owner, name)
def finish(self, modobj=None):
"""register callbacks for consistency"""
super().finish(modobj)
if modobj:
if self.hasStructRW:
def cb(value, modobj=modobj, structparam=self):
for membername, param in structparam.paramdict.items():
setattr(modobj, param.name, value[membername])
modobj.valueCallbacks[self.name].append(cb)
else:
for membername, param in self.paramdict.items():
def cb(value, modobj=modobj, structparam=self, membername=membername):
if not structparam.insideRW:
prev = dict(getattr(modobj, structparam.name))
prev[membername] = value
setattr(modobj, structparam.name, prev)
modobj.valueCallbacks[param.name].append(cb)

View File

@ -120,6 +120,7 @@ class MagneticField(Drivable):
) )
heatswitch = Attached(Switch, description='name of heat switch device') heatswitch = Attached(Switch, description='name of heat switch device')
# pylint: disable=invalid-name
Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303) Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303)
status = Parameter(datatype=TupleOf(EnumType(Status), StringType())) status = Parameter(datatype=TupleOf(EnumType(Status), StringType()))
@ -193,6 +194,7 @@ class MagneticField(Drivable):
self.log.error(self, 'main thread exited unexpectedly!') self.log.error(self, 'main thread exited unexpectedly!')
def stop(self): def stop(self):
"""stop at current value"""
self.write_target(self.read_value()) self.write_target(self.read_value())

View File

@ -641,6 +641,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
sleep(0.3) sleep(0.3)
def stop(self): def stop(self):
"""cease driving, go to IDLE state"""
self._dev.Stop() self._dev.Stop()

View File

@ -17,248 +17,690 @@
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# ***************************************************************************** # *****************************************************************************
import sys
import time import time
from frappy.core import Drivable, Parameter, Command, Property, ERROR, WARN, BUSY, IDLE, Done, nopoll import threading
from frappy.features import HasTargetLimits, HasSimpleOffset from frappy.core import Drivable, Parameter, Command, Property, Module, HasIO, \
ERROR, WARN, BUSY, IDLE, nopoll, Limit
from frappy.datatypes import IntRange, FloatRange, StringType, BoolType from frappy.datatypes import IntRange, FloatRange, StringType, BoolType
from frappy.errors import ConfigError, BadValueError from frappy.errors import BadValueError, HardwareError, ConfigError
sys.path.append('/home/l_samenv/Documents/anc350/Linux64/userlib/lib')
from PyANC350v4 import Positioner from PyANC350v4 import Positioner
DIRECTION_NAME = {1: 'forward', -1: 'backward'} class IO(Module):
"""'communication' module for attocube controller
why an extra class:
class FreezeStatus: - HasIO assures that a single common communicator is used
"""freeze status for some time - access must be thread safe
hardware quite often does not treat status correctly: on a target change it
may take some time to return the 'busy' status correctly.
in classes with this mixin, within :meth:`write_target` call
self.freeze_status(0.5, BUSY, 'changed target')
a wrapper around read_status will take care that the status will be the given value,
for at least the given delay. This does NOT cover the case when self.status is set
directly from an other method.
""" """
__freeze_status_until = 0 uri = Property('dummy uri, only one controller may exists',
StringType())
export = False
_hw = None
_lock = None
used_axes = set()
def __init_subclass__(cls): def initModule(self):
def wrapped(self, inner=cls.read_status): if self._hw is None:
if time.time() < self.__freeze_status_until: IO._lock = threading.Lock()
return Done IO._hw = Positioner()
return inner(self) super().initModule()
cls.read_status = wrapped def shutdownModule(self):
super().__init_subclass__() if IO._hw:
IO._hw.disconnect()
IO._hw = None
def freeze_status(self, delay, code=BUSY, text='changed target'): def configureAQuadBIn(self, axisNo, enable, resolution):
"""freezze status to the given value for the given delay""" """Enables and configures the A-Quad-B (quadrature) input for the target position.
self.__freeze_status_until = time.time() + delay Parameters
self.status = code, text axisNo Axis number (0 ... 2)
enable Enable (1) or disable (0) A-Quad-B input
resolution A-Quad-B step width in m. Internal resolution is 1 nm.
Returns
None
"""
with self._lock:
return self._hw.configureAQuadBIn(axisNo, enable, resolution)
def configureAQuadBOut(self, axisNo, enable, resolution, clock):
"""Enables and configures the A-Quad-B output of the current position.
Parameters
axisNo Axis number (0 ... 2)
enable Enable (1) or disable (0) A-Quad-B output
resolution A-Quad-B step width in m; internal resolution is 1 nm
clock Clock of the A-Quad-B output [s]. Allowed range is 40ns ... 1.3ms; internal resulution is 20ns.
Returns
None
"""
with self._lock:
return self._hw.configureAQuadBOut(axisNo, enable, resolution, clock)
def configureExtTrigger(self, axisNo, mode):
"""Enables the input trigger for steps.
Parameters
axisNo Axis number (0 ... 2)
mode Disable (0), Quadratur (1), Trigger(2) for external triggering
Returns
None
"""
with self._lock:
return self._hw.configureExtTrigger(axisNo, mode)
def configureNslTriggerAxis(self, axisNo):
"""Selects Axis for NSL Trigger.
Parameters
axisNo Axis number (0 ... 2)
Returns
None
"""
with self._lock:
return self._hw.configureNslTriggerAxis(axisNo)
def configureRngTrigger(self, axisNo, lower, upper):
"""Configure lower position for range Trigger.
Parameters
axisNo Axis number (0 ... 2)
lower Lower position for range trigger (nm)
upper Upper position for range trigger (nm)
Returns
None
"""
with self._lock:
return self._hw.configureRngTrigger(axisNo, lower, upper)
def configureRngTriggerEps(self, axisNo, epsilon):
"""Configure hysteresis for range Trigger.
Parameters
axisNo Axis number (0 ... 2)
epsilon hysteresis in nm / mdeg
Returns
None
"""
with self._lock:
return self._hw.configureRngTriggerEps(axisNo, epsilon)
def configureRngTriggerPol(self, axisNo, polarity):
"""Configure lower position for range Trigger.
Parameters
axisNo Axis number (0 ... 2)
polarity Polarity of trigger signal when position is between lower and upper Low(0) and High(1)
Returns
None
"""
with self._lock:
return self._hw.configureRngTriggerPol(axisNo, polarity)
def getActuatorName(self, axisNo):
"""Get the name of the currently selected actuator
Parameters
axisNo Axis number (0 ... 2)
Returns
name Name of the actuator
"""
with self._lock:
return self._hw.getActuatorName(axisNo)
def getActuatorType(self, axisNo):
"""Get the type of the currently selected actuator
Parameters
axisNo Axis number (0 ... 2)
Returns
type_ Type of the actuator {0: linear, 1: goniometer, 2: rotator}
"""
with self._lock:
return self._hw.getActuatorType(axisNo)
def getAmplitude(self, axisNo):
"""Reads back the amplitude parameter of an axis.
Parameters
axisNo Axis number (0 ... 2)
Returns
amplitude Amplitude V
"""
with self._lock:
return self._hw.getAmplitude(axisNo)
def getAxisStatus(self, axisNo):
"""Reads status information about an axis of the device.
Parameters
axisNo Axis number (0 ... 2)
Returns
connected Output: If the axis is connected to a sensor.
enabled Output: If the axis voltage output is enabled.
moving Output: If the axis is moving.
target Output: If the target is reached in automatic positioning
eotFwd Output: If end of travel detected in forward direction.
eotBwd Output: If end of travel detected in backward direction.
error Output: If the axis' sensor is in error state.
"""
with self._lock:
return self._hw.getAxisStatus(axisNo)
def getFrequency(self, axisNo):
"""Reads back the frequency parameter of an axis.
Parameters
axisNo Axis number (0 ... 2)
Returns
frequency Output: Frequency in Hz
"""
with self._lock:
return self._hw.getFrequency(axisNo)
def getPosition(self, axisNo):
"""Retrieves the current actuator position. For linear type actuators the position unit is m; for goniometers and rotators it is degree.
Parameters
axisNo Axis number (0 ... 2)
Returns
position Output: Current position [m] or [°]
"""
with self._lock:
return self._hw.getPosition(axisNo)
def measureCapacitance(self, axisNo):
"""Performs a measurement of the capacitance of the piezo motor and returns the result. If no motor is connected, the result will be 0. The function doesn't return before the measurement is complete; this will take a few seconds of time.
Parameters
axisNo Axis number (0 ... 2)
Returns
cap Output: Capacitance [F]
"""
with self._lock:
return self._hw.measureCapacitance(axisNo)
def selectActuator(self, axisNo, actuator):
"""Selects the actuator to be used for the axis from actuator presets.
Parameters
axisNo Axis number (0 ... 2)
actuator Actuator selection (0 ... 255)
0: ANPg101res
1: ANGt101res
2: ANPx51res
3: ANPx101res
4: ANPx121res
5: ANPx122res
6: ANPz51res
7: ANPz101res
8: ANR50res
9: ANR51res
10: ANR101res
11: Test
Returns
None
"""
with self._lock:
return self._hw.selectActuator(axisNo, actuator)
def setAmplitude(self, axisNo, amplitude):
"""Sets the amplitude parameter for an axis
Parameters
axisNo Axis number (0 ... 2)
amplitude Amplitude in V, internal resolution is 1 mV
Returns
None
"""
with self._lock:
return self._hw.setAmplitude(axisNo, amplitude)
def setAxisOutput(self, axisNo, enable, autoDisable):
"""Enables or disables the voltage output of an axis.
Parameters
axisNo Axis number (0 ... 2)
enable Enables (1) or disables (0) the voltage output.
autoDisable If the voltage output is to be deactivated automatically when end of travel is detected.
Returns
None
"""
with self._lock:
return self._hw.setAxisOutput(axisNo, enable, autoDisable)
def setDcVoltage(self, axisNo, voltage):
"""Sets the DC level on the voltage output when no sawtooth based motion is active.
Parameters
axisNo Axis number (0 ... 2)
voltage DC output voltage [V], internal resolution is 1 mV
Returns
None
"""
with self._lock:
return self._hw.setDcVoltage(axisNo, voltage)
def setFrequency(self, axisNo, frequency):
"""Sets the frequency parameter for an axis
Parameters
axisNo Axis number (0 ... 2)
frequency Frequency in Hz, internal resolution is 1 Hz
Returns
None
"""
with self._lock:
return self._hw.setFrequency(axisNo, frequency)
def setTargetPosition(self, axisNo, target):
"""Sets the target position for automatic motion, see ANC_startAutoMove. For linear type actuators the position unit is m, for goniometers and rotators it is degree.
Parameters
axisNo Axis number (0 ... 2)
target Target position [m] or [°]. Internal resulution is 1 nm or 1 µ°.
Returns
None
"""
with self._lock:
return self._hw.setTargetPosition(axisNo, target)
def setTargetRange(self, axisNo, targetRg):
"""Defines the range around the target position where the target is considered to be reached.
Parameters
axisNo Axis number (0 ... 2)
targetRg Target range [m] or [°]. Internal resulution is 1 nm or 1 µ°.
Returns
None
"""
with self._lock:
return self._hw.setTargetRange(axisNo, targetRg)
def startAutoMove(self, axisNo, enable, relative):
"""Switches automatic moving (i.e. following the target position) on or off
Parameters
axisNo Axis number (0 ... 2)
enable Enables (1) or disables (0) automatic motion
relative If the target position is to be interpreted absolute (0) or relative to the current position (1)
Returns
None
"""
with self._lock:
return self._hw.startAutoMove(axisNo, enable, relative)
def startContinuousMove(self, axisNo, start, backward):
"""Starts or stops continous motion in forward direction. Other kinds of motions are stopped.
Parameters
axisNo Axis number (0 ... 2)
start Starts (1) or stops (0) the motion
backward If the move direction is forward (0) or backward (1)
Returns
None
"""
with self._lock:
return self._hw.startContinuousMove(axisNo, start, backward)
def startSingleStep(self, axisNo, backward):
"""Triggers a single step in desired direction.
Parameters
axisNo Axis number (0 ... 2)
backward If the step direction is forward (0) or backward (1)
Returns
None
"""
with self._lock:
return self._hw.startSingleStep(axisNo, backward)
class Axis(HasTargetLimits, FreezeStatus, Drivable): class Stopped(RuntimeError):
"""thread was stopped"""
class Axis(HasIO, Drivable):
axis = Property('axis number', IntRange(0, 2), 0) axis = Property('axis number', IntRange(0, 2), 0)
value = Parameter('axis position', FloatRange(unit='deg')) value = Parameter('axis position', FloatRange(unit='deg'))
frequency = Parameter('frequency', FloatRange(1, unit='Hz'), readonly=False) frequency = Parameter('frequency', FloatRange(1, unit='Hz'), readonly=False)
amplitude = Parameter('amplitude', FloatRange(0, unit='V'), readonly=False) amplitude = Parameter('amplitude', FloatRange(0, unit='V'), readonly=False)
gear = Parameter('gear factor', FloatRange(), readonly=False, default=1, initwrite=True) gear = Parameter('gear factor', FloatRange(), readonly=False, value=1)
tolerance = Parameter('positioning tolerance', FloatRange(0, unit='$'), readonly=False, default=0.01) tolerance = Parameter('positioning tolerance', FloatRange(0, unit='$'),
output = Parameter('enable output', BoolType(), readonly=False) readonly=False, default=0.01)
sensor_connected = Parameter('a sensor is connected', BoolType())
info = Parameter('axis info', StringType()) info = Parameter('axis info', StringType())
statusbits = Parameter('status bits', StringType()) statusbits = Parameter('status bits', StringType())
step_mode = Parameter('step mode (soft closed loop)', BoolType(),
default=False, readonly=False, group='step_mode')
timeout = Parameter('timeout after no progress detected', FloatRange(0),
default=1, readonly=False, group='step_mode')
steps_fwd = Parameter('forward steps / main unit', FloatRange(0), unit='$/s',
default=0, readonly=False, group='step_mode')
steps_bwd = Parameter('backward steps / main unit', FloatRange(0, unit='$/s'),
default=0, readonly=False, group='step_mode')
delay = Parameter('delay between tries within loop', FloatRange(0, unit='s'),
readonly=False, default=0.05, group='step_mode')
maxstep = Parameter('max. step duration', FloatRange(0, unit='s'),
default=0.25, readonly=False, group='step_mode')
prop = Parameter('factor for control loop', FloatRange(0, 1),
readonly=False, default=0.8, group='step_mode')
uri = 'ANC'
ioClass = IO
target_min = Limit()
target_max = Limit()
_hw = Positioner() fast_interval = 0.25
_hw = None
_scale = 1 # scale for custom units _scale = 1 # scale for custom units
_move_steps = 0 # number of steps to move (used by move command)
SCALES = {'deg': 1, 'm': 1, 'mm': 1000, 'um': 1000000, 'µm': 1000000} SCALES = {'deg': 1, 'm': 1, 'mm': 1000, 'um': 1000000, 'µm': 1000000}
_direction = 1 # move direction _thread = None
_idle_status = IDLE, '' _moving_since = 0
_error_state = '' # empty string: no error _status = IDLE, ''
_history = None _calib_range = None
_check_sensor = False _try_cnt = 0
_try_count = 0 _at_target = False
def __init__(self, name, logger, opts, srv): def initModule(self):
unit = opts.pop('unit', 'deg') super().initModule()
opts['value.unit'] = unit self._stopped = threading.Event()
if self.axis in IO.used_axes:
raise ConfigError(f'a module with axisNo={self.axis} already exists')
IO.used_axes.add(self.axis)
def initialReads(self):
self.read_info()
super().initialReads()
def shutdownModule(self):
IO.used_axes.discard(self.axis)
def read_value(self):
if self._thread:
return self.value
try: try:
self._scale = self.SCALES[unit] * opts.get('gear', 1) return self._read_pos()
except KeyError as e: except Stopped:
raise ConfigError('unsupported unit: %s' % unit) return self.value
super().__init__(name, logger, opts, srv)
def write_gear(self, value): def write_gear(self, value):
self._scale = self.SCALES[self.parameters['value'].datatype.unit] * self.gear self._scale = self.SCALES[self.parameters['value'].datatype.unit] * self.gear
return value return value
def startModule(self, start_events):
super().startModule(start_events)
start_events.queue(self.read_info)
def check_value(self, value):
"""check if value allows moving in current direction"""
if self._direction > 0:
if value > self.target_limits[1]:
raise BadValueError('above upper limit')
elif value < self.target_limits[0]:
raise BadValueError('below lower limit')
def read_value(self):
pos = self._hw.getPosition(self.axis) * self._scale
if self.isBusy():
try:
self.check_value(pos)
except BadValueError as e:
self._stop()
self._idle_status = ERROR, str(e)
return pos
def read_frequency(self): def read_frequency(self):
return self._hw.getFrequency(self.axis) return self.io.getFrequency(self.axis)
def write_frequency(self, value): def write_frequency(self, value):
self._hw.setFrequency(self.axis, value) self.io.setFrequency(self.axis, value)
return self._hw.getFrequency(self.axis) return self.io.getFrequency(self.axis)
def read_amplitude(self): def read_amplitude(self):
return self._hw.getAmplitude(self.axis) return self.io.getAmplitude(self.axis)
def write_amplitude(self, value): def write_amplitude(self, value):
self._hw.setAmplitude(self.axis, value) self.io.setAmplitude(self.axis, value)
return self._hw.getAmplitude(self.axis) return self.io.getAmplitude(self.axis)
def write_tolerance(self, value): def read_statusbits(self):
self._hw.setTargetRange(self.axis, value / self._scale) self._get_status()
return value return self.statusbits
def write_output(self, value): def _get_status(self):
self._hw.setAxisOutput(self.axis, enable=value, autoDisable=0) """get axis status
return value
- update self.sensor_connected and self.statusbits
- return <moving flag>, <error flag>, <reason>
<moving flag> is True whn moving
<in_error> is True when in error
<reason> is an error text, when in error, 'at target' or '' otherwise
"""
statusbits = self.io.getAxisStatus(self.axis)
self.sensor_connected, self._output, moving, at_target, fwd_stuck, bwd_stuck, error = statusbits
self.statusbits = ''.join(k for k, v in zip('OTFBE', (self._output,) + statusbits[3:]) if v)
if error:
return ERROR, 'other error'
if bwd_stuck:
return ERROR, 'end of travel backward'
if fwd_stuck:
return ERROR, 'end of travel forward'
target_reached = at_target > self._at_target
self._at_target = at_target
if self._moving_since:
if target_reached:
return IDLE, 'at target'
if time.time() < self._moving_since + 0.25:
return BUSY, 'started'
if at_target:
return IDLE, 'at target'
if moving and self._output:
return BUSY, 'moving'
return WARN, 'stopped by unknown reason'
if self._moving_since is False:
return IDLE, 'stopped'
if not self.step_mode and at_target:
return IDLE, 'at target'
return IDLE, ''
def read_status(self): def read_status(self):
statusbits = self._hw.getAxisStatus(self.axis) status = self._get_status()
sensor, self.output, moving, attarget, eot_fwd, eot_bwd, sensor_error = statusbits if self.step_mode:
self.statusbits = ''.join((k for k, v in zip('SOMTFBE', statusbits) if v)) return self._status
if self._move_steps: if self._moving_since:
if not (eot_fwd or eot_bwd): if status[0] != BUSY:
return BUSY, 'moving by steps' self._moving_since = 0
if not sensor: self.setFastPoll(False)
self._error_state = 'no sensor connected' return status
elif sensor_error:
self._error_state = 'sensor error' def _wait(self, delay):
elif eot_fwd: if self._stopped.wait(delay):
self._error_state = 'end of travel forward' raise Stopped()
elif eot_bwd:
self._error_state = 'end of travel backward' def _read_pos(self):
if not self.sensor_connected:
return 0
poslist = []
for i in range(9):
if i:
self._wait(0.001)
poslist.append(self.io.getPosition(self.axis) * self._scale)
self._poslist = sorted(poslist)
return self._poslist[len(poslist) // 2] # median
def _run_drive(self, target):
self.value = self._read_pos()
self.status = self._status = BUSY, 'drive by steps'
deadline = time.time() + self.timeout
max_steps = self.maxstep * self.frequency
while True:
for _ in range(2):
dif = target - self.value
steps_per_unit = self.steps_bwd if dif < 0 else self.steps_fwd
tol = max(self.tolerance, 0.6 / steps_per_unit) # avoid a tolerance less than 60% of a step
if abs(dif) > tol * 3:
break
# extra wait time when already close
self._wait(2 * self.delay)
self.read_value()
status = None
if abs(dif) < tol:
status = IDLE, 'in tolerance'
elif self._poslist[2] <= target <= self._poslist[-3]: # target within noise
status = IDLE, 'within noise'
elif dif > 0:
steps = min(max_steps, min(dif, (dif + tol) * self.prop) * steps_per_unit)
else:
steps = max(-max_steps, max(dif, (dif - tol) * self.prop) * steps_per_unit)
if status or steps == 0:
self._status = status
break
if round(steps) == 0: # this should not happen
self._status = WARN, 'steps=0'
break
self._move_steps(steps)
if self._step_size > self.prop * 0.25 / steps_per_unit:
# some progress happened
deadline = time.time() + self.timeout
elif time.time() > deadline:
self._status = WARN, 'timeout - no progress'
break
self.read_status()
def _thread_wrapper(self, func, *args):
try:
func(*args)
except Stopped as e:
self._status = IDLE, str(e)
except Exception as e:
self._status = ERROR, f'{type(e).__name__} - {e}'
finally:
self.io.setAxisOutput(self.axis, enable=0, autoDisable=0)
self.setFastPoll(False)
self._stopped.clear()
self._thread = None
def _stop_thread(self):
if self._thread:
self._stopped.set()
self._thread.join()
def _start_thread(self, *args):
self._stop_thread()
thread = threading.Thread(target=self._thread_wrapper, args=args)
self._thread = thread
thread.start()
def write_target(self, target):
if not self.sensor_connected:
raise HardwareError('no sensor connected')
self._stop_thread()
self.io.setTargetRange(self.axis, self.tolerance / self._scale)
if self.step_mode:
self.status = BUSY, 'changed target'
self._start_thread(self._run_drive, target)
else: else:
if self._error_state and not DIRECTION_NAME[self._direction] in self._error_state: self._try_cnt = 0
self._error_state = '' self.setFastPoll(True, self.fast_interval)
status_text = 'moving' if self._try_count == 0 else 'moving (retry %d)' % self._try_count self.io.setTargetPosition(self.axis, target / self._scale)
if moving and self._history is not None: # history None: moving by steps self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
self._history.append(self.value) self.io.startAutoMove(self.axis, enable=1, relative=0)
if len(self._history) < 5: self._moving_since = time.time()
return BUSY, status_text self.status = self._get_status()
beg = self._history.pop(0) return target
if abs(beg - self.target) < self.tolerance:
# reset normal tolerance
self._stop()
self._idle_status = IDLE, 'in tolerance'
return self._idle_status
# self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
if (self.value - beg) * self._direction > 0:
return BUSY, status_text
self._try_count += 1
if self._try_count < 10:
self.log.warn('no progress retry %d', self._try_count)
return BUSY, status_text
self._idle_status = WARN, 'no progress'
if self._error_state:
self._try_count += 1
if self._try_count < 10 and self._history is not None:
self.log.warn('end of travel retry %d', self._try_count)
self.write_target(self.target)
return Done
self._idle_status = WARN, self._error_state
if self.status[0] != IDLE:
self._stop()
return self._idle_status
def write_target(self, value): @Command()
if value == self.read_value(): def stop(self):
return value if self.step_mode:
self.check_limits(value) self._stop_thread()
self._try_count = 0 self._status = IDLE, 'stopped'
self._direction = 1 if value > self.value else -1 elif self._moving_since:
# if self._error_state and DIRECTION_NAME[-self._direction] not in self._error_state: self._moving_since = False # indicate stop
# raise BadValueError('can not move (%s)' % self._error_state) self.read_status()
self._move_steps = 0
self.write_output(1)
# try first with 50 % of tolerance
self._hw.setTargetRange(self.axis, self.tolerance * 0.5 / self._scale)
for itry in range(5):
try:
self._hw.setTargetPosition(self.axis, value / self._scale)
self._hw.startAutoMove(self.axis, enable=1, relative=0)
except Exception as e:
if itry == 4:
raise
self.log.warn('%r', e)
self._history = [self.value]
self._idle_status = IDLE, ''
self.freeze_status(1, BUSY, 'changed target')
self.setFastPoll(True, 1)
return value
def doPoll(self): @Command(IntRange())
if self._move_steps == 0: def move(self, steps):
super().doPoll() """relative move by number of steps"""
self._stop_thread()
self.read_value()
if steps > 0:
if self.value > self.target_max:
raise BadValueError('above upper limit')
elif self.value < self.target_min:
raise BadValueError('below lower limit')
self.status = self._status = BUSY, 'moving relative'
self._start_thread(self._run_move, steps)
def _run_move(self, steps):
self.setFastPoll(True, self.fast_interval)
self._move_steps(steps)
self.status = self._status = IDLE, ''
def _move_steps(self, steps):
steps = round(steps)
if not steps:
return return
self._hw.startSingleStep(self.axis, self._direction < 0) previous = self._read_pos()
self._move_steps -= self._direction self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
if self._move_steps % int(self.frequency) == 0: # poll value and status every second # wait for output is really on
super().doPoll() for i in range(100):
self._wait(0.001)
self._get_status()
if self._output:
break
else:
raise ValueError('can not switch on output')
for cnt in range(abs(steps)):
self.io.setAxisOutput(self.axis, enable=1, autoDisable=0)
if not self._thread:
raise Stopped('stopped')
self.io.startSingleStep(self.axis, steps < 0)
self._wait(1 / self.frequency)
self._get_status()
if cnt and not self._output:
steps = cnt
break
self._wait(self.delay)
self.value = self._read_pos()
self._step_size = (self.value - previous) / steps
@Command(IntRange(0))
def calib_steps(self, delta):
"""calibrate steps_fwd and steps_bwd using <delta> steps forwards and backwards"""
if not self.sensor_connected:
raise HardwareError('no sensor connected')
self._stop_thread()
self._status = BUSY, 'calibrate step size'
self.read_status()
self._start_thread(self._run_calib, delta)
def _run_calib(self, steps):
self.value = self._read_pos()
if self._calib_range is None or abs(self.target - self.value) > self._calib_range:
self.target = self.value
maxfwd = 0
maxbwd = 0
cntfwd = 0
cntbwd = 0
self._calib_range = 0
for i in range(10):
if self.value <= self.target:
self._status = BUSY, 'move forwards'
self.read_status()
self._move_steps(steps)
while True:
self._move_steps(steps)
if self._step_size and self._output:
maxfwd = max(maxfwd, self._step_size)
cntfwd += 1
if self.value > self.target:
break
else:
self._status = BUSY, 'move backwards'
self.read_status()
self._move_steps(-steps)
while True:
self._move_steps(-steps)
if self._step_size:
maxbwd = max(maxbwd, self._step_size)
cntbwd += 1
if self.value < self.target:
break
# keep track how far we had to go for calibration
self._calib_range = max(self._calib_range, abs(self.value - self.target))
if cntfwd >= 3 and cntbwd >= 3:
self.steps_fwd = 1 / maxfwd
self.steps_bwd = 1 / maxbwd
self._status = IDLE, 'calib step size done'
break
else:
self._status = WARN, 'calib step size failed'
self.read_status()
@nopoll @nopoll
def read_info(self): def read_info(self):
"""read info from controller""" """read info from controller"""
cap = self._hw.measureCapacitance(self.axis) * 1e9 axistype = ['linear', 'gonio', 'rotator'][self.io.getActuatorType(self.axis)]
axistype = ['linear', 'gonio', 'rotator'][self._hw.getActuatorType(self.axis)] name = self.io.getActuatorName(self.axis)
return '%s %s %.3gnF' % (self._hw.getActuatorName(self.axis), axistype, cap) cap = self.io.measureCapacitance(self.axis) * 1e9
return f'{name} {axistype} {cap:.3g}nF'
def _stop(self):
self._move_steps = 0
self._history = None
for _ in range(5):
try:
self._hw.startAutoMove(self.axis, enable=0, relative=0)
break
except Exception as e:
if itry == 4:
raise
self.log.warn('%r', e)
self._hw.setTargetRange(self.axis, self.tolerance / self._scale)
self.setFastPoll(False)
@Command()
def stop(self):
self._idle_status = IDLE, 'stopped' if self.isBusy() else ''
self._stop()
self.status = self._idle_status
@Command(IntRange())
def move(self, value):
"""relative move by number of steps"""
self._direction = 1 if value > 0 else -1
self.check_value(self.value)
self._history = None
if DIRECTION_NAME[self._direction] in self._error_state:
raise BadValueError('can not move (%s)' % self._error_state)
self._move_steps = value
self._idle_status = IDLE, ''
self.read_status()
self.setFastPoll(True, 1/self.frequency)

313
frappy_psi/cetoni_pump.py Normal file
View File

@ -0,0 +1,313 @@
libpath = '/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/python/src/'
import sys
if libpath not in sys.path:
sys.path.append(libpath)
from frappy.core import Drivable, Readable, StringIO, HasIO, FloatRange, IntRange, StringType, BoolType, EnumType, \
Parameter, Property, PersistentParam, Command, IDLE, BUSY, ERROR, WARN, Attached, Module
from qmixsdk import qmixbus
from qmixsdk import qmixpump
from qmixsdk import qmixvalve
from qmixsdk.qmixpump import ContiFlowProperty, ContiFlowSwitchingMode
from qmixsdk.qmixbus import UnitPrefix, TimeUnit
import time
class LabCannBus(Module):
deviceconfig = Property('config files', StringType(),default="/home/l_samenv/frappy/cetoniSDK/CETONI_SDK_Raspi_64bit_v20220627/config/dual_pumps")
def earlyInit(self):
super().earlyInit()
self.bus = qmixbus.Bus()
self.bus.open(self.deviceconfig, "")
def initModule(self):
super().initModule()
self.bus.start()
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200 50 3')
def shutdownModule(self):
"""Close the connection"""
self.bus.stop()
self.bus.close()
class SyringePump(Drivable):
io = Attached()
pump_name = Property('name of pump', StringType(),default="Nemesys_S_1_Pump")
valve_name = Property('name of valve', StringType(),default="Nemesys_S_1_Valve")
inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
value = Parameter('volume', FloatRange(unit='uL'))
status = Parameter()
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
max_volume = Parameter('max volume', FloatRange(0,100000, unit='uL',), readonly=True)
target_flow_rate = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
real_flow_rate = Parameter('actual flow rate', FloatRange(unit='uL/s'), readonly=True)
target = Parameter('target volume', FloatRange(unit='uL'), readonly=False)
no_of_valve_pos = Property('number of valve positions', IntRange(0,10), default=1)
valve_pos = Parameter('valve position', EnumType('valve', CLOSED=0, APP=1, RES=2, OPEN=3), readonly=False)
force = Parameter('syringe force', FloatRange(unit='kN'), readonly=True)
max_force = Parameter('max device force', FloatRange(unit='kN'), readonly=True)
force_limit = Parameter('user force limit', FloatRange(unit='kN'), readonly=False)
_resolving_force_overload = False
def initModule(self):
super().initModule()
self.pump = qmixpump.Pump()
self.pump.lookup_by_name(self.pump_name)
self.valve = qmixvalve.Valve()
self.valve.lookup_by_name(self.valve_name)
def initialReads(self):
if self.pump.is_in_fault_state():
self.pump.clear_fault()
if not self.pump.is_enabled():
self.pump.enable(True)
self.pump.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
self.max_flow_rate = round(self.pump.get_flow_rate_max(),2)
self.max_volume = round(self.pump.get_volume_max(),2)
self.valve_pos = self.valve.actual_valve_position()
self.target_flow_rate = round(self.max_flow_rate * 0.5,2)
self.target = max(0, round(self.pump.get_fill_level(),2))
self.pump.enable_force_monitoring(True)
self.max_force = self.pump.get_max_device_force()
self.force_limit = self.max_force
def read_value(self):
return round(self.pump.get_fill_level(),2)
def write_target(self, target):
if self.read_valve_pos() == 0 :
self.status = ERROR, 'Cannot pump if valve is closed'
self.log.warn('Cannot pump if valve is closed')
return target
else:
self.pump.set_fill_level(target, self.target_flow_rate)
self.status = BUSY, 'Target changed'
self.log.info(f'Started pumping at {self.target_flow_rate} ul/s')
return target
def write_target_flow_rate(self, rate):
self.target_flow_rate = rate
return rate
def read_real_flow_rate(self):
return round(self.pump.get_flow_is(),2)
def read_valve_pos(self):
return self.valve.actual_valve_position()
def write_valve_pos(self, target_pos):
self.valve.switch_valve_to_position(target_pos)
return target_pos
def read_force(self):
return round(self.pump.read_force_sensor(),3)
def read_force_limit(self):
return self.pump.get_force_limit()
def write_force_limit(self, limit):
self.pump.write_force_limit(limit)
return limit
def read_status(self):
fault_state = self.pump.is_in_fault_state()
pumping = self.pump.is_pumping()
pump_enabled = self.pump.is_enabled()
safety_stop_active = self.pump.is_force_safety_stop_active()
if fault_state == True:
return ERROR, 'Pump in fault state'
elif self._resolving_force_overload :
return BUSY, 'Resolving force overload'
elif safety_stop_active:
return ERROR, 'Pressure safety stop'
elif not pump_enabled:
return ERROR, 'Pump not enabled'
elif pumping == True:
return BUSY, f'Pumping {self.real_flow_rate} ul/s'
elif self.read_valve_pos() == 0:
return IDLE, 'Valve closed'
else:
return IDLE, ''
@Command
def stop(self):
self.pump.stop_pumping()
self.target = self.pump.get_fill_level()
self.status = BUSY, 'Stopping'
@Command
def clear_errors(self):
"""Clear fault state and enable pump"""
if self.pump.is_in_fault_state():
self.pump.clear_fault()
self.log.info('Cleared faults')
if not self.pump.is_enabled():
self.pump.enable(True)
self.log.info('Pump was disabled, re-enabling')
self.target = max(0,round(self.value,2))
self.status = IDLE, ''
@Command
def resolve_force_overload(self):
"""Resolve a force overload situation"""
if not self.pump.is_force_safety_stop_active():
self.status = ERROR, 'No force overload detected'
self.log.warn('No force overload to be resolved')
return
self._resolving_force_overload = True
self.status = BUSY, 'Resolving force overload'
self.pump.enable_force_monitoring(False)
flow = 0 - self.pump.get_flow_rate_max() / 100
self.pump.generate_flow(flow)
safety_stop_active = False
while not safety_stop_active:
time.sleep(0.1)
safety_stop_active = self.pump.is_force_safety_stop_active()
self.pump.stop_pumping()
self.pump.enable_force_monitoring(True)
time.sleep(0.3)
self._resolving_force_overload = False
self.status = self.read_status()
class ContiFlowPump(Drivable):
io = Attached()
inner_diameter_set = Property('inner diameter', FloatRange(), default=1)
piston_stroke_set = Property('piston stroke', FloatRange(), default=60)
crossflow_seconds = Property('crossflow duration', FloatRange(unit='s'),default=2)
value = PersistentParam('flow rate', FloatRange(unit='uL/s'))
status = PersistentParam()
max_refill_flow = Parameter('max refill flow', FloatRange(unit='uL/s'), readonly=True)
refill_flow = Parameter('refill flow', FloatRange(unit='uL/s'), readonly=False)
max_flow_rate = Parameter('max flow rate', FloatRange(0,100000, unit='uL/s',), readonly=True)
target = Parameter('target flow rate', FloatRange(unit='uL/s'), readonly=False)
def initModule(self):
super().initModule()
self.pump = qmixpump.ContiFlowPump()
self.pump.lookup_by_name("ContiFlowPump_1")
def initialReads(self):
if self.pump.is_in_fault_state():
self.pump.clear_fault()
if not self.pump.is_enabled():
self.pump.enable(True)
self.syringe_pump1 = self.pump.get_syringe_pump(0)
self.syringe_pump1.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.syringe_pump2 = self.pump.get_syringe_pump(1)
self.syringe_pump2.set_syringe_param(self.inner_diameter_set, self.piston_stroke_set)
self.pump.set_volume_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres)
self.pump.set_flow_unit(qmixpump.UnitPrefix.micro, qmixpump.VolumeUnit.litres, qmixpump.TimeUnit.per_second)
self.pump.set_device_property(ContiFlowProperty.SWITCHING_MODE, ContiFlowSwitchingMode.CROSS_FLOW)
self.max_refill_flow = self.pump.get_device_property(ContiFlowProperty.MAX_REFILL_FLOW)
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, self.max_refill_flow / 2.0)
self.pump.set_device_property(ContiFlowProperty.CROSSFLOW_DURATION_S, self.crossflow_seconds)
self.pump.set_device_property(ContiFlowProperty.OVERLAP_DURATION_S, 0)
self.max_flow_rate = self.pump.get_flow_rate_max()
self.target = 0
def read_value(self):
return round(self.pump.get_flow_is(),3)
def write_target(self, target):
if target <= 0:
self.pump.stop_pumping()
self.status = self.read_status()
return 0
else:
self.pump.generate_flow(target)
self.status = BUSY, 'Target changed'
return target
def read_refill_flow(self):
return round(self.pump.get_device_property(ContiFlowProperty.REFILL_FLOW),3)
def write_refill_flow(self, refill_flow):
self.pump.set_device_property(ContiFlowProperty.REFILL_FLOW, refill_flow)
self.max_flow_rate = self.pump.get_flow_rate_max()
return refill_flow
def read_status(self):
fault_state = self.pump.is_in_fault_state()
pumping = self.pump.is_pumping()
pump_enabled = self.pump.is_enabled()
pump_initialised = self.pump.is_initialized()
pump_initialising = self.pump.is_initializing()
if fault_state == True:
return ERROR, 'Pump in fault state'
elif not pump_enabled:
return ERROR, 'Pump not enabled'
elif not pump_initialised:
return WARN, 'Pump not initialised'
elif pump_initialising:
return BUSY, 'Pump initialising'
elif pumping == True:
return BUSY, 'Pumping'
else:
return IDLE, ''
@Command
def stop(self):
self.pump.stop_pumping()
self.target = 0
self.status = BUSY, 'Stopping'
@Command
def clear_errors(self):
"""Clear fault state and enable pump"""
if self.pump.is_in_fault_state():
self.pump.clear_fault()
self.log.info('Cleared faults')
if not self.pump.is_enabled():
self.pump.enable(True)
self.log.info('Pump was disabled, re-enabling')
self.target = 0
self.status = IDLE, ''
@Command
def initialise(self):
"""Initialise the ConfiFlow pump"""
self.pump.initialize()

104
frappy_psi/gilsonpump.py Normal file
View File

@ -0,0 +1,104 @@
# Author: Wouter Gruenewald<wouter.gruenewald@psi.ch>
from frappy.core import StringType, BoolType, EnumType, FloatRange, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, BUSY, Drivable
class PeristalticPump(Drivable):
value = Parameter('Pump speed', FloatRange(0,100,unit="%"), default=0)
target = Parameter('Target pump speed', FloatRange(0,100,unit="%"), default=0)
status = Parameter()
addr_AO = Property('Address of the analog out', StringType())
addr_dir_relay = Property('Address of the direction relay', StringType())
addr_run_relay = Property('Address of the running relay', StringType())
direction = Parameter('pump direction', EnumType('direction', CLOCKWISE=0, ANTICLOCKWISE=1), default=0, readonly=False)
active = Parameter('pump running', BoolType(), default=False, readonly=False)
def initModule(self):
super().initModule()
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_mode', 'w') as f :
f.write('V')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('1')
def shutdownModule(self):
'''Disable analog output'''
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write('0')
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO+'_enabled', 'w') as f :
f.write('0')
def read_value(self):
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'r') as f :
raw_value = f.read().strip('\n')
value = (int(raw_value) / 5000) * 100
return value
def write_target(self, target):
raw_value = (target / 100)*5000
with open('/sys/class/ionopimax/analog_out/'+self.addr_AO, 'w') as f :
f.write(str(int(raw_value)))
return target
def read_direction(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
raw_direction = f.read().strip('\n')
if raw_direction == '0' or raw_direction == 'F':
return 0
if raw_direction == '1' or raw_direction == 'S':
return 1
else:
return None
def write_direction(self, direction):
if direction == 0:
raw_direction = '0'
elif direction == 1:
raw_direction = '1'
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'w') as f :
f.write(raw_direction)
return direction
def read_active(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
raw_active = f.read().strip('\n')
if raw_active == '0' or raw_active == 'F':
return False
elif raw_active == '1' or raw_active == 'S':
return True
else:
return None
def write_active(self, active):
if active == False:
raw_active = '0'
elif active == True:
raw_active = '1'
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'w') as f :
f.write(raw_active)
return active
def read_status(self):
with open('/sys/class/ionopimax/digital_out/'+self.addr_dir_relay, 'r') as f :
raw_direction = f.read().strip('\n')
with open('/sys/class/ionopimax/digital_out/'+self.addr_run_relay, 'r') as f :
raw_active = f.read().strip('\n')
if raw_direction == 'F' or raw_direction == 'S':
return ERROR, 'Fault on direction relay'
elif raw_active == 'F' or raw_active == 'S':
return ERROR, 'Fault on pump activation relay'
elif self.active == True:
return BUSY, 'Pump running'
else:
return IDLE, ''
@Command
def stop(self):
self.write_active(False)

View File

@ -198,6 +198,10 @@ class MotorValve(PersistentMixin, Drivable):
@Command @Command
def stop(self): def stop(self):
"""stop at current position
state will probably be undefined
"""
self._state.stop() self._state.stop()
self.motor.stop() self.motor.stop()

View File

@ -22,7 +22,7 @@
"""modules to access parameters""" """modules to access parameters"""
from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \ from frappy.core import Drivable, EnumType, IDLE, Attached, StringType, Property, \
Parameter, FloatRange, BoolType, Readable, ERROR Parameter, BoolType, FloatRange, Readable, ERROR, nopoll
from frappy.errors import ConfigError from frappy.errors import ConfigError
from frappy_psi.convergence import HasConvergence from frappy_psi.convergence import HasConvergence
from frappy_psi.mixins import HasRamp from frappy_psi.mixins import HasRamp
@ -72,19 +72,21 @@ class Driv(Drivable):
raise ConfigError('illegal recursive read/write module') raise ConfigError('illegal recursive read/write module')
super().checkProperties() super().checkProperties()
#def registerUpdates(self): def registerUpdates(self):
# self.read.valueCallbacks[self.read_param].append(self.update_value) self.read.addCallback(self.read_param, self.announceUpdate, 'value')
# self.write.valueCallbacks[self.write_param].append(self.update_target) self.write.addCallback(self.write_param, self.announceUpdate, 'target')
#
#def startModule(self, start_events):
# start_events.queue(self.registerUpdates)
# super().startModule(start_events)
def startModule(self, start_events):
start_events.queue(self.registerUpdates)
super().startModule(start_events)
@nopoll
def read_value(self): def read_value(self):
return getattr(self.read, f'{self.read_param}') return getattr(self.read, f'read_{self.read_param}')()
@nopoll
def read_target(self): def read_target(self):
return getattr(self.write, f'{self.write_param}') return getattr(self.write, f'read_{self.write_param}')()
def read_status(self): def read_status(self):
return IDLE, '' return IDLE, ''
@ -130,7 +132,7 @@ def set_enabled(modobj, value):
def get_value(obj, default): def get_value(obj, default):
"""get the value of given module. if not valid, return the limit (min_high or max_low)""" """get the value of given module. if not valid, return the default"""
if not getattr(obj, 'enabled', True): if not getattr(obj, 'enabled', True):
return default return default
# consider also that a value 0 is invalid # consider also that a value 0 is invalid

View File

@ -62,7 +62,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2), encoder_mode = Parameter('how to treat the encoder', EnumType('encoder', NO=0, READ=1, CHECK=2),
default=1, readonly=False) default=1, readonly=False)
check_limit_switches = Parameter('whethter limit switches are checked',BoolType(), check_limit_switches = Parameter('whether limit switches are checked',BoolType(),
default=0, readonly=False) default=0, readonly=False)
value = PersistentParam('angle', FloatRange(unit='deg')) value = PersistentParam('angle', FloatRange(unit='deg'))
status = PersistentParam() status = PersistentParam()
@ -90,6 +90,8 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active', status_bits = ['power stage error', 'undervoltage', 'overtemperature', 'active',
'lower switch active', 'upper switch active', 'step failure', 'encoder error'] 'lower switch active', 'upper switch active', 'step failure', 'encoder error']
_doing_reference = False
def get(self, cmd): def get(self, cmd):
return self.communicate(f'{self.address:x}{self.axis}{cmd}') return self.communicate(f'{self.address:x}{self.axis}{cmd}')
@ -178,10 +180,14 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
def doPoll(self): def doPoll(self):
super().doPoll() super().doPoll()
if self._running and not self.isBusy(): if self._running and not self.isBusy() and not self._doing_reference:
if time.time() > self._stopped_at + 5: if time.time() > self._stopped_at + 5:
self.log.warning('stop motor not started by us') self.log.warning('stop motor not started by us')
self.hw_stop() self.hw_stop()
if self._doing_reference and self.get('=H') == 'E' :
self.status = IDLE, ''
self.target = 0
self._doing_reference = False
def read_status(self): def read_status(self):
hexstatus = 0x100 hexstatus = 0x100
@ -207,6 +213,9 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
if status[0] == ERROR: if status[0] == ERROR:
self._blocking_error = status[1] self._blocking_error = status[1]
return status return status
if self._doing_reference and self.get('=H') == 'N':
status = BUSY, 'Doing reference run'
return status
return super().read_status() # status from state machine return super().read_status() # status from state machine
def check_moving(self): def check_moving(self):
@ -346,3 +355,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.status = 'IDLE', 'after error reset' self.status = 'IDLE', 'after error reset'
self._blocking_error = None self._blocking_error = None
self.target = self.value # clear error in target self.target = self.value # clear error in target
@Command
def make_ref_run(self):
'''Do reference run'''
self._doing_reference = True
self.status = BUSY, 'Doing reference run'
self.communicate(f'{self.address:x}{self.axis}0-')

View File

@ -483,6 +483,10 @@ class Temp(PpmsDrivable):
self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp)
def stop(self): def stop(self):
"""set setpoint to current value
but restrict to values between last target and current target
"""
if not self.isDriving(): if not self.isDriving():
return return
if self.status[0] != StatusType.STABILIZING: if self.status[0] != StatusType.STABILIZING:
@ -612,6 +616,7 @@ class Field(PpmsDrivable):
# do not execute FIELD command, as this would trigger a ramp up of leads current # do not execute FIELD command, as this would trigger a ramp up of leads current
def stop(self): def stop(self):
"""stop at current driven Field"""
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)
@ -714,6 +719,7 @@ class Position(PpmsDrivable):
return value # do not execute MOVE command, as this would trigger an unnecessary move return value # do not execute MOVE command, as this would trigger an unnecessary move
def stop(self): def stop(self):
"""stop motor"""
if not self.isDriving(): if not self.isDriving():
return return
newtarget = clamp(self._last_target, self.value, self.target) newtarget = clamp(self._last_target, self.value, self.target)

View File

@ -141,5 +141,6 @@ class TemperatureLoopTC1(SensorTC1, Drivable):
return value return value
def stop(self): def stop(self):
"""stop at current value (does nothing if ramp is not used)"""
if self.control and self.ramp_used: if self.control and self.ramp_used:
self.write_target(self.value) self.write_target(self.value)

View File

@ -0,0 +1,70 @@
from frappy.core import StringType, BoolType, Parameter, Property, PersistentParam, Command, IDLE, ERROR, WARN, Writable
import time
class RheoTrigger(Writable):
addr = Property('Port address', StringType())
value = Parameter('Output state', BoolType(), default=0)
target = Parameter('target', BoolType(), default=0, readonly=False)
status = Parameter()
doBeep = Property('Make noise', BoolType(), default=0)
_status = 0
def initModule(self):
super().initModule()
with open('/sys/class/ionopimax/digital_io/'+self.addr+'_mode', 'w') as f :
f.write('out')
if self.doBeep:
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200 50 3')
def read_value(self):
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'r') as f :
file_value = f.read()
if file_value == '0\n':
value = False
self._status = 0
elif file_value == '1\n':
value = True
self._status = 1
else:
self._status = -1
value = False
return value
def write_target(self,target):
if target == self.value:
return target
else:
with open('/sys/class/ionopimax/digital_io/'+self.addr, 'w') as f :
if target == True:
f.write('1')
elif target == False:
f.write('0')
time.sleep(0.05)
if self.doBeep:
with open('/sys/class/ionopimax/buzzer/beep', 'w') as f :
f.write('200')
self.status = self.read_status()
return target
def read_status(self):
self.value = self.read_value()
if self._status == 0:
return IDLE, 'Signal low'
elif self._status == 1:
return IDLE, 'Signal high'
else:
return ERROR, 'Cannot read status'
@Command
def toggle(self):
"""Toggle output"""
value = self.read_value()
if value == True:
self.write_target(False)
else:
self.write_target(True)

View File

@ -39,12 +39,13 @@ from os.path import expanduser, join, exists
from frappy.client import ProxyClient from frappy.client import ProxyClient
from frappy.datatypes import ArrayOf, BoolType, \ from frappy.datatypes import ArrayOf, BoolType, \
EnumType, FloatRange, IntRange, StringType EnumType, FloatRange, IntRange, StringType
from frappy.errors import ConfigError, HardwareError, secop_error, CommunicationFailedError from frappy.core import IDLE, BUSY, ERROR
from frappy.errors import ConfigError, HardwareError, CommunicationFailedError
from frappy.lib import generalConfig, mkthread from frappy.lib import generalConfig, mkthread
from frappy.lib.asynconn import AsynConn, ConnectionClosed from frappy.lib.asynconn import AsynConn, ConnectionClosed
from frappy.modules import Attached, Command, Done, Drivable, \ from frappy.modulebase import Done
from frappy.modules import Attached, Command, Drivable, \
Module, Parameter, Property, Readable, Writable Module, Parameter, Property, Readable, Writable
from frappy.protocol.dispatcher import make_update
CFG_HEADER = """Node('%(config)s.sea.psi.ch', CFG_HEADER = """Node('%(config)s.sea.psi.ch',
@ -107,7 +108,6 @@ class SeaClient(ProxyClient, Module):
service = Property("main/stick/addons", StringType(), default='') service = Property("main/stick/addons", StringType(), default='')
visibility = 'expert' visibility = 'expert'
default_json_file = {} default_json_file = {}
_connect_thread = None
_instance = None _instance = None
_last_connect = 0 _last_connect = 0
@ -124,6 +124,8 @@ class SeaClient(ProxyClient, Module):
self.shutdown = False self.shutdown = False
self.path2param = {} self.path2param = {}
self._write_lock = threading.Lock() self._write_lock = threading.Lock()
self._connect_thread = None
self._connected = False
config = opts.get('config') config = opts.get('config')
if isinstance(config, dict): if isinstance(config, dict):
config = config['value'] config = config['value']
@ -135,14 +137,11 @@ class SeaClient(ProxyClient, Module):
Module.__init__(self, name, log, opts, srv) Module.__init__(self, name, log, opts, srv)
def doPoll(self): def doPoll(self):
if not self.asynio and time.time() > self._last_connect + 10: if not self._connected and time.time() > self._last_connect + 10:
with self._write_lock: if not self._last_connect:
# make sure no more connect thread is running self.log.info('reconnect to SEA %s', self.service)
if self._connect_thread and self._connect_thread.isAlive(): if self._connect_thread is None:
return self._connect_thread = mkthread(self._connect)
if not self._last_connect:
self.log.info('reconnect to SEA %s', self.service)
self._connect_thread = mkthread(self._connect, None)
def register_obj(self, module, obj): def register_obj(self, module, obj):
self.objects.add(obj) self.objects.add(obj)
@ -150,99 +149,105 @@ class SeaClient(ProxyClient, Module):
self.path2param.setdefault(k, []).extend(v) self.path2param.setdefault(k, []).extend(v)
self.register_callback(module.name, module.updateEvent) self.register_callback(module.name, module.updateEvent)
def _connect(self, started_callback): def _connect(self):
self.asynio = None try:
if self.syncio: if self.syncio:
# trigger syncio reconnect in self.request()
try:
self.syncio.disconnect()
except Exception:
pass
self.syncio = None
self._last_connect = time.time()
if self._instance:
try:
from servicemanager import SeaManager # pylint: disable=import-outside-toplevel
SeaManager().do_start(self._instance)
except ImportError:
pass
if '//' not in self.uri:
self.uri = 'tcp://' + self.uri
self.asynio = AsynConn(self.uri)
reply = self.asynio.readline()
if reply != b'OK':
raise CommunicationFailedError('reply %r should be "OK"' % reply)
for _ in range(2):
self.asynio.writeline(b'Spy 007')
reply = self.asynio.readline()
if reply == b'Login OK':
break
else:
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
result = self.request('frappy_config %s %s' % (self.service, self.config))
if result.startswith('ERROR:'):
raise CommunicationFailedError(f'reply from frappy_config: {result}')
# frappy_async_client switches to the json protocol (better for updates)
self.asynio.writeline(b'frappy_async_client')
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
self._connect_thread = None
mkthread(self._rxthread, started_callback)
def request(self, command, quiet=False):
"""send a request and wait for reply"""
with self._write_lock:
if not self.syncio or not self.syncio.connection:
if not self.asynio or not self.asynio.connection:
try:
self._connect_thread.join()
except AttributeError:
pass
# let doPoll do the reconnect
self.pollInfo.trigger(True)
raise ConnectionClosed('disconnected - reconnect later')
self.syncio = AsynConn(self.uri)
assert self.syncio.readline() == b'OK'
self.syncio.writeline(b'seauser seaser')
assert self.syncio.readline() == b'Login OK'
self.log.info('connected to %s', self.uri)
try:
self.syncio.flush_recv()
ft = 'fulltransAct' if quiet else 'fulltransact'
self.syncio.writeline(('%s %s' % (ft, command)).encode())
result = None
deadline = time.time() + 10
while time.time() < deadline:
reply = self.syncio.readline()
if reply is None:
continue
reply = reply.decode()
if reply.startswith('TRANSACTIONSTART'):
result = []
continue
if reply == 'TRANSACTIONFINISHED':
if result is None:
self.log.info('missing TRANSACTIONSTART on: %s', command)
return ''
if not result:
return ''
return '\n'.join(result)
if result is None:
self.log.info('swallow: %s', reply)
continue
if not result:
result = [reply.split('=', 1)[-1]]
else:
result.append(reply)
except ConnectionClosed:
try: try:
self.syncio.disconnect() self.syncio.disconnect()
except Exception: except Exception:
pass pass
self.syncio = None self._last_connect = time.time()
raise if self._instance:
raise TimeoutError('no response within 10s') try:
from servicemanager import SeaManager # pylint: disable=import-outside-toplevel
SeaManager().do_start(self._instance)
except ImportError:
pass
if '//' not in self.uri:
self.uri = 'tcp://' + self.uri
self.asynio = AsynConn(self.uri)
reply = self.asynio.readline()
if reply != b'OK':
raise CommunicationFailedError('reply %r should be "OK"' % reply)
for _ in range(2):
self.asynio.writeline(b'Spy 007')
reply = self.asynio.readline()
if reply == b'Login OK':
break
else:
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
self.syncio = AsynConn(self.uri)
assert self.syncio.readline() == b'OK'
self.syncio.writeline(b'seauser seaser')
assert self.syncio.readline() == b'Login OK'
self.log.info('connected to %s', self.uri)
def _rxthread(self, started_callback): result = self.raw_request('frappy_config %s %s' % (self.service, self.config))
if result.startswith('ERROR:'):
raise CommunicationFailedError(f'reply from frappy_config: {result}')
# frappy_async_client switches to the json protocol (better for updates)
self.asynio.writeline(b'frappy_async_client')
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
self._connected = True
mkthread(self._rxthread)
finally:
self._connect_thread = None
def request(self, command, quiet=False):
with self._write_lock:
if not self._connected:
if self._connect_thread is None:
# let doPoll do the reconnect
self.pollInfo.trigger(True)
raise ConnectionClosed('disconnected - reconnect is tried later')
return self.raw_request(command, quiet)
def raw_request(self, command, quiet=False):
"""send a request and wait for reply"""
try:
self.syncio.flush_recv()
ft = 'fulltransAct' if quiet else 'fulltransact'
self.syncio.writeline(('%s %s' % (ft, command)).encode())
result = None
deadline = time.time() + 10
while time.time() < deadline:
reply = self.syncio.readline()
if reply is None:
continue
reply = reply.decode()
if reply.startswith('TRANSACTIONSTART'):
result = []
continue
if reply == 'TRANSACTIONFINISHED':
if result is None:
self.log.info('missing TRANSACTIONSTART on: %s', command)
return ''
if not result:
return ''
return '\n'.join(result)
if result is None:
self.log.info('swallow: %s', reply)
continue
if not result:
result = [reply.split('=', 1)[-1]]
else:
result.append(reply)
raise TimeoutError('no response within 10s')
except ConnectionClosed:
self.close_connections()
raise
def close_connections(self):
connections = self.syncio, self.asynio
self._connected = False
self.syncio = self.asynio = None
for conn in connections:
try:
conn.disconnect()
except Exception:
pass
def _rxthread(self):
recheck = None recheck = None
while not self.shutdown: while not self.shutdown:
if recheck and time.time() > recheck: if recheck and time.time() > recheck:
@ -258,11 +263,7 @@ class SeaClient(ProxyClient, Module):
if reply is None: if reply is None:
continue continue
except ConnectionClosed: except ConnectionClosed:
try: self.close_connections()
self.asynio.disconnect()
except Exception:
pass
self.asynio = None
break break
try: try:
msg = json.loads(reply) msg = json.loads(reply)
@ -289,9 +290,6 @@ class SeaClient(ProxyClient, Module):
data = msg['data'] data = msg['data']
if flag == 'finish' and obj == 'get_all_param': if flag == 'finish' and obj == 'get_all_param':
# first updates have finished # first updates have finished
if started_callback:
started_callback()
started_callback = None
continue continue
if flag != 'hdbevent': if flag != 'hdbevent':
if obj not in ('frappy_async_client', 'get_all_param'): if obj not in ('frappy_async_client', 'get_all_param'):
@ -352,7 +350,7 @@ class SeaClient(ProxyClient, Module):
class SeaConfigCreator(SeaClient): class SeaConfigCreator(SeaClient):
def startModule(self, start_events): def startModule(self, start_events):
"""save objects (and sub-objects) description and exit""" """save objects (and sub-objects) description and exit"""
self._connect(None) self._connect()
reply = self.request('describe_all') reply = self.request('describe_all')
reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n')) reply = ''.join('' if line.startswith('WARNING') else line for line in reply.split('\n'))
description, reply = json.loads(reply) description, reply = json.loads(reply)
@ -644,22 +642,7 @@ class SeaModule(Module):
if upd: if upd:
upd(value, timestamp, readerror) upd(value, timestamp, readerror)
return return
try: self.announceUpdate(parameter, value, readerror, timestamp)
pobj = self.parameters[parameter]
except KeyError:
self.log.error('do not know %s:%s', self.name, parameter)
raise
pobj.timestamp = timestamp
# should be done here: deal with clock differences
if not readerror:
try:
pobj.value = value # store the value even in case of a validation error
pobj.value = pobj.datatype(value)
except Exception as e:
readerror = secop_error(e)
pobj.readerror = readerror
if pobj.export:
self.secNode.srv.dispatcher.broadcast_event(make_update(self.name, pobj))
def initModule(self): def initModule(self):
self.io.register_obj(self, self.sea_object) self.io.register_obj(self, self.sea_object)
@ -670,20 +653,35 @@ class SeaModule(Module):
class SeaReadable(SeaModule, Readable): class SeaReadable(SeaModule, Readable):
_readerror = None
_status = IDLE, ''
def update_value(self, value, timestamp, readerror):
# make sure status is always ERROR when reading value fails
self._readerror = readerror
if readerror:
self.read_status() # forced ERROR status
self.announceUpdate('value', value, readerror, timestamp)
else: # order is important
self.value = value # includes announceUpdate
self.read_status() # send event for ordinary self._status
def update_status(self, value, timestamp, readerror): def update_status(self, value, timestamp, readerror):
if readerror: if readerror:
value = repr(readerror) value = f'{readerror.name} - {readerror}'
if value == '': if value == '':
self.status = (self.Status.IDLE, '') self._status = IDLE, ''
else: else:
self.status = (self.Status.ERROR, value) self._status = ERROR, value
self.read_status()
def read_status(self): def read_status(self):
return self.status if self._readerror:
return ERROR, f'{self._readerror.name} - {self._readerror}'
return self._status
class SeaWritable(SeaModule, Writable): class SeaWritable(SeaReadable, Writable):
def read_value(self): def read_value(self):
return self.target return self.target
@ -693,20 +691,13 @@ class SeaWritable(SeaModule, Writable):
self.value = value self.value = value
class SeaDrivable(SeaModule, Drivable): class SeaDrivable(SeaReadable, Drivable):
_sea_status = ''
_is_running = 0 _is_running = 0
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self._run_event = threading.Event() self._run_event = threading.Event()
def read_status(self):
return self.status
# def read_target(self):
# return self.target
def write_target(self, value): def write_target(self, value):
self._run_event.clear() self._run_event.clear()
self.io.query(f'run {self.sea_object} {value}') self.io.query(f'run {self.sea_object} {value}')
@ -714,25 +705,20 @@ class SeaDrivable(SeaModule, Drivable):
self.log.warn('target changed but is_running stays 0') self.log.warn('target changed but is_running stays 0')
return value return value
def update_status(self, value, timestamp, readerror):
if not readerror:
self._sea_status = value
self.updateStatus()
def update_is_running(self, value, timestamp, readerror): def update_is_running(self, value, timestamp, readerror):
if not readerror: if not readerror:
self._is_running = value self._is_running = value
self.updateStatus() self.read_status()
if value: if value:
self._run_event.set() self._run_event.set()
def updateStatus(self): def read_status(self):
if self._sea_status: status = super().read_status()
self.status = (self.Status.ERROR, self._sea_status) if self._is_running:
elif self._is_running: if status[0] >= ERROR:
self.status = (self.Status.BUSY, 'driving') return ERROR, 'BUSY + ' + status[1]
else: return BUSY, 'driving'
self.status = (self.Status.IDLE, '') return status
def updateTarget(self, module, parameter, value, timestamp, readerror): def updateTarget(self, module, parameter, value, timestamp, readerror):
if value is not None: if value is not None:

View File

@ -27,7 +27,7 @@ import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error from scipy.interpolate import splev, splrep # pylint: disable=import-error
from frappy.core import Attached, BoolType, Parameter, Readable, StringType, \ from frappy.core import Attached, BoolType, Parameter, Readable, StringType, \
FloatRange FloatRange, nopoll
def linear(x): def linear(x):
@ -195,35 +195,40 @@ class Sensor(Readable):
if self.description == '_': if self.description == '_':
self.description = f'{self.rawsensor!r} calibrated with curve {self.calib!r}' self.description = f'{self.rawsensor!r} calibrated with curve {self.calib!r}'
def doPoll(self):
self.read_status()
def write_calib(self, value): def write_calib(self, value):
self._calib = CalCurve(value) self._calib = CalCurve(value)
return value return value
def update_value(self, value): def _get_value(self, rawvalue):
if self.abs: if self.abs:
value = abs(float(value)) rawvalue = abs(float(rawvalue))
self.value = self._calib(value) return self._calib(rawvalue)
self._value_error = None
def error_update_value(self, err): def _get_status(self, rawstatus):
if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370 return rawstatus if self._value_error is None else (self.Status.ERROR, self._value_error)
self._value_error = None
return None
self._value_error = repr(err)
raise err
def update_status(self, value): def update_value(self, rawvalue, err=None):
if self._value_error is None: if err:
self.status = value if self.abs and str(err) == 'R_UNDER': # hack: ignore R_UNDER from ls370
self._value_error = None
return
err = repr(err)
else: else:
self.status = self.Status.ERROR, self._value_error try:
self.value = self._get_value(rawvalue)
except Exception as e:
err = repr(e)
if err != self._value_error:
self._value_error = err
self.status = self._get_status(self.rawsensor.status)
def update_status(self, rawstatus):
self.status = self._get_status(rawstatus)
@nopoll
def read_value(self): def read_value(self):
return self._calib(self.rawsensor.read_value()) return self._get_value(self.rawsensor.read_value())
@nopoll
def read_status(self): def read_status(self):
self.update_status(self.rawsensor.status) return self._get_status(self.rawsensor.read_status())
return self.status

View File

@ -411,6 +411,7 @@ class Uniax(PersistentMixin, Drivable):
@Command() @Command()
def stop(self): def stop(self):
"""stop motor and control"""
if self.motor.isBusy(): if self.motor.isBusy():
self.log.info('stop motor') self.log.info('stop motor')
self.motor_stop() self.motor_stop()

155
test/test_callbacks.py Normal file
View File

@ -0,0 +1,155 @@
# *****************************************************************************
#
# 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>
#
# *****************************************************************************
"""test parameter callbacks"""
from test.test_modules import LoggerStub, ServerStub
import pytest
from frappy.core import Module, Parameter, FloatRange
from frappy.errors import WrongTypeError
WRONG_TYPE = WrongTypeError()
class Mod(Module):
a = Parameter('', FloatRange())
b = Parameter('', FloatRange())
c = Parameter('', FloatRange())
def read_a(self):
raise WRONG_TYPE
def read_b(self):
raise WRONG_TYPE
def read_c(self):
raise WRONG_TYPE
class Dbl(Module):
a = Parameter('', FloatRange())
b = Parameter('', FloatRange())
c = Parameter('', FloatRange())
_error_a = None
_value_b = None
_error_c = None
def update_a(self, value, err=None):
# treat error updates
try:
self.a = value * 2
except TypeError: # value is None -> err
self.announceUpdate('a', None, err)
def update_b(self, value):
self._value_b = value
# error updates are ignored
self.b = value * 2
def make(cls):
logger = LoggerStub()
srv = ServerStub({})
return cls('mod1', logger, {'description': ''}, srv)
def test_simple_callback():
mod1 = make(Mod)
result = []
def cbfunc(arg1, arg2, value):
result[:] = arg1, arg2, value
mod1.addCallback('a', cbfunc, 'ARG1', 'arg2')
mod1.a = 1.5
assert result == ['ARG1', 'arg2', 1.5]
result.clear()
with pytest.raises(WrongTypeError):
mod1.read_a()
assert not result # callback function is NOT called
def test_combi_callback():
mod1 = make(Mod)
result = []
def cbfunc(arg1, arg2, value, err=None):
result[:] = arg1, arg2, value, err
mod1.addCallback('a', cbfunc, 'ARG1', 'arg2')
mod1.a = 1.5
assert result == ['ARG1', 'arg2', 1.5, None]
result.clear()
with pytest.raises(WrongTypeError):
mod1.read_a()
assert result[:3] == ['ARG1', 'arg2', None] # callback function called with value None
assert isinstance(result[3], WrongTypeError)
def test_autoupdate():
mod1 = make(Mod)
mod2 = make(Dbl)
mod1.registerCallbacks(mod2, autoupdate=['c'])
result = {}
def cbfunc(pname, *args):
result[pname] = args
for param in 'a', 'b', 'c':
mod2.addCallback(param, cbfunc, param)
# test update_a without error
mod1.a = 5
assert mod2.a == 10
assert result.pop('a') == (10,)
# test update_a with error
with pytest.raises(WrongTypeError):
mod1.read_a()
assert result.pop('a') == (None, WRONG_TYPE)
# test that update_b is ignored in case of error
mod1.b = 3
assert mod2.b == 6 # no error
assert result.pop('b') == (6,)
with pytest.raises(WrongTypeError):
mod1.read_b()
assert 'b' not in result
# test autoupdate
mod1.c = 3
assert mod2.c == 3
assert result['c'] == (3,)
with pytest.raises(WrongTypeError):
mod1.read_c()
assert result['c'] == (None, WRONG_TYPE)

View File

@ -18,12 +18,14 @@
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""test frappy.mixins.HasCtrlPars""" """test frappy.extparams"""
from test.test_modules import LoggerStub, ServerStub from test.test_modules import LoggerStub, ServerStub
import pytest
from frappy.core import FloatRange, Module, Parameter from frappy.core import FloatRange, Module, Parameter
from frappy.structparam import StructParam from frappy.extparams import StructParam, FloatEnumParam
from frappy.errors import ProgrammingError
def test_with_read_ctrlpars(): def test_with_read_ctrlpars():
@ -130,3 +132,76 @@ def test_order_dependence1():
def test_order_dependence2(): def test_order_dependence2():
test_with_read_ctrlpars() test_with_read_ctrlpars()
test_without_read_ctrlpars() test_without_read_ctrlpars()
def test_float_enum():
class Mod(Module):
vrange = FloatEnumParam('voltage range', [
(1, '50uV'), '200 µV', '1mV', ('5mV', 0.006), (9, 'max', 0.024)], 'V')
gain = FloatEnumParam('gain factor', ('1', '2', '4', '8'), idx_name='igain')
dist = FloatEnumParam('distance', ('1m', '1mm', '1µm'), unit='m')
_vrange_idx = None
def write_vrange_idx(self, value):
self._vrange_idx = value
logger = LoggerStub()
updates = {}
srv = ServerStub(updates)
m = Mod('m', logger, {'description': ''}, srv)
assert m.write_vrange_idx(1) == 1
assert m._vrange_idx == '50uV'
assert m._vrange_idx == 1
assert m.vrange == 5e-5
assert m.write_vrange_idx(2) == 2
assert m._vrange_idx == '200 µV'
assert m._vrange_idx == 2
assert m.vrange == 2e-4
assert m.write_vrange(6e-5) == 5e-5 # round to the next value
assert m._vrange_idx == '50uV'
assert m._vrange_idx == 1
assert m.write_vrange(20e-3) == 24e-3 # round to the next value
assert m._vrange_idx == 'max'
assert m._vrange_idx == 9
for idx in range(4):
value = 2 ** idx
updates.clear()
assert m.write_igain(idx) == idx
assert updates == {'m': {'igain': idx, 'gain': value}}
assert m.igain == idx
assert m.igain == str(value)
assert m.gain == value
for idx in range(4):
value = 2 ** idx
assert m.write_gain(value) == value
assert m.igain == idx
assert m.igain == str(value)
for idx in range(3):
value = 10 ** (-3 * idx)
assert m.write_dist(value) == value
assert m.dist_idx == idx
@pytest.mark.parametrize('labels, unit, error', [
(FloatRange(), '', 'not a datatype'), # 2nd arg must not be a datatype
([(1, 2, 3)], '', 'must be strings'), # label is not a string
([(1, '1V', 3, 4)], 'V', 'labels or tuples'), # 4-tuple
([('1A', 3, 4)], 'A', 'labels or tuples'), # two values after label
(('1m', (0, '1k')), '', 'conflicts with'), # two times index 0
(['1mV', '10mA'], 'V', 'not the form'), # wrong unit
(['.mV'], 'V', 'not the form'), # bad number
(['mV'], 'V', 'not the form'), # missing number
(['1+mV'], 'V', 'not the form'), # bad number
])
def test_bad_float_enum(labels, unit, error):
with pytest.raises(ProgrammingError, match=error):
class Mod(Module): # pylint:disable=unused-variable
param = FloatEnumParam('', labels, unit)

View File

@ -23,6 +23,8 @@
import sys import sys
import threading import threading
import importlib
from glob import glob
import pytest import pytest
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
@ -440,12 +442,12 @@ def test_override():
assert Mod.value.value == 5 assert Mod.value.value == 5
assert Mod.stop.description == "no decorator needed" assert Mod.stop.description == "no decorator needed"
class Mod2(Drivable): class Mod2(Mod):
@Command()
def stop(self): def stop(self):
pass pass
assert Mod2.stop.description == Drivable.stop.description # inherit doc string
assert Mod2.stop.description == Mod.stop.description
def test_command_config(): def test_command_config():
@ -920,3 +922,24 @@ def test_interface_classes(bases, iface_classes):
pass pass
m = Mod('mod', LoggerStub(), {'description': 'test'}, srv) m = Mod('mod', LoggerStub(), {'description': 'test'}, srv)
assert m.interface_classes == iface_classes assert m.interface_classes == iface_classes
all_drivables = set()
for pyfile in glob('frappy_*/*.py'):
module = pyfile[:-3].replace('/', '.')
try:
importlib.import_module(module)
except Exception as e:
print(module, e)
continue
for obj_ in sys.modules[module].__dict__.values():
if isinstance(obj_, type) and issubclass(obj_, Drivable):
all_drivables.add(obj_)
@pytest.mark.parametrize('modcls', all_drivables)
def test_stop_doc(modcls):
# make sure that implemented stop methods have a doc string
if (modcls.stop.description == Drivable.stop.description
and modcls.stop.func != Drivable.stop.func):
assert modcls.stop.func.__doc__ # stop method needs a doc string