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

This commit is contained in:
zolliker 2022-08-22 15:56:48 +02:00
commit 9673f7b242
10 changed files with 387 additions and 48 deletions

View File

@ -0,0 +1,14 @@
[NODE]
description = CryoTel be-filter BOA
id = be-filter-boa.addon.sea.psi.ch
[sea_addons]
class = secop_psi.sea.SeaClient
description = addons sea connection for be-filter-boa.addon
config = be-filter-boa.addon
service = addons
[befilter]
class = secop_psi.sea.SeaReadable
iodev = sea_addons
sea_object = befilter

View File

@ -82,6 +82,16 @@ description = dynamic needle valve position
slot = DB8.P1,DB4.G1
io = itc1
[mf]
class = secop_psi.ips_mercury.Field
description = magnetic field
slot = GRPZ
io = ips
tolerance = 0.001
wait_stable_field = 60
target.max = 11
persistent_limit = 7
[lev]
class = secop_psi.mercury.HeLevel
description = LHe level
@ -156,14 +166,6 @@ description = coil temperature
slot = MB1.T1
io = ips
[mf]
class = secop_psi.ips_mercury.Field
description = magnetic field
slot = GRPZ
io = ips
tolerance = 0.001
target.max = 11
[om_io]
description = dom motor IO
class = secop_psi.phytron.PhytronIO

View File

@ -0,0 +1,19 @@
{"befilter": {"base": "/befilter", "params": [
{"path": "", "type": "float", "kids": 12},
{"path": "send", "type": "text", "readonly": false, "cmd": "befilter send", "visibility": 3},
{"path": "status", "type": "text", "visibility": 3},
{"path": "cool", "type": "enum", "enum": {"on": 0, "off": 1}, "readonly": false, "cmd": "befilter cool"},
{"path": "control", "type": "enum", "enum": {"auto_power": 1, "manual_power": 0, "controlled_T": 2}, "readonly": false, "cmd": "befilter control", "description": "recommended mode: auto_power, use coolpower or holdpower depending on T"},
{"path": "set", "type": "float", "readonly": false, "cmd": "befilter set"},
{"path": "setpower", "type": "float", "readonly": false, "cmd": "befilter setpower", "visibility": 3},
{"path": "coolpower", "type": "float", "readonly": false, "cmd": "befilter coolpower", "visibility": 3},
{"path": "holdpower", "type": "float", "readonly": false, "cmd": "befilter holdpower", "visibility": 3},
{"path": "cool_threshold", "type": "float", "readonly": false, "cmd": "befilter cool_threshold", "description": "switch to coolpower above this value", "visibility": 3},
{"path": "hold_threshold", "type": "float", "readonly": false, "cmd": "befilter hold_threshold", "description": "switch to holdpower below this value", "visibility": 3},
{"path": "power", "type": "float"},
{"path": "filter", "type": "none", "kids": 5},
{"path": "filter/period", "type": "float", "readonly": false, "cmd": "befilter filter/period", "description": "oszillation period / sec"},
{"path": "filter/amplitude", "type": "float", "readonly": false, "cmd": "befilter filter/amplitude", "description": "oszillation amplitude / K (+/-)"},
{"path": "filter/precision", "type": "float", "readonly": false, "cmd": "befilter filter/precision"},
{"path": "filter/raw", "type": "float"},
{"path": "filter/intdif", "type": "float"}]}}

263
secop/client/interactive.py Normal file
View File

