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

This commit is contained in:
zolliker 2023-07-03 17:51:43 +02:00
commit 19f965bced
19 changed files with 290 additions and 140 deletions

View File

@ -0,0 +1,18 @@
Node('cfg/sea/camea-be-filter.cfg',
'Camea Be-Filter',
interface='5000',
name='camea-be-filter',
)
Mod('sea_addons',
'secop_psi.sea.SeaClient',
'addons sea connection for camea-be-filter.addon',
config='camea-be-filter.addon',
service='addons',
)
Mod('t_be_filter',
'secop_psi.sea.SeaReadable',
io='sea_addons',
sea_object='t_be_filter',
)

View File

@ -58,6 +58,12 @@ Mod('hemot',
sea_object='hemot', sea_object='hemot',
) )
Mod('nvflow',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='nvflow',
)
Mod('table', Mod('table',
'frappy_psi.sea.SeaReadable', '', 'frappy_psi.sea.SeaReadable', '',
io='sea_main', io='sea_main',

View File

@ -237,10 +237,10 @@
{"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10}, {"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10},
{"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3},
{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running", "visibility": 3}, {"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running"},
{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3}, {"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3},
{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3}, {"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3},
{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve", "visibility": 3}, {"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve"},
{"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2", "visibility": 3}, {"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2", "visibility": 3},
{"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3},
{"path": "health", "type": "float"}]}, {"path": "health", "type": "float"}]},
@ -278,6 +278,16 @@
{"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"}, {"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"},
{"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]}, {"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]},
"nvflow": {"base": "/nvflow", "params": [
{"path": "", "type": "float", "kids": 7},
{"path": "send", "type": "text", "readonly": false, "cmd": "nvflow send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "stddev", "type": "float"},
{"path": "nsamples", "type": "int", "readonly": false, "cmd": "nvflow nsamples"},
{"path": "offset", "type": "float", "readonly": false, "cmd": "nvflow offset"},
{"path": "scale", "type": "float", "readonly": false, "cmd": "nvflow scale"},
{"path": "save", "type": "bool", "readonly": false, "cmd": "nvflow save", "description": "unchecked: current calib is not saved. set checked: save calib"}]},
"table": {"base": "/table", "params": [ "table": {"base": "/table", "params": [
{"path": "", "type": "none", "kids": 17}, {"path": "", "type": "none", "kids": 17},
{"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3}, {"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3},

View File

@ -7,7 +7,7 @@ Mod('triton',
'frappy_psi.mercury.IO', 'frappy_psi.mercury.IO',
'connection to triton software', 'connection to triton software',
uri='tcp://linse-dil5:33576', uri='tcp://linse-dil5:33576',
timeout=5.0, timeout=25.0,
) )
Mod('ts', Mod('ts',
@ -23,6 +23,7 @@ Mod('htr_mix',
'mix. chamber heater', 'mix. chamber heater',
slot='H1,T5', slot='H1,T5',
io='triton', io='triton',
resistivity = 100
) )
Mod('htr_sorb', Mod('htr_sorb',

View File

@ -8,7 +8,24 @@ what the framwork does for you.
Startup Startup
....... .......
TODO: describe startup: init methods, first polls On startup several methods are called. First :meth:`earlyInit` is called on all modules.
Use this to initialize attributes independent of other modules, if you can not initialize
as a class attribute, for example for mutable attributes.
Then :meth:`initModule` is called for all modules.
Use it to initialize things related to other modules, for example registering callbacks.
After this, :meth:`startModule` is called with a callback function argument.
:func:`frappy.modules.Module.startModule` starts the poller thread, calling
:meth:`writeInitParams` for writing initial parameters to hardware, followed
by :meth:`initialReads`. The latter is meant for reading values from hardware,
which are not polled continuously. Then all parameters configured for poll are polled
by calling the corresponding read_*() method. The end of this last initialisation
step is indicated to the server by the callback function.
After this, the poller thread starts regular polling, see next section.
When overriding one of above methods, do not forget to super call.
.. _polling: .. _polling:

View File

@ -95,5 +95,23 @@ Example code:
return self.read_target() # return the read back value return self.read_target() # return the read back value
Parameter Initialisation
------------------------
Initial values of parameters might be given by several different sources:
1) value argument of a Parameter declaration
2) read from HW
3) read from persistent data file
4) value given in config file
For (2) the programmer might decide for any parameter to poll it regularely from the
hardware. In this case changes from an other input, for example a keyboard or other
interface of the connected devices would be updated continuously in Frappy.
If there is no such other input, or if the programmer decides that such other
data sources are not to be considered, the hardware parameter might be read in just
once on startup, :func:`frappy.modules.Module.initialReads` may be overriden.
This method is called once on startup, before the regular polls start.
.. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies .. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies

View File

@ -12,7 +12,7 @@ Module Base Classes
................... ...................
.. autoclass:: frappy.modules.Module .. autoclass:: frappy.modules.Module
:members: earlyInit, initModule, startModule :members: earlyInit, initModule, startModule, initialReads
.. autoclass:: frappy.modules.Readable .. autoclass:: frappy.modules.Readable
:members: Status :members: Status

View File

@ -125,7 +125,7 @@ class Config(dict):
continue continue
if name not in self.module_names: if name not in self.module_names:
self.module_names.add(name) self.module_names.add(name)
self.modules.append(mod) self[name] = mod
def process_file(filename, log): def process_file(filename, log):

View File

@ -1163,9 +1163,9 @@ class ValueType(DataType):
The optional (callable) validator can be used to restrict values to a The optional (callable) validator can be used to restrict values to a
certain type. certain type.
For example using `ValueType(dict)` would ensure only values that can be For example using ``ValueType(dict)`` would ensure only values that can be
turned into a dictionary can be used in this instance, as the conversion turned into a dictionary can be used in this instance, as the conversion
`dict(value)` is called for validation. ``dict(value)`` is called for validation.
Notes: Notes:
The validator must either accept a value by returning it or the converted value, The validator must either accept a value by returning it or the converted value,

View File

@ -63,12 +63,15 @@ class SECoPError(RuntimeError):
"""format with info about raising methods """format with info about raising methods
:param stripped: strip last method. :param stripped: strip last method.
Use stripped=True (or str()) for the following cases, as the last method can be derived from the context:
- stored in pobj.readerror: read_<pobj.name>
- error message from a change command: write_<pname>
- error message from a read command: read_<pname>
Use stripped=False for the log file, as the related parameter is not known
:return: the formatted error message :return: the formatted error message
Use stripped=True (or str()) for the following cases, as the last method can be derived from the context:
- stored in pobj.readerror: read_<pobj.name>
- error message from a change command: write_<pname>
- error message from a read command: read_<pname>
Use stripped=False for the log file, as the related parameter is not known
""" """
mlist = self.raising_methods mlist = self.raising_methods
if mlist and stripped: if mlist and stripped:

View File

@ -133,6 +133,14 @@ class IOBase(Communicator):
self._lock = threading.RLock() self._lock = threading.RLock()
def connectStart(self): def connectStart(self):
if not self.is_connected:
uri = self.uri
self._conn = AsynConn(uri, self._eol_read,
default_settings=self.default_settings)
self.is_connected = True
self.checkHWIdent()
def checkHWIdent(self):
raise NotImplementedError raise NotImplementedError
def closeConnection(self): def closeConnection(self):
@ -218,12 +226,19 @@ class StringIO(IOBase):
default='\n', settable=True) default='\n', settable=True)
encoding = Property('used encoding', datatype=StringType(), encoding = Property('used encoding', datatype=StringType(),
default='ascii', settable=True) default='ascii', settable=True)
identification = Property(''' identification = Property(
identification '''identification
a list of tuples with commands and expected responses as regexp, a list of tuples with commands and expected responses as regexp,
to be sent on connect''', to be sent on connect''',
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) datatype=ArrayOf(TupleOf(StringType(), StringType())),
default=[], export=False)
retry_first_idn = Property(
'''retry first identification message
a flag to indicate whether the first message should be resent once to
avoid data that may still be in the buffer to garble the message''',
datatype=BoolType(), default=False)
def _convert_eol(self, value): def _convert_eol(self, value):
if isinstance(value, str): if isinstance(value, str):
@ -248,16 +263,27 @@ class StringIO(IOBase):
raise ValueError('end_of_line for read must not be empty') raise ValueError('end_of_line for read must not be empty')
self._eol_write = self._convert_eol(eol[-1]) self._eol_write = self._convert_eol(eol[-1])
def connectStart(self): def checkHWIdent(self):
if not self.is_connected: if not self.identification:
uri = self.uri return
self._conn = AsynConn(uri, self._eol_read, default_settings=self.default_settings) idents = iter(self.identification)
self.is_connected = True command, regexp = next(idents)
for command, regexp in self.identification: reply = self.communicate(command)
reply = self.communicate(command) if not re.match(regexp, reply):
if not re.match(regexp, reply): if self.retry_first_idn:
self.closeConnection() self.log.debug('first ident command not successful.'
raise CommunicationFailedError(f'bad response: {reply} does not match {regexp}') ' retrying in case of garbage data.')
idents = iter(self.identification)
else:
self.closeConnection()
raise CommunicationFailedError(f'bad response: {reply!r}'
f' does not match {regexp!r}')
for command, regexp in idents:
reply = self.communicate(command)
if not re.match(regexp, reply):
self.closeConnection()
raise CommunicationFailedError(f'bad response: {reply!r}'
f' does not match {regexp!r}')
@Command(StringType(), result=StringType()) @Command(StringType(), result=StringType())
def communicate(self, command): def communicate(self, command):
@ -356,19 +382,19 @@ class BytesIO(IOBase):
- a two digit hexadecimal number (byte value) - a two digit hexadecimal number (byte value)
- a character - a character
- ?? indicating ignored bytes in responses - ?? indicating ignored bytes in responses
""", datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) """, datatype=ArrayOf(TupleOf(StringType(), StringType())),
default=[], export=False)
def connectStart(self): _eol_read = b''
if not self.is_connected:
uri = self.uri def checkHWIdent(self):
self._conn = AsynConn(uri, b'', default_settings=self.default_settings) for request, expected in self.identification:
self.is_connected = True replylen, replypat = make_regexp(expected)
for request, expected in self.identification: reply = self.communicate(make_bytes(request), replylen)
replylen, replypat = make_regexp(expected) if not replypat.match(reply):
reply = self.communicate(make_bytes(request), replylen) self.closeConnection()
if not replypat.match(reply): raise CommunicationFailedError(f'bad response: {reply!r}'
self.closeConnection() ' does not match {expected!r}')
raise CommunicationFailedError(f'bad response: {reply!r} does not match {expected!r}')
@Command((BLOBType(), IntRange(0)), result=BLOBType()) @Command((BLOBType(), IntRange(0)), result=BLOBType())
def communicate(self, request, replylen): # pylint: disable=arguments-differ def communicate(self, request, replylen): # pylint: disable=arguments-differ

View File

@ -53,7 +53,7 @@ class HasControlledBy:
to be called from the write_target method to be called from the write_target method
""" """
if self.controlled_by: if self.controlled_by:
self.controlled_by = 0 self.controlled_by = 0 # self
for deactivate_control in self.inputCallbacks.values(): for deactivate_control in self.inputCallbacks.values():
deactivate_control(self.name) deactivate_control(self.name)
@ -74,6 +74,10 @@ class HasOutputModule:
if self.output_module: if self.output_module:
self.output_module.register_input(self.name, self.deactivate_control) self.output_module.register_input(self.name, self.deactivate_control)
def set_control_active(self, active):
"""to be overridden for switching hw control"""
self.control_active = active
def activate_control(self): def activate_control(self):
"""method to switch control_active on """method to switch control_active on
@ -85,10 +89,10 @@ class HasOutputModule:
if name != self.name: if name != self.name:
deactivate_control(self.name) deactivate_control(self.name)
out.controlled_by = self.name out.controlled_by = self.name
self.control_active = True self.set_control_active(True)
def deactivate_control(self, source): def deactivate_control(self, source=None):
"""called when an other module takes over control""" """called when an other module takes over control"""
if self.control_active: if self.control_active:
self.control_active = False self.set_control_active(False)
self.log.warning(f'switched to manual mode by {source}') self.log.warning(f'switched to manual mode by {source or self.name}')

View File

@ -629,6 +629,16 @@ class Module(HasAccessibles):
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger()) mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
self.startModuleDone = True self.startModuleDone = True
def initialReads(self):
"""initial reads to be done
override to read initial values from HW, when it is not desired
to poll them afterwards
called from the poll thread, after writeInitParams but before
all parameters are polled once
"""
def doPoll(self): def doPoll(self):
"""polls important parameters like value and status """polls important parameters like value and status
@ -678,15 +688,10 @@ class Module(HasAccessibles):
before polling, parameters which need hardware initialisation are written before polling, parameters which need hardware initialisation are written
""" """
for mobj in modules: polled_modules = [m for m in modules if m.enablePoll]
mobj.writeInitParams()
modules = [m for m in modules if m.enablePoll]
if not modules: # no polls needed - exit thread
started_callback()
return
if hasattr(self, 'registerReconnectCallback'): if hasattr(self, 'registerReconnectCallback'):
# self is a communicator supporting reconnections # self is a communicator supporting reconnections
def trigger_all(trg=self.triggerPoll, polled_modules=modules): def trigger_all(trg=self.triggerPoll, polled_modules=polled_modules):
for m in polled_modules: for m in polled_modules:
m.pollInfo.last_main = 0 m.pollInfo.last_main = 0
m.pollInfo.last_slow = 0 m.pollInfo.last_slow = 0
@ -694,7 +699,7 @@ class Module(HasAccessibles):
self.registerReconnectCallback('trigger_polls', trigger_all) self.registerReconnectCallback('trigger_polls', trigger_all)
# collect all read functions # collect all read functions
for mobj in 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.valueCallbacks:
@ -704,17 +709,32 @@ class Module(HasAccessibles):
rfunc = getattr(mobj, 'read_' + pname) rfunc = getattr(mobj, 'read_' + pname)
if rfunc.poll: if rfunc.poll:
pinfo.polled_parameters.append((mobj, rfunc, pobj)) pinfo.polled_parameters.append((mobj, rfunc, pobj))
# call all read functions a first time while True:
try: try:
for m in modules: for mobj in modules:
for mobj, rfunc, _ in m.pollInfo.polled_parameters: # TODO when needed: here we might add a call to a method :meth:`beforeWriteInit`
mobj.callPollFunc(rfunc, raise_com_failed=True) mobj.writeInitParams()
except CommunicationFailedError as e: mobj.initialReads()
# when communication failed, probably all parameters and may be more modules are affected. # call all read functions a first time
# as this would take a lot of time (summed up timeouts), we do not continue for m in polled_modules:
# trying and let the server accept connections, further polls might success later for mobj, rfunc, _ in m.pollInfo.polled_parameters:
self.log.error('communication failure on startup: %s', e) mobj.callPollFunc(rfunc, raise_com_failed=True)
started_callback() # TODO when needed: here we might add calls to a method :meth:`afterInitPolls`
break
except CommunicationFailedError as e:
# when communication failed, probably all parameters and may be more modules are affected.
# as this would take a lot of time (summed up timeouts), we do not continue
# trying and let the server accept connections, further polls might success later
if started_callback:
self.log.error('communication failure on startup: %s', e)
started_callback()
started_callback = None
self.triggerPoll.wait(0.1) # wait for reconnection or max 10 sec.
break
if started_callback:
started_callback()
if not polled_modules: # no polls needed - exit thread
return
to_poll = () to_poll = ()
while True: while True:
now = time.time() now = time.time()

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
# ***************************************************************************** # *****************************************************************************
# #
# This program is free software; you can redistribute it and/or modify it under # This program is free software; you can redistribute it and/or modify it under
@ -257,12 +256,12 @@ class BasePyTangoDevice:
Wraps command execution and attribute operations of the given Wraps command execution and attribute operations of the given
device with logging and exception mapping. device with logging and exception mapping.
""" """
dev.command_inout = self._applyGuardToFunc(dev.command_inout) dev.__dict__['command_inout'] = self._applyGuardToFunc(dev.command_inout)
dev.write_attribute = self._applyGuardToFunc(dev.write_attribute, dev.__dict__['write_attribute'] = self._applyGuardToFunc(dev.write_attribute,
'attr_write') 'attr_write')
dev.read_attribute = self._applyGuardToFunc(dev.read_attribute, dev.__dict__['read_attribute'] = self._applyGuardToFunc(dev.read_attribute,
'attr_read') 'attr_read')
dev.attribute_query = self._applyGuardToFunc(dev.attribute_query, dev.__dict__['attribute_query'] = self._applyGuardToFunc(dev.attribute_query,
'attr_query') 'attr_query')
return dev return dev

View File

@ -25,10 +25,10 @@ import math
import re import re
import time import time
from frappy.core import Drivable, HasIO, Writable, StatusType, \ from frappy.core import Command, Drivable, HasIO, Writable, StatusType, \
Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll
from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf
from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError from frappy.errors import HardwareError, ProgrammingError, ConfigError
from frappy_psi.convergence import HasConvergence from frappy_psi.convergence import HasConvergence
from frappy.states import Retry, Finish from frappy.states import Retry, Finish
from frappy.mixins import HasOutputModule, HasControlledBy from frappy.mixins import HasOutputModule, HasControlledBy
@ -218,7 +218,6 @@ class HasInput(HasControlledBy, MercuryChannel):
class Loop(HasOutputModule, MercuryChannel, Drivable): class Loop(HasOutputModule, MercuryChannel, Drivable):
"""common base class for loops""" """common base class for loops"""
output_module = Attached(HasInput, mandatory=False) output_module = Attached(HasInput, mandatory=False)
control_active = Parameter(readonly=False)
ctrlpars = Parameter( ctrlpars = Parameter(
'pid (proportional band, integral time, differential time', 'pid (proportional band, integral time, differential time',
StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')), StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')),
@ -226,14 +225,15 @@ class Loop(HasOutputModule, MercuryChannel, Drivable):
) )
enable_pid_table = Parameter('', BoolType(), readonly=False) enable_pid_table = Parameter('', BoolType(), readonly=False)
def set_output(self, active, source='HW'): def set_output(self, active, source=None):
if active: if active:
self.activate_control() self.activate_control()
else: else:
self.deactivate_control(source) self.deactivate_control(source)
def set_target(self, target): def set_target(self, target):
self.set_output(True) if not self.control_active:
self.activate_control()
self.target = target self.target = target
def read_enable_pid_table(self): def read_enable_pid_table(self):
@ -254,9 +254,16 @@ class Loop(HasOutputModule, MercuryChannel, Drivable):
def read_status(self): def read_status(self):
return IDLE, '' return IDLE, ''
@Command()
def control_off(self):
"""switch control off"""
# remark: this is needed in frappy_psi.trition.TemperatureLoop, as the heater
# output is not available there. We define it here as a convenience for the user.
self.write_control_active(False)
class ConvLoop(HasConvergence, Loop): class ConvLoop(HasConvergence, Loop):
def deactivate_control(self, source): def deactivate_control(self, source=None):
if self.control_active: if self.control_active:
super().deactivate_control(source) super().deactivate_control(source)
self.convergence_state.start(self.inactive_state) self.convergence_state.start(self.inactive_state)
@ -372,16 +379,14 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
super().doPoll() super().doPoll()
self.read_setpoint() self.read_setpoint()
def read_control_active(self): def set_control_active(self, active):
active = self.query(f'DEV::{self.ENABLE}', off_on) super().set_control_active(active)
self.set_output(active) self.change(f'DEV::{self.ENABLE}', active, off_on)
return active
def write_control_active(self, value): def initialReads(self):
if value: # initialize control active from HW
raise RangeError('write to target to switch control on') active = self.query(f'DEV::{self.ENABLE}', off_on)
self.set_output(value, 'user') super().set_output(active, 'HW')
return self.change(f'DEV::{self.ENABLE}', value, off_on)
@nopoll # polled by read_setpoint @nopoll # polled by read_setpoint
def read_target(self): def read_target(self):
@ -413,7 +418,7 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
self.change(f'DEV::{self.ENABLE}', True, off_on) self.change(f'DEV::{self.ENABLE}', True, off_on)
super().set_target(target) super().set_target(target)
def deactivate_control(self, source): def deactivate_control(self, source=None):
if self.__ramping: if self.__ramping:
self.__ramping = False self.__ramping = False
# stop ramping setpoint # stop ramping setpoint
@ -508,14 +513,16 @@ class PressureLoop(PressureSensor, HasControlledBy, ConvLoop):
output_module = Attached(ValvePos, mandatory=False) output_module = Attached(ValvePos, mandatory=False)
tolerance = Parameter(default=0.1) tolerance = Parameter(default=0.1)
def read_control_active(self): def set_control_active(self, active):
active = self.query('DEV::PRES:LOOP:FAUT', off_on) super().set_control_active(active)
self.set_output(active) if not active:
return active self.self_controlled() # switches off auto flow
return self.change('DEV::PRES:LOOP:FAUT', active, off_on)
def write_control_active(self, value): def initialReads(self):
self.set_output(value, 'user') # initialize control active from HW
return self.change('DEV::PRES:LOOP:FAUT', value, off_on) active = self.query('DEV::PRES:LOOP:FAUT', off_on)
super().set_output(active, 'HW')
def read_target(self): def read_target(self):
return self.query('DEV::PRES:LOOP:PRST') return self.query('DEV::PRES:LOOP:PRST')
@ -560,14 +567,15 @@ class HasAutoFlow:
if value: if value:
self.needle_valve.controlled_by = self.name self.needle_valve.controlled_by = self.name
else: else:
if self.needle_valve.control_active:
self.needle_valve.set_target(self.flowpars[1][0]) # flow min
if self.needle_valve.controlled_by != SELF: if self.needle_valve.controlled_by != SELF:
self.needle_valve.controlled_by = SELF self.needle_valve.controlled_by = SELF
self.needle_valve.write_target(self.flowpars[1][0]) # flow min
return value return value
def auto_flow_off(self): def auto_flow_off(self, source=None):
if self.auto_flow: if self.auto_flow:
self.log.warning('switch auto flow off') self.log.warning(f'switched auto flow off by {source or self.name}')
self.write_auto_flow(False) self.write_auto_flow(False)

View File

@ -83,7 +83,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
ioClass = PhytronIO ioClass = PhytronIO
_step_size = None # degree / step _step_size = None # degree / step
_blocking_error = None # None or a string indicating the reason of an error needing reset _blocking_error = None # None or a string indicating the reason of an error needing clear_errors
_running = False # status indicates motor is running _running = False # status indicates motor is running
STATUS_MAP = { STATUS_MAP = {
@ -121,10 +121,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
if not axisbit & active_axes: # power cycle detected and this axis not yet active if not axisbit & active_axes: # power cycle detected and this axis not yet active
self.set('P37S', axisbit | active_axes) # activate axis self.set('P37S', axisbit | active_axes) # activate axis
if now < self.alive_time + 7 * 24 * 3600: # the device was running within last week if now < self.alive_time + 7 * 24 * 3600: # the device was running within last week
# inform the user about the loss of position by the need of doing reset_error # inform the user about the loss of position by the need of doing clear_errors
self._blocking_error = 'lost position' self._blocking_error = 'lost position'
else: # do reset silently else: # do silently
self.reset_error() self.clear_errors()
self.alive_time = now self.alive_time = now
self.saveParameters() self.saveParameters()
return now return now
@ -171,7 +171,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
def write_target(self, value): def write_target(self, value):
self.read_alive_time() self.read_alive_time()
if self._blocking_error: if self._blocking_error:
self.status = ERROR, 'reset needed after ' + self._blocking_error self.status = ERROR, 'clear_errors needed after ' + self._blocking_error
raise HardwareError(self.status[1]) raise HardwareError(self.status[1])
self.saveParameters() self.saveParameters()
if self.backlash: if self.backlash:
@ -261,7 +261,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.start_machine(self.stopping, status=(BUSY, 'stopping')) self.start_machine(self.stopping, status=(BUSY, 'stopping'))
@Command @Command
def reset_error(self): def clear_errors(self):
"""Reset error, set position to encoder""" """Reset error, set position to encoder"""
self.read_value() self.read_value()
if self._blocking_error: if self._blocking_error:
@ -286,3 +286,4 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.read_value() self.read_value()
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

View File

@ -111,6 +111,7 @@ class SeaClient(ProxyClient, Module):
_connect_thread = None _connect_thread = None
_service_manager = None _service_manager = None
_instance = None _instance = None
_last_connect = 0
def __init__(self, name, log, opts, srv): def __init__(self, name, log, opts, srv):
nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id') nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id')
@ -135,6 +136,10 @@ class SeaClient(ProxyClient, Module):
ProxyClient.__init__(self) ProxyClient.__init__(self)
Module.__init__(self, name, log, opts, srv) Module.__init__(self, name, log, opts, srv)
def doPoll(self):
if not self.asynio and time.time() > self._last_connect + 10:
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)
for k, v in module.path2param.items(): for k, v in module.path2param.items():
@ -146,6 +151,7 @@ class SeaClient(ProxyClient, Module):
self._connect_thread = mkthread(self._connect, start_events.get_trigger()) self._connect_thread = mkthread(self._connect, start_events.get_trigger())
def _connect(self, started_callback): def _connect(self, started_callback):
self._last_connect = time.time()
if self._instance: if self._instance:
if not self._service_manager: if not self._service_manager:
if self._service_manager is None: if self._service_manager is None:
@ -192,36 +198,40 @@ class SeaClient(ProxyClient, Module):
self.syncio.writeline(b'seauser seaser') self.syncio.writeline(b'seauser seaser')
assert self.syncio.readline() == b'Login OK' assert self.syncio.readline() == b'Login OK'
self.log.info('connected to %s', self.uri) self.log.info('connected to %s', self.uri)
self.syncio.flush_recv() try:
ft = 'fulltransAct' if quiet else 'fulltransact' self.syncio.flush_recv()
self.syncio.writeline(('%s %s' % (ft, command)).encode()) ft = 'fulltransAct' if quiet else 'fulltransact'
result = None self.syncio.writeline(('%s %s' % (ft, command)).encode())
deadline = time.time() + 10 result = None
while time.time() < deadline: deadline = time.time() + 10
try: while time.time() < deadline:
reply = self.syncio.readline() reply = self.syncio.readline()
if reply is None: if reply is None:
continue continue
except ConnectionClosed: reply = reply.decode()
break if reply.startswith('TRANSACTIONSTART'):
reply = reply.decode() result = []
if reply.startswith('TRANSACTIONSTART'): continue
result = [] if reply == 'TRANSACTIONFINISHED':
continue if result is None:
if reply == 'TRANSACTIONFINISHED': self.log.info('missing TRANSACTIONSTART on: %s', command)
return ''
if not result:
return ''
return '\n'.join(result)
if result is None: if result is None:
self.log.info('missing TRANSACTIONSTART on: %s', command) self.log.info('swallow: %s', reply)
return '' continue
if not result: if not result:
return '' result = [reply.split('=', 1)[-1]]
return '\n'.join(result) else:
if result is None: result.append(reply)
self.log.info('swallow: %s', reply) except ConnectionClosed:
continue try:
if not result: self.syncio.disconnect()
result = [reply.split('=', 1)[-1]] except Exception:
else: pass
result.append(reply) self.syncio = None
raise TimeoutError('no response within 10s') raise TimeoutError('no response within 10s')
def _rxthread(self, started_callback): def _rxthread(self, started_callback):
@ -231,6 +241,11 @@ class SeaClient(ProxyClient, Module):
if reply is None: if reply is None:
continue continue
except ConnectionClosed: except ConnectionClosed:
try:
self.asynio.disconnect()
except Exception:
pass
self.asynio = None
break break
try: try:
msg = json.loads(reply) msg = json.loads(reply)
@ -463,7 +478,6 @@ class SeaModule(Module):
descr['params'].pop(0) descr['params'].pop(0)
else: else:
# filter by relative paths # filter by relative paths
# rel_paths = rel_paths.split()
result = [] result = []
is_running = None is_running = None
for rpath in rel_paths: for rpath in rel_paths:

