Merge branch 'wip' of gitlab.psi.ch-samenv:samenv/frappy into wip

This commit is contained in:
zolliker 2023-09-14 11:10:12 +02:00
commit c39aef10aa
14 changed files with 107 additions and 73 deletions

View File

@ -13,4 +13,5 @@ Mod('stickrot',
'stick rotation, typically not used as omega',
io='stick_io',
encoder_mode='CHECK',
backlash=-1,
)

View File

@ -43,6 +43,7 @@ Mod('mf',
'frappy_psi.sea.SeaDrivable', '',
io='sea_main',
sea_object='mf',
rel_paths=['.', 'gen', 'ips'],
)
Mod('lev',

View File

@ -1,5 +1,5 @@
{"hcp": {"base": "/hcp", "params": [
{"path": "", "type": "float", "kids": 10},
{"path": "", "type": "float", "readonly": false, "cmd": "hcp set", "kids": 10},
{"path": "send", "type": "text", "readonly": false, "cmd": "hcp send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "set", "type": "float", "readonly": false, "cmd": "hcp set"},

View File

@ -18,7 +18,7 @@ Mod('ts',
)
Mod('hcp',
'frappy_psi.sea.SeaReadable', '',
'frappy_psi.sea.SeaWritable', '',
io='sea_stick',
sea_object='hcp',
)

View File

@ -200,7 +200,10 @@ implemented the readable stuff. We need to define some properties of the ``targe
parameter and add a property ``loop`` indicating, which control loop and
heater output we use.
In addition, we have to implement the methods ``write_target`` and ``read_target``:
In addition, we have to implement the method ``write_target``. Remark: we do not
implement ``read_target`` here, because the lakeshore does not offer to read back the
real target. The SETP command is returning the working setpoint, which may be distinct
from target during a ramp.
.. code:: python
@ -216,11 +219,9 @@ In addition, we have to implement the methods ``write_target`` and ``read_target
def write_target(self, target):
# we always use a request / reply scheme
reply = self.communicate(f'SETP {self.loop},{target};SETP?{self.loop}')
return float(reply)
self.communicate(f'SETP {self.loop},{target};*OPC?')
return target
def read_target(self):
return float(self.communicate(f'SETP?{self.loop}'))
In order to test this, we will need to change the entry module ``T`` in the
configuration file:
@ -272,13 +273,12 @@ There are two things still missing:
def write_target(self, target):
# reactivate heater in case it was switched off
self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}')
reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}')
self.communicate(f'SETP {self.loop},{target};*OPC?')
self._driving = True
# Setting the status attribute triggers an update message for the SECoP status
# parameter. This has to be done before returning from this method!
self.status = BUSY, 'target changed'
return float(reply)
return target
...
def read_status(self):
@ -403,4 +403,10 @@ Appendix 2: Extract from the LakeShore Manual
Command (340) RANGE? *term*
Command (336/350) RANGE?<loop> *term*
Reply <range> *term*
**Operation Complete Query**
----------------------------------------------
Command *OPC?
Reply 1
Description in Frappy, we append this command to request in order
to generate a reply
====================== =======================

View File

@ -261,7 +261,6 @@ class SecopClient(ProxyClient):
"""a general SECoP client"""
reconnect_timeout = 10
_running = False
_shutdown = False
_rxthread = None
_txthread = None
_connthread = None
@ -283,6 +282,7 @@ class SecopClient(ProxyClient):
self.uri = uri
self.nodename = uri
self._lock = RLock()
self._shutdown = Event()
def __del__(self):
try:
@ -303,7 +303,7 @@ class SecopClient(ProxyClient):
else:
self._set_state(False, 'connecting')
deadline = time.time() + try_period
while not self._shutdown:
while not self._shutdown.is_set():
try:
self.io = AsynConn(self.uri) # timeout 1 sec
self.io.writeline(IDENTREQUEST.encode('utf-8'))
@ -339,8 +339,8 @@ class SecopClient(ProxyClient):
# stay online for now, if activated
self._set_state(self.online and self.activate)
raise
time.sleep(1)
if not self._shutdown:
self._shutdown.wait(1)
if not self._shutdown.is_set():
self.log.info('%s ready', self.nodename)
def __txthread(self):
@ -436,7 +436,7 @@ class SecopClient(ProxyClient):
self.log.error('rxthread ended with %r', e)
self._rxthread = None
self.disconnect(False)
if self._shutdown:
if self._shutdown.is_set():
return
if self.activate:
self.log.info('try to reconnect to %s', self.uri)
@ -454,7 +454,7 @@ class SecopClient(ProxyClient):
self._connthread = mkthread(self._reconnect, connected_callback)
def _reconnect(self, connected_callback=None):
while not self._shutdown:
while not self._shutdown.is_set():
try:
self.connect()
if connected_callback:
@ -474,15 +474,15 @@ class SecopClient(ProxyClient):
self.log.info('continue trying to reconnect')
# self.log.warning(formatExtendedTraceback())
self._set_state(False)
time.sleep(self.reconnect_timeout)
self._shutdown.wait(self.reconnect_timeout)
else:
time.sleep(1)
self._shutdown.wait(1)
self._connthread = None
def disconnect(self, shutdown=True):
self._running = False
if shutdown:
self._shutdown = True
self._shutdown.set()
self._set_state(False, 'shutdown')
if self._connthread:
if self._connthread == current_thread():

View File

@ -191,8 +191,8 @@ class HasUnit:
class FloatRange(HasUnit, DataType):
"""(restricted) float type
:param minval: (property **min**)
:param maxval: (property **max**)
:param min: (property **min**)
:param max: (property **max**)
:param kwds: any of the properties below
"""
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
@ -203,11 +203,11 @@ class FloatRange(HasUnit, DataType):
relative_resolution = Property('relative resolution', Stub('FloatRange', 0),
extname='relative_resolution', default=1.2e-7)
def __init__(self, minval=None, maxval=None, **kwds):
def __init__(self, min=None, max=None, **kwds): # pylint: disable=redefined-builtin
super().__init__()
kwds['min'] = minval if minval is not None else -sys.float_info.max
kwds['max'] = maxval if maxval is not None else sys.float_info.max
self.set_properties(**kwds)
self.set_properties(min=min if min is not None else -sys.float_info.max,
max=max if max is not None else sys.float_info.max,
**kwds)
def checkProperties(self):
self.default = 0 if self.min <= 0 <= self.max else self.min
@ -247,9 +247,9 @@ class FloatRange(HasUnit, DataType):
def __repr__(self):
hints = self.get_info()
if 'min' in hints:
hints['minval'] = hints.pop('min')
hints['min'] = hints.pop('min')
if 'max' in hints:
hints['maxval'] = hints.pop('max')
hints['max'] = hints.pop('max')
return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items()))
def export_value(self, value):
@ -281,18 +281,18 @@ class FloatRange(HasUnit, DataType):
class IntRange(DataType):
"""restricted int type
:param minval: (property **min**)
:param maxval: (property **max**)
:param min: (property **min**)
:param max: (property **max**)
"""
min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True)
max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True)
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
# unit = Property('physical unit', StringType(), extname='unit', default='')
def __init__(self, minval=None, maxval=None):
def __init__(self, min=None, max=None): # pylint: disable=redefined-builtin
super().__init__()
self.set_properties(min=DEFAULT_MIN_INT if minval is None else minval,
max=DEFAULT_MAX_INT if maxval is None else maxval)
self.set_properties(min=DEFAULT_MIN_INT if min is None else min,
max=DEFAULT_MAX_INT if max is None else max)
def checkProperties(self):
self.default = 0 if self.min <= 0 <= self.max else self.min
@ -364,8 +364,8 @@ class IntRange(DataType):
class ScaledInteger(HasUnit, DataType):
"""scaled integer (= fixed resolution float) type
:param minval: (property **min**)
:param maxval: (property **max**)
:param min: (property **min**)
:param max: (property **max**)
:param kwds: any of the properties below
note: limits are for the scaled float value
@ -380,7 +380,8 @@ class ScaledInteger(HasUnit, DataType):
relative_resolution = Property('relative resolution', FloatRange(0),
extname='relative_resolution', default=1.2e-7)
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
# pylint: disable=redefined-builtin
def __init__(self, scale, min=None, max=None, absolute_resolution=None, **kwds):
super().__init__()
try:
scale = float(scale)
@ -390,8 +391,8 @@ class ScaledInteger(HasUnit, DataType):
absolute_resolution = scale
self.set_properties(
scale=scale,
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
min=DEFAULT_MIN_INT * scale if min is None else float(min),
max=DEFAULT_MAX_INT * scale if max is None else float(max),
absolute_resolution=absolute_resolution,
**kwds)
@ -1327,11 +1328,11 @@ DATATYPES = {
'bool': lambda **kwds:
BoolType(),
'int': lambda min, max, **kwds:
IntRange(minval=min, maxval=max),
IntRange(min=min, max=max),
'scaled': lambda scale, min, max, **kwds:
ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **floatargs(kwds)),
ScaledInteger(scale=scale, min=min*scale, max=max*scale, **floatargs(kwds)),
'double': lambda min=None, max=None, **kwds:
FloatRange(minval=min, maxval=max, **floatargs(kwds)),
FloatRange(min=min, max=max, **floatargs(kwds)),
'blob': lambda maxbytes, minbytes=0, **kwds:
BLOBType(minbytes=minbytes, maxbytes=maxbytes),
'string': lambda minchars=0, maxchars=None, isUTF8=False, **kwds:

View File

@ -63,6 +63,7 @@ class HasIO(Module):
io = self.ioClass(ioname, srv.log.getChild(ioname), opts, srv) # pylint: disable=not-callable
io.callingModule = []
srv.modules[ioname] = io
srv.dispatcher.register_module(io, ioname)
self.ioDict[self.uri] = ioname
self.io = ioname

View File

@ -30,10 +30,10 @@ Remarks:
import sys
from logging import DEBUG, INFO, addLevelName
import mlzlog
from frappy.errors import NoSuchModuleError
from frappy.server import Server
from frappy.config import load_config, Mod as ConfigMod
from frappy.lib import generalConfig
from frappy.protocol import dispatcher
USAGE = """create config on the fly:
@ -77,27 +77,25 @@ class MainLogger:
self.log.handlers[0].setLevel(LOG_LEVELS['comlog'])
class Dispatcher:
def __init__(self, name, log, opts, srv):
self.log = log
self._modules = {}
self.equipment_id = opts.pop('equipment_id', name)
class Dispatcher(dispatcher.Dispatcher):
def __init__(self, name, log, options, srv):
super().__init__(name, log, options, srv)
self.log = srv.log # overwrite child logger
def announce_update(self, modulename, pname, pobj):
if pobj.readerror:
value = repr(pobj.readerror)
else:
value = pobj.value
self.log.info('%s:%s %r', modulename, pname, value)
logobj = self._modules.get(modulename, self)
# self.log.info('%s:%s %r', modulename, pname, value)
logobj.log.info('%s %r', pname, value)
def register_module(self, moduleobj, modulename, export=True):
self.log.info('registering %s', modulename)
super().register_module(moduleobj, modulename, export)
setattr(main, modulename, moduleobj)
self._modules[modulename] = moduleobj
def get_module(self, modulename):
if modulename in self._modules:
return self._modules[modulename]
raise NoSuchModuleError(f'Module {modulename!r} does not exist on this SEC-Node!')
self.get_module(modulename)
logger = MainLogger()