@ -0,0 +1,263 @@
# -*- 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
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
#
# *****************************************************************************
"""simple interactive python client"""
import sys
import time
import json
from queue import Queue
from secop.client import SecopClient
from secop.errors import SECoPError
USAGE = """
Usage:
from secop.client.interactive import Client
client = Client('localhost:5000') # start client.
# this connects and creates objects for all SECoP modules in the main namespace
<module> # list all parameters
<module>.<param> = <value> # change parameter
<module>(<target>) # set target and wait until not busy
# 'status' and 'value' changes are shown every 1 sec
client.mininterval = 0.2 # change minimal update interval to 0.2 sec (default is 1 second)
<module>.watch(1) # watch changes of all parameters of a module
<module>.watch(0) # remove all watching
<module>.watch(status=1, value=1) # add 'status' and 'value' to watched parameters
<module>.watch(value=0) # remove 'value' from watched parameters
"""
main = sys.modules['__main__']
class Logger:
def __init__(self, loglevel='info'):
func = self.noop
for lev in 'debug', 'info', 'warning', 'error':
if lev == loglevel:
func = self.emit
setattr(self, lev, func)
@staticmethod
def emit(fmt, *args, **kwds):
print(str(fmt) % args)
@staticmethod
def noop(fmt, *args, **kwds):
pass
class PrettyFloat(float):
def __repr__(self):
result = '%.12g' % self
if '.' in result or 'e' in result:
return result
return result + '.'
class Module:
def __init__(self, name, secnode):
self._name = name
self._secnode = secnode
self._parameters = list(secnode.modules[name]['parameters'])
self._commands = list(secnode.modules[name]['commands'])
self._running = None
self._status = None
props = secnode.modules[name]['properties']
self._title = '# %s (%s)' % (props.get('implementation', ''), props.get('interface_classes', [''])[0])
def _one_line(self, pname, minwid=0):
"""return <module>.<param> = <value> truncated to one line"""
try:
value = getattr(self, pname)
# make floats appear with 7 digits only
r = repr(json.loads(json.dumps(value), parse_float=PrettyFloat))
except Exception as e:
r = repr(e)
unit = getattr(type(self), pname).unit
if unit:
r += ' %s' % unit
pname = pname.ljust(minwid)
vallen = 113 - len(self._name) - len(pname)
if len(r) > vallen:
r = r[:vallen - 4] + ' ...'
return '%s.%s = %s' % (self._name, pname, r)
def _isBusy(self):
return 300 <= self.status[0] < 400
def _status_value_update(self, m, p, status, t, e):
if self._running:
try:
self._running.put(True)
if self._running and not self._isBusy():
self._running.put(False)
except TypeError: # may happen when _running is removed during above lines
pass
def _watch_parameter(self, m, pname, *args, forced=False, mininterval=0):
"""show parameter update"""
pobj = getattr(type(self), pname)
if not args:
args = self._secnode.cache[self._name, pname]
value = args[0]
now = time.time()
if (value != pobj.prev and now >= pobj.prev_time + mininterval) or forced:
self._secnode.log.info('%s', self._one_line(pname))
pobj.prev = value
pobj.prev_time = now
def watch(self, *args, **kwds):
enabled = {}
for arg in args:
if arg == 1: # or True
enabled.update({k: True for k in self._parameters})
elif arg == 0: # or False
enabled.update({k: False for k in self._parameters})
else:
enabled.update(arg)
enabled.update(kwds)
for pname, enable in enabled.items():
self._secnode.unregister_callback((self._name, pname), updateEvent=self._watch_parameter)
if enable:
self._secnode.register_callback((self._name, pname), updateEvent=self._watch_parameter)
def read(self, pname='value'):
value, _, error = self._secnode.readParameter(self._name, pname)
if error:
raise error
return value
def __call__(self, target=None):
if target is None:
return self.read()
self.target = target # this sets self._running
type(self).value.prev = None # show at least one value
show_final_value = True
try:
while self._running.get():
self._watch_parameter(self._name, 'value', mininterval=self._secnode.mininterval)
self._watch_parameter(self._name, 'status')
except KeyboardInterrupt:
self._secnode.log.info('-- interrupted --')
self._running = None
self._watch_parameter(self._name, 'status')
self._secnode.readParameter(self._name, 'value')
self._watch_parameter(self._name, 'value', forced=show_final_value)
return self.value
def __repr__(self):
wid = max(len(k) for k in self._parameters)
return '%s\n%s\nCommands: %s' % (
self._title,
'\n'.join(self._one_line(k, wid) for k in self._parameters),
', '.join(k + '()' for k in self._commands))
class Param:
def __init__(self, name, unit=None):
self.name = name
self.prev = None
self.prev_time = 0
self.unit = unit
def __get__(self, obj, owner):
if obj is None:
return self
value, _, error = obj._secnode.cache[obj._name, self.name]
if error:
raise error
return value
def __set__(self, obj, value):
if self.name == 'target':
obj._running = Queue()
try:
obj._secnode.setParameter(obj._name, self.name, value)
except SECoPError as e:
obj._secnode.log.error(repr(e))
class Command:
def __init__(self, name, modname, secnode):
self.name = name
self.modname = modname
self.exec = secnode.execCommand
def call(self, *args, **kwds):
if kwds:
if args:
raise TypeError('mixed arguments forbidden')
result, _ = self.exec(self.modname, self.name, kwds)
else:
result, _ = self.exec(self.modname, self.name, args or None)
return result
def __get__(self, obj, owner=None):
if obj is None:
return self
return self.call
class Client(SecopClient):
activate = True
secnodes = {}
mininterval = 1
def __init__(self, uri, loglevel='info'):
# remove previous client:
prev = self.secnodes.pop(uri, None)
if prev:
prev.log.info('remove previous client to %s', uri)
for modname in prev.modules:
prevnode = getattr(getattr(main, modname, None), 'secnode', None)
if prevnode == prev:
prev.log.info('remove previous module %s', modname)
delattr(main, modname)
prev.disconnect()
self.secnodes[uri] = self
super().__init__(uri, Logger(loglevel))
self.connect()
for modname, moddesc in self.modules.items():
prev = getattr(main, modname, None)
if prev is None:
self.log.info('create module %s', modname)
else:
if getattr(prev, 'secnode', None) is None:
self.log.error('skip module %s overwriting a global variable' % modname)
continue
self.log.info('overwrite module %s', modname)
attrs = {}
for pname, pinfo in moddesc['parameters'].items():
unit = pinfo['datainfo'].get('unit')
attrs[pname] = Param(pname, unit)
for cname in moddesc['commands']:
attrs[cname] = Command(cname, modname, self)
mobj = type('M_%s' % modname, (Module,), attrs)(modname, self)
if 'status' in mobj._parameters:
self.register_callback((modname, 'status'), updateEvent=mobj._status_value_update)
self.register_callback((modname, 'value'), updateEvent=mobj._status_value_update)
setattr(main, modname, mobj)
self.log.info('%s', USAGE)