View File

@ -25,7 +25,7 @@ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUS
Done, Property Done, Property
from frappy.datatypes import EnumType, FloatRange, StringType from frappy.datatypes import EnumType, FloatRange, StringType
from frappy.lib.enum import Enum from frappy.lib.enum import Enum
from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput
from frappy_psi import mercury from frappy_psi import mercury
actions = Enum(none=0, condense=1, circulate=2, collect=3) actions = Enum(none=0, condense=1, circulate=2, collect=3)
@ -256,15 +256,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop):
ctrlpars = Parameter('pid (gain, integral (inv. time), differential time') ctrlpars = Parameter('pid (gain, integral (inv. time), differential time')
system_channel = Property('system channel name', StringType(), 'MC') system_channel = Property('system channel name', StringType(), 'MC')
def write_control_active(self, value): def set_control_active(self, active):
if self.system_channel: if self.system_channel:
self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str) self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str)
if value: if active:
self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str) self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str)
if self.output_module: if self.output_module:
limit = self.output_module.read_limit() or None # None: max. limit limit = self.output_module.read_limit()
self.output_module.write_limit(limit) self.output_module.write_limit(limit)
return super().write_control_active(value)
class HeaterOutput(HasInput, MercuryChannel, Writable): class HeaterOutput(HasInput, MercuryChannel, Writable):
@ -286,7 +285,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
return self.value return self.value
def write_target(self, value): def write_target(self, value):
self.write_controlled_by(SELF) self.self_controlled()
if self.resistivity: if self.resistivity:
# round to the next voltage step # round to the next voltage step
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
@ -301,13 +300,12 @@ class HeaterOutputWithRange(HeaterOutput):
def read_limit(self): def read_limit(self):
maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA
if maxcur == 0:
maxcur = 100 # mA
return self.read_resistivity() * maxcur ** 2 # uW return self.read_resistivity() * maxcur ** 2 # uW
def write_limit(self, value): def write_limit(self, value):
if value is None: maxcur = sqrt(value / self.read_resistivity())
maxcur = 100 # max. allowed current 100mA
else:
maxcur = sqrt(value / self.read_resistivity())
for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100: for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100:
if cur > maxcur * 0.999: if cur > maxcur * 0.999:
maxcur = cur maxcur = cur

View File

@ -249,15 +249,22 @@ class Mod(HasStates, Drivable):
self._my_time += 1 self._my_time += 1
class Started(RuntimeError):
pass
def create_module(): def create_module():
updates = [] updates = []
obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates)) obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates))
obj.initModule() obj.initModule()
obj.statelist = [] obj.statelist = []
try: try:
obj._Module__pollThread(obj.polledModules, None) def started():
except TypeError: raise Started()
pass # None is not callable # run __pollThread until Started is raised (after initial phase)
obj._Module__pollThread(obj.polledModules, started)
except Started:
pass
updates.clear() updates.clear()
return obj, updates return obj, updates