From a520e6e1e41a716e4e6ffb7c20d7fb240845c1d3 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 23 Jul 2020 16:12:14 +0200 Subject: [PATCH 1/5] more flexible end_of_line in stringio in the previous version, it was not possible to give a ASCII nul character as end_of_line, because StringType refuses this - end_of_line might be given as bytes, str or int - end_of_line might be given as tuple (eol_read, eol_write) Change-Id: I8b7942320ad3ffe162cdf3a673e113a66a84fb93 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23496 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- secop/stringio.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/secop/stringio.py b/secop/stringio.py index e08cc6b..9ef68ed 100644 --- a/secop/stringio.py +++ b/secop/stringio.py @@ -28,7 +28,7 @@ import threading import re from secop.lib.asynconn import AsynConn, ConnectionClosed from secop.modules import Module, Communicator, Parameter, Command, Property, Attached -from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf +from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType from secop.errors import CommunicationFailedError, CommunicationSilentError from secop.poller import REGULAR from secop.metaclass import Done @@ -43,7 +43,7 @@ class StringIO(Communicator): 'uri': Property('hostname:portnumber', datatype=StringType()), 'end_of_line': - Property('end_of_line character', datatype=StringType(), + Property('end_of_line character', datatype=ValueType(), default='\n', settable=True), 'encoding': Property('used encoding', datatype=StringType(), @@ -73,13 +73,32 @@ class StringIO(Communicator): def earlyInit(self): self._conn = None self._lock = threading.RLock() - self._end_of_line = self.end_of_line.encode(self.encoding) + eol = self.end_of_line + if isinstance(eol, (tuple, list)): + if len(eol) not in (1, 2): + raise ValueError('invalid end_of_line: %s' % eol) + else: + eol = [eol] + # eol for read and write might be distinct + self._eol_read = self._convert_eol(eol[0]) + if not self._eol_read: + raise ValueError('end_of_line for read must not be empty') + self._eol_write = self._convert_eol(eol[-1]) self._last_error = None + def _convert_eol(self, value): + if isinstance(value, str): + return value.encode(self.encoding) + if isinstance(value, int): + return bytes([value]) + if isinstance(value, bytes): + return value + raise ValueError('invalid end_of_line: %s' % repr(value)) + def connectStart(self): if not self.is_connected: uri = self.uri - self._conn = AsynConn(uri, self._end_of_line) + self._conn = AsynConn(uri, self._eol_read) self.is_connected = True for command, regexp in self.identification: reply = self.do_communicate(command) @@ -159,10 +178,10 @@ class StringIO(Communicator): try: with self._lock: # read garbage and wait before send - if self.wait_before: - cmds = command.split(self.end_of_line) + if self.wait_before and self._eol_write: + cmds = command.encode(self.encoding).split(self._eol_write) else: - cmds = [command] + cmds = [command.encode(self.encoding)] garbage = None try: for cmd in cmds: @@ -171,8 +190,8 @@ class StringIO(Communicator): if garbage is None: # read garbage only once garbage = self._conn.flush_recv() if garbage: - self.log.debug('garbage: %s', garbage.decode(self.encoding)) - self._conn.send((cmd + self.end_of_line).encode(self.encoding)) + self.log.debug('garbage: %r', garbage) + self._conn.send(cmd + self._eol_write) reply = self._conn.readline(self.timeout) except ConnectionClosed: self.closeConnection() From aa4c8f1f043fa174aa9645bfafb0c3a7a0d5d9f9 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Thu, 25 Jun 2020 12:02:17 +0200 Subject: [PATCH 2/5] improvements on PPMS and LS370 - PPMS: improved machanism for 10 K waiting - LS370: fixed an issue with auto range + LS370: show test for all status bits Change-Id: Ia6454141917893f0e5c6c4351df3a864942bb629 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23495 Tested-by: Jenkins Automated Tests Reviewed-by: Markus Zolliker --- secop/lib/__init__.py | 11 ++++ secop_psi/ls370res.py | 20 +++--- secop_psi/ls370sim.py | 148 ++++++++++++++++++++---------------------- secop_psi/ppms.py | 77 +++++++++++++++------- secop_psi/ppmssim.py | 2 +- 5 files changed, 148 insertions(+), 110 deletions(-) diff --git a/secop/lib/__init__.py b/secop/lib/__init__.py index 66af30e..c712950 100644 --- a/secop/lib/__init__.py +++ b/secop/lib/__init__.py @@ -247,3 +247,14 @@ def getfqdn(name=''): def getGeneralConfig(): return CONFIG + + +def formatStatusBits(sword, labels, start=0): + """Return a list of labels according to bit state in `sword` starting + with bit `start` and the first label in `labels`. + """ + result = [] + for i, lbl in enumerate(labels, start): + if sword & (1 << i) and lbl: + result.append(lbl) + return result diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py index a590445..999449f 100644 --- a/secop_psi/ls370res.py +++ b/secop_psi/ls370res.py @@ -27,6 +27,7 @@ from secop.metaclass import Done from secop.datatypes import FloatRange, IntRange, EnumType, BoolType from secop.stringio import HasIodev from secop.poller import Poller, REGULAR +from secop.lib import formatStatusBits import secop.iohandler Status = Drivable.Status @@ -49,10 +50,7 @@ filterhdl = IOHandler('filter', 'FILTER?%(channel)d', '%d,%d,%d') scan = IOHandler('scan', 'SCAN?', '%d,%d') -STATUS_TEXT = {0: ''} -for bit, text in enumerate('CS_OVL VCM_OVL VMIX_OVL R_OVER R_UNDER T_OVER T_UNDER'.split()): - for i in range(1 << bit, 2 << bit): - STATUS_TEXT[i] = text +STATUS_BIT_LABELS = 'CS_OVL VCM_OVL VMIX_OVL VDIF_OVL R_OVER R_UNDER T_OVER T_UNDER'.split() class StringIO(secop.stringio.StringIO): @@ -177,13 +175,13 @@ class ResChannel(HasIodev, Readable): return result def read_status(self): - if self.channel != self._main.channel: - return Done if not self.enabled: return [self.Status.DISABLED, 'disabled'] + if self.channel != self._main.channel: + return Done result = int(self.sendRecv('RDGST?%d' % self.channel)) result &= 0x37 # mask T_OVER and T_UNDER (change this when implementing temperatures instead of resistivities) - statustext = STATUS_TEXT[result] + statustext = ' '.join(formatStatusBits(result, STATUS_BIT_LABELS)) if statustext: return [self.Status.ERROR, statustext] return [self.Status.IDLE, ''] @@ -191,10 +189,11 @@ class ResChannel(HasIodev, Readable): def analyze_rdgrng(self, iscur, exc, rng, autorange, excoff): result = dict(range=rng) if autorange: - result['auotrange'] = 'hard' - elif self.autorange == 'hard': - result['autorange'] = 'soft' + result['autorange'] = 'hard' + #elif self.autorange == 'hard': + # result['autorange'] = 'soft' # else: do not change autorange + self.log.info('%s range %r %r %r' % (self.name, rng, autorange, self.autorange)) if excoff: result.update(iexc=0, vexc=0) elif iscur: @@ -225,6 +224,7 @@ class ResChannel(HasIodev, Readable): if change.autorange == 'soft': if rng < self.minrange: rng = self.minrange + self.autorange = change.autorange return iscur, exc, rng, autorange, excoff def analyze_inset(self, on, dwell, pause, curve, tempco): diff --git a/secop_psi/ls370sim.py b/secop_psi/ls370sim.py index 8fc0ece..c55e298 100644 --- a/secop_psi/ls370sim.py +++ b/secop_psi/ls370sim.py @@ -1,77 +1,71 @@ -#!/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 -# ***************************************************************************** -"""a very simple simulator for a LakeShore Model 370""" - -from secop.modules import Communicator -#from secop.lib import mkthread - -class Ls370Sim(Communicator): - CHANNEL_COMMANDS = [ - ('RDGR?%d', '1.0'), - ('RDGST?%d', '0'), - ('RDGRNG?%d', '0,5,5,0,0'), - ('INSET?%d', '1,5,5,0,0'), - ('FILTER?%d', '1,5,80'), - ] - OTHER_COMMANDS = [ - ('*IDN?', 'LSCI,MODEL370,370184,05302003'), - ('SCAN?', '3,1'), - ] - def earlyInit(self): - self._data = dict(self.OTHER_COMMANDS) - for fmt, v in self.CHANNEL_COMMANDS: - for chan in range(1,17): - self._data[fmt % chan] = v - # mkthread(self.run) - - def do_communicate(self, command): - # simulation part, time independent - for channel in range(1,17): - _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') - if excoff == '1': - self._data['RDGST?%d' % channel] = '4' - else: - self._data['RDGST?%d' % channel] = '0' - - chunks = command.split(';') - reply = [] - for chunk in chunks: - if '?' in chunk: - reply.append(self._data[chunk]) - else: - for nqarg in (1,0): - if nqarg == 0: - qcmd, arg = chunk.split(' ', 1) - qcmd += '?' - else: - qcmd, arg = chunk.split(',', nqarg) - qcmd = qcmd.replace(' ', '?', 1) - if qcmd in self._data: - self._data[qcmd] = arg - break - #if command.startswith('R'): - # print('> %s\t< %s' % (command, reply)) - return ';'.join(reply) - - #def run(self): - # # time dependent simulation - # while True: - # time.sleep(1) +#!/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 +# ***************************************************************************** +"""a very simple simulator for a LakeShore Model 370""" + +from secop.modules import Communicator + +class Ls370Sim(Communicator): + CHANNEL_COMMANDS = [ + ('RDGR?%d', '1.0'), + ('RDGST?%d', '0'), + ('RDGRNG?%d', '0,5,5,0,0'), + ('INSET?%d', '1,5,5,0,0'), + ('FILTER?%d', '1,5,80'), + ] + OTHER_COMMANDS = [ + ('*IDN?', 'LSCI,MODEL370,370184,05302003'), + ('SCAN?', '3,1'), + ] + def earlyInit(self): + self._data = dict(self.OTHER_COMMANDS) + for fmt, v in self.CHANNEL_COMMANDS: + for chan in range(1,17): + self._data[fmt % chan] = v + # mkthread(self.run) + + def do_communicate(self, command): + # simulation part, time independent + for channel in range(1,17): + _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') + if excoff == '1': + self._data['RDGST?%d' % channel] = '6' + else: + self._data['RDGST?%d' % channel] = '0' + + chunks = command.split(';') + reply = [] + for chunk in chunks: + if '?' in chunk: + reply.append(self._data[chunk]) + else: + for nqarg in (1,0): + if nqarg == 0: + qcmd, arg = chunk.split(' ', 1) + qcmd += '?' + else: + qcmd, arg = chunk.split(',', nqarg) + qcmd = qcmd.replace(' ', '?', 1) + if qcmd in self._data: + self._data[qcmd] = arg + break + #if command.startswith('R'): + # print('> %s\t< %s' % (command, reply)) + return ';'.join(reply) diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 2c1848b..76342dc 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -209,8 +209,18 @@ class UserChannel(Channel): 'no': Property('channel number', datatype=IntRange(0, 0), export=False, default=0), + 'linkenable': + Property('name of linked channel for enabling', + datatype=StringType(), export=False, default=''), + } + def write_enabled(self, enabled): + other = self._iodev.modules.get(self.linkenable, None) + if other: + other.enabled = enabled + return enabled + class DriverChannel(Channel): drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') @@ -410,8 +420,11 @@ class Temp(PpmsMixin, Drivable): Parameter('intermediate set point', datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), 'ramp': - Parameter('ramping speed', readonly=False, handler=temp, default=0, + Parameter('ramping speed', readonly=False, default=0, datatype=FloatRange(0, 20, unit='K/min')), + 'workingramp': + Parameter('intermediate ramp value', + datatype=FloatRange(0, 20, unit='K/min'), handler=temp), 'approachmode': Parameter('how to approach target!', readonly=False, handler=temp, datatype=EnumType(ApproachMode)), @@ -458,6 +471,7 @@ class Temp(PpmsMixin, Drivable): general_stop = False _cool_deadline = 0 _wait_at10 = False + _ramp_at_limit = False def update_value_status(self, value, packed_status): """update value and status""" @@ -469,10 +483,10 @@ class Temp(PpmsMixin, Drivable): status = self.STATUS_MAP.get(status_code, (self.Status.ERROR, 'unknown status code %d' % status_code)) now = time.time() if value > 11: - # when starting from T > 40, this will be 15 min. + # when starting from T > 50, this will be 15 min. # when starting from lower T, it will be less # when ramping with 2 K/min or less, the deadline is now - self._cool_deadline = max(self._cool_deadline, now + min(30, value - 10) * 30) # 30 sec / K + self._cool_deadline = max(self._cool_deadline, now + min(40, value - 10) * 30) # 30 sec / K elif self._wait_at10: if now > self._cool_deadline: self._wait_at10 = False @@ -506,24 +520,40 @@ class Temp(PpmsMixin, Drivable): self._expected_target_time = 0 self.status = status - def analyze_temp(self, setpoint, ramp, approachmode): + def analyze_temp(self, setpoint, workingramp, approachmode): + if (setpoint, workingramp, approachmode) == self._last_settings: + # update parameters only on change, as 'ramp' and 'approachmode' are + # not always sent to the hardware + return {} + self._last_settings = setpoint, workingramp, approachmode if setpoint != 10 or not self._wait_at10: + self.log.debug('read back target %g %r' % (setpoint, self._wait_at10)) self.target = setpoint - result = dict(setpoint=setpoint) - # we update ramp and approachmode only at init - if self.ramp == 0: - result['ramp'] = ramp - result['approachmode'] = approachmode + if workingramp != 2 or not self._ramp_at_limit: + self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit)) + self.ramp = workingramp + result = dict(setpoint=setpoint, workingramp=workingramp) + self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp))) return result def change_temp(self, change): - if 10 >= self.value > change.setpoint: - ramp = min(2, change.ramp) - print('ramplimit', change.ramp, self.value, ramp) - else: - ramp = change.ramp - self.calc_expected(change.setpoint, ramp) - return change.setpoint, ramp, change.approachmode + ramp = change.ramp + setpoint = change.setpoint + wait_at10 = False + ramp_at_limit = False + if self.value > 11: + if setpoint <= 10: + wait_at10 = True + setpoint = 10 + elif self.value > setpoint: + if ramp >= 2: + ramp = 2 + ramp_at_limit = True + self._wait_at10 = wait_at10 + self._ramp_at_limit = ramp_at_limit + self.calc_expected(setpoint, ramp) + self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit)) + return setpoint, ramp, change.approachmode def write_target(self, target): self._stopped = False @@ -532,24 +562,22 @@ class Temp(PpmsMixin, Drivable): self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') self._last_change = time.time() - if self.value > 10 > target and self.ramp > 2: - self._wait_at10 = True - self.temp.write(self, 'setpoint', 10) - else: - self._wait_at10 = False - self.temp.write(self, 'setpoint', target) + self.temp.write(self, 'setpoint', target) + self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10))) return target def write_approachmode(self, value): if self.isDriving(): self.temp.write(self, 'approachmode', value) return Done + self.approachmode = value return None # do not execute TEMP command, as this would trigger an unnecessary T change def write_ramp(self, value): if self.isDriving(): self.temp.write(self, 'ramp', value) return Done + # self.ramp = value return None # do not execute TEMP command, as this would trigger an unnecessary T change def calc_expected(self, target, ramp): @@ -656,6 +684,7 @@ class Field(PpmsMixin, Drivable): self.status = status def analyze_field(self, target, ramp, approachmode, persistentmode): + # print('last_settings tt %s' % repr(self._last_settings)) if (target, ramp, approachmode, persistentmode) == self._last_settings: # we update parameters only on change, as 'ramp' and 'approachmode' are # not always sent to the hardware @@ -669,6 +698,7 @@ class Field(PpmsMixin, Drivable): def write_target(self, target): if abs(self.target - self.value) <= 2e-5 and target == self.target: + self.target = target return None # avoid ramping leads self._status_before_change = self.status self._stopped = False @@ -679,6 +709,7 @@ class Field(PpmsMixin, Drivable): def write_persistentmode(self, mode): if abs(self.target - self.value) <= 2e-5 and mode == self.persistentmode: + self.persistentmode = mode return None # avoid ramping leads self._last_change = time.time() self._status_before_change = self.status @@ -688,6 +719,7 @@ class Field(PpmsMixin, Drivable): return Done def write_ramp(self, value): + self.ramp = value if self.isDriving(): self.field.write(self, 'ramp', value) return Done @@ -805,6 +837,7 @@ class Position(PpmsMixin, Drivable): if self.isDriving(): self.move.write(self, 'speed', value) return Done + self.speed = value return None # do not execute MOVE command, as this would trigger an unnecessary move def do_stop(self): diff --git a/secop_psi/ppmssim.py b/secop_psi/ppmssim.py index 0a661b7..2079500 100644 --- a/secop_psi/ppmssim.py +++ b/secop_psi/ppmssim.py @@ -53,7 +53,7 @@ class PpmsSim: def __init__(self): self.status = NamedList('t mf ch pos', 1, 1, 1, 1) self.st = 0x1111 - self.t = 200 + self.t = 15 self.temp = NamedList('target ramp amode', 200., 1, 0, fast=self.t, delay=10) self.mf = 100 self.field = NamedList('target ramp amode pmode', 0, 50, 0, 0) From 2eb0aeba0dd93996f326ded13f7459b232ff4363 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 28 Jul 2020 14:23:33 +0200 Subject: [PATCH 3/5] add readbytes method to AsynConn + flush_recv now also clears _rxbuffer Change-Id: I33c7ea1a9a1d8b663e5cd3bd81cf7ad43448e0fa Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23548 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- secop/lib/asynconn.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/secop/lib/asynconn.py b/secop/lib/asynconn.py index 2bcb1ff..97be5c4 100644 --- a/secop/lib/asynconn.py +++ b/secop/lib/asynconn.py @@ -125,6 +125,28 @@ class AsynConn: return None self._rxbuffer += data + def readbytes(self, nbytes, timeout=None): + """read one line + + return either bytes or None if not enough data available within 1 sec (self.timeout) + if a non-zero timeout is given, a timeout error is raised instead of returning None + the timeout effectively used will not be lower than self.timeout (1 sec) + """ + if timeout: + end = time.time() + timeout + while len(self._rxbuffer) < nbytes: + data = self.recv() + if not data: + if timeout: + if time.time() < end: + continue + raise TimeoutError('timeout in readbytes (%g sec)' % timeout) + return None + self._rxbuffer += data + line = self._rxbuffer[:nbytes] + self._rxbuffer = self._rxbuffer[nbytes:] + return line + def writeline(self, line): self.send(line + self.end_of_line) @@ -154,9 +176,10 @@ class AsynTcp(AsynConn): def flush_recv(self): """flush recv buffer""" - data = [] + data = [self._rxbuffer] while select.select([self.connection], [], [], 0)[0]: data.append(self.recv()) + self._rxbuffer = b'' return b''.join(data) def recv(self): @@ -244,7 +267,9 @@ class AsynSerial(AsynConn): self.connection.write(data) def flush_recv(self): - return self.connection.read(self.connection.in_waiting) + result = self._rxbuffer + self.connection.read(self.connection.in_waiting) + self._rxbuffer = b'' + return result def recv(self): """return bytes received within 1 sec""" From 7af4d572aba987f3904572034557b6c7379b9f63 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 29 Jul 2020 14:11:28 +0200 Subject: [PATCH 4/5] Param(..., initwrite=True) works only with poll=True check this when creating a Parameter Change-Id: I5d45f25fd67682de45b51c842323e9582f69e6e3 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/23547 Tested-by: Jenkins Automated Tests Reviewed-by: Enrico Faulhaber Reviewed-by: Markus Zolliker --- secop/params.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/secop/params.py b/secop/params.py index f32068d..40904a6 100644 --- a/secop/params.py +++ b/secop/params.py @@ -139,8 +139,11 @@ class Parameter(Accessible): datatype.setProperty('unit', unit) super(Parameter, self).__init__(**kwds) - if self.readonly and self.initwrite: - raise ProgrammingError('can not have both readonly and initwrite!') + if self.initwrite: + if self.readonly: + raise ProgrammingError('can not have both readonly and initwrite!') + if not self.poll: + raise ProgrammingError('only polled parameters can have initwrite!') if self.constant is not None: self.properties['readonly'] = True From 3043200012d809651f83197ff8dc02010a0cc62b Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Fri, 25 Sep 2020 12:05:16 +0200 Subject: [PATCH 5/5] fix initwrite behaviour with handlers, a parameter from the cfg file which is not the first of parameters with the same handler were not written. fix: write_ method is called for all parameters in .writeDict even if there is no poll entry. with this fix, when a parameter has the property initwrite=True, the write_ method is called even when is not polled and even when .pollerClass is None Change-Id: I9b397deb5b20709fc4fa7c860c85b251a204c7f6 --- secop/modules.py | 54 ++++++++++++++++++++++----------------------- secop/params.py | 10 ++++----- secop/poller.py | 6 ++++- test/test_poller.py | 3 ++- 4 files changed, 38 insertions(+), 35 deletions(-) diff --git a/secop/modules.py b/secop/modules.py index 0940ba5..980e056 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -346,16 +346,6 @@ class Module(HasProperties, metaclass=ModuleMeta): def initModule(self): self.log.debug('empty %s.initModule()' % self.__class__.__name__) - def startModule(self, started_callback): - """runs after init of all modules - - started_callback to be called when thread spawned by late_init - or, if not implemented, immediately - might return a timeout value, if different from default - """ - self.log.debug('empty %s.startModule()' % self.__class__.__name__) - started_callback() - def pollOneParam(self, pname): """poll parameter with proper error handling""" try: @@ -367,23 +357,34 @@ class Module(HasProperties, metaclass=ModuleMeta): except Exception: self.log.error(formatException()) - def writeOrPoll(self, pname): - """write configured value for a parameter, if any, else poll + def writeInitParams(self, started_callback=None): + """write values for parameters with configured values + this must be called at the beginning of the poller thread with proper error handling """ - try: - if pname in self.writeDict: - self.log.debug('write parameter %s', pname) - getattr(self, 'write_' + pname)(self.writeDict.pop(pname)) - else: - getattr(self, 'read_' + pname)() - except SilentError: - pass - except SECoPError as e: - self.log.error(str(e)) - except Exception: - self.log.error(formatException()) + for pname in list(self.writeDict): + if pname in self.writeDict: # this might not be true with handlers + try: + self.log.debug('initialize parameter %s', pname) + getattr(self, 'write_' + pname)(self.writeDict.pop(pname)) + except SilentError: + pass + except SECoPError as e: + self.log.error(str(e)) + except Exception: + self.log.error(formatException()) + if started_callback: + started_callback() + + def startModule(self, started_callback): + """runs after init of all modules + + started_callback to be called when the thread spawned by startModule + has finished its initial work + might return a timeout value, if different from default + """ + mkthread(self.writeInitParams, started_callback) class Readable(Module): @@ -424,7 +425,7 @@ class Readable(Module): # use basic poller for legacy code mkthread(self.__pollThread, started_callback) else: - started_callback() + super().startModule(self, started_callback) def __pollThread(self, started_callback): while True: @@ -439,8 +440,7 @@ class Readable(Module): def __pollThread_inner(self, started_callback): """super simple and super stupid per-module polling thread""" - for pname in list(self.writeDict): - self.writeOrPoll(pname) + self.writeInitParams() i = 0 fastpoll = self.pollParams(i) started_callback() diff --git a/secop/params.py b/secop/params.py index 40904a6..af26423 100644 --- a/secop/params.py +++ b/secop/params.py @@ -115,7 +115,8 @@ class Parameter(Accessible): settable=False, default=False), 'handler': Property('[internal] overload the standard read and write functions', ValueType(), export=False, default=None, mandatory=False, settable=False), - 'initwrite': Property('[internal] write this parameter on initialization (default None: write if given in config)', + 'initwrite': Property('[internal] write this parameter on initialization' + ' (default None: write if given in config)', NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False), } @@ -139,11 +140,8 @@ class Parameter(Accessible): datatype.setProperty('unit', unit) super(Parameter, self).__init__(**kwds) - if self.initwrite: - if self.readonly: - raise ProgrammingError('can not have both readonly and initwrite!') - if not self.poll: - raise ProgrammingError('only polled parameters can have initwrite!') + if self.initwrite and self.readonly: + raise ProgrammingError('can not have both readonly and initwrite!') if self.constant is not None: self.properties['readonly'] = True diff --git a/secop/poller.py b/secop/poller.py index 9c29d37..852803c 100644 --- a/secop/poller.py +++ b/secop/poller.py @@ -118,8 +118,10 @@ class Poller(PollerBase): self._stopped = False self.maxwait = 3600 self.name = name + self.modules = [] # used for writeInitParams only def add_to_poller(self, module): + self.modules.append(module) factors = self.DEFAULT_FACTORS.copy() try: factors[DYNAMIC] = module.fast_pollfactor @@ -227,11 +229,13 @@ class Poller(PollerBase): # nothing to do (else we might call time.sleep(float('inf')) below started_callback() return + for module in self.modules: + module.writeInitParams() # do all polls once and, at the same time, insert due info for _, queue in sorted(self.queues.items()): # do SLOW polls first for idx, (_, _, (_, module, pobj, pname, factor)) in enumerate(queue): lastdue = time.time() - module.writeOrPoll(pname) + module.pollOneParam(pname) due = lastdue + min(self.maxwait, module.pollinterval * factor) # in python 3 comparing tuples need some care, as not all objects # are comparable. Inserting a unique idx solves the problem. diff --git a/test/test_poller.py b/test/test_poller.py index e23a0b7..d36eb67 100644 --- a/test/test_poller.py +++ b/test/test_poller.py @@ -163,7 +163,8 @@ class Module: def pollOneParam(self, pname): getattr(self, 'read_' + pname)() - writeOrPoll = pollOneParam + def writeInitParams(self): + pass def __repr__(self): rdict = self.__dict__.copy()