diff --git a/cfg/addons/be-filter-boa.cfg b/cfg/addons/be-filter-boa.cfg new file mode 100644 index 0000000..304c4be --- /dev/null +++ b/cfg/addons/be-filter-boa.cfg @@ -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 diff --git a/cfg/main/ill5.cfg b/cfg/main/ill5.cfg index f2c5e94..288b2a5 100644 --- a/cfg/main/ill5.cfg +++ b/cfg/main/ill5.cfg @@ -13,12 +13,6 @@ class = secop_psi.sea.SeaDrivable io = sea_main sea_object = tt -[tm] -class = secop_psi.sea.SeaDrivable -io = sea_main -sea_object = tt -rel_paths = tm - [cc] class = secop_psi.sea.SeaReadable io = sea_main diff --git a/cfg/main/mb11.cfg b/cfg/main/mb11.cfg index 56b4248..359b2ba 100644 --- a/cfg/main/mb11.cfg +++ b/cfg/main/mb11.cfg @@ -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,13 +166,6 @@ description = coil temperature slot = MB1.T1 io = ips -[mf] -class = secop_psi.ips_mercury.Field -description = magnetic field -slot = GRPZ -io = ips -target.max = 11 - [om_io] description = dom motor IO class = secop_psi.phytron.PhytronIO diff --git a/cfg/sea/be-filter-boa.addon.json b/cfg/sea/be-filter-boa.addon.json new file mode 100644 index 0000000..c170c4d --- /dev/null +++ b/cfg/sea/be-filter-boa.addon.json @@ -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"}]}} diff --git a/cfg/sea/ill5.config.json b/cfg/sea/ill5.config.json index c960299..1e1c09c 100644 --- a/cfg/sea/ill5.config.json +++ b/cfg/sea/ill5.config.json @@ -1,5 +1,5 @@ {"tt": {"base": "/tt", "params": [ -{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 19}, +{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 17}, {"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3}, @@ -62,8 +62,6 @@ {"path": "set/integ", "type": "float", "readonly": false, "cmd": "tt set/integ", "description": "bigger means faster"}, {"path": "set/deriv", "type": "float", "readonly": false, "cmd": "tt set/deriv"}, {"path": "display", "type": "text", "readonly": false, "cmd": "tt display"}, -{"path": "dout", "type": "int", "readonly": false, "cmd": "tt dout"}, -{"path": "dinp", "type": "int"}, {"path": "remote", "type": "bool"}]}, "cc": {"base": "/cc", "params": [ @@ -116,23 +114,23 @@ {"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit"}, {"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft"}, {"path": "hea", "type": "enum", "enum": {"0": 0, "1": 1, "6": 2}, "readonly": false, "cmd": "cc hea"}, -{"path": "hch", "type": "int", "readonly": false, "cmd": "cc hch", "visibility": 3}, -{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0", "visibility": 3}, -{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos.", "visibility": 3}, -{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos.", "visibility": 3}, -{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)", "visibility": 3}, -{"path": "h0", "type": "float", "visibility": 3}, -{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, -{"path": "h1", "type": "float", "visibility": 3}, -{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, -{"path": "h2", "type": "float", "visibility": 3}, -{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, -{"path": "h3", "type": "float", "visibility": 3}, -{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, -{"path": "h4", "type": "float", "visibility": 3}, -{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, -{"path": "h5", "type": "float", "visibility": 3}, -{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, +{"path": "hch", "type": "int", "readonly": false, "cmd": "cc hch"}, +{"path": "hwr0", "type": "float", "readonly": false, "cmd": "cc hwr0"}, +{"path": "hem0", "type": "float", "readonly": false, "cmd": "cc hem0", "description": "sensor length in mm from top to empty pos."}, +{"path": "hfu0", "type": "float", "readonly": false, "cmd": "cc hfu0", "description": "sensor length in mm from top to full pos."}, +{"path": "hd0", "type": "float", "readonly": false, "cmd": "cc hd0", "description": "external sensor drive current (mA)"}, +{"path": "h0", "type": "float"}, +{"path": "hs0", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h1", "type": "float"}, +{"path": "hs1", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h2", "type": "float"}, +{"path": "hs2", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h3", "type": "float"}, +{"path": "hs3", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h4", "type": "float"}, +{"path": "hs4", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, +{"path": "h5", "type": "float"}, +{"path": "hs5", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}}, {"path": "hfb", "type": "float"}, {"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"}, {"path": "nu", "type": "float"}, @@ -225,28 +223,29 @@ {"path": "state", "type": "text"}]}, "hefill": {"base": "/hefill", "params": [ -{"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2, "manualfill": 3}, "readonly": false, "cmd": "hefill", "kids": 6}, +{"path": "", "type": "enum", "enum": {"watching": 0, "fill": 1, "inactive": 2, "manualfill": 3}, "readonly": false, "cmd": "hefill", "kids": 7}, {"path": "send", "type": "text", "readonly": false, "cmd": "hefill send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "fast", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf"}, {"path": "state", "type": "text"}, {"path": "hefull", "type": "float", "readonly": false, "cmd": "cc hh"}, -{"path": "helow", "type": "float", "readonly": false, "cmd": "cc hl"}]}, +{"path": "helow", "type": "float", "readonly": false, "cmd": "cc hl"}, +{"path": "smooth", "type": "float"}]}, "hepump": {"base": "/hepump", "params": [ {"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": "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": "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": "running", "type": "bool", "readonly": false, "cmd": "hepump running"}, +{"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco"}, +{"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto"}, +{"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"}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "health", "type": "float"}]}, "hemot": {"base": "/hepump/hemot", "params": [ -{"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "visibility": 3, "kids": 30}, +{"path": "", "type": "float", "readonly": false, "cmd": "run hemot", "kids": 30}, {"path": "send", "type": "text", "readonly": false, "cmd": "hemot send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, {"path": "is_running", "type": "int", "readonly": false, "cmd": "hemot is_running", "visibility": 3}, @@ -296,4 +295,14 @@ {"path": "tbl_tt_dblctrl_prop_up", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_up", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}, {"path": "fix_tt_dblctrl_prop_lo", "type": "bool", "readonly": false, "cmd": "table fix_tt_dblctrl_prop_lo"}, {"path": "val_tt_dblctrl_prop_lo", "type": "float"}, -{"path": "tbl_tt_dblctrl_prop_lo", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_lo", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]}} +{"path": "tbl_tt_dblctrl_prop_lo", "type": "text", "readonly": false, "cmd": "table tbl_tt_dblctrl_prop_lo", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]}, + +"prep0v": {"base": "/prep0v", "params": [ +{"path": "", "type": "text", "readonly": false, "cmd": "prep0v", "kids": 2}, +{"path": "send", "type": "text", "readonly": false, "cmd": "prep0v send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}]}, + +"prep0": {"base": "/prep0", "params": [ +{"path": "", "type": "text", "readonly": false, "cmd": "prep0", "kids": 2}, +{"path": "send", "type": "text", "readonly": false, "cmd": "prep0 send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}]}} diff --git a/cfg/stick/ill5p.cfg b/cfg/stick/ill5p.cfg new file mode 100644 index 0000000..12f04ee --- /dev/null +++ b/cfg/stick/ill5p.cfg @@ -0,0 +1,16 @@ +[NODE] +description = ILL5 sample stick for pressure cells +id = ill5p.stick.sea.psi.ch + +[sea_stick] +class = secop_psi.sea.SeaClient +description = stick SEA connection to ill5p.stick +config = ill5p.stick +service = stick + +[ts] +class = secop_psi.sea.SeaReadable +io = sea_stick +sea_object = tt +json_file = ill5.config.json +rel_paths = ts diff --git a/cfg/stick/mb11stick.cfg b/cfg/stick/mb11stick.cfg new file mode 100644 index 0000000..8f900e3 --- /dev/null +++ b/cfg/stick/mb11stick.cfg @@ -0,0 +1,43 @@ +[NODE] +description = MB11 standard sample stick +id = mb11.stick.sea.psi.ch + + +[INTERFACE] +uri = tcp://5000 + +[itc] +class = secop.proxy.Proxy +remote_class = secop_psi.mercury.IO +description = connection to MB11 mercury +module = itc1 +#uri = mb11-ts:3001 +#timeout = 5 + +#[t3] +#class = secop_psi.softcal.Sensor +#rawsensor = r2 +##calib USstick +#calib = /home/l_samenv/sea/tcl/calcurves/X163059.340 +##calib dilatometer stick +#calib = /home/l_samenv/sea/tcl/calcurves/X86023.340 +## unknown +#calib = /home/l_samenv/sea/tcl/calcurves/X70197.340 +#svalue.unit = K + + + + +[ts] +class = secop_psi.mercury.TemperatureLoop +description = sample temperature +output_module = htr_ts +slot = MB1.T1 +io = itc +tolerance = 1 + +[htr_ts] +class = secop_psi.mercury.HeaterOutput +description = sample stick heater power +slot = MB0.H1 +io = itc diff --git a/secop/client/interactive.py b/secop/client/interactive.py new file mode 100644 index 0000000..85d6428 --- /dev/null +++ b/secop/client/interactive.py @@ -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 +# +# ***************************************************************************** +"""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 + + # list all parameters +. = # change parameter +() # 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) + +.watch(1) # watch changes of all parameters of a module +.watch(0) # remove all watching +.watch(status=1, value=1) # add 'status' and 'value' to watched parameters +.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 . = 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) diff --git a/secop/datatypes.py b/secop/datatypes.py index 3f5f3df..5da9a99 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -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""" diff --git a/secop/lib/enum.py b/secop/lib/enum.py index 0fb0b38..2bdcf1c 100644 --- a/secop/lib/enum.py +++ b/secop/lib/enum.py @@ -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) diff --git a/secop/lib/statemachine.py b/secop/lib/statemachine.py index 6ed94c6..309cabf 100644 --- a/secop/lib/statemachine.py +++ b/secop/lib/statemachine.py @@ -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 diff --git a/secop_psi/convergence.py b/secop_psi/convergence.py index 15833b5..385a098 100644 --- a/secop_psi/convergence.py +++ b/secop_psi/convergence.py @@ -89,6 +89,10 @@ class HasConvergence: """to be called from write_target""" self.convergence_state.start(self.state_approach) + def interrupt_state(self): + """to be called from stop""" + self.convergence_state.start(self.state_instable) + def state_approach(self, state): """approaching, checking progress (busy)""" state.spent_inside = 0 @@ -157,3 +161,10 @@ class HasConvergence: else: state.spent_inside = max(0, state.spent_inside - state.delta()) return Retry() + + def state_interrupt(self, state): + self.status = IDLE, 'stopped' # stop called + return self.state_instable + + def stop(self): + self.convergence_state.start(self.state_interrupt) diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index e965f5e..6020359 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -35,6 +35,7 @@ CURRENT_CHECK_SIZE = 2 class Field(MercuryChannel, Magfield): action = Parameter('action', EnumType(Action), readonly=False) setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0) + voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) forced_persistent_field = Parameter( 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) @@ -43,13 +44,13 @@ class Field(MercuryChannel, Magfield): _field_mismatch = None nslaves = 3 slave_currents = None - _init = True + __init = True def read_value(self): self.current = self.query('PSU:SIG:FLD') pf = self.query('PSU:SIG:PFLD') - if self._init: - self._init = False + if self.__init: + self.__init = False self.persistent_field = pf if self.switch_heater != 0 or self._field_mismatch is None: self.forced_persistent_field = False @@ -86,12 +87,14 @@ 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): return self.query('PSU:ATOB') + def read_voltage(self): + return self.query('PSU:SIG:VOLT') + def read_setpoint(self): return self.query('PSU:SIG:FSET') @@ -116,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): diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index 5a71ff4..1d800b2 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -80,20 +80,25 @@ 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 - switch_time = None + __init = True + _last_target = 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.__init: + self.__init = False 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 - self.write_target(self.persistent_value) + self.write_target(self.persistent_field) + else: + self._last_target = self.persistent_field else: self.read_value() self._state.cycle() @@ -109,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): @@ -145,7 +151,8 @@ class Magfield(HasLimits, Drivable): def start_field_change(self, state): self.setFastPoll(True, 1.0) self.status = Status.PREPARING, 'changed target field' - if self.persistent_field == self.target: + if (self.target == self._last_target and + abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off return self.start_ramp_to_field @@ -158,7 +165,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_field(self, state): """ramping, wait for current at persistent field""" - if self.persistent_field == self.target: # short cut + if (self.target == self._last_target and + abs(self.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off if abs(self.current - self.persistent_field) > self.tolerance: if state.init: @@ -175,32 +183,44 @@ 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""" - self._super_sw_check = False - 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.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): """wait for switch heater open""" - if self.persistent_field == self.target: # short cut + 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() - state.set_point = self.target + if state.now - self.switch_on_time < self.wait_switch_on: + return Retry() + self._last_target = self.target return self.start_ramp_to_target def start_ramp_to_target(self, state): @@ -212,8 +232,8 @@ class Magfield(HasLimits, Drivable): def ramp_to_target(self, state): """ramp field to target""" - if state.set_point != self.target: # target changed - state.set_point = self.target + if self.target != self._last_target: # target was changed + self._last_target = self.target return self.start_ramp_to_target self.persistent_field = self.value # Remarks: assume there is a ramp limiting feature @@ -226,6 +246,9 @@ class Magfield(HasLimits, Drivable): def stabilize_field(self, state): """stabilize field""" + if self.target != self._last_target: # target was changed + self._last_target = self.target + return self.start_ramp_to_target self.persistent_field = self.value if state.now - state.stabilize_start < self.wait_stable_field: if state.init: @@ -241,38 +264,46 @@ 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._super_sw_check = False - self.write_switch_heater(False) - if not self._super_sw_check: - raise ProgrammingError('missing super call in write_switch_heater') return self.switch_off def switch_off(self, state): """wait for switch heater closed""" - if self.persistent_field != self.target: # redo - return self.start_switch_on - if self.mode == Mode.DRIVEN: + if self.target != self._last_target or self.mode == Mode.DRIVEN: + # target or mode has changed -> redo + 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): """start ramping current to target - initiate ramp to zero (with corresponding ramp rate + initiate ramp to zero (with corresponding ramp rate) should return ramp_to_zero """ raise NotImplementedError def ramp_to_zero(self, state): """ramp field to zero""" - if self.persistent_field != self.target or self.mode == Mode.DRIVEN: # redo + if self.target != self._last_target or self.mode == Mode.DRIVEN: + # target or mode has changed -> redo + self._last_target = None return self.start_field_change if abs(self.current) > self.tolerance: if state.init: diff --git a/secop_psi/sea.py b/secop_psi/sea.py index f4b0eb6..749207c 100644 --- a/secop_psi/sea.py +++ b/secop_psi/sea.py @@ -401,7 +401,6 @@ class SeaModule(Module): hdbpath = None # hdbpath for main writable def __new__(cls, name, logger, cfgdict, srv): - print('N', cls, name) if hasattr(srv, 'extra_sea_modules'): extra_modules = srv.extra_sea_modules else: @@ -667,7 +666,7 @@ class SeaDrivable(SeaModule, Drivable): def write_target(self, value): self.io.query('run %s %s' % (self.sea_object, value)) - #self.status = [self.Status.BUSY, 'driving'] + # self.status = [self.Status.BUSY, 'driving'] return value def update_status(self, value, timestamp, readerror): diff --git a/test/test_lib_enum.py b/test/test_lib_enum.py index 97228e1..7077834 100644 --- a/test/test_lib_enum.py +++ b/test/test_lib_enum.py @@ -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