diff --git a/cfg/addons/camea-be-filter_cfg.py b/cfg/addons/camea-be-filter_cfg.py new file mode 100644 index 0000000..84f491c --- /dev/null +++ b/cfg/addons/camea-be-filter_cfg.py @@ -0,0 +1,18 @@ +Node('cfg/sea/camea-be-filter.cfg', + 'Camea Be-Filter', + interface='5000', + name='camea-be-filter', +) + +Mod('sea_addons', + 'secop_psi.sea.SeaClient', + 'addons sea connection for camea-be-filter.addon', + config='camea-be-filter.addon', + service='addons', +) + +Mod('t_be_filter', + 'secop_psi.sea.SeaReadable', + io='sea_addons', + sea_object='t_be_filter', +) diff --git a/cfg/main/ori3_cfg.py b/cfg/main/ori3_cfg.py index 82486de..7713010 100644 --- a/cfg/main/ori3_cfg.py +++ b/cfg/main/ori3_cfg.py @@ -58,6 +58,12 @@ Mod('hemot', sea_object='hemot', ) +Mod('nvflow', + 'frappy_psi.sea.SeaReadable', '', + io='sea_main', + sea_object='nvflow', +) + Mod('table', 'frappy_psi.sea.SeaReadable', '', io='sea_main', diff --git a/cfg/sea/ori3.config.json b/cfg/sea/ori3.config.json index c960299..230cff2 100644 --- a/cfg/sea/ori3.config.json +++ b/cfg/sea/ori3.config.json @@ -237,10 +237,10 @@ {"path": "", "type": "enum", "enum": {"xds35_auto": 0, "xds35_manual": 1, "sv65": 2, "other": 3, "no": -1}, "readonly": false, "cmd": "hepump", "description": "xds35: scroll pump, sv65: leybold", "kids": 10}, {"path": "send", "type": "text", "readonly": false, "cmd": "hepump send", "visibility": 3}, {"path": "status", "type": "text", "visibility": 3}, -{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running", "visibility": 3}, +{"path": "running", "type": "bool", "readonly": false, "cmd": "hepump running"}, {"path": "eco", "type": "bool", "readonly": false, "cmd": "hepump eco", "visibility": 3}, {"path": "auto", "type": "bool", "readonly": false, "cmd": "hepump auto", "visibility": 3}, -{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve", "visibility": 3}, +{"path": "valve", "type": "enum", "enum": {"closed": 0, "closing": 1, "opening": 2, "opened": 3, "undefined": 4}, "readonly": false, "cmd": "hepump valve"}, {"path": "eco_t_lim", "type": "float", "readonly": false, "cmd": "hepump eco_t_lim", "description": "switch off eco mode when T_set < eco_t_lim and T < eco_t_lim * 2", "visibility": 3}, {"path": "calib", "type": "float", "readonly": false, "cmd": "hepump calib", "visibility": 3}, {"path": "health", "type": "float"}]}, @@ -278,6 +278,16 @@ {"path": "customadr", "type": "text", "readonly": false, "cmd": "hemot customadr"}, {"path": "custompar", "type": "float", "readonly": false, "cmd": "hemot custompar"}]}, +"nvflow": {"base": "/nvflow", "params": [ +{"path": "", "type": "float", "kids": 7}, +{"path": "send", "type": "text", "readonly": false, "cmd": "nvflow send", "visibility": 3}, +{"path": "status", "type": "text", "visibility": 3}, +{"path": "stddev", "type": "float"}, +{"path": "nsamples", "type": "int", "readonly": false, "cmd": "nvflow nsamples"}, +{"path": "offset", "type": "float", "readonly": false, "cmd": "nvflow offset"}, +{"path": "scale", "type": "float", "readonly": false, "cmd": "nvflow scale"}, +{"path": "save", "type": "bool", "readonly": false, "cmd": "nvflow save", "description": "unchecked: current calib is not saved. set checked: save calib"}]}, + "table": {"base": "/table", "params": [ {"path": "", "type": "none", "kids": 17}, {"path": "send", "type": "text", "readonly": false, "cmd": "table send", "visibility": 3}, diff --git a/cfg/stick/dil5_cfg.py b/cfg/stick/dil5_cfg.py index 1f5d38a..a31867b 100644 --- a/cfg/stick/dil5_cfg.py +++ b/cfg/stick/dil5_cfg.py @@ -7,7 +7,7 @@ Mod('triton', 'frappy_psi.mercury.IO', 'connection to triton software', uri='tcp://linse-dil5:33576', - timeout=5.0, + timeout=25.0, ) Mod('ts', @@ -23,6 +23,7 @@ Mod('htr_mix', 'mix. chamber heater', slot='H1,T5', io='triton', + resistivity = 100 ) Mod('htr_sorb', diff --git a/doc/source/magic.rst b/doc/source/magic.rst index 4a5f347..0baf40b 100644 --- a/doc/source/magic.rst +++ b/doc/source/magic.rst @@ -8,7 +8,24 @@ what the framwork does for you. Startup ....... -TODO: describe startup: init methods, first polls +On startup several methods are called. First :meth:`earlyInit` is called on all modules. +Use this to initialize attributes independent of other modules, if you can not initialize +as a class attribute, for example for mutable attributes. + +Then :meth:`initModule` is called for all modules. +Use it to initialize things related to other modules, for example registering callbacks. + +After this, :meth:`startModule` is called with a callback function argument. +:func:`frappy.modules.Module.startModule` starts the poller thread, calling +:meth:`writeInitParams` for writing initial parameters to hardware, followed +by :meth:`initialReads`. The latter is meant for reading values from hardware, +which are not polled continuously. Then all parameters configured for poll are polled +by calling the corresponding read_*() method. The end of this last initialisation +step is indicated to the server by the callback function. +After this, the poller thread starts regular polling, see next section. + +When overriding one of above methods, do not forget to super call. + .. _polling: diff --git a/doc/source/programming.rst b/doc/source/programming.rst index 5c2e186..776e7dc 100644 --- a/doc/source/programming.rst +++ b/doc/source/programming.rst @@ -95,5 +95,23 @@ Example code: return self.read_target() # return the read back value +Parameter Initialisation +------------------------ + +Initial values of parameters might be given by several different sources: + +1) value argument of a Parameter declaration +2) read from HW +3) read from persistent data file +4) value given in config file + +For (2) the programmer might decide for any parameter to poll it regularely from the +hardware. In this case changes from an other input, for example a keyboard or other +interface of the connected devices would be updated continuously in Frappy. +If there is no such other input, or if the programmer decides that such other +data sources are not to be considered, the hardware parameter might be read in just +once on startup, :func:`frappy.modules.Module.initialReads` may be overriden. +This method is called once on startup, before the regular polls start. + .. TODO: io, state machine, persistent parameters, rwhandler, datatypes, features, commands, proxies diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 19360c2..cd624af 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -12,7 +12,7 @@ Module Base Classes ................... .. autoclass:: frappy.modules.Module - :members: earlyInit, initModule, startModule + :members: earlyInit, initModule, startModule, initialReads .. autoclass:: frappy.modules.Readable :members: Status diff --git a/frappy/config.py b/frappy/config.py index 1b77f15..186e57a 100644 --- a/frappy/config.py +++ b/frappy/config.py @@ -125,7 +125,7 @@ class Config(dict): continue if name not in self.module_names: self.module_names.add(name) - self.modules.append(mod) + self[name] = mod def process_file(filename, log): diff --git a/frappy/datatypes.py b/frappy/datatypes.py index 28dad55..c93de9f 100644 --- a/frappy/datatypes.py +++ b/frappy/datatypes.py @@ -1163,9 +1163,9 @@ class ValueType(DataType): The optional (callable) validator can be used to restrict values to a certain type. - For example using `ValueType(dict)` would ensure only values that can be + For example using ``ValueType(dict)`` would ensure only values that can be turned into a dictionary can be used in this instance, as the conversion - `dict(value)` is called for validation. + ``dict(value)`` is called for validation. Notes: The validator must either accept a value by returning it or the converted value, diff --git a/frappy/errors.py b/frappy/errors.py index 3b47068..68c9ebd 100644 --- a/frappy/errors.py +++ b/frappy/errors.py @@ -63,12 +63,15 @@ class SECoPError(RuntimeError): """format with info about raising methods :param stripped: strip last method. - Use stripped=True (or str()) for the following cases, as the last method can be derived from the context: - - stored in pobj.readerror: read_ - - error message from a change command: write_ - - error message from a read command: read_ - Use stripped=False for the log file, as the related parameter is not known :return: the formatted error message + + Use stripped=True (or str()) for the following cases, as the last method can be derived from the context: + + - stored in pobj.readerror: read_ + - error message from a change command: write_ + - error message from a read command: read_ + + Use stripped=False for the log file, as the related parameter is not known """ mlist = self.raising_methods if mlist and stripped: diff --git a/frappy/io.py b/frappy/io.py index dc669cd..3201963 100644 --- a/frappy/io.py +++ b/frappy/io.py @@ -133,6 +133,14 @@ class IOBase(Communicator): self._lock = threading.RLock() def connectStart(self): + if not self.is_connected: + uri = self.uri + self._conn = AsynConn(uri, self._eol_read, + default_settings=self.default_settings) + self.is_connected = True + self.checkHWIdent() + + def checkHWIdent(self): raise NotImplementedError def closeConnection(self): @@ -218,12 +226,19 @@ class StringIO(IOBase): default='\n', settable=True) encoding = Property('used encoding', datatype=StringType(), default='ascii', settable=True) - identification = Property(''' - identification + identification = Property( + '''identification - a list of tuples with commands and expected responses as regexp, - to be sent on connect''', - datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) + a list of tuples with commands and expected responses as regexp, + to be sent on connect''', + datatype=ArrayOf(TupleOf(StringType(), StringType())), + default=[], export=False) + retry_first_idn = Property( + '''retry first identification message + + a flag to indicate whether the first message should be resent once to + avoid data that may still be in the buffer to garble the message''', + datatype=BoolType(), default=False) def _convert_eol(self, value): if isinstance(value, str): @@ -248,16 +263,27 @@ class StringIO(IOBase): raise ValueError('end_of_line for read must not be empty') self._eol_write = self._convert_eol(eol[-1]) - def connectStart(self): - if not self.is_connected: - uri = self.uri - self._conn = AsynConn(uri, self._eol_read, default_settings=self.default_settings) - self.is_connected = True - for command, regexp in self.identification: - reply = self.communicate(command) - if not re.match(regexp, reply): - self.closeConnection() - raise CommunicationFailedError(f'bad response: {reply} does not match {regexp}') + def checkHWIdent(self): + if not self.identification: + return + idents = iter(self.identification) + command, regexp = next(idents) + reply = self.communicate(command) + if not re.match(regexp, reply): + if self.retry_first_idn: + self.log.debug('first ident command not successful.' + ' retrying in case of garbage data.') + idents = iter(self.identification) + else: + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply!r}' + f' does not match {regexp!r}') + for command, regexp in idents: + reply = self.communicate(command) + if not re.match(regexp, reply): + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply!r}' + f' does not match {regexp!r}') @Command(StringType(), result=StringType()) def communicate(self, command): @@ -356,19 +382,19 @@ class BytesIO(IOBase): - a two digit hexadecimal number (byte value) - a character - ?? indicating ignored bytes in responses - """, datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) + """, datatype=ArrayOf(TupleOf(StringType(), StringType())), + default=[], export=False) - def connectStart(self): - if not self.is_connected: - uri = self.uri - self._conn = AsynConn(uri, b'', default_settings=self.default_settings) - self.is_connected = True - for request, expected in self.identification: - replylen, replypat = make_regexp(expected) - reply = self.communicate(make_bytes(request), replylen) - if not replypat.match(reply): - self.closeConnection() - raise CommunicationFailedError(f'bad response: {reply!r} does not match {expected!r}') + _eol_read = b'' + + def checkHWIdent(self): + for request, expected in self.identification: + replylen, replypat = make_regexp(expected) + reply = self.communicate(make_bytes(request), replylen) + if not replypat.match(reply): + self.closeConnection() + raise CommunicationFailedError(f'bad response: {reply!r}' + ' does not match {expected!r}') @Command((BLOBType(), IntRange(0)), result=BLOBType()) def communicate(self, request, replylen): # pylint: disable=arguments-differ diff --git a/frappy/mixins.py b/frappy/mixins.py index 8bfad2c..d35f09b 100644 --- a/frappy/mixins.py +++ b/frappy/mixins.py @@ -53,7 +53,7 @@ class HasControlledBy: to be called from the write_target method """ if self.controlled_by: - self.controlled_by = 0 + self.controlled_by = 0 # self for deactivate_control in self.inputCallbacks.values(): deactivate_control(self.name) @@ -74,6 +74,10 @@ class HasOutputModule: if self.output_module: self.output_module.register_input(self.name, self.deactivate_control) + def set_control_active(self, active): + """to be overridden for switching hw control""" + self.control_active = active + def activate_control(self): """method to switch control_active on @@ -85,10 +89,10 @@ class HasOutputModule: if name != self.name: deactivate_control(self.name) out.controlled_by = self.name - self.control_active = True + self.set_control_active(True) - def deactivate_control(self, source): + def deactivate_control(self, source=None): """called when an other module takes over control""" if self.control_active: - self.control_active = False - self.log.warning(f'switched to manual mode by {source}') + self.set_control_active(False) + self.log.warning(f'switched to manual mode by {source or self.name}') diff --git a/frappy/modules.py b/frappy/modules.py index 9844b2f..9f7a03e 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -629,6 +629,16 @@ class Module(HasAccessibles): mkthread(self.__pollThread, self.polledModules, start_events.get_trigger()) self.startModuleDone = True + def initialReads(self): + """initial reads to be done + + override to read initial values from HW, when it is not desired + to poll them afterwards + + called from the poll thread, after writeInitParams but before + all parameters are polled once + """ + def doPoll(self): """polls important parameters like value and status @@ -678,15 +688,10 @@ class Module(HasAccessibles): before polling, parameters which need hardware initialisation are written """ - for mobj in modules: - mobj.writeInitParams() - modules = [m for m in modules if m.enablePoll] - if not modules: # no polls needed - exit thread - started_callback() - return + polled_modules = [m for m in modules if m.enablePoll] if hasattr(self, 'registerReconnectCallback'): # self is a communicator supporting reconnections - def trigger_all(trg=self.triggerPoll, polled_modules=modules): + def trigger_all(trg=self.triggerPoll, polled_modules=polled_modules): for m in polled_modules: m.pollInfo.last_main = 0 m.pollInfo.last_slow = 0 @@ -694,7 +699,7 @@ class Module(HasAccessibles): self.registerReconnectCallback('trigger_polls', trigger_all) # collect all read functions - for mobj in modules: + for mobj in polled_modules: pinfo = mobj.pollInfo = PollInfo(mobj.pollinterval, self.triggerPoll) # trigger a poll interval change when self.pollinterval changes. if 'pollinterval' in mobj.valueCallbacks: @@ -704,17 +709,32 @@ class Module(HasAccessibles): rfunc = getattr(mobj, 'read_' + pname) if rfunc.poll: pinfo.polled_parameters.append((mobj, rfunc, pobj)) - # call all read functions a first time - try: - for m in modules: - for mobj, rfunc, _ in m.pollInfo.polled_parameters: - mobj.callPollFunc(rfunc, raise_com_failed=True) - except CommunicationFailedError as e: - # when communication failed, probably all parameters and may be more modules are affected. - # as this would take a lot of time (summed up timeouts), we do not continue - # trying and let the server accept connections, further polls might success later - self.log.error('communication failure on startup: %s', e) - started_callback() + while True: + try: + for mobj in modules: + # TODO when needed: here we might add a call to a method :meth:`beforeWriteInit` + mobj.writeInitParams() + mobj.initialReads() + # call all read functions a first time + for m in polled_modules: + for mobj, rfunc, _ in m.pollInfo.polled_parameters: + mobj.callPollFunc(rfunc, raise_com_failed=True) + # TODO when needed: here we might add calls to a method :meth:`afterInitPolls` + break + except CommunicationFailedError as e: + # when communication failed, probably all parameters and may be more modules are affected. + # as this would take a lot of time (summed up timeouts), we do not continue + # trying and let the server accept connections, further polls might success later + if started_callback: + self.log.error('communication failure on startup: %s', e) + started_callback() + started_callback = None + self.triggerPoll.wait(0.1) # wait for reconnection or max 10 sec. + break + if started_callback: + started_callback() + if not polled_modules: # no polls needed - exit thread + return to_poll = () while True: now = time.time() diff --git a/frappy_mlz/entangle.py b/frappy_mlz/entangle.py index 16783a7..8a417b7 100644 --- a/frappy_mlz/entangle.py +++ b/frappy_mlz/entangle.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ***************************************************************************** # # This program is free software; you can redistribute it and/or modify it under @@ -257,12 +256,12 @@ class BasePyTangoDevice: Wraps command execution and attribute operations of the given device with logging and exception mapping. """ - dev.command_inout = self._applyGuardToFunc(dev.command_inout) - dev.write_attribute = self._applyGuardToFunc(dev.write_attribute, + dev.__dict__['command_inout'] = self._applyGuardToFunc(dev.command_inout) + dev.__dict__['write_attribute'] = self._applyGuardToFunc(dev.write_attribute, 'attr_write') - dev.read_attribute = self._applyGuardToFunc(dev.read_attribute, + dev.__dict__['read_attribute'] = self._applyGuardToFunc(dev.read_attribute, 'attr_read') - dev.attribute_query = self._applyGuardToFunc(dev.attribute_query, + dev.__dict__['attribute_query'] = self._applyGuardToFunc(dev.attribute_query, 'attr_query') return dev diff --git a/frappy_psi/mercury.py b/frappy_psi/mercury.py index 8c88dfd..5a8294b 100644 --- a/frappy_psi/mercury.py +++ b/frappy_psi/mercury.py @@ -25,10 +25,10 @@ import math import re import time -from frappy.core import Drivable, HasIO, Writable, StatusType, \ +from frappy.core import Command, Drivable, HasIO, Writable, StatusType, \ Parameter, Property, Readable, StringIO, Attached, IDLE, RAMPING, nopoll from frappy.datatypes import EnumType, FloatRange, StringType, StructOf, BoolType, TupleOf -from frappy.errors import HardwareError, ProgrammingError, ConfigError, RangeError +from frappy.errors import HardwareError, ProgrammingError, ConfigError from frappy_psi.convergence import HasConvergence from frappy.states import Retry, Finish from frappy.mixins import HasOutputModule, HasControlledBy @@ -218,7 +218,6 @@ class HasInput(HasControlledBy, MercuryChannel): class Loop(HasOutputModule, MercuryChannel, Drivable): """common base class for loops""" output_module = Attached(HasInput, mandatory=False) - control_active = Parameter(readonly=False) ctrlpars = Parameter( 'pid (proportional band, integral time, differential time', StructOf(p=FloatRange(0, unit='$'), i=FloatRange(0, unit='min'), d=FloatRange(0, unit='min')), @@ -226,14 +225,15 @@ class Loop(HasOutputModule, MercuryChannel, Drivable): ) enable_pid_table = Parameter('', BoolType(), readonly=False) - def set_output(self, active, source='HW'): + def set_output(self, active, source=None): if active: self.activate_control() else: self.deactivate_control(source) def set_target(self, target): - self.set_output(True) + if not self.control_active: + self.activate_control() self.target = target def read_enable_pid_table(self): @@ -254,9 +254,16 @@ class Loop(HasOutputModule, MercuryChannel, Drivable): def read_status(self): return IDLE, '' + @Command() + def control_off(self): + """switch control off""" + # remark: this is needed in frappy_psi.trition.TemperatureLoop, as the heater + # output is not available there. We define it here as a convenience for the user. + self.write_control_active(False) + class ConvLoop(HasConvergence, Loop): - def deactivate_control(self, source): + def deactivate_control(self, source=None): if self.control_active: super().deactivate_control(source) self.convergence_state.start(self.inactive_state) @@ -372,16 +379,14 @@ class TemperatureLoop(TemperatureSensor, ConvLoop): super().doPoll() self.read_setpoint() - def read_control_active(self): - active = self.query(f'DEV::{self.ENABLE}', off_on) - self.set_output(active) - return active + def set_control_active(self, active): + super().set_control_active(active) + self.change(f'DEV::{self.ENABLE}', active, off_on) - def write_control_active(self, value): - if value: - raise RangeError('write to target to switch control on') - self.set_output(value, 'user') - return self.change(f'DEV::{self.ENABLE}', value, off_on) + def initialReads(self): + # initialize control active from HW + active = self.query(f'DEV::{self.ENABLE}', off_on) + super().set_output(active, 'HW') @nopoll # polled by read_setpoint def read_target(self): @@ -413,7 +418,7 @@ class TemperatureLoop(TemperatureSensor, ConvLoop): self.change(f'DEV::{self.ENABLE}', True, off_on) super().set_target(target) - def deactivate_control(self, source): + def deactivate_control(self, source=None): if self.__ramping: self.__ramping = False # stop ramping setpoint @@ -508,14 +513,16 @@ class PressureLoop(PressureSensor, HasControlledBy, ConvLoop): output_module = Attached(ValvePos, mandatory=False) tolerance = Parameter(default=0.1) - def read_control_active(self): - active = self.query('DEV::PRES:LOOP:FAUT', off_on) - self.set_output(active) - return active + def set_control_active(self, active): + super().set_control_active(active) + if not active: + self.self_controlled() # switches off auto flow + return self.change('DEV::PRES:LOOP:FAUT', active, off_on) - def write_control_active(self, value): - self.set_output(value, 'user') - return self.change('DEV::PRES:LOOP:FAUT', value, off_on) + def initialReads(self): + # initialize control active from HW + active = self.query('DEV::PRES:LOOP:FAUT', off_on) + super().set_output(active, 'HW') def read_target(self): return self.query('DEV::PRES:LOOP:PRST') @@ -560,14 +567,15 @@ class HasAutoFlow: if value: self.needle_valve.controlled_by = self.name else: + if self.needle_valve.control_active: + self.needle_valve.set_target(self.flowpars[1][0]) # flow min if self.needle_valve.controlled_by != SELF: self.needle_valve.controlled_by = SELF - self.needle_valve.write_target(self.flowpars[1][0]) # flow min return value - def auto_flow_off(self): + def auto_flow_off(self, source=None): if self.auto_flow: - self.log.warning('switch auto flow off') + self.log.warning(f'switched auto flow off by {source or self.name}') self.write_auto_flow(False) diff --git a/frappy_psi/phytron.py b/frappy_psi/phytron.py index 36062db..66f5b9f 100644 --- a/frappy_psi/phytron.py +++ b/frappy_psi/phytron.py @@ -83,7 +83,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): ioClass = PhytronIO _step_size = None # degree / step - _blocking_error = None # None or a string indicating the reason of an error needing reset + _blocking_error = None # None or a string indicating the reason of an error needing clear_errors _running = False # status indicates motor is running STATUS_MAP = { @@ -121,10 +121,10 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): if not axisbit & active_axes: # power cycle detected and this axis not yet active self.set('P37S', axisbit | active_axes) # activate axis if now < self.alive_time + 7 * 24 * 3600: # the device was running within last week - # inform the user about the loss of position by the need of doing reset_error + # inform the user about the loss of position by the need of doing clear_errors self._blocking_error = 'lost position' - else: # do reset silently - self.reset_error() + else: # do silently + self.clear_errors() self.alive_time = now self.saveParameters() return now @@ -171,7 +171,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): def write_target(self, value): self.read_alive_time() if self._blocking_error: - self.status = ERROR, 'reset needed after ' + self._blocking_error + self.status = ERROR, 'clear_errors needed after ' + self._blocking_error raise HardwareError(self.status[1]) self.saveParameters() if self.backlash: @@ -261,7 +261,7 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): self.start_machine(self.stopping, status=(BUSY, 'stopping')) @Command - def reset_error(self): + def clear_errors(self): """Reset error, set position to encoder""" self.read_value() if self._blocking_error: @@ -286,3 +286,4 @@ class Motor(HasOffset, HasStates, PersistentMixin, HasIO, Drivable): self.read_value() self.status = 'IDLE', 'after error reset' self._blocking_error = None + self.target = self.value # clear error in target diff --git a/frappy_psi/sea.py b/frappy_psi/sea.py index 5bc6276..bf294f7 100644 --- a/frappy_psi/sea.py +++ b/frappy_psi/sea.py @@ -111,6 +111,7 @@ class SeaClient(ProxyClient, Module): _connect_thread = None _service_manager = None _instance = None + _last_connect = 0 def __init__(self, name, log, opts, srv): nodename = srv.node_cfg.get('name') or srv.node_cfg.get('equipment_id') @@ -135,6 +136,10 @@ class SeaClient(ProxyClient, Module): ProxyClient.__init__(self) Module.__init__(self, name, log, opts, srv) + def doPoll(self): + if not self.asynio and time.time() > self._last_connect + 10: + self._connect_thread = mkthread(self._connect, None) + def register_obj(self, module, obj): self.objects.add(obj) for k, v in module.path2param.items(): @@ -146,6 +151,7 @@ class SeaClient(ProxyClient, Module): self._connect_thread = mkthread(self._connect, start_events.get_trigger()) def _connect(self, started_callback): + self._last_connect = time.time() if self._instance: if not self._service_manager: if self._service_manager is None: @@ -192,36 +198,40 @@ class SeaClient(ProxyClient, Module): self.syncio.writeline(b'seauser seaser') assert self.syncio.readline() == b'Login OK' self.log.info('connected to %s', self.uri) - self.syncio.flush_recv() - ft = 'fulltransAct' if quiet else 'fulltransact' - self.syncio.writeline(('%s %s' % (ft, command)).encode()) - result = None - deadline = time.time() + 10 - while time.time() < deadline: - try: + try: + self.syncio.flush_recv() + ft = 'fulltransAct' if quiet else 'fulltransact' + self.syncio.writeline(('%s %s' % (ft, command)).encode()) + result = None + deadline = time.time() + 10 + while time.time() < deadline: reply = self.syncio.readline() if reply is None: continue - except ConnectionClosed: - break - reply = reply.decode() - if reply.startswith('TRANSACTIONSTART'): - result = [] - continue - if reply == 'TRANSACTIONFINISHED': + reply = reply.decode() + if reply.startswith('TRANSACTIONSTART'): + result = [] + continue + if reply == 'TRANSACTIONFINISHED': + if result is None: + self.log.info('missing TRANSACTIONSTART on: %s', command) + return '' + if not result: + return '' + return '\n'.join(result) if result is None: - self.log.info('missing TRANSACTIONSTART on: %s', command) - return '' + self.log.info('swallow: %s', reply) + continue if not result: - return '' - return '\n'.join(result) - if result is None: - self.log.info('swallow: %s', reply) - continue - if not result: - result = [reply.split('=', 1)[-1]] - else: - result.append(reply) + result = [reply.split('=', 1)[-1]] + else: + result.append(reply) + except ConnectionClosed: + try: + self.syncio.disconnect() + except Exception: + pass + self.syncio = None raise TimeoutError('no response within 10s') def _rxthread(self, started_callback): @@ -231,6 +241,11 @@ class SeaClient(ProxyClient, Module): if reply is None: continue except ConnectionClosed: + try: + self.asynio.disconnect() + except Exception: + pass + self.asynio = None break try: msg = json.loads(reply) @@ -463,7 +478,6 @@ class SeaModule(Module): descr['params'].pop(0) else: # filter by relative paths - # rel_paths = rel_paths.split() result = [] is_running = None for rpath in rel_paths: diff --git a/frappy_psi/triton.py b/frappy_psi/triton.py index 99f7f40..2ea892d 100644 --- a/frappy_psi/triton.py +++ b/frappy_psi/triton.py @@ -25,7 +25,7 @@ from frappy.core import Writable, Parameter, Readable, Drivable, IDLE, WARN, BUS Done, Property from frappy.datatypes import EnumType, FloatRange, StringType from frappy.lib.enum import Enum -from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput, SELF +from frappy_psi.mercury import MercuryChannel, Mapped, off_on, HasInput from frappy_psi import mercury actions = Enum(none=0, condense=1, circulate=2, collect=3) @@ -256,15 +256,14 @@ class TemperatureLoop(ScannerChannel, mercury.TemperatureLoop): ctrlpars = Parameter('pid (gain, integral (inv. time), differential time') system_channel = Property('system channel name', StringType(), 'MC') - def write_control_active(self, value): + def set_control_active(self, active): if self.system_channel: self.change('SYS:DR:CHAN:%s' % self.system_channel, self.slot.split(',')[0], str) - if value: + if active: self.change('DEV::TEMP:LOOP:FILT:ENAB', 'ON', str) if self.output_module: - limit = self.output_module.read_limit() or None # None: max. limit + limit = self.output_module.read_limit() self.output_module.write_limit(limit) - return super().write_control_active(value) class HeaterOutput(HasInput, MercuryChannel, Writable): @@ -286,7 +285,7 @@ class HeaterOutput(HasInput, MercuryChannel, Writable): return self.value def write_target(self, value): - self.write_controlled_by(SELF) + self.self_controlled() if self.resistivity: # round to the next voltage step value = round(sqrt(value * self.resistivity)) ** 2 / self.resistivity @@ -301,13 +300,12 @@ class HeaterOutputWithRange(HeaterOutput): def read_limit(self): maxcur = self.query('DEV::TEMP:LOOP:RANGE') # mA + if maxcur == 0: + maxcur = 100 # mA return self.read_resistivity() * maxcur ** 2 # uW def write_limit(self, value): - if value is None: - maxcur = 100 # max. allowed current 100mA - else: - maxcur = sqrt(value / self.read_resistivity()) + maxcur = sqrt(value / self.read_resistivity()) for cur in 0.0316, 0.1, 0.316, 1, 3.16, 10, 31.6, 100: if cur > maxcur * 0.999: maxcur = cur diff --git a/test/test_statemachine.py b/test/test_statemachine.py index 2e2efa2..2a49021 100644 --- a/test/test_statemachine.py +++ b/test/test_statemachine.py @@ -249,15 +249,22 @@ class Mod(HasStates, Drivable): self._my_time += 1 +class Started(RuntimeError): + pass + + def create_module(): updates = [] obj = Mod('obj', LoggerStub(), {'description': ''}, ServerStub(updates)) obj.initModule() obj.statelist = [] try: - obj._Module__pollThread(obj.polledModules, None) - except TypeError: - pass # None is not callable + def started(): + raise Started() + # run __pollThread until Started is raised (after initial phase) + obj._Module__pollThread(obj.polledModules, started) + except Started: + pass updates.clear() return obj, updates