View File

@ -130,10 +130,11 @@ class Stub(DataType):
this workaround because datatypes need properties with datatypes defined later
"""
def __init__(self, datatype_name, *args):
def __init__(self, datatype_name, *args, **kwds):
super().__init__()
self.name = datatype_name
self.args = args
self.kwds = kwds
def __call__(self, value):
"""validate"""
@ -151,7 +152,7 @@ class Stub(DataType):
for prop in dtcls.propertyDict.values():
stub = prop.datatype
if isinstance(stub, cls):
prop.datatype = globals()[stub.name](*stub.args)
prop.datatype = globals()[stub.name](*stub.args, **stub.kwds)
# SECoP types:
@ -165,7 +166,7 @@ class FloatRange(DataType):
"""
min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max)
max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max)
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='')
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0),
extname='absolute_resolution', default=0.0)
@ -343,7 +344,7 @@ class ScaledInteger(DataType):
scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True)
min = Property('low limit', FloatRange(), extname='min', mandatory=True)
max = Property('high limit', FloatRange(), extname='max', mandatory=True)
unit = Property('physical unit', Stub('StringType'), extname='unit', default='')
unit = Property('physical unit', Stub('StringType', isUTF8=True), extname='unit', default='')
fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g')
absolute_resolution = Property('absolute resolution', FloatRange(0),
extname='absolute_resolution', default=0.0)
@ -760,7 +761,7 @@ class ArrayOf(DataType):
def __call__(self, value):
"""validate an external representation to an internal one"""
if isinstance(value, (tuple, list)):
try:
# check number of elements
if self.minlen is not None and len(value) < self.minlen:
raise BadValueError(
@ -771,8 +772,9 @@ class ArrayOf(DataType):
'Array too big, holds at most %d elements!' % self.minlen)
# apply subtype valiation to all elements and return as list
return tuple(self.members(elem) for elem in value)
raise BadValueError(
'Can not convert %s to ArrayOf DataType!' % repr(value))
except TypeError:
raise BadValueError('%s can not be converted to ArrayOf DataType!'
% type(value).__name__) from None
def export_value(self, value):
"""returns a python object fit for serialisation"""

View File

@ -106,6 +106,9 @@ class EnumMember:
def __repr__(self):
return '<%s%s (%d)>' % (self.enum.name + '.' if self.enum.name else '', self.name, self.value)
def __bool__(self):
return bool(self.value)
# numeric operations: delegate to int. Do we really need any of those?
def __add__(self, other):
return self.value.__add__(other.value if isinstance(other, EnumMember) else other)
@ -242,7 +245,7 @@ class Enum(dict):
name = ''
def __init__(self, name='', parent=None, **kwds):
super(Enum, self).__init__()
super().__init__()
if isinstance(name, (dict, Enum)) and parent is None:
# swap if only parent is given as positional argument
name, parent = '', name
@ -309,17 +312,17 @@ class Enum(dict):
try:
return self[key]
except KeyError as e:
raise AttributeError(str(e))
raise AttributeError(str(e)) from None
def __setattr__(self, key, value):
if self.name and key != 'name':
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setattr__(key, value)
super().__setattr__(key, value)
def __setitem__(self, key, value):
if self.name:
raise TypeError('Enum %r can not be changed!' % self.name)
super(Enum, self).__setitem__(key, value)
super().__setitem__(key, value)
def __delitem__(self, key):
raise TypeError('Enum %r can not be changed!' % self.name)

View File

@ -186,7 +186,6 @@ class StateMachine:
ret = self.state(self)
self.init = False
if self.stopped:
self.log.debug('%r', self.stopped)
self.last_error = self.stopped
self.cleanup(self)
self.stopped = False
@ -269,7 +268,6 @@ class StateMachine:
self.stopped = Restart
with self._lock: # wait for running cycle finished
if self.stopped: # cleanup is not yet done
self.log.debug('restart')
self.last_error = self.stopped
self.cleanup(self) # ignore return state on restart
self.stopped = False

View File

@ -87,7 +87,6 @@ class Field(MercuryChannel, Magfield):
return self.query('PSU:SIG:SWHT', off_on)
def write_switch_heater(self, value):
super().write_switch_heater(value)
return self.change('PSU:SIG:SWHT', value, off_on)
def read_atob(self):
@ -120,16 +119,27 @@ class Field(MercuryChannel, Magfield):
return current / self.atob
return 0
def start_ramp_to_field(self, state):
self.change('PSU:SIG:FSET', self.persistent_field)
def set_and_go(self, value):
self.change('PSU:SIG:FSET', value)
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_set') == 'run_to_set'
def start_ramp_to_field(self, state):
try:
self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError):
state.switch_undef = self.switch_on_time or state.now
return self.wait_for_switch
return self.ramp_to_field
def wait_for_switch(self, state):
if self.now - self.switch_undef < self.wait_switch_on:
return Retry()
self.set_and_go(self.persistent_field)
return self.ramp_to_field
def start_ramp_to_target(self, state):
self.change('PSU:SIG:FSET', self.target)
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_set') == 'run_to_set'
self.set_and_go(self.target)
return self.ramp_to_target
def start_ramp_to_zero(self, state):

View File

@ -80,17 +80,19 @@ class Magfield(HasLimits, Drivable):
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6)
wait_stable_field = Parameter(
'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31)
persistent_limit = Parameter(
'above this limit, lead currents are not driven to 0',
FloatRange(0, unit='$'), readonly=False, default=99)
_state = None
__init = True
_super_sw_check = False
_last_target = None
switch_time = None
switch_on_time = None
switch_off_time = None
def doPoll(self):
if self.__init:
self.__init = False
self.switch_time = time.time()
if self.read_switch_heater() and self.mode == Mode.PERSISTENT:
self.read_value() # check for persistent field mismatch
# switch off heater from previous live or manual intervention
@ -112,6 +114,7 @@ class Magfield(HasLimits, Drivable):
def initModule(self):
super().initModule()
self.registerCallbacks(self) # for update_switch_heater
self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup_state)
def write_target(self, target):
@ -180,23 +183,28 @@ class Magfield(HasLimits, Drivable):
return Retry()
return self.start_switch_on
def write_switch_heater(self, value):
"""implementations must super call this!"""
self._super_sw_check = True
if value != self.switch_heater:
self.switch_time = time.time()
return value
def update_switch_heater(self, value):
"""is called whenever switch heater was changed"""
if value != 0:
self.switch_off_time = None
if self.switch_on_time is None:
self.switch_on_time = time.time()
else:
self.switch_on_time = None
if self.switch_off_time is None:
self.switch_off_time = time.time()
def start_switch_on(self, state):
"""switch heater on"""
if self.switch_heater != 0:
self.status = Status.PREPARING, 'wait for heater on'
else:
if self.switch_heater == 0:
self.status = Status.PREPARING, 'turn switch heater on'
self._super_sw_check = False
self.write_switch_heater(True)
if not self._super_sw_check:
raise ProgrammingError('missing super call in write_switch_heater')
try:
self.write_switch_heater(True)
except Exception as e:
self.log.warning('write_switch_heater %r', e)
return Retry()
else:
self.status = Status.PREPARING, 'wait for heater on'
return self.switch_on
def switch_on(self, state):
@ -204,7 +212,13 @@ class Magfield(HasLimits, Drivable):
if (self.target == self._last_target and
abs(self.target - self.persistent_field) <= self.tolerance): # short cut
return self.check_switch_off
if state.now - self.switch_time < self.wait_switch_on:
self.read_switch_heater()
if self.switch_on_time is None:
if state.now - self.switch_off_time > 10:
self.log.warning('switch turned off manually?')
return self.start_switch_on
return Retry()
if state.now - self.switch_on_time < self.wait_switch_on:
return Retry()
self._last_target = self.target
return self.start_ramp_to_target
@ -250,12 +264,11 @@ class Magfield(HasLimits, Drivable):
def start_switch_off(self, state):
"""turn off switch heater"""
if self.switch_heater != 0:
if self.switch_heater == 1:
self.status = Status.FINALIZING, 'turn switch heater off'
self.write_switch_heater(False)
else:
self.status = Status.FINALIZING, 'wait for heater off'
self.write_switch_heater(False)
# no check for super call needed here (would have been detected in start_switch_on)
return self.switch_off
def switch_off(self, state):
@ -265,8 +278,17 @@ class Magfield(HasLimits, Drivable):
self._last_target = None
return self.start_switch_on
self.persistent_field = self.value
if state.now - self.switch_time < self.wait_switch_off:
self.read_switch_heater()
if self.switch_off_time is None:
if state.now - self.switch_on_time > 10:
self.log.warning('switch turned on manually?')
return self.start_switch_off
return Retry()
if state.now - self.switch_off_time < self.wait_switch_off:
return Retry()
if abs(self.value) > self.persistent_limit:
self.status = Status.IDLE, 'leads current at field, switch off'
return self.finish_state
return self.start_ramp_to_zero
def start_ramp_to_zero(self, state):

View File

@ -77,3 +77,9 @@ def test_Enum():
assert e3.c >= e2.a
assert e3.b <= e2.b
assert Enum({'self': 0, 'other': 1})('self') == 0
def test_Enum_bool():
e = Enum('OffOn', off=0, on=1)
assert bool(e(0)) is False
assert bool(e(1)) is True