From 498dbd417977905a2d8d917ee4a3935ae6a8cbd5 Mon Sep 17 00:00:00 2001 From: camea Date: Tue, 4 Oct 2022 15:11:57 +0200 Subject: [PATCH 01/11] sea.py: fix status datatype sea status is a string (not a tuple) and needs therefore special treatment. interestingly this bug did not yet appear ... --- secop_psi/sea.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/secop_psi/sea.py b/secop_psi/sea.py index a91f798..123112b 100644 --- a/secop_psi/sea.py +++ b/secop_psi/sea.py @@ -533,14 +533,17 @@ class SeaModule(Module): if key == 'target': kwds['readonly'] = False prev = cls.accessibles[key] - pobj = Parameter(**kwds) - merged_properties = prev.propertyValues.copy() - pobj.updateProperties(merged_properties) - pobj.merge(merged_properties) - datatype = kwds.get('datatype', cls.accessibles[key].datatype) + if key == 'status': + # special case: status from sea is a string, not the status tuple + pobj = prev.copy() + else: + pobj = Parameter(**kwds) + merged_properties = prev.propertyValues.copy() + pobj.updateProperties(merged_properties) + pobj.merge(merged_properties) else: pobj = Parameter(**kwds) - datatype = pobj.datatype + datatype = pobj.datatype if issubclass(cls, SeaWritable) and key == 'target': kwds['readonly'] = False attributes['value'] = Parameter(**kwds) From 0c93ca8c75296b4b663a318f6955b5ea5c1031e6 Mon Sep 17 00:00:00 2001 From: camea Date: Tue, 18 Oct 2022 16:41:04 +0200 Subject: [PATCH 02/11] add comment "do not use mb11stick" --- cfg/stick/mb11stick.cfg | 45 +++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/cfg/stick/mb11stick.cfg b/cfg/stick/mb11stick.cfg index 8f900e3..e435253 100644 --- a/cfg/stick/mb11stick.cfg +++ b/cfg/stick/mb11stick.cfg @@ -1,16 +1,19 @@ -[NODE] -description = MB11 standard sample stick -id = mb11.stick.sea.psi.ch +# DO NOT USE +# use 'mb11std' instead of 'mb11', 'mb11stick' +# as the communication proxy for itc does not work yet +[NODE] +description = MB11 standard sample stick (do not use) +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 +#[itc] +#class = secop.proxy.Proxy +#remote_class = secop_psi.mercury.IO +#description = connection to MB11 mercury +#module = itc1 #uri = mb11-ts:3001 #timeout = 5 @@ -25,19 +28,17 @@ module = itc1 #calib = /home/l_samenv/sea/tcl/calcurves/X70197.340 #svalue.unit = K +#[ts] +#class = secop.proxy.Proxy +#remote_class = secop_psi.mercury.TemperatureLoop +#description = sample temperature +#module = T_sample +#io = itc1 +#[htr_ts] +#class = secop.proxy.Proxy +#remote_class = secop_psi.mercury.HeaterOutput +#description = sample stick heater power +#module = htr_sample +#io = itc1 - -[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 From df428f4c0c0aaa19ff3369b05d1b61b10f4c2546 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Tue, 15 Nov 2022 16:57:00 +0100 Subject: [PATCH 03/11] check readback and try up to 3 times when it does not match --- secop_psi/mercury.py | 85 +++++++++++++++++++++++++++++--------------- 1 file changed, 57 insertions(+), 28 deletions(-) diff --git a/secop_psi/mercury.py b/secop_psi/mercury.py index 0aebc82..431d721 100644 --- a/secop_psi/mercury.py +++ b/secop_psi/mercury.py @@ -38,6 +38,7 @@ SELF = 0 def as_float(value): + """converts string (with unit) to float and float to string""" if isinstance(value, str): return float(VALUE_UNIT.match(value).group(1)) return '%g' % value @@ -82,7 +83,7 @@ class MercuryChannel(HasIO): return 'DEV:%s:%s%s%s' % (slot, head, sep, tail) return adr - def multiquery(self, adr, names=(), convert=as_float): + def multiquery(self, adr, names=(), convert=as_float, debug=None): """get parameter(s) in mercury syntax :param adr: the 'address part' of the SCPI command @@ -99,26 +100,36 @@ class MercuryChannel(HasIO): self.slot='DB5.P1,DB3.G1' # -> take second slot -> query command will be READ:DEV:DB3.G1:PRES:SIG:PERC """ + # TODO: if the need arises: allow convert to be a list adr = self._complete_adr(adr) cmd = 'READ:%s:%s' % (adr, ':'.join(names)) - reply = self.communicate(cmd) - head = 'STAT:%s:' % adr - try: - assert reply.startswith(head) - replyiter = iter(reply[len(head):].split(':')) - keys, result = zip(*zip(replyiter, replyiter)) - assert keys == tuple(names) - return tuple(convert(r) for r in result) - except (AssertionError, AttributeError, ValueError): - time.sleep(0.1) # in case this was the answer of a previous command - raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from None + msg = '' + for _ in range(3): + if msg: + self.log.warning('%s', msg) + reply = self.communicate(cmd) + if debug is not None: + debug.append(reply) + head = 'STAT:%s:' % adr + try: + assert reply.startswith(head) + replyiter = iter(reply[len(head):].split(':')) + keys, result = zip(*zip(replyiter, replyiter)) + assert keys == tuple(names) + return tuple(convert(r) for r in result) + except (AssertionError, AttributeError, ValueError): + time.sleep(0.1) # in case this was the answer of a previous command + msg = 'invalid reply %r to cmd %r' % (reply, cmd) + else: + raise HardwareError(msg) from None - def multichange(self, adr, values, convert=as_float): + def multichange(self, adr, values, convert=as_float, tolerance=0): """set parameter(s) in mercury syntax :param adr: as in see multiquery method :param values: [(name1, value1), (name2, value2) ...] :param convert: a converter function (converts given value to string and replied string to value) + :param tolerance: tolerance for readback check :return: the values as tuple Example: @@ -128,22 +139,40 @@ class MercuryChannel(HasIO): self.slot='DB6.T1,DB1.H1' # and take first slot -> change command will be SET:DEV:DB6.T1:TEMP:LOOP:P:5:I:2:D:0 """ + # TODO: if the need arises: allow convert and or tolerance to be a list adr = self._complete_adr(adr) params = ['%s:%s' % (k, convert(v)) for k, v in values] cmd = 'SET:%s:%s' % (adr, ':'.join(params)) - reply = self.communicate(cmd) - head = 'STAT:SET:%s:' % adr - - try: - assert reply.startswith(head) - replyiter = iter(reply[len(head):].split(':')) - keys, result, valid = zip(*zip(replyiter, replyiter, replyiter)) - assert keys == tuple(k for k, _ in values) - assert any(v == 'VALID' for v in valid) - return tuple(convert(r) for r in result) - except (AssertionError, AttributeError, ValueError) as e: - time.sleep(0.1) # in case of missed replies this might help to skip garbage - raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e + for _ in range(3): # try 3 times or until readback result matches + t = time.time() + reply = self.communicate(cmd) + head = 'STAT:SET:%s:' % adr + try: + assert reply.startswith(head) + replyiter = iter(reply[len(head):].split(':')) + # reshuffle reply=(k1, r1, v1, k2, r2, v1) --> keys = (k1, k2), result = (r1, r2), valid = (v1, v2) + keys, result, valid = zip(*zip(replyiter, replyiter, replyiter)) + assert keys == tuple(k for k, _ in values) + assert any(v == 'VALID' for v in valid) + result = tuple(convert(r) for r in result) + except (AssertionError, AttributeError, ValueError) as e: + time.sleep(0.1) # in case of missed replies this might help to skip garbage + raise HardwareError('invalid reply %r to cmd %r' % (reply, cmd)) from e + keys = [v[0] for v in values] + debug = [] + readback = self.multiquery(adr, keys, convert, debug) + for k, r, b in zip(keys, result, readback): + if convert == as_float: + tol = max(abs(r) * 1e-3, abs(b) * 1e-3, tolerance) + if abs(r - b) > tol: + break + elif r != b: + break + else: + return readback + self.log.warning('sent: %s', cmd) + self.log.warning('got: %s', debug[0]) + return readback def query(self, adr, convert=as_float): """query a single parameter @@ -153,9 +182,9 @@ class MercuryChannel(HasIO): adr, _, name = adr.rpartition(':') return self.multiquery(adr, [name], convert)[0] - def change(self, adr, value, convert=as_float): + def change(self, adr, value, convert=as_float, tolerance=0): adr, _, name = adr.rpartition(':') - return self.multichange(adr, [(name, value)], convert)[0] + return self.multichange(adr, [(name, value)], convert, tolerance)[0] class TemperatureSensor(MercuryChannel, Readable): From 49b9bfa11d385ddd5c296f853f74f79e7af9e32d Mon Sep 17 00:00:00 2001 From: l_samenv Date: Tue, 15 Nov 2022 16:57:52 +0100 Subject: [PATCH 04/11] improvements for triton - set cooldown channel befor setting action - improve max heater setting --- secop_psi/triton.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/secop_psi/triton.py b/secop_psi/triton.py index f7bf713..533f5ee 100644 --- a/secop_psi/triton.py +++ b/secop_psi/triton.py @@ -20,11 +20,13 @@ # ***************************************************************************** """oxford instruments triton (kelvinoxjt dil)""" -from math import sqrt -from secop.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, Done -from secop.datatypes import EnumType, FloatRange +from math import sqrt, log10 +from secop.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUSY, ERROR, \ + Done, Property +from secop.datatypes import EnumType, FloatRange, StringType from secop.lib.enum import Enum from secop_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF +from secop.lib import clamp import secop_psi.mercury as mercury actions = Enum(none=0, condense=1, circulate=2, collect=3) @@ -35,6 +37,7 @@ actions_map.mapping['NONE'] = actions.none # when writing, STOP is used instead class Action(MercuryChannel, Writable): channel_type = 'ACTN' + cooldown_channel = Property('cool down channel', StringType(), 'T5') value = Parameter('running action', EnumType(actions)) target = Parameter('action to do', EnumType(none=0, condense=1, collect=3), readonly=False) _target = 0 @@ -47,6 +50,7 @@ class Action(MercuryChannel, Writable): def write_target(self, value): self._target = value + self.change('SYS:DR:CHAN:COOL', self.cooldown_channel, str) return self.change('SYS:DR:ACTN', value, actions_map) # actions: @@ -245,10 +249,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop): ENABLE = 'TEMP:LOOP:MODE' ENABLE_RAMP = 'TEMP:LOOP:RAMP:ENAB' RAMP_RATE = 'TEMP:LOOP:RAMP:RATE' + enable_pid_table = None # remove, does not work on triton + ctrlpars = Parameter('pid (gain, integral (inv. time), differential time') + system_channel = Property('system channel name', StringType(), 'MC') def write_control_active(self, value): - self.change('SYS:DR:CHAN:MC', 'T5', str) + if self.system_channel: + self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str) if value: self.change('TEMP:LOOP:FILT:ENAB', 'ON', str) if self.output_module: @@ -260,7 +268,7 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop): class HeaterOutput(HasInput, MercuryChannel, Writable): """heater output""" channel_type = 'HTR' - value = Parameter('heater output', FloatRange(unit='W')) + value = Parameter('heater output', FloatRange(unit='uW')) target = Parameter('heater output', FloatRange(0, unit='$'), readonly=False) resistivity = Parameter('heater resistivity', FloatRange(unit='Ohm')) @@ -268,7 +276,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): return self.query('HTR:RES') def read_value(self): - return self.query('HTR:SIG:POWR') * 1e-6 + return round(self.query('HTR:SIG:POWR'), 3) def read_target(self): if self.controlled_by != 0: @@ -277,24 +285,33 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): def write_target(self, value): self.write_controlled_by(SELF) - return self.change('HTR:SIG:POWR', value * 1e6) + if self.resistivity: + # round to the next voltage step + value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity + return round(self.change('HTR:SIG:POWR', value), 3) class HeaterOutputWithRange(HeaterOutput): """heater output with heater range""" channel_type = 'HTR,TEMP' - limit = Parameter('max. heater power', FloatRange(unit='W'), readonly=False) + limit = Parameter('max. heater power', FloatRange(unit='uW'), readonly=False) def read_limit(self): - maxcur = self.query('TEMP:LOOP:RANGE') * 0.001 # mA -> A - return self.read_resistivity() * maxcur ** 2 + maxcur = self.query('TEMP:LOOP:RANGE') # mA + return self.read_resistivity() * maxcur ** 2 # uW def write_limit(self, value): if value is None: - maxcur = 0.1 # max. allowed current 100mA + maxcur = 100 # max. allowed current 100mA else: maxcur = sqrt(value / self.read_resistivity()) - self.change('TEMP:LOOP:RANGE', maxcur * 1000) + for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100: + if cur > maxcur * 0.999: + maxcur = cur + break + else: + maxcur = cur + self.change('TEMP:LOOP:RANGE', maxcur) return self.read_limit() From 4405b2b02c0227a9fad2bd0afc437ad6ebcd7db7 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Mon, 21 Nov 2022 14:08:17 +0100 Subject: [PATCH 05/11] new state machine, HasStates mixin --- secop/lib/newstatemachine.py | 246 +++++++++++++++++++++++++++++++++++ secop/modules.py | 2 +- secop/states.py | 156 ++++++++++++++++++++++ 3 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 secop/lib/newstatemachine.py create mode 100644 secop/states.py diff --git a/secop/lib/newstatemachine.py b/secop/lib/newstatemachine.py new file mode 100644 index 0000000..a23c37d --- /dev/null +++ b/secop/lib/newstatemachine.py @@ -0,0 +1,246 @@ +# -*- 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 +# +# ***************************************************************************** +"""a simple, but powerful state machine + +Mechanism +--------- + +The code for the state machine is NOT to be implemented as a subclass +of StateMachine, but usually as functions or methods of an other object. +The created state object may hold variables needed for the state. +A state function may return either: +- a function for the next state to transition to +- Retry to keep the state and call the state function again +- or Finish for finishing + + +Initialisation Code +------------------- + +For code to be called only after a state transition, use statemachine.init. + +def state_x(stateobj): + if stateobj.init: + ... code to be execute only after entering state x ... + ... further code ... + + +Restart +------- + +To restart the statemachine, call statemachine.start. The current task is interrupted, +the cleanup sequence is called, and after this the machien is restarted with the +arguments of the start method. + + +Stop +---- + +To stop the statemachine, call statemachine.stop. The current task is interrupted, +the cleanup sequence is called, and the machine finishes. + + +Cleaning Up +----------- + +A cleanup function might be added as arguments to StateMachine.start. +On error, stop or restart, the cleanup sequence will be executed. +The cleanup itself is not be interrupted: +- if a further exeception is raised, the machine is interrupted immediately +- if start or stop is called again, a previous start or stop is ignored +""" + +import time +import threading +from logging import getLogger +from secop.lib import UniqueObject + +Retry = UniqueObject('Retry') +Finish = UniqueObject('Finish') + + +class Start: + def __init__(self, newstate, kwds): + self.newstate = newstate + self.kwds = kwds # statemachine attributes + + +class Stop: + pass + + +class StateMachine: + """a simple, but powerful state machine""" + # class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start` + statefunc = None # the current statefunc + now = None # the current time (avoid mutiple calls within a state) + init = True # True only in the first call of a state after a transition + _last_time = 0 # for delta method + next_task = None # None or an instance of Start or Stop + cleanup_reason = None # None or an instance of Exception, Start or Stop + + def __init__(self, statefunc=None, logger=None, **kwds): + """initialize state machine + + :param statefunc: if given, this is the first statefunc + :param logger: an optional logger + :param kwds: any attributes for the state object + """ + self.cleanup = None + self.transition = None + self.maxloops = 10 # the maximum number of statefunc functions called in sequence without Retry + self.now = time.time() # avoid calling time.time several times per statefunc + self.log = logger or getLogger('dummy') + self._lock = threading.Lock() + self._update_attributes(kwds) + if statefunc: + self.start(statefunc) + + def _update_attributes(self, kwds): + """update allowed attributes""" + cls = type(self) + for key, value in kwds.items(): + if hasattr(cls, key): + raise AttributeError('can not set %s.%s' % (cls.__name__, key)) + setattr(self, key, value) + + def _cleanup(self, reason): + if isinstance(reason, Exception): + self.log.warning('%s: raised %r', self.statefunc.__name__, reason) + elif isinstance(reason, Stop): + self.log.debug('stopped in %s', self.statefunc.__name__) + else: # must be Start + self.log.debug('restart %s during %s', reason.newstate.__name__, self.statefunc.__name__) + if self.cleanup_reason is None: + self.cleanup_reason = reason + if not self.cleanup: + return None # no cleanup needed or cleanup already handled + with self._lock: + cleanup, self.cleanup = self.cleanup, None + ret = None + try: + ret = cleanup(self) + if not (ret is None or callable(ret)): + self.log.error('%s: return value must be callable or None, not %r', + self.statefunc.__name__, ret) + ret = None + except Exception as e: + self.log.exception('%r raised in cleanup', e) + return ret + + @property + def is_active(self): + return bool(self.statefunc) + + def _new_state(self, statefunc): + if self.transition: + self.transition(self, statefunc) + self.init = True + self.statefunc = statefunc + self._last_time = self.now + + def cycle(self): + """do one cycle + + call state functions until Retry is returned + """ + for _ in range(2): + if self.statefunc: + for _ in range(self.maxloops): + self.now = time.time() + if self.next_task and not self.cleanup_reason: + # interrupt only when not cleaning up + ret = self._cleanup(self.next_task) + else: + try: + ret = self.statefunc(self) + self.init = False + if ret is Retry: + return + if ret is Finish: + break + if not callable(ret): + ret = self._cleanup(RuntimeError( + '%s: return value must be callable, Retry or Finish, not %r' + % (self.statefunc.__name__, ret))) + except Exception as e: + ret = self._cleanup(e) + if ret is None: + break + self._new_state(ret) + else: + ret = self._cleanup(RuntimeError( + '%s: too many states chained - probably infinite loop' % self.statefunc.__name__)) + if ret: + self._new_state(ret) + continue + if self.cleanup_reason is None: + self.log.debug('finish in state %r', self.statefunc.__name__) + self._new_state(None) + if self.next_task: + with self._lock: + action, self.next_task = self.next_task, None + self.cleanup_reason = None + if isinstance(action, Start): + self._new_state(action.newstate) + self._update_attributes(action.kwds) + + def start(self, statefunc, **kwds): + """start with a new state + + :param statefunc: the first state + :param kwds: items to put as attributes on the state machine + """ + kwds.setdefault('cleanup', None) # cleanup must be given on each restart + with self._lock: + self.next_task = Start(statefunc, kwds) + + def stop(self): + """stop machine, go to idle state""" + with self._lock: + self.next_task = Stop() + + def delta(self, mindelta=0): + """helper method for time dependent control + + :param mindelta: minimum time since last call + :return: time delta or None when less than min delta time has passed + + to be called from within an statefunc + + Usage: + + def state_x(self, state): + delta = state.delta(5) + if delta is None: + return # less than 5 seconds have passed, we wait for the next cycle + # delta is >= 5, and the zero time for delta is set + + # now we can use delta for control calculations + + remark: in the first step after start, state.delta(0) returns nearly 0 + """ + delta = self.now - self._last_time + if delta < mindelta: + return None + self._last_time = self.now + return delta diff --git a/secop/modules.py b/secop/modules.py index db6d296..78f5b13 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -246,7 +246,7 @@ class PollInfo: self.fast_flag = False self.trigger_event = trigger_event - def trigger(self): + def trigger(self, immediate=False): """trigger a recalculation of poll due times""" self.trigger_event.set() diff --git a/secop/states.py b/secop/states.py new file mode 100644 index 0000000..be63d01 --- /dev/null +++ b/secop/states.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# -*- 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 +# +# ***************************************************************************** +"""state machine mixin + +handles status depending on statemachine state +""" + + +from secop.core import BUSY, IDLE, ERROR, Parameter, Command, Done +from secop.errors import ProgrammingError +from secop.lib.newstatemachine import StateMachine, Retry, Finish, Start, Stop + + +class status_code: + """decorator for state methods""" + def __init__(self, code, text=None): + self.code = code + self.text = text + + def __set_name__(self, owner, name): + if not issubclass(owner, HasStates): + raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__) + self.cls = owner + self.name = name + if 'statusMap' not in owner.__dict__: + # we need a copy on each inheritance level + owner.statusMap = owner.statusMap.copy() + owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text + setattr(owner, name, self.func) # replace with original method + + def __call__(self, func): + self.func = func + return self + + +class HasStates: + status = Parameter() # make sure this is a parameter + all_status_changes = False # when true, send also updates for status changes within a cycle + _state_machine = None + _status = IDLE, '' + statusMap = {} + + def init_state_machine(self, **kwds): + self._state_machine = StateMachine( + logger=self.log, + idle_status=(IDLE, ''), + transition=self.state_transition, + reset_fast_poll=False, + status=(IDLE, ''), + **kwds) + + def initModule(self): + super().initModule() + self.init_state_machine() + + def state_transition(self, sm, newstate): + """handle status updates""" + status = self.get_status(newstate) + if sm.next_task: + if isinstance(sm.next_task, Stop): + if newstate and status is not None: + status = status[0], 'stopping (%s)' % status[1] + elif newstate: + # restart case + if status is not None: + status = sm.status[0], 'restarting (%s)' % status[1] + else: + # start case + status = self.get_status(sm.next_task.newstate, BUSY) + if status: + sm.status = status + if self.all_status_changes: + self.read_status() + + def get_status(self, statefunc, default_code=None): + if statefunc is None: + status = self._state_machine.idle_status or (ERROR, 'Finish was returned without final status') + else: + name = statefunc.__name__ + status = self.statusMap.get(name) + if status is None and default_code is not None: + status = default_code, name.replace('_', ' ') + return status + + def read_status(self): + sm = self._state_machine + if sm.status == self.status: + return Done + return sm.status + + def doPoll(self): + super().doPoll() + sm = self._state_machine + sm.cycle() + if sm.statefunc is None: + if sm.reset_fast_poll: + sm.reset_fast_poll = False + self.setFastPoll(False) + self.read_status() + + def start_machine(self, statefunc, fast_poll=True, **kwds): + sm = self._state_machine + sm.status = self.get_status(statefunc, BUSY) + if sm.statefunc: + sm.status = sm.status[0], 'restarting' + sm.start(statefunc, **kwds) + self.read_status() + if fast_poll: + sm.reset_fast_poll = True + self.setFastPoll(True) + self.pollInfo.trigger(True) # trigger poller + + def stop_machine(self, stopped_status=(IDLE, 'stopped')): + sm = self._state_machine + if sm.is_active: + sm.idle_status = stopped_status + sm.stop() + sm.status = self.get_status(sm.statefunc, sm.status[0])[0], 'stopping' + self.read_status() + self.pollInfo.trigger(True) # trigger poller + + @Command + def stop(self): + self.stop_machine() + + def final_status(self, code=IDLE, text=''): + """final status + + Usage: + + return self.final_status('IDLE', 'machine idle') + """ + sm = self._state_machine + sm.idle_status = code, text + sm.cleanup = None + return Finish From 1b2e364f701dcb9ba1f531d5bbf29637b2287186 Mon Sep 17 00:00:00 2001 From: l_samenv Date: Mon, 21 Nov 2022 14:37:53 +0100 Subject: [PATCH 06/11] magfield adapted to new state machine --- secop_psi/dilsc.py | 97 +++++++++ secop_psi/ips_mercury.py | 311 ++++++++++++++++++++--------- secop_psi/magfield.py | 418 +++++++++++++++++++++------------------ secop_psi/vector.py | 89 +++++++++ 4 files changed, 632 insertions(+), 283 deletions(-) create mode 100644 secop_psi/dilsc.py create mode 100644 secop_psi/vector.py diff --git a/secop_psi/dilsc.py b/secop_psi/dilsc.py new file mode 100644 index 0000000..6c52b84 --- /dev/null +++ b/secop_psi/dilsc.py @@ -0,0 +1,97 @@ +# -*- 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 +# ***************************************************************************** +"""vector field""" + +from secop.core import Drivable, Done, BUSY, IDLE, WARN, ERROR +from secop.errors import BadValueError +from secop_psi.vector import Vector + + +DECREASE = 1 +INCREASE = 2 + + +class VectorField(Vector, Drivable): + _state = None + + def doPoll(self): + """periodically called method""" + try: + if self._starting: + # first decrease components + driving = False + for target, component in zip(self.target, self.components): + if target * component.value < 0: + # change sign: drive to zero first + target = 0 + if abs(target) < abs(component.target): + if target != component.target: + component.write_target(target) + if component.isDriving(): + driving = True + if driving: + return + # now we can go to the final targets + for target, component in zip(self.target, self.components): + component.write_target(target) + self._starting = False + else: + for component in self.components: + if component.isDriving(): + return + self.setFastPoll(False) + finally: + super().doPoll() + + def merge_status(self): + names = [c.name for c in self.components if c.status[0] >= ERROR] + if names: + return ERROR, 'error in %s' % ', '.join(names) + names = [c.name for c in self.components if c.isDriving()] + if self._state: + # self.log.info('merge %r', [c.status for c in self.components]) + if names: + direction = 'down ' if self._state == DECREASE else '' + return BUSY, 'ramping %s%s' % (direction, ', '.join(names)) + if self.status[0] == BUSY: + return self.status + return BUSY, 'driving' + if names: + return WARN, 'moving %s directly' % ', '.join(names) + names = [c.name for c in self.components if c.status[0] >= WARN] + if names: + return WARN, 'warnings in %s' % ', '.join(names) + return IDLE, '' + + def write_target(self, value): + """initiate target change""" + # first make sure target is valid + for target, component in zip(self.target, self.components): + # check against limits if individual components + component.check_limits(target) + if sum(v * v for v in value) > 1: + raise BadValueError('norm of vector too high') + self.log.info('decrease') + self.setFastPoll(True) + self.target = value + self._state = DECREASE + self.doPoll() + self.log.info('done write_target %r', value) + return Done diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index 4c14ccf..c4fbf75 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -21,10 +21,10 @@ """oxford instruments mercury IPS power supply""" import time -from secop.core import Parameter, EnumType, FloatRange, BoolType +from secop.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY from secop.lib.enum import Enum from secop.errors import BadValueError, HardwareError -from secop_psi.magfield import Magfield +from secop_psi.magfield import Magfield, SimpleMagfield, Status from secop_psi.mercury import MercuryChannel, off_on, Mapped from secop.lib.statemachine import Retry @@ -34,54 +34,39 @@ hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, CURRENT_CHECK_SIZE = 2 -class Field(MercuryChannel, Magfield): +class SimpleField(MercuryChannel, SimpleMagfield): + nunits = Property('number of IPS subunits', IntRange(1, 6), default=1) 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) - I1 = Parameter('master current', FloatRange(unit='A'), default=0) - I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0) - I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0) - V1 = Parameter('master voltage', FloatRange(unit='V'), default=0) - V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0) - V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0) - forced_persistent_field = Parameter( - 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) - + working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0) channel_type = 'PSU' - _field_mismatch = None - nslaves = 3 slave_currents = None - __init = True + classdict = {} - def doPoll(self): - super().doPoll() - self.read_current() + def __new__(cls, name, logger, cfgdict, srv): + base = cls.__bases__[1] + nunits = cfgdict.get('nunits', 1) + if nunits == 1: + obj = object.__new__(cls) + return obj + classname = cls.__name__ + str(nunits) + newclass = cls.classdict.get(classname) + if not newclass: + # create individual current and voltage parameters dynamically + attrs = {} + for i in range(1, nunits + 1): + attrs['I%d' % i] = Parameter('slave %s current' % i, FloatRange(unit='A'), default=0) + attrs['V%d' % i] = Parameter('slave %s voltage' % i, FloatRange(unit='V'), default=0) + + newclass = type(classname, (cls,), attrs) + cls.classdict[classname] = newclass + obj = object.__new__(newclass) + return obj def read_value(self): - self.current = self.query('PSU:SIG:FLD') - pf = self.query('PSU:SIG:PFLD') - if self.__init: - self.__init = False - self.persistent_field = pf - if self.switch_heater == self.switch_heater.on or self._field_mismatch is None: - self.forced_persistent_field = False - self._field_mismatch = False - return self.current - self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance - return pf - - def write_persistent_field(self, value): - if self.forced_persistent_field: - self._field_mismatch = False - return value - raise BadValueError('changing persistent field needs forced_persistent_field=True') - - def write_target(self, target): - if self._field_mismatch: - self.forced_persistent_field = True - raise BadValueError('persistent field does not match - set persistent field to guessed value first') - return super().write_target(target) + return self.query('PSU:SIG:FLD') def read_ramp(self): return self.query('PSU:SIG:RFST') @@ -95,86 +80,224 @@ class Field(MercuryChannel, Magfield): def write_action(self, value): return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp) - def read_switch_heater(self): - value = self.query('PSU:SIG:SWHT', off_on) - now = time.time() - if value != self.switch_heater: - if now < (self.switch_time[self.switch_heater] or 0) + 10: - # probably switch heater was changed, but IPS reply is not yet updated - return self.switch_heater - return value - - def write_switch_heater(self, 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_working_ramp(self): + return self.query('PSU:SIG:RFLD') + def read_setpoint(self): return self.query('PSU:SIG:FSET') + def set_and_go(self, value): + self.setpoint = 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_target(self, sm): + # if self.action != 'hold': + # assert self.write_action('hold') == 'hold' + # return Retry + self.set_and_go(sm.target) + sm.try_cnt = 5 + return self.ramp_to_target + + def ramp_to_target(self, sm): + try: + return super().ramp_to_target(sm) + except HardwareError: + sm.try_cnt -= 1 + if sm.try_cnt < 0: + raise + self.set_and_go(sm.target) + return Retry + + def final_status(self, *args, **kwds): + print('FINAL-hold') + self.write_action('hold') + return super().final_status(*args, **kwds) + + def on_restart(self, sm): + print('ON_RESTART-hold', sm.sm) + self.write_action('hold') + return super().on_restart(sm) + + +class Field(SimpleField, Magfield): + wait_switch_on = Parameter( + 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60) + wait_switch_off = Parameter( + 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=True, default=60) + forced_persistent_field = Parameter( + 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) + + _field_mismatch = None + __init = True + __switch_heater_fix = 0 + + def doPoll(self): + super().doPoll() + self.read_current() + + def startModule(self, start_events): + # on restart, assume switch is changed long time ago, if not, the mercury + # will complain and this will be handled in start_ramp_to_field + self.switch_on_time = 0 + self.switch_off_time = 0 + self.switch_heater = self.query('PSU:SIG:SWHT', off_on) + super().startModule(start_events) + + def read_value(self): + current = self.query('PSU:SIG:FLD') + pf = self.query('PSU:SIG:PFLD') + if self.__init: + self.__init = False + self.persistent_field = pf + if self.switch_heater == self.switch_heater.on or self._field_mismatch is None: + self.forced_persistent_field = False + self._field_mismatch = False + return current + self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance + return pf + def read_current(self): if self.slave_currents is None: - self.slave_currents = [[] for _ in range(self.nslaves + 1)] - current = self.query('PSU:SIG:CURR') - for i in range(self.nslaves + 1): - if i: + self.slave_currents = [[] for _ in range(self.nunits + 1)] + if self.nunits > 1: + for i in range(1, self.nunits + 1): curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i) volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i) setattr(self, 'I%d' % i, curri) setattr(self, 'V%d' % i, volti) self.slave_currents[i].append(curri) - else: - self.slave_currents[i].append(current) - min_i = min(self.slave_currents[i]) - max_i = max(self.slave_currents[i]) - min_ = min(self.slave_currents[0]) / self.nslaves - max_ = max(self.slave_currents[0]) / self.nslaves - if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE: - self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:] - if i and (min_i -1 > max_ or min_ > max_i + 1): - self.log.warning('individual currents mismatch %r', self.slave_currents) + current = self.query('PSU:SIG:CURR') + self.slave_currents[0].append(current) + min_ = min(self.slave_currents[0]) / self.nunits + max_ = max(self.slave_currents[0]) / self.nunits + # keep one element more for the total current (first and last measurement is a total) + self.slave_currents[0] = self.slave_currents[0][-CURRENT_CHECK_SIZE-1:] + for i in range(1, self.nunits + 1): + min_i = min(self.slave_currents[i]) + max_i = max(self.slave_currents[i]) + if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE: + self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:] + if min_i - 0.1 > max_ or min_ > max_i + 0.1: # use an arbitrary 0.1 A tolerance + self.log.warning('individual currents mismatch %r', self.slave_currents) + else: + current = self.query('PSU:SIG:CURR') if self.atob: return current / self.atob return 0 - 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 write_persistent_field(self, value): + if self.forced_persistent_field: + self._field_mismatch = False + return value + raise BadValueError('changing persistent field needs forced_persistent_field=True') - def start_ramp_to_field(self, state): + def write_target(self, target): + if self._field_mismatch: + self.forced_persistent_field = True + raise BadValueError('persistent field does not match - set persistent field to guessed value first') + return super().write_target(target) + + def read_switch_heater(self): + value = self.query('PSU:SIG:SWHT', off_on) + now = time.time() + if value != self.switch_heater: + if now < self.__switch_heater_fix: + # probably switch heater was changed, but IPS reply is not yet updated + if self.switch_heater: + self.switch_on_time = time.time() + else: + self.switch_off_time = time.time() + return self.switch_heater + return value + + def read_wait_switch_on(self): + return self.query('PSU:SWONT') * 0.001 + + def read_wait_switch_off(self): + return self.query('PSU:SWOFT') * 0.001 + + def write_switch_heater(self, value): + if value == self.read_switch_heater(): + self.log.info('switch heater already %r', value) + # we do not want to restart the timer + return value + self.__switch_heater_fix = time.time() + 10 + return self.change('PSU:SIG:SWHT', value, off_on) + + def start_ramp_to_field(self, sm): + if abs(self.current - self.persistent_field) <= self.tolerance: + self.log.info('leads %g are already at %g', self.current, self.persistent_field) + return self.ramp_to_field try: self.set_and_go(self.persistent_field) - except (HardwareError, AssertionError): - state.switch_undef = self.switch_time[self.switch_heater.on] or state.now + except (HardwareError, AssertionError) as e: + if self.switch_heater: + self.log.warn('switch is already on!') + return self.ramp_to_field + self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field) + return Retry + self.status = Status.PREPARING, 'wait for switch off' + sm.after_wait = self.ramp_to_field return self.wait_for_switch return self.ramp_to_field - def ramp_to_field(self, state): - if self.action != 'run_to_set': - self.status = Status.PREPARING, 'restart ramp to field' - return self.start_ramp_to_field - return super().ramp_to_field(state) - - def wait_for_switch(self, state): - if state.now - state.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.set_and_go(self.target) + def start_ramp_to_target(self, sm): + sm.try_cnt = 5 + try: + self.set_and_go(sm.target) + except (HardwareError, AssertionError) as e: + self.log.warn('switch not yet ready %r', e) + self.status = Status.PREPARING, 'wait for switch on' + sm.after_wait = self.ramp_to_target + return self.wait_for_switch return self.ramp_to_target - def start_ramp_to_zero(self, state): - assert self.write_action('hold') == 'hold' - assert self.write_action('run_to_zero') == 'run_to_zero' + def ramp_to_field(self, sm): + try: + return super().ramp_to_field(sm) + except HardwareError: + sm.try_cnt -= 1 + if sm.try_cnt < 0: + raise + self.set_and_go(sm.persistent_field) + return Retry + + def wait_for_switch(self, sm): + if not self.delay(10): + return Retry + try: + self.log.warn('try again') + # try again + self.set_and_go(self.persistent_field) + except (HardwareError, AssertionError) as e: + return Retry + return sm.after_wait + + def start_ramp_to_zero(self, sm): + try: + assert self.write_action('hold') == 'hold' + assert self.write_action('run_to_zero') == 'run_to_zero' + except (HardwareError, AssertionError) as e: + self.log.warn('switch not yet ready %r', e) + self.status = Status.PREPARING, 'wait for switch off' + sm.after_wait = self.ramp_to_zero + return self.wait_for_switch return self.ramp_to_zero - def finish_state(self, state): - self.write_action('hold') - super().finish_state(state) + def ramp_to_zero(self, sm): + try: + return super().ramp_to_zero(sm) + except HardwareError: + sm.try_cnt -= 1 + if sm.try_cnt < 0: + raise + assert self.write_action('hold') == 'hold' + assert self.write_action('run_to_zero') == 'run_to_zero' + return Retry diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index b7d42f6..ee1ac82 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -20,12 +20,12 @@ """generic persistent magnet driver""" import time -from secop.core import Drivable, Parameter, Done +from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType from secop.features import HasLimits -from secop.errors import ConfigError, ProgrammingError +from secop.errors import ConfigError, ProgrammingError, HardwareError from secop.lib.enum import Enum -from secop.lib.statemachine import Retry, StateMachine +from secop.states import Retry, HasStates, status_code UNLIMITED = FloatRange() @@ -48,52 +48,23 @@ OFF = 0 ON = 1 -class Magfield(HasLimits, Drivable): +class SimpleMagfield(HasStates, HasLimits, Drivable): value = Parameter('magnetic field', datatype=FloatRange(unit='T')) - status = Parameter(datatype=StatusType(Status)) - mode = Parameter( - 'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) + ramp = Parameter( + 'wanted ramp rate for field', FloatRange(unit='$/min'), readonly=False) + # export only when different from ramp: + workingramp = Parameter( + 'effective ramp rate for field', FloatRange(unit='$/min'), export=False) tolerance = Parameter( 'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002) - switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON), - readonly=False, default=0) - persistent_field = Parameter( - 'persistent field', FloatRange(unit='$'), readonly=False) - current = Parameter( - 'leads current (in units of field)', FloatRange(unit='$')) - ramp = Parameter( - 'ramp rate for field', FloatRange(unit='$/min'), readonly=False) trained = Parameter( 'trained field (positive)', TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')), readonly=False, default=(0, 0)) - # TODO: time_to_target - # profile = Parameter( - # 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), - # readonly=False) - # profile_training = Parameter( - # 'ramp limit table when in training', - # ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False) - # TODO: the following parameters should be changed into properties after tests - wait_switch_on = Parameter( - 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=60) - wait_switch_off = Parameter( - 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=60) - wait_stable_leads = Parameter( - '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=30) - persistent_limit = Parameter( - 'above this limit, lead currents are not driven to 0', - FloatRange(0, unit='$'), readonly=False, default=99) + 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31) - _state = None _last_target = None - switch_time = None, None - - def doPoll(self): - self.read_value() - self._state.cycle() def checkProperties(self): dt = self.parameters['target'].datatype @@ -104,216 +75,285 @@ class Magfield(HasLimits, Drivable): dt.min = -max_ super().checkProperties() - 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 startModule(self, start_events): - start_events.queue(self.startupCheck) - super().startModule(start_events) - - def startupCheck(self): - 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_mode(self.mode) - self.write_target(self.persistent_field) - else: - self._last_target = self.persistent_field - - def write_target(self, target): - self.check_limits(target) - self.target = target - if not self._state.is_active: - # as long as the state machine is still running, it takes care of changing targets - self._state.start(self.start_field_change) - self.doPoll() - return Done - - def write_mode(self, value): - self.mode = value - if not self._state.is_active: - self._state.start(self.start_field_change) - self.doPoll() - return Done - - def cleanup_state(self, state): - self.status = Status.ERROR, repr(state.last_error) - self.log.error('in state %s: %r', state.state.__name__, state.last_error) - self.setFastPoll(False) - if self.switch_heater != 0: - self.persistent_field = self.read_value() - if self.mode != Mode.DRIVEN: - self.log.warning('turn switch heater off') - self.write_switch_heater(0) - def stop(self): """keep field at current value""" # let the state machine do the needed steps to finish self.write_target(self.value) - def start_field_change(self, state): + def write_target(self, target): + self.check_limits(target) + self.start_machine(self.start_field_change, target=target) + return target + + def init_progress(self, sm, value): + sm.prev_point = sm.now, value + + def get_progress(self, sm, value): + """return the time passed for at least one tolerance step""" + t, v = sm.prev_point + dif = abs(v - value) + tdif = sm.now - t + if dif > self.tolerance: + sm.prev_point = sm.now, value + return tdif + + @status_code(BUSY, 'start ramp to target') + def start_field_change(self, sm): self.setFastPoll(True, 1.0) - self.status = Status.PREPARING, 'changed target field' - if (self.target == self._last_target and - abs(self.target - self.persistent_field) <= self.tolerance): # short cut + return self.start_ramp_to_target + + @status_code(BUSY, 'ramping field') + def ramp_to_target(self, sm): + if sm.init: + self.init_progress(sm, self.value) + # Remarks: assume there is a ramp limiting feature + if abs(self.value - sm.target) > self.tolerance: + if self.get_progress(sm, self.value): + return Retry + raise HardwareError('no progress') + sm.stabilize_start = time.time() + return self.stabilize_field + + @status_code(BUSY, 'stabilizing field') + def stabilize_field(self, sm): + if sm.now - sm.stabilize_start < self.wait_stable_field: + return Retry + return self.final_status() + + def read_workingramp(self): + return self.ramp + + +class Magfield(SimpleMagfield): + status = Parameter(datatype=StatusType(Status)) + mode = Parameter( + 'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) + switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON), + readonly=False, default=0) + persistent_field = Parameter( + 'persistent field', FloatRange(unit='$'), readonly=False) + current = Parameter( + 'leads current (in units of field)', FloatRange(unit='$')) + # TODO: time_to_target + # profile = Parameter( + # 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), + # readonly=False) + # profile_training = Parameter( + # 'ramp limit table when in training', + # ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False) + # TODO: the following parameters should be changed into properties after tests + wait_switch_on = Parameter( + 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61) + wait_switch_off = Parameter( + 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61) + wait_stable_leads = Parameter( + 'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6) + persistent_limit = Parameter( + 'above this limit, lead currents are not driven to 0', + FloatRange(0, unit='$'), readonly=False, default=99) + leads_ramp_tmo = Parameter( + 'timeout for leads ramp progress', + FloatRange(0, unit='s'), readonly=False, default=30) + ramp_tmo = Parameter( + 'timeout for field ramp progress', + FloatRange(0, unit='s'), readonly=False, default=30) + __init = True + switch_on_time = None + switch_off_time = None + + def doPoll(self): + 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_field) + else: + self._last_target = self.persistent_field + else: + super().doPoll() + + def initModule(self): + super().initModule() + self.registerCallbacks(self) # for update_switch_heater + + def write_mode(self, value): + self.start_machine(self.start_field_change, cleanup=self.cleanup, target=self.target, mode=value) + return value + + def write_target(self, target): + self.check_limits(target) + self.start_machine(self.start_field_change, cleanup=self.cleanup, target=target, mode=self.mode) + return target + + def cleanup(self, sm): # sm is short for statemachine + if self.switch_heater != 0: + self.persistent_field = self.read_value() + if sm.mode != Mode.DRIVEN: + self.log.warning('turn switch heater off') + self.write_switch_heater(0) + + @status_code('PREPARING') + def start_field_change(self, sm): + self.setFastPoll(True, 1.0) + if sm.target == self.persistent_field or ( + sm.target == self._last_target and + abs(sm.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off + if self.switch_heater: + return self.start_switch_on return self.start_ramp_to_field - def start_ramp_to_field(self, state): + @status_code('PREPARING') + def start_ramp_to_field(self, sm): """start ramping current to persistent field - should return ramp_to_field + initiate ramp to persistent field (with corresponding ramp rate) + the implementation should return ramp_to_field """ raise NotImplementedError - def ramp_to_field(self, state): - """ramping, wait for current at persistent field""" - 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: - self.status = Status.PREPARING, 'ramping leads current to field' - return Retry() - state.stabilize_start = time.time() + @status_code('PREPARING', 'ramp leads to match field') + def ramp_to_field(self, sm): + if sm.init: + sm.stabilize_start = 0 # in case current is already at field + self.init_progress(sm, self.current) + dif = abs(self.current - self.persistent_field) + if dif > self.tolerance: + tdif = self.get_progress(sm, self.current) + if tdif > self.leads_ramp_tmo: + raise HardwareError('no progress') + sm.stabilize_start = None # force reset + return Retry + if sm.stabilize_start is None: + sm.stabilize_start = sm.now return self.stabilize_current - def stabilize_current(self, state): - """wait for stable current at persistent field""" - if state.now - state.stabilize_start < self.wait_stable_leads: - if state.init: - self.status = Status.PREPARING, 'stabilizing leads current' - return Retry() + @status_code('PREPARING') + def stabilize_current(self, sm): + if sm.now - sm.stabilize_start < self.wait_stable_leads: + return Retry return self.start_switch_on def update_switch_heater(self, value): """is called whenever switch heater was changed""" - switch_time = self.switch_time[value] - if switch_time is None: - switch_time = time.time() - self.switch_time = [None, None] - self.switch_time[value] = switch_time + print('SW', value) + if value == 0: + if self.switch_off_time is None: + self.log.info('restart switch_off_time') + self.switch_off_time = time.time() + self.switch_on_time = None + else: + if self.switch_on_time is None: + self.log.info('restart switch_on_time') + self.switch_on_time = time.time() + self.switch_off_time = None - def start_switch_on(self, state): - """switch heater on""" - if self.switch_heater == 0: + @status_code('PREPARING') + def start_switch_on(self, sm): + if self.read_switch_heater() == 0: self.status = Status.PREPARING, 'turn switch heater on' try: self.write_switch_heater(True) except Exception as e: self.log.warning('write_switch_heater %r', e) - return Retry() + return Retry else: self.status = Status.PREPARING, 'wait for heater on' - return self.switch_on + return self.wait_for_switch_on - def switch_on(self, state): - """wait for switch heater open""" - if (self.target == self._last_target and - abs(self.target - self.persistent_field) <= self.tolerance): # short cut + @status_code('PREPARING') + def wait_for_switch_on(self, sm): + if (sm.target == self._last_target and + abs(sm.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off - self.read_switch_heater() - if self.switch_time[ON] is None: + self.read_switch_heater() # trigger switch_on/off_time + if self.switch_heater == 0: self.log.warning('switch turned off manually?') return self.start_switch_on - if state.now - self.switch_time[ON] < self.wait_switch_on: - return Retry() - self._last_target = self.target + if sm.now - self.switch_on_time < self.wait_switch_on: + if sm.delta(10): + self.log.info('waited for %g sec', sm.now - self.switch_on_time) + return Retry + self._last_target = sm.target return self.start_ramp_to_target - def start_ramp_to_target(self, state): - """start ramping current to target + @status_code('RAMPING') + def start_ramp_to_target(self, sm): + """start ramping current to target field - should return ramp_to_target + initiate ramp to target + the implementation should return ramp_to_target """ raise NotImplementedError - def ramp_to_target(self, state): - """ramp field to target""" - if self.target != self._last_target: # target was changed - self._last_target = self.target - return self.start_ramp_to_target + @status_code('RAMPING') + def ramp_to_target(self, sm): self.persistent_field = self.value - # Remarks: assume there is a ramp limiting feature - if abs(self.value - self.target) > self.tolerance: - if state.init: - self.status = Status.RAMPING, 'ramping field' - return Retry() - state.stabilize_start = time.time() + dif = abs(self.value - sm.target) + if sm.init: + sm.stabilize_start = 0 # in case current is already at target + self.init_progress(sm, self.value) + if dif > self.tolerance: + sm.stabilize_start = sm.now + tdif = self.get_progress(sm, self.value) + if tdif > self.workingramp / self.tolerance * 60 + self.ramp_tmo: + raise HardwareError('no progress') + sm.stabilize_start = None + return Retry + if sm.stabilize_start is None: + sm.stabilize_start = sm.now return self.stabilize_field - 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 + @status_code('STABILIZING') + def stabilize_field(self, sm): self.persistent_field = self.value - if state.now - state.stabilize_start < self.wait_stable_field: - if state.init: - self.status = Status.STABILIZING, 'stabilizing field' - return Retry() + if sm.now > sm.stablize_start + self.wait_stable_field: + return Retry return self.check_switch_off - def check_switch_off(self, state): - if self.mode == Mode.DRIVEN: - self.status = Status.PREPARED, 'driven' - return self.finish_state + def check_switch_off(self, sm): + if sm.mode == Mode.DRIVEN: + return self.final_status(Status.PREPARED, 'driven') return self.start_switch_off - def start_switch_off(self, state): - """turn off switch heater""" + @status_code('FINALIZING') + def start_switch_off(self, sm): 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' - return self.switch_off + return self.wait_for_switch_off - def switch_off(self, state): - """wait for switch heater closed""" - 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 + @status_code('FINALIZING') + def wait_for_switch_off(self, sm): self.persistent_field = self.value self.read_switch_heater() - if self.switch_time[OFF] is None: + if self.switch_off_time is None: self.log.warning('switch turned on manually?') return self.start_switch_off - if state.now - self.switch_time[OFF] < self.wait_switch_off: - return Retry() + if sm.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.final_status(Status.IDLE, 'leads current at field, switch off') return self.start_ramp_to_zero - def start_ramp_to_zero(self, state): - """start ramping current to target + @status_code('FINALIZING') + def start_ramp_to_zero(self, sm): + """start ramping current to zero initiate ramp to zero (with corresponding ramp rate) - should return ramp_to_zero + the implementation should return ramp_to_zero """ raise NotImplementedError - def ramp_to_zero(self, state): + @status_code('FINALIZING') + def ramp_to_zero(self, sm): """ramp field to zero""" - 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 sm.init: + self.init_progress(sm, self.current) if abs(self.current) > self.tolerance: - if state.init: - self.status = Status.FINALIZING, 'ramp leads to zero' - return Retry() - if self.mode == Mode.DISABLED and self.persistent_field == 0: - self.status = Status.DISABLED, 'disabled' - else: - self.status = Status.IDLE, 'persistent mode' - return self.finish_state - - def finish_state(self, state): - """finish""" - self.setFastPoll(False) - return None + if self.get_progress(sm, self.current, self.ramp) > self.leads_ramp_tmo: + raise HardwareError('no progress') + return Retry + if sm.mode == Mode.DISABLED and self.persistent_field == 0: + return self.final_status(Status.DISABLED, 'disabled') + return self.final_status(Status.IDLE, 'persistent mode') diff --git a/secop_psi/vector.py b/secop_psi/vector.py new file mode 100644 index 0000000..1a46780 --- /dev/null +++ b/secop_psi/vector.py @@ -0,0 +1,89 @@ +# -*- 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 +# ***************************************************************************** +"""generic 3D vector""" + +from secop.core import Attached, Drivable, Readable, Parameter, Done +from secop.datatypes import FloatRange, TupleOf, StatusType, Enum + + +class VectorRd(Readable): + """generic readable vector""" + value = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange())) + x = Attached() + y = Attached() + z = Attached() + pollFuncs = None + components = None + + def initModule(self): + super().initModule() + members = [] + status_codes = {} # collect all possible status codes + components = [] + for name in 'xyz': + component = getattr(self, name) + members.append(component.parameters['value'].datatype.copy()) + components.append(component) + for code in component.status[0].enum.members: + status_codes[int(code)] = code.name + self.parameters['value'].datatype = TupleOf(*members) + self.parameters['status'].datatype = StatusType(Enum( + 'status', **{k: v for v, k in status_codes.items()})) + self.components = components + + def doPoll(self): + for component in self.components: + component.doPoll() + # update + component.pollInfo.last_main = self.pollInfo.last_main + self.value = self.merge_value() + self.status = self.merge_status() + + def merge_value(self): + return [c.value for c in self.components] + + def merge_status(self): + status = -1, '' + for c in self.components: + if c.status[0] > status[0]: + status = c.status + return status + + def read_value(self): + return tuple((c.read_value() for c in self.components)) + + def read_status(self): + [c.read_status() for c in self.components] + return self.merge_status() + + +class Vector(Drivable, VectorRd): + """generic drivable vector""" + target = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange())) + + def initModule(self): + super().initModule() + members = [] + for component in self.components: + members.append(component.parameters['target'].datatype.copy()) + self.parameters['target'].datatype = TupleOf(*members) + + def write_target(self, value): + return tuple((c.write_target(v) for v, c in zip(value, self.components))) \ No newline at end of file From 4f5cc3059e63a4aebf631058d682a887cc447c7b Mon Sep 17 00:00:00 2001 From: camea Date: Wed, 23 Nov 2022 22:41:23 +0100 Subject: [PATCH 07/11] add FT furnace --- cfg/main/ft.cfg | 22 +++++ cfg/sea/ft.config.json | 183 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 205 insertions(+) create mode 100644 cfg/main/ft.cfg create mode 100644 cfg/sea/ft.config.json diff --git a/cfg/main/ft.cfg b/cfg/main/ft.cfg new file mode 100644 index 0000000..5e82ee6 --- /dev/null +++ b/cfg/main/ft.cfg @@ -0,0 +1,22 @@ +[NODE] +description = FT tantalum furnace (1400 K) +id = ft.config.sea.psi.ch + +[sea_main] +class = secop_psi.sea.SeaClient +description = main sea connection for fw.config +config = ft.config +service = main + +[ts] +class = secop_psi.sea.SeaDrivable +io = sea_main +sea_object = tt +rel_paths = . ts + +[t2] +class = secop_psi.sea.SeaReadable +io = sea_main +sea_object = tt +rel_paths = tm + diff --git a/cfg/sea/ft.config.json b/cfg/sea/ft.config.json new file mode 100644 index 0000000..853eace --- /dev/null +++ b/cfg/sea/ft.config.json @@ -0,0 +1,183 @@ +{"tt": {"base": "/tt", "params": [ +{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 20}, +{"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}, +{"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3}, +{"path": "target", "type": "float"}, +{"path": "running", "type": "int"}, +{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"}, +{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"}, +{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"}, +{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4}, +{"path": "log/mean", "type": "float", "visibility": 3}, +{"path": "log/m2", "type": "float", "visibility": 3}, +{"path": "log/stddev", "type": "float", "visibility": 3}, +{"path": "log/n", "type": "float", "visibility": 3}, +{"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "visibility": 3, "kids": 9}, +{"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift", "visibility": 3}, +{"path": "dblctrl/mode", "type": "enum", "enum": {"disabled": -1, "inactive": 0, "stable": 1, "up": 2, "down": 3}, "readonly": false, "cmd": "tt dblctrl/mode", "visibility": 3}, +{"path": "dblctrl/shift_up", "type": "float", "visibility": 3}, +{"path": "dblctrl/shift_lo", "type": "float", "visibility": 3}, +{"path": "dblctrl/t_min", "type": "float", "visibility": 3}, +{"path": "dblctrl/t_max", "type": "float", "visibility": 3}, +{"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2", "visibility": 3}, +{"path": "dblctrl/prop_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up", "visibility": 3}, +{"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo", "visibility": 3}, +{"path": "ts", "type": "float", "kids": 4}, +{"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1}, +{"path": "ts/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts/curve/points", "visibility": 3}, +{"path": "ts/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"}, +{"path": "ts/stddev", "type": "float"}, +{"path": "ts/raw", "type": "float"}, +{"path": "tm", "type": "float", "kids": 4}, +{"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1}, +{"path": "tm/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt tm/curve/points", "visibility": 3}, +{"path": "tm/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"}, +{"path": "tm/stddev", "type": "float"}, +{"path": "tm/raw", "type": "float"}, +{"path": "te", "type": "float", "kids": 4}, +{"path": "te/curve", "type": "text", "readonly": false, "cmd": "tt te/curve", "kids": 1}, +{"path": "te/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt te/curve/points", "visibility": 3}, +{"path": "te/alarm", "type": "float", "readonly": false, "cmd": "tt te/alarm"}, +{"path": "te/stddev", "type": "float"}, +{"path": "te/raw", "type": "float"}, +{"path": "p", "type": "float", "kids": 4}, +{"path": "p/curve", "type": "text", "readonly": false, "cmd": "tt p/curve", "kids": 1}, +{"path": "p/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt p/curve/points", "visibility": 3}, +{"path": "p/alarm", "type": "float", "readonly": false, "cmd": "tt p/alarm"}, +{"path": "p/stddev", "type": "float"}, +{"path": "p/raw", "type": "float"}, +{"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18}, +{"path": "set/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt set/mode"}, +{"path": "set/reg", "type": "float"}, +{"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, +{"path": "set/wramp", "type": "float", "readonly": false, "cmd": "tt set/wramp"}, +{"path": "set/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"}, +{"path": "set/channel", "type": "text", "readonly": false, "cmd": "tt set/channel"}, +{"path": "set/limit", "type": "float", "readonly": false, "cmd": "tt set/limit"}, +{"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"}, +{"path": "set/maxheater", "type": "text", "readonly": false, "cmd": "tt set/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"}, +{"path": "set/linearpower", "type": "float", "readonly": false, "cmd": "tt set/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"}, +{"path": "set/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"}, +{"path": "set/maxpower", "type": "float", "readonly": false, "cmd": "tt set/maxpower", "description": "maximum power [W]"}, +{"path": "set/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"}, +{"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"}, +{"path": "set/power", "type": "float"}, +{"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"}, +{"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"}]}, + +"table": {"base": "/table", "params": [ +{"path": "", "type": "none", "kids": 8}, +{"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "fix_tt_set_prop", "type": "bool", "readonly": false, "cmd": "table fix_tt_set_prop"}, +{"path": "val_tt_set_prop", "type": "float"}, +{"path": "tbl_tt_set_prop", "type": "text", "readonly": false, "cmd": "table tbl_tt_set_prop", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}, +{"path": "fix_tt_set_integ", "type": "bool", "readonly": false, "cmd": "table fix_tt_set_integ"}, +{"path": "val_tt_set_integ", "type": "float"}, +{"path": "tbl_tt_set_integ", "type": "text", "readonly": false, "cmd": "table tbl_tt_set_integ", "description": "enter value pair separated with colon T1:par1 T2:par2 ..."}]}, + +"cc": {"base": "/cc", "params": [ +{"path": "", "type": "bool", "kids": 96}, +{"path": "send", "type": "text", "readonly": false, "cmd": "cc send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "autodevice", "type": "bool", "readonly": false, "cmd": "cc autodevice"}, +{"path": "fav", "type": "bool", "readonly": false, "cmd": "cc fav"}, +{"path": "f", "type": "float", "visibility": 3}, +{"path": "fs", "type": "enum", "enum": {"ok": 0, "no_sens": 1}, "readonly": false, "cmd": "cc fs", "visibility": 3}, +{"path": "mav", "type": "bool", "readonly": false, "cmd": "cc mav"}, +{"path": "fm", "type": "enum", "enum": {"idle": 0, "opening": 1, "closing": 2, "opened": 3, "closed": 4, "no_motor": 5}, "visibility": 3}, +{"path": "fa", "type": "enum", "enum": {"fixed": 0, "controlled": 1, "automatic": 2, "offline": 3}, "readonly": false, "cmd": "cc fa", "visibility": 3}, +{"path": "mp", "type": "float", "readonly": false, "cmd": "cc mp", "visibility": 3}, +{"path": "msp", "type": "float", "visibility": 3}, +{"path": "mmp", "type": "float", "visibility": 3}, +{"path": "mc", "type": "float", "readonly": false, "cmd": "cc mc", "visibility": 3}, +{"path": "mfc", "type": "float", "readonly": false, "cmd": "cc mfc", "visibility": 3}, +{"path": "moc", "type": "float", "readonly": false, "cmd": "cc moc", "visibility": 3}, +{"path": "mtc", "type": "float", "readonly": false, "cmd": "cc mtc", "visibility": 3}, +{"path": "mtl", "type": "float", "visibility": 3}, +{"path": "mft", "type": "float", "readonly": false, "cmd": "cc mft", "visibility": 3}, +{"path": "mt", "type": "float", "visibility": 3}, +{"path": "mo", "type": "float", "visibility": 3}, +{"path": "mcr", "type": "float", "visibility": 3}, +{"path": "mot", "type": "float", "visibility": 3}, +{"path": "mw", "type": "float", "readonly": false, "cmd": "cc mw", "description": "correction pulse after automatic open", "visibility": 3}, +{"path": "hav", "type": "bool", "readonly": false, "cmd": "cc hav"}, +{"path": "h", "type": "float", "visibility": 3}, +{"path": "hr", "type": "float", "visibility": 3}, +{"path": "hc", "type": "float", "visibility": 3}, +{"path": "hu", "type": "float", "visibility": 3}, +{"path": "hh", "type": "float", "readonly": false, "cmd": "cc hh", "visibility": 3}, +{"path": "hl", "type": "float", "readonly": false, "cmd": "cc hl", "visibility": 3}, +{"path": "htf", "type": "float", "readonly": false, "cmd": "cc htf", "description": "meas. period in fast mode", "visibility": 3}, +{"path": "hts", "type": "float", "readonly": false, "cmd": "cc hts", "description": "meas. period in slow mode", "visibility": 3}, +{"path": "hd", "type": "float", "readonly": false, "cmd": "cc hd", "visibility": 3}, +{"path": "hwr", "type": "float", "readonly": false, "cmd": "cc hwr", "visibility": 3}, +{"path": "hem", "type": "float", "readonly": false, "cmd": "cc hem", "description": "sensor length in mm from top to empty pos.", "visibility": 3}, +{"path": "hfu", "type": "float", "readonly": false, "cmd": "cc hfu", "description": "sensor length in mm from top to full pos.", "visibility": 3}, +{"path": "hcd", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3, "manual": 7}, "readonly": false, "cmd": "cc hcd", "visibility": 3}, +{"path": "hv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4}, "visibility": 3}, +{"path": "hsf", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, +{"path": "ha", "type": "bool", "readonly": false, "cmd": "cc ha", "visibility": 3}, +{"path": "hm", "type": "bool", "visibility": 3}, +{"path": "hf", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf", "visibility": 3}, +{"path": "hbe", "type": "bool", "readonly": false, "cmd": "cc hbe", "visibility": 3}, +{"path": "hmf", "type": "float", "visibility": 3}, +{"path": "hms", "type": "float", "visibility": 3}, +{"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit", "visibility": 3}, +{"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft", "visibility": 3}, +{"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": "hfb", "type": "float", "visibility": 3}, +{"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"}, +{"path": "nu", "type": "float", "visibility": 3}, +{"path": "nl", "type": "float", "visibility": 3}, +{"path": "nth", "type": "float", "readonly": false, "cmd": "cc nth", "visibility": 3}, +{"path": "ntc", "type": "float", "readonly": false, "cmd": "cc ntc", "visibility": 3}, +{"path": "ntm", "type": "float", "readonly": false, "cmd": "cc ntm", "visibility": 3}, +{"path": "ns", "type": "enum", "enum": {"sens_ok": 0, "no_sens": 1, "short_circuit": 2, "upside_down": 3, "sens_warm": 4, "empty": 5}, "visibility": 3}, +{"path": "na", "type": "bool", "readonly": false, "cmd": "cc na", "visibility": 3}, +{"path": "nv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4, "boost": 5}, "visibility": 3}, +{"path": "nc", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3}, "readonly": false, "cmd": "cc nc", "visibility": 3}, +{"path": "nfb", "type": "float", "visibility": 3}, +{"path": "cda", "type": "float"}, +{"path": "cdb", "type": "float"}, +{"path": "cba", "type": "float"}, +{"path": "cbb", "type": "float"}, +{"path": "cvs", "type": "int"}, +{"path": "csp", "type": "int"}, +{"path": "cdv", "type": "text", "readonly": false, "cmd": "cc cdv"}, +{"path": "cic", "type": "text", "readonly": false, "cmd": "cc cic"}, +{"path": "cin", "type": "text"}, +{"path": "cds", "type": "enum", "enum": {"local": 0, "remote": 1, "loading": 2, "by_code": 3, "by_touch": 4}, "readonly": false, "cmd": "cc cds"}, +{"path": "timing", "type": "bool", "readonly": false, "cmd": "cc timing"}, +{"path": "tc", "type": "float", "visibility": 3}, +{"path": "tn", "type": "float", "visibility": 3}, +{"path": "th", "type": "float", "visibility": 3}, +{"path": "tf", "type": "float", "visibility": 3}, +{"path": "tm", "type": "float", "visibility": 3}, +{"path": "tv", "type": "float", "visibility": 3}, +{"path": "tq", "type": "float", "visibility": 3}, +{"path": "bdl", "type": "float", "readonly": false, "cmd": "cc bdl"}]}} From f14bd553a8892644325cbcaca7b24a6be63a0d3f Mon Sep 17 00:00:00 2001 From: dmc Date: Thu, 24 Nov 2022 07:49:36 +0100 Subject: [PATCH 08/11] disable closed loop on MA10 rotator --- cfg/main/ma10.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfg/main/ma10.cfg b/cfg/main/ma10.cfg index 67494cb..023e336 100644 --- a/cfg/main/ma10.cfg +++ b/cfg/main/ma10.cfg @@ -68,5 +68,5 @@ description = stick rotation, typically used for omega class = secop_psi.phytron.Motor io = om_io sign = -1 -encoder_mode = CHECK +encoder_mode = READ From 4e8636a62065871741ac59d5e1d121c6e7ff09ea Mon Sep 17 00:00:00 2001 From: dmc Date: Thu, 24 Nov 2022 07:50:24 +0100 Subject: [PATCH 09/11] heliox: make temperatures seperate modules --- cfg/stick/heliox.cfg | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/cfg/stick/heliox.cfg b/cfg/stick/heliox.cfg index e0240f8..724167b 100644 --- a/cfg/stick/heliox.cfg +++ b/cfg/stick/heliox.cfg @@ -13,7 +13,26 @@ class = secop_psi.sea.SeaDrivable io = sea_stick sea_object = ts -[th] -class = secop_psi.sea.SeaDrivable +[T_sorb] +class = secop_psi.sea.SeaReadable io = sea_stick sea_object = th +rel_paths = sorb + +[T_plate] +class = secop_psi.sea.SeaReadable +io = sea_stick +sea_object = th +rel_paths = plate + +[T_low] +class = secop_psi.sea.SeaReadable +io = sea_stick +sea_object = th +rel_paths = low + +[T_pot] +class = secop_psi.sea.SeaReadable +io = sea_stick +sea_object = th +rel_paths = pot From 99220400f1428ea26c6c09041a45030e2e56426f Mon Sep 17 00:00:00 2001 From: zebra Date: Thu, 24 Nov 2022 07:55:36 +0100 Subject: [PATCH 10/11] add FS furnace --- cfg/main/fs.cfg | 21 ++++++ cfg/sea/fs.config.json | 160 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 cfg/main/fs.cfg create mode 100644 cfg/sea/fs.config.json diff --git a/cfg/main/fs.cfg b/cfg/main/fs.cfg new file mode 100644 index 0000000..2721dfe --- /dev/null +++ b/cfg/main/fs.cfg @@ -0,0 +1,21 @@ +[NODE] +description = small furnace +id = fs.config.sea.psi.ch + +[sea_main] +class = secop_psi.sea.SeaClient +description = main sea connection for fs.config +config = fs.config +service = main + +[tt] +class = secop_psi.sea.SeaDrivable +io = sea_main +sea_object = tt +rel_paths = . tm + +[ts] +class = secop_psi.sea.SeaDrivable +io = sea_main +sea_object = tt +rel_paths = ts diff --git a/cfg/sea/fs.config.json b/cfg/sea/fs.config.json new file mode 100644 index 0000000..a62740b --- /dev/null +++ b/cfg/sea/fs.config.json @@ -0,0 +1,160 @@ +{"tt": {"base": "/tt", "params": [ +{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 18}, +{"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}, +{"path": "mainloop", "type": "text", "readonly": false, "cmd": "tt mainloop", "visibility": 3}, +{"path": "target", "type": "float"}, +{"path": "running", "type": "int"}, +{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"}, +{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"}, +{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"}, +{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4}, +{"path": "log/mean", "type": "float", "visibility": 3}, +{"path": "log/m2", "type": "float", "visibility": 3}, +{"path": "log/stddev", "type": "float", "visibility": 3}, +{"path": "log/n", "type": "float", "visibility": 3}, +{"path": "dblctrl", "type": "bool", "readonly": false, "cmd": "tt dblctrl", "kids": 9}, +{"path": "dblctrl/tshift", "type": "float", "readonly": false, "cmd": "tt dblctrl/tshift"}, +{"path": "dblctrl/mode", "type": "enum", "enum": {"disabled": -1, "inactive": 0, "stable": 1, "up": 2, "down": 3}, "readonly": false, "cmd": "tt dblctrl/mode"}, +{"path": "dblctrl/shift_up", "type": "float"}, +{"path": "dblctrl/shift_lo", "type": "float"}, +{"path": "dblctrl/t_min", "type": "float"}, +{"path": "dblctrl/t_max", "type": "float"}, +{"path": "dblctrl/int2", "type": "float", "readonly": false, "cmd": "tt dblctrl/int2"}, +{"path": "dblctrl/prop_up", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_up"}, +{"path": "dblctrl/prop_lo", "type": "float", "readonly": false, "cmd": "tt dblctrl/prop_lo"}, +{"path": "ts", "type": "float", "kids": 4}, +{"path": "ts/curve", "type": "text", "readonly": false, "cmd": "tt ts/curve", "kids": 1}, +{"path": "ts/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt ts/curve/points", "visibility": 3}, +{"path": "ts/alarm", "type": "float", "readonly": false, "cmd": "tt ts/alarm"}, +{"path": "ts/stddev", "type": "float"}, +{"path": "ts/raw", "type": "float"}, +{"path": "tm", "type": "float", "kids": 4}, +{"path": "tm/curve", "type": "text", "readonly": false, "cmd": "tt tm/curve", "kids": 1}, +{"path": "tm/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt tm/curve/points", "visibility": 3}, +{"path": "tm/alarm", "type": "float", "readonly": false, "cmd": "tt tm/alarm"}, +{"path": "tm/stddev", "type": "float"}, +{"path": "tm/raw", "type": "float"}, +{"path": "set", "type": "float", "readonly": false, "cmd": "tt set", "kids": 18}, +{"path": "set/mode", "type": "enum", "enum": {"disabled": -1, "off": 0, "controlling": 1, "manual": 2}, "readonly": false, "cmd": "tt set/mode"}, +{"path": "set/reg", "type": "float"}, +{"path": "set/ramp", "type": "float", "readonly": false, "cmd": "tt set/ramp", "description": "maximum ramp in K/min (0: ramp off)"}, +{"path": "set/wramp", "type": "float", "readonly": false, "cmd": "tt set/wramp"}, +{"path": "set/smooth", "type": "float", "readonly": false, "cmd": "tt set/smooth", "description": "smooth time (minutes)"}, +{"path": "set/channel", "type": "text", "readonly": false, "cmd": "tt set/channel"}, +{"path": "set/limit", "type": "float", "readonly": false, "cmd": "tt set/limit"}, +{"path": "set/resist", "type": "float", "readonly": false, "cmd": "tt set/resist"}, +{"path": "set/maxheater", "type": "text", "readonly": false, "cmd": "tt set/maxheater", "description": "maximum heater limit, units should be given without space: W, mW, A, mA"}, +{"path": "set/linearpower", "type": "float", "readonly": false, "cmd": "tt set/linearpower", "description": "when not 0, it is the maximum effective power, and the power is linear to the heater output"}, +{"path": "set/maxpowerlim", "type": "float", "description": "the maximum power limit (before any booster or converter)"}, +{"path": "set/maxpower", "type": "float", "readonly": false, "cmd": "tt set/maxpower", "description": "maximum power [W]"}, +{"path": "set/maxcurrent", "type": "float", "description": "the maximum current before any booster or converter"}, +{"path": "set/manualpower", "type": "float", "readonly": false, "cmd": "tt set/manualpower"}, +{"path": "set/power", "type": "float"}, +{"path": "set/prop", "type": "float", "readonly": false, "cmd": "tt set/prop", "description": "bigger means more gain"}, +{"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": [ +{"path": "", "type": "bool", "kids": 96}, +{"path": "send", "type": "text", "readonly": false, "cmd": "cc send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "autodevice", "type": "bool", "readonly": false, "cmd": "cc autodevice"}, +{"path": "fav", "type": "bool", "readonly": false, "cmd": "cc fav"}, +{"path": "f", "type": "float", "visibility": 3}, +{"path": "fs", "type": "enum", "enum": {"ok": 0, "no_sens": 1}, "readonly": false, "cmd": "cc fs", "visibility": 3}, +{"path": "mav", "type": "bool", "readonly": false, "cmd": "cc mav"}, +{"path": "fm", "type": "enum", "enum": {"idle": 0, "opening": 1, "closing": 2, "opened": 3, "closed": 4, "no_motor": 5}, "visibility": 3}, +{"path": "fa", "type": "enum", "enum": {"fixed": 0, "controlled": 1, "automatic": 2, "offline": 3}, "readonly": false, "cmd": "cc fa", "visibility": 3}, +{"path": "mp", "type": "float", "readonly": false, "cmd": "cc mp", "visibility": 3}, +{"path": "msp", "type": "float", "visibility": 3}, +{"path": "mmp", "type": "float", "visibility": 3}, +{"path": "mc", "type": "float", "readonly": false, "cmd": "cc mc", "visibility": 3}, +{"path": "mfc", "type": "float", "readonly": false, "cmd": "cc mfc", "visibility": 3}, +{"path": "moc", "type": "float", "readonly": false, "cmd": "cc moc", "visibility": 3}, +{"path": "mtc", "type": "float", "readonly": false, "cmd": "cc mtc", "visibility": 3}, +{"path": "mtl", "type": "float", "visibility": 3}, +{"path": "mft", "type": "float", "readonly": false, "cmd": "cc mft", "visibility": 3}, +{"path": "mt", "type": "float", "visibility": 3}, +{"path": "mo", "type": "float", "visibility": 3}, +{"path": "mcr", "type": "float", "visibility": 3}, +{"path": "mot", "type": "float", "visibility": 3}, +{"path": "mw", "type": "float", "readonly": false, "cmd": "cc mw", "description": "correction pulse after automatic open", "visibility": 3}, +{"path": "hav", "type": "bool", "readonly": false, "cmd": "cc hav"}, +{"path": "h", "type": "float", "visibility": 3}, +{"path": "hr", "type": "float", "visibility": 3}, +{"path": "hc", "type": "float", "visibility": 3}, +{"path": "hu", "type": "float", "visibility": 3}, +{"path": "hh", "type": "float", "readonly": false, "cmd": "cc hh", "visibility": 3}, +{"path": "hl", "type": "float", "readonly": false, "cmd": "cc hl", "visibility": 3}, +{"path": "htf", "type": "float", "readonly": false, "cmd": "cc htf", "description": "meas. period in fast mode", "visibility": 3}, +{"path": "hts", "type": "float", "readonly": false, "cmd": "cc hts", "description": "meas. period in slow mode", "visibility": 3}, +{"path": "hd", "type": "float", "readonly": false, "cmd": "cc hd", "visibility": 3}, +{"path": "hwr", "type": "float", "readonly": false, "cmd": "cc hwr", "visibility": 3}, +{"path": "hem", "type": "float", "readonly": false, "cmd": "cc hem", "description": "sensor length in mm from top to empty pos.", "visibility": 3}, +{"path": "hfu", "type": "float", "readonly": false, "cmd": "cc hfu", "description": "sensor length in mm from top to full pos.", "visibility": 3}, +{"path": "hcd", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3, "manual": 7}, "readonly": false, "cmd": "cc hcd", "visibility": 3}, +{"path": "hv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4}, "visibility": 3}, +{"path": "hsf", "type": "enum", "enum": {"sens_ok": 0, "sens_warm": 1, "no_sens": 2, "timeout": 3, "not_yet_read": 4, "disabled": 5}, "visibility": 3}, +{"path": "ha", "type": "bool", "readonly": false, "cmd": "cc ha", "visibility": 3}, +{"path": "hm", "type": "bool", "visibility": 3}, +{"path": "hf", "type": "enum", "enum": {"slow": 0, "fast": 1}, "readonly": false, "cmd": "cc hf", "visibility": 3}, +{"path": "hbe", "type": "bool", "readonly": false, "cmd": "cc hbe", "visibility": 3}, +{"path": "hmf", "type": "float", "visibility": 3}, +{"path": "hms", "type": "float", "visibility": 3}, +{"path": "hit", "type": "float", "readonly": false, "cmd": "cc hit", "visibility": 3}, +{"path": "hft", "type": "int", "readonly": false, "cmd": "cc hft", "visibility": 3}, +{"path": "hea", "type": "enum", "enum": {"0": 0, "1": 1, "6": 6}, "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": "hfb", "type": "float", "visibility": 3}, +{"path": "nav", "type": "bool", "readonly": false, "cmd": "cc nav"}, +{"path": "nu", "type": "float", "visibility": 3}, +{"path": "nl", "type": "float", "visibility": 3}, +{"path": "nth", "type": "float", "readonly": false, "cmd": "cc nth", "visibility": 3}, +{"path": "ntc", "type": "float", "readonly": false, "cmd": "cc ntc", "visibility": 3}, +{"path": "ntm", "type": "float", "readonly": false, "cmd": "cc ntm", "visibility": 3}, +{"path": "ns", "type": "enum", "enum": {"sens_ok": 0, "no_sens": 1, "short_circuit": 2, "upside_down": 3, "sens_warm": 4, "empty": 5}, "visibility": 3}, +{"path": "na", "type": "bool", "readonly": false, "cmd": "cc na", "visibility": 3}, +{"path": "nv", "type": "enum", "enum": {"fill_valve_off": 0, "filling": 1, "no_fill_valve": 2, "timeout": 3, "timeout1": 4, "boost": 5}, "visibility": 3}, +{"path": "nc", "type": "enum", "enum": {"stop": 0, "fill": 1, "off": 2, "auto": 3}, "readonly": false, "cmd": "cc nc", "visibility": 3}, +{"path": "nfb", "type": "float", "visibility": 3}, +{"path": "cda", "type": "float"}, +{"path": "cdb", "type": "float"}, +{"path": "cba", "type": "float"}, +{"path": "cbb", "type": "float"}, +{"path": "cvs", "type": "int"}, +{"path": "csp", "type": "int"}, +{"path": "cdv", "type": "text", "readonly": false, "cmd": "cc cdv"}, +{"path": "cic", "type": "text", "readonly": false, "cmd": "cc cic"}, +{"path": "cin", "type": "text"}, +{"path": "cds", "type": "enum", "enum": {"local": 0, "remote": 1, "loading": 2, "by_code": 3, "by_touch": 4}, "readonly": false, "cmd": "cc cds"}, +{"path": "timing", "type": "bool", "readonly": false, "cmd": "cc timing"}, +{"path": "tc", "type": "float", "visibility": 3}, +{"path": "tn", "type": "float", "visibility": 3}, +{"path": "th", "type": "float", "visibility": 3}, +{"path": "tf", "type": "float", "visibility": 3}, +{"path": "tm", "type": "float", "visibility": 3}, +{"path": "tv", "type": "float", "visibility": 3}, +{"path": "tq", "type": "float", "visibility": 3}, +{"path": "bdl", "type": "float", "readonly": false, "cmd": "cc bdl"}]}} From 397ca350f84b55ab1f374db63d95b74a68b321d0 Mon Sep 17 00:00:00 2001 From: zebra Date: Thu, 24 Nov 2022 07:56:09 +0100 Subject: [PATCH 11/11] add FLAMP furnace --- cfg/main/flamp.cfg | 52 +++++++++++++++++++++++++ cfg/sea/flamp.config.json | 80 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 cfg/main/flamp.cfg create mode 100644 cfg/sea/flamp.config.json diff --git a/cfg/main/flamp.cfg b/cfg/main/flamp.cfg new file mode 100644 index 0000000..f390498 --- /dev/null +++ b/cfg/main/flamp.cfg @@ -0,0 +1,52 @@ +[NODE] +description = lamp oven control (from manuel knecht) +id = flamp.config.sea.psi.ch + +[sea_main] +class = secop_psi.sea.SeaClient +description = main sea connection for flamp.config +config = flamp.config +service = main + +[tt] +class = secop_psi.sea.SeaDrivable +io = sea_main +sea_object = tt +rel_paths = . t1 + +[t2] +class = secop_psi.sea.SeaReadable +io = sea_main +sea_object = tt +rel_paths = t2 + +[current] +class = secop_psi.sea.SeaReadable +io = sea_main +sea_object = current +extra_modules = i1, i2, i3, i4 + +[i1] +class = secop_psi.sea.SeaReadable +io = sea_main +single_module = current.i1 + +[i2] +class = secop_psi.sea.SeaReadable +io = sea_main +single_module = current.i2 + +[i3] +class = secop_psi.sea.SeaReadable +io = sea_main +single_module = current.i3 + +[i4] +class = secop_psi.sea.SeaReadable +io = sea_main +single_module = current.i4 + +[pv] +class = secop_psi.sea.SeaReadable +io = sea_main +sea_object = pv diff --git a/cfg/sea/flamp.config.json b/cfg/sea/flamp.config.json new file mode 100644 index 0000000..b610aa8 --- /dev/null +++ b/cfg/sea/flamp.config.json @@ -0,0 +1,80 @@ +{"tt": {"base": "/tt", "params": [ +{"path": "", "type": "float", "readonly": false, "cmd": "run tt", "description": "tt", "kids": 40}, +{"path": "send", "type": "text", "readonly": false, "cmd": "tt send", "visibility": 3}, +{"path": "status", "type": "text", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "is_running", "type": "int", "readonly": false, "cmd": "tt is_running", "visibility": 3}, +{"path": "set", "type": "float", "readonly": false, "cmd": "tt set"}, +{"path": "target", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "running", "type": "int", "readonly": false, "cmd": "run tt"}, +{"path": "tolerance", "type": "float", "readonly": false, "cmd": "tt tolerance"}, +{"path": "maxwait", "type": "float", "readonly": false, "cmd": "tt maxwait"}, +{"path": "settle", "type": "float", "readonly": false, "cmd": "tt settle"}, +{"path": "log", "type": "text", "readonly": false, "cmd": "tt log", "visibility": 3, "kids": 4}, +{"path": "log/mean", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/m2", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/stddev", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "log/n", "type": "float", "readonly": false, "cmd": "run tt", "visibility": 3}, +{"path": "limit", "type": "float", "readonly": false, "cmd": "tt limit"}, +{"path": "t1", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3}, +{"path": "t1/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "t1/curve", "type": "text", "readonly": false, "cmd": "tt t1/curve", "kids": 1}, +{"path": "t1/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t1/curve/points"}, +{"path": "t1/valid", "type": "bool", "readonly": false, "cmd": "run tt"}, +{"path": "t2", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3}, +{"path": "t2/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "t2/curve", "type": "text", "readonly": false, "cmd": "tt t2/curve", "kids": 1}, +{"path": "t2/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t2/curve/points"}, +{"path": "t2/valid", "type": "bool", "readonly": false, "cmd": "run tt"}, +{"path": "t3", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3}, +{"path": "t3/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "t3/curve", "type": "text", "readonly": false, "cmd": "tt t3/curve", "kids": 1}, +{"path": "t3/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t3/curve/points"}, +{"path": "t3/valid", "type": "bool", "readonly": false, "cmd": "run tt"}, +{"path": "t4", "type": "float", "readonly": false, "cmd": "run tt", "kids": 3}, +{"path": "t4/raw", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "t4/curve", "type": "text", "readonly": false, "cmd": "tt t4/curve", "kids": 1}, +{"path": "t4/curve/points", "type": "floatvarar", "readonly": false, "cmd": "tt t4/curve/points"}, +{"path": "t4/valid", "type": "bool", "readonly": false, "cmd": "run tt"}, +{"path": "tref", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "tout", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "toutmax", "type": "float", "readonly": false, "cmd": "tt toutmax"}, +{"path": "toutmin", "type": "float", "readonly": false, "cmd": "tt toutmin"}, +{"path": "ctrlmode", "type": "enum", "enum": {"ok": 0, "off": 1, "illegal_channel": 2, "no_sensor": 3, "no_waterflow": 4, "bad_vacuum": 5, "tc_overflow": 6, "wall_T_overflow": 7}, "readonly": false, "cmd": "tt ctrlmode"}, +{"path": "ramp", "type": "float", "readonly": false, "cmd": "tt ramp"}, +{"path": "smooth", "type": "float", "readonly": false, "cmd": "tt smooth"}, +{"path": "prop", "type": "float", "readonly": false, "cmd": "tt prop", "description": "proportional gain for T slope control"}, +{"path": "int", "type": "float", "readonly": false, "cmd": "tt int", "description": "time constant for T slope control"}, +{"path": "powerset", "type": "float", "readonly": false, "cmd": "tt powerset"}, +{"path": "power", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "resist", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "maxpower", "type": "float", "readonly": false, "cmd": "tt maxpower"}, +{"path": "maxheater", "type": "float", "readonly": false, "cmd": "tt maxheater"}, +{"path": "output", "type": "float", "readonly": false, "cmd": "tt output"}, +{"path": "manualpower", "type": "bool", "readonly": false, "cmd": "tt manualpower"}, +{"path": "ctrlchan", "type": "int", "readonly": false, "cmd": "tt ctrlchan"}, +{"path": "interlock_state", "type": "enum", "enum": {"ok": 0, "no_waterflow": 1, "bad_vacuum": 2, "no_waterflow_bad_vacuum": 3}, "readonly": false, "cmd": "run tt"}, +{"path": "interlock_mask", "type": "enum", "enum": {"no_check": 0, "check_water_only": 1, "check_vacuum_only": 2, "check_all": 3}, "readonly": false, "cmd": "tt interlock_mask"}, +{"path": "sramp", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "slope", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "v_htr", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "i_htr", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "htr", "type": "float", "readonly": false, "cmd": "run tt"}, +{"path": "powerprop", "type": "float", "readonly": false, "cmd": "run tt"}]}, + +"current": {"base": "/current", "params": [ +{"path": "", "type": "float", "kids": 7}, +{"path": "send", "type": "text", "readonly": false, "cmd": "current send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "i1", "type": "float"}, +{"path": "i2", "type": "float"}, +{"path": "i3", "type": "float"}, +{"path": "i4", "type": "float"}, +{"path": "ib", "type": "float"}]}, + +"pv": {"base": "/pv", "params": [ +{"path": "", "type": "float", "kids": 5}, +{"path": "send", "type": "text", "readonly": false, "cmd": "pv send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "sp1", "type": "text"}, +{"path": "sp2", "type": "text"}, +{"path": "sps", "type": "text"}]}}