View File

@ -54,7 +54,7 @@ class SimBase:
attrs['write_' + k] = writer
return object.__new__(type(f'SimBase_{devname}', (cls,), attrs))
return super().__new__(type(f'SimBase_{devname}', (cls,), attrs))
def initModule(self):
super().initModule()

View File

@ -69,24 +69,21 @@ class TemperatureSensor(HasIO, Readable):
class TemperatureLoop(TemperatureSensor, Drivable):
# lakeshore loop number to be used for this module
loop = Property('lakeshore loop', IntRange(1, 2), default=1)
target = Parameter(datatype=FloatRange(min=0, max=1500, unit='K'))
target = Parameter(datatype=FloatRange(unit='K', min=0, max=1500))
heater_range = Property('heater power range', IntRange(0, 5)) # max. 3 on LakeShore 336
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly = False)
tolerance = Parameter('convergence criterion', FloatRange(0), default=0.1, readonly=False)
_driving = False
def write_target(self, target):
# reactivate heater in case it was switched off
# the command has to be changed in case of model 340 to f'RANGE {self.heater_range};RANGE?'
self.communicate(f'RANGE {self.loop},{self.heater_range};RANGE?{self.loop}')
reply = self.communicate(f'SETP {self.loop},{target};SETP? {self.loop}')
self.communicate(f'SETP {self.loop},{target};*OPC?')
self._driving = True
# Setting the status attribute triggers an update message for the SECoP status
# parameter. This has to be done before returning from this method!
self.status = BUSY, 'target changed'
return float(reply)
def read_target(self):
return float(self.communicate(f'SETP?{self.loop}'))
return target
def read_status(self):
code = int(self.communicate(f'RDGST?{self.channel}'))

View File

@ -23,12 +23,36 @@
"""modules to access parameters"""
from frappy.core import Drivable, IDLE, Attached, StringType, Property, \
Parameter, FloatRange
Parameter, FloatRange, Readable
from frappy.errors import ConfigError
from frappy_psi.convergence import HasConvergence
from frappy_psi.mixins import HasRamp
class Par(Readable):
value = Parameter(datatype=FloatRange(unit='$'))
read = Attached(description='<module>.<parameter> for read')
unit = Property('main unit', StringType())
def setProperty(self, key, value):
if key == 'read':
value, param = value.split('.')
setattr(self, f'{key}_param', param)
super().setProperty(key, value)
def checkProperties(self):
self.applyMainUnit(self.unit)
if self.read == self.name :
raise ConfigError('illegal recursive read/write module')
super().checkProperties()
def read_value(self):
return getattr(self.read, f'{self.read_param}')
def read_status(self):
return IDLE, ''
class Driv(Drivable):
value = Parameter(datatype=FloatRange(unit='$'))
target = Parameter(datatype=FloatRange(unit='$'))

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
@ -185,8 +184,9 @@ class SeaClient(ProxyClient, Module):
break
else:
raise CommunicationFailedError('reply %r should be "Login OK"' % reply)
self.request('frappy_config %s %s' % (self.service, self.config))
result = self.request('frappy_config %s %s' % (self.service, self.config))
if result not in {'0', '1'}:
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())
@ -248,7 +248,16 @@ class SeaClient(ProxyClient, Module):
raise TimeoutError('no response within 10s')
def _rxthread(self, started_callback):
recheck = None
while not self.shutdown:
if recheck and time.time() > recheck:
# try to collect device changes within 1 sec
recheck = None
result = self.request('check_config %s %s' % (self.service, self.config))
if result == '1':
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
else:
self.DISPATCHER.shutdown()
try:
reply = self.asynio.readline()
if reply is None:
@ -307,11 +316,7 @@ class SeaClient(ProxyClient, Module):
if mplist is None:
if path.startswith('/device'):
if path == '/device/changetime':
result = self.request('check_config %s %s' % (self.service, self.config))
if result == '1':
self.asynio.writeline(('get_all_param ' + ' '.join(self.objects)).encode())
else:
self.DISPATCHER.shutdown()
recheck = time.time() + 1
elif path.startswith('/device/frappy_%s' % self.service) and value == '':
self.DISPATCHER.shutdown()
else:

View File

@ -160,9 +160,9 @@ def test_ScaledInteger():
with pytest.raises(ProgrammingError):
ScaledInteger('xc', 'Yx')
with pytest.raises(ProgrammingError):
ScaledInteger(scale=0, minval=1, maxval=2)
ScaledInteger(scale=0, min=1, max=2)
with pytest.raises(ProgrammingError):
ScaledInteger(scale=-10, minval=1, maxval=2)
ScaledInteger(scale=-10, min=1, max=2)
# check that unit can be changed
dt.setProperty('unit', 'A')
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300,
@ -563,7 +563,7 @@ def test_get_datatype():
assert isinstance(get_datatype(
{'type': 'scaled', 'scale':0.03, 'min':-99, 'max':111}), ScaledInteger)
dt = ScaledInteger(scale=0.03, minval=0, maxval=9.9)
dt = ScaledInteger(scale=0.03, min=0, max=9.9)
assert dt.export_datatype() == {'type': 'scaled', 'max':330, 'min':0, 'scale':0.03}
assert get_datatype(dt.export_datatype()).export_datatype() == dt.export_datatype()