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',
)
Mod('nvflow',
'frappy_psi.sea.SeaReadable', '',
io='sea_main',
sea_object='nvflow',
)
Mod('table',
'frappy_psi.sea.SeaReadable', '',
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": "send", "type": "text", "readonly": false, "cmd": "hepump send", "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": "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": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3},
{"path": "health", "type": "float"}]},
@ -278,6 +278,16 @@
{"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"},
{"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": [
{"path": "", "type": "none", "kids": 17},
{"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3},

View File

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

View File

@ -8,7 +8,24 @@ what the framwork does for you.
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:

View File

@ -95,5 +95,23 @@ Example code:
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

View File

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

View File

@ -125,7 +125,7 @@ class Config(dict):
continue
if name not in self.module_names:
self.module_names.add(name)
self.modules.append(mod)
self[name] = mod
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
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
`dict(value)` is called for validation.
``dict(value)`` is called for validation.
Notes:
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
: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
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
if mlist and stripped:

View File

@ -133,6 +133,14 @@ class IOBase(Communicator):
self._lock = threading.RLock()
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
def closeConnection(self):
@ -218,12 +226,19 @@ class StringIO(IOBase):
default='\n', settable=True)
encoding = Property('used encoding', datatype=StringType(),
default='ascii', settable=True)
identification = Property('''
identification
identification = Property(
'''identification
a list of tuples with commands and expected responses as regexp,
to be sent on connect''',
datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
a list of tuples with commands and expected responses as regexp,
to be sent on connect''',
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):
if isinstance(value, str):
@ -248,16 +263,27 @@ class StringIO(IOBase):
raise ValueError('end_of_line for read must not be empty')
self._eol_write = self._convert_eol(eol[-1])
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
for command, regexp in self.identification:
reply = self.communicate(command)
if not re.match(regexp, reply):
self.closeConnection()
raise CommunicationFailedError(f'bad response: {reply} does not match {regexp}')
def checkHWIdent(self):
if not self.identification:
return
idents = iter(self.identification)
command, regexp = next(idents)
reply = self.communicate(command)
if not re.match(regexp, reply):
if self.retry_first_idn:
self.log.debug('first ident command not successful.'
' 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())
def communicate(self, command):
@ -356,19 +382,19 @@ class BytesIO(IOBase):
- a two digit hexadecimal number (byte value)
- a character
- ?? indicating ignored bytes in responses
""", datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False)
""", datatype=ArrayOf(TupleOf(StringType(), StringType())),
default=[], export=False)
def connectStart(self):
if not self.is_connected:
uri = self.uri
self._conn = AsynConn(uri, b'', default_settings=self.default_settings)
self.is_connected = True
for request, expected in self.identification:
replylen, replypat = make_regexp(expected)
reply = self.communicate(make_bytes(request), replylen)
if not replypat.match(reply):
self.closeConnection()
raise CommunicationFailedError(f'bad response: {reply!r} does not match {expected!r}')
_eol_read = b''
def checkHWIdent(self):
for request, expected in self.identification:
replylen, replypat = make_regexp(expected)
reply = self.communicate(make_bytes(request), replylen)
if not replypat.match(reply):
self.closeConnection()
raise CommunicationFailedError(f'bad response: {reply!r}'
' does not match {expected!r}')
@Command((BLOBType(), IntRange(0)), result=BLOBType())
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
"""
if self.controlled_by:
self.controlled_by = 0
self.controlled_by = 0 # self
for deactivate_control in self.inputCallbacks.values():
deactivate_control(self.name)
@ -74,6 +74,10 @@ class HasOutputModule:
if self.output_module:
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):
"""method to switch control_active on
@ -85,10 +89,10 @@ class HasOutputModule:
if name != self.name:
deactivate_control(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"""
if self.control_active:
self.control_active = False
self.log.warning(f'switched to manual mode by {source}')
self.set_control_active(False)
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())
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):
"""polls important parameters like value and status
@ -678,15 +688,10 @@ class Module(HasAccessibles):
before polling, parameters which need hardware initialisation are written
"""
for mobj in modules:
mobj.writeInitParams()
modules = [m for m in modules if m.enablePoll]
if not modules: # no polls needed - exit thread
started_callback()
return
polled_modules = [m for m in modules if m.enablePoll]
if hasattr(self, 'registerReconnectCallback'):
# 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:
m.pollInfo.last_main = 0
m.pollInfo.last_slow = 0
@ -694,7 +699,7 @@ class Module(HasAccessibles):
self.registerReconnectCallback('trigger_polls', trigger_all)
# collect all read functions
for mobj in modules:
for mobj in polled_modules:
pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll)
# trigger a poll interval change when self.pollinterval changes.
if 'pollinterval' in mobj.valueCallbacks:
@ -704,17 +709,32 @@ class Module(HasAccessibles):
rfunc = getattr(mobj, 'read_' + pname)
if rfunc.poll:
pinfo.polled_parameters.append((mobj, rfunc, pobj))
# call all read functions a first time
try:
for m in modules:
for mobj, rfunc, _ in m.pollInfo.polled_parameters:
mobj.callPollFunc(rfunc, raise_com_failed=True)
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
self.log.error('communication failure on startup: %s', e)
started_callback()
while True:
try:
for mobj in modules:
# TODO when needed: here we might add a call to a method :meth:`beforeWriteInit`
mobj.writeInitParams()
mobj.initialReads()
# call all read functions a first time
for m in polled_modules:
for mobj, rfunc, _ in m.pollInfo.polled_parameters:
mobj.callPollFunc(rfunc, raise_com_failed=True)
# 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 = ()
while True:
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
@ -257,12 +256,12 @@ class BasePyTangoDevice:
Wraps command execution and attribute operations of the given
device with logging and exception mapping.
"""
dev.command_inout = self._applyGuardToFunc(dev.command_inout)
dev.write_attribute = self._applyGuardToFunc(dev.write_attribute,
dev.__dict__['command_inout'] = self._applyGuardToFunc(dev.command_inout)
dev.__dict__['write_attribute'] = self._applyGuardToFunc(dev.write_attribute,
'attr_write')
dev.read_attribute = self._applyGuardToFunc(dev.read_attribute,
dev.__dict__['read_attribute'] = self._applyGuardToFunc(dev.read_attribute,
'attr_read')
dev.attribute_query = self._applyGuardToFunc(dev.attribute_query,
dev.__dict__['attribute_query'] = self._applyGuardToFunc(dev.attribute_query,
'attr_query')
return dev

View File

@ -25,10 +25,10 @@ import math
import re
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
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.states import Retry, Finish
from frappy.mixins import HasOutputModule, HasControlledBy
@ -218,7 +218,6 @@ class HasInput(HasControlledBy, MercuryChannel):
class Loop(HasOutputModule, MercuryChannel, Drivable):
"""common base class for loops"""
output_module = Attached(HasInput, mandatory=False)
control_active = Parameter(readonly=False)
ctrlpars = Parameter(
'pid (proportional band, integral time, differential time',
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)
def set_output(self, active, source='HW'):
def set_output(self, active, source=None):
if active:
self.activate_control()
else:
self.deactivate_control(source)
def set_target(self, target):
self.set_output(True)
if not self.control_active:
self.activate_control()
self.target = target
def read_enable_pid_table(self):
@ -254,9 +254,16 @@ class Loop(HasOutputModule, MercuryChannel, Drivable):
def read_status(self):
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):
def deactivate_control(self, source):
def deactivate_control(self, source=None):
if self.control_active:
super().deactivate_control(source)
self.convergence_state.start(self.inactive_state)
@ -372,16 +379,14 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
super().doPoll()
self.read_setpoint()
def read_control_active(self):
active = self.query(f'DEV::{self.ENABLE}', off_on)
self.set_output(active)
return active
def set_control_active(self, active):
super().set_control_active(active)
self.change(f'DEV::{self.ENABLE}', active, off_on)
def write_control_active(self, value):
if value:
raise RangeError('write to target to switch control on')
self.set_output(value, 'user')
return self.change(f'DEV::{self.ENABLE}', value, off_on)
def initialReads(self):
# initialize control active from HW
active = self.query(f'DEV::{self.ENABLE}', off_on)
super().set_output(active, 'HW')
@nopoll # polled by read_setpoint
def read_target(self):
@ -413,7 +418,7 @@ class TemperatureLoop(TemperatureSensor, ConvLoop):
self.change(f'DEV::{self.ENABLE}', True, off_on)
super().set_target(target)
def deactivate_control(self, source):
def deactivate_control(self, source=None):
if self.__ramping:
self.__ramping = False
# stop ramping setpoint
@ -508,14 +513,16 @@ class PressureLoop(PressureSensor, HasControlledBy, ConvLoop):
output_module = Attached(ValvePos, mandatory=False)
tolerance = Parameter(default=0.1)
def read_control_active(self):
active = self.query('DEV::PRES:LOOP:FAUT', off_on)
self.set_output(active)
return active
def set_control_active(self, active):
super().set_control_active(active)
if not active:
self.self_controlled() # switches off auto flow
return self.change('DEV::PRES:LOOP:FAUT', active, off_on)
def write_control_active(self, value):
self.set_output(value, 'user')
return self.change('DEV::PRES:LOOP:FAUT', value, off_on)
def initialReads(self):
# initialize control active from HW
active = self.query('DEV::PRES:LOOP:FAUT', off_on)
super().set_output(active, 'HW')
def read_target(self):
return self.query('DEV::PRES:LOOP:PRST')
@ -560,14 +567,15 @@ class HasAutoFlow:
if value:
self.needle_valve.controlled_by = self.name
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:
self.needle_valve.controlled_by = SELF
self.needle_valve.write_target(self.flowpars[1][0]) # flow min
return value
def auto_flow_off(self):
def auto_flow_off(self, source=None):
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)

View File

@ -83,7 +83,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
ioClass = PhytronIO
_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
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
self.set('P37S', axisbit | active_axes) # activate axis
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'
else: # do reset silently
self.reset_error()
else: # do silently
self.clear_errors()
self.alive_time = now
self.saveParameters()
return now
@ -171,7 +171,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
def write_target(self, value):
self.read_alive_time()
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])
self.saveParameters()
if self.backlash:
@ -261,7 +261,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.start_machine(self.stopping, status=(BUSY, 'stopping'))
@Command
def reset_error(self):
def clear_errors(self):
"""Reset error, set position to encoder"""
self.read_value()
if self._blocking_error:
@ -286,3 +286,4 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable):
self.read_value()
self.status = 'IDLE', 'after error reset'
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
_service_manager = None
_instance = None
_last_connect = 0
def __init__(self, name, log, opts, srv):
nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id')
@ -135,6 +136,10 @@ class SeaClient(ProxyClient, Module):
ProxyClient.__init__(self)
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):
self.objects.add(obj)
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())
def _connect(self, started_callback):
self._last_connect = time.time()
if self._instance:
if not self._service_manager:
if self._service_manager is None:
@ -192,36 +198,40 @@ class SeaClient(ProxyClient, Module):
self.syncio.writeline(b'seauser seaser')
assert self.syncio.readline() == b'Login OK'
self.log.info('connected to %s', self.uri)
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:
try:
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
except ConnectionClosed:
break
reply = reply.decode()
if reply.startswith('TRANSACTIONSTART'):
result = []
continue
if reply == 'TRANSACTIONFINISHED':
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('missing TRANSACTIONSTART on: %s', command)
return ''
self.log.info('swallow: %s', reply)
continue
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)
result = [reply.split('=', 1)[-1]]
else:
result.append(reply)
except ConnectionClosed:
try:
self.syncio.disconnect()
except Exception:
pass
self.syncio = None
raise TimeoutError('no response within 10s')
def _rxthread(self, started_callback):
@ -231,6 +241,11 @@ class SeaClient(ProxyClient, Module):
if reply is None:
continue
except ConnectionClosed:
try:
self.asynio.disconnect()
except Exception:
pass
self.asynio = None
break
try:
msg = json.loads(reply)
@ -463,7 +478,6 @@ class SeaModule(Module):
descr['params'].pop(0)
else:
# filter by relative paths
# rel_paths = rel_paths.split()
result = []
is_running = None
for rpath in rel_paths:

View File

@ -25,7 +25,7 @@ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUS
Done, Property
from frappy.datatypes import EnumType, FloatRange, StringType
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
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')
system_channel = Property('system channel name', StringType(), 'MC')
def write_control_active(self, value):
def set_control_active(self, active):
if self.system_channel:
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)
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)
return super().write_control_active(value)
class HeaterOutput(HasInput, MercuryChannel, Writable):
@ -286,7 +285,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable):
return self.value
def write_target(self, value):
self.write_controlled_by(SELF)
self.self_controlled()
if self.resistivity:
# round to the next voltage step
value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity
@ -301,13 +300,12 @@ class HeaterOutputWithRange(HeaterOutput):
def read_limit(self):
maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA
if maxcur == 0:
maxcur = 100 # mA
return self.read_resistivity() * maxcur ** 2 # uW
def write_limit(self, value):
if value is None:
maxcur = 100 # max. allowed current 100mA
else:
maxcur = sqrt(value / self.read_resistivity())
maxcur = sqrt(value / self.read_resistivity())
for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100:
if cur > maxcur * 0.999:
maxcur = cur

View File

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