Merge branch 'wip' of gitlab.psi.ch-samenv:samenv/frappy into wip
This commit is contained in:
commit
c39aef10aa
@ -13,4 +13,5 @@ Mod('stickrot',
|
||||
'stick rotation, typically not used as omega',
|
||||
io='stick_io',
|
||||
encoder_mode='CHECK',
|
||||
backlash=-1,
|
||||
)
|
||||
|
@ -43,6 +43,7 @@ Mod('mf',
|
||||
'frappy_psi.sea.SeaDrivable', '',
|
||||
io='sea_main',
|
||||
sea_object='mf',
|
||||
rel_paths=['.', 'gen', 'ips'],
|
||||
)
|
||||
|
||||
Mod('lev',
|
||||
|
@ -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"},
|
||||
|
@ -18,7 +18,7 @@ Mod('ts',
|
||||
)
|
||||
|
||||
Mod('hcp',
|
||||
'frappy_psi.sea.SeaReadable', '',
|
||||
'frappy_psi.sea.SeaWritable', '',
|
||||
io='sea_stick',
|
||||
sea_object='hcp',
|
||||
)
|
||||
|
@ -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
|
||||
====================== =======================
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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}'))
|
||||
|
@ -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='$'))
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user