From 7c9296fe2e8d5dc9080080b4aab0956ab807db16 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 8 Mar 2022 10:54:04 +0100 Subject: [PATCH] even more merges from gerrit Change-Id: I4cfddc8fd4157ceae353789f2f60d834ec05974e --- secop/datatypes.py | 2 ++ secop/io.py | 3 +- secop/logging.py | 2 ++ secop/modules.py | 2 +- secop/protocol/dispatcher.py | 19 ++++++++--- secop_psi/ls370sim.py | 1 - secop_psi/ppms.py | 2 +- secop_psi/ppmssim.py | 9 ++++-- secop_psi/softcal.py | 62 ++++++++++++++++++++++++++---------- 9 files changed, 74 insertions(+), 28 deletions(-) diff --git a/secop/datatypes.py b/secop/datatypes.py index ada53f6..12ce4ec 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -879,6 +879,8 @@ class StructOf(DataType): :param optional: a list of optional members :param members: names as keys and types as values for all members """ + # Remark: assignment of parameters containing partial structs in their datatype + # are (and can) not be handled here! This has to be done manually in the write method def __init__(self, optional=None, **members): super().__init__() self.members = members diff --git a/secop/io.py b/secop/io.py index 48edf0c..9aad8f8 100644 --- a/secop/io.py +++ b/secop/io.py @@ -253,6 +253,7 @@ class StringIO(IOBase): if not self.is_connected: self.read_is_connected() # try to reconnect if not self._conn: + self.log.debug('can not connect to %r' % self.uri) raise CommunicationSilentError('can not connect to %r' % self.uri) try: with self._lock: @@ -410,7 +411,7 @@ class BytesIO(IOBase): :return: the full reply (replyheader + additional bytes) When the reply length is variable, :meth:`communicate` should be called - with the `replylen` argument set to minimum expected length of the reply. + with the `replylen` argument set to the minimum expected length of the reply. Typically this method determines then the length of additional bytes from the already received bytes (replyheader) and/or the request and calls :meth:`readBytes` to get the remaining bytes. diff --git a/secop/logging.py b/secop/logging.py index 2b3291e..cd8cf41 100644 --- a/secop/logging.py +++ b/secop/logging.py @@ -1,6 +1,7 @@ #!/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 @@ -17,6 +18,7 @@ # # Module authors: # Markus Zolliker +# # ***************************************************************************** diff --git a/secop/modules.py b/secop/modules.py index fc1929c..5623d93 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -41,7 +41,7 @@ from secop.logging import RemoteLogHandler, HasComlog generalConfig.defaults['disable_value_range_check'] = False # check for problematic value range by default -Done = UniqueObject('already set') +Done = UniqueObject('Done') """a special return value for a read/write function indicating that the setter is triggered already""" diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 311f1d8..58a0e4b 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -125,15 +125,22 @@ class Dispatcher: """registers new connection""" self._connections.append(conn) - def remove_connection(self, conn): - """removes now longer functional connection""" - if conn in self._connections: - self._connections.remove(conn) + def reset_connection(self, conn): + """remove all subscriptions for a connection + + to be called on the identification message + """ for _evt, conns in list(self._subscriptions.items()): conns.discard(conn) self.set_all_log_levels(conn, 'off') self._active_connections.discard(conn) + def remove_connection(self, conn): + """removes now longer functional connection""" + if conn in self._connections: + self._connections.remove(conn) + self.reset_connection(conn) + def register_module(self, moduleobj, modulename, export=True): self.log.debug('registering module %r as %s (export=%r)' % (moduleobj, modulename, export)) @@ -299,6 +306,10 @@ class Dispatcher: self.log.error('should have been handled in the interface!') def handle__ident(self, conn, specifier, data): + # Remark: the following line is needed due to issue 66. + self.reset_connection(conn) + # The other stuff in issue 66 ('error_closed' message), has to be implemented + # if and when frappy will support serial server connections return (IDENTREPLY, None, None) def handle_describe(self, conn, specifier, data): diff --git a/secop_psi/ls370sim.py b/secop_psi/ls370sim.py index cde2d64..969d566 100644 --- a/secop_psi/ls370sim.py +++ b/secop_psi/ls370sim.py @@ -42,7 +42,6 @@ class Ls370Sim(Communicator): for fmt, v in self.CHANNEL_COMMANDS: for chan in range(1,17): self._data[fmt % chan] = v - # mkthread(self.run) def communicate(self, command): self.comLog('> %s' % command) diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 1fe265f..d376e2b 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -168,7 +168,7 @@ class Channel(PpmsBase): datatype=IntRange(1, 4), export=False) def earlyInit(self): - Readable.earlyInit(self) + super().earlyInit() if not self.channel: self.channel = self.name diff --git a/secop_psi/ppmssim.py b/secop_psi/ppmssim.py index fd4686c..1ab0e89 100644 --- a/secop_psi/ppmssim.py +++ b/secop_psi/ppmssim.py @@ -26,6 +26,7 @@ import time def num(string): return json.loads(string) + class NamedList: def __init__(self, keys, *args, **kwargs): self.__keys__ = keys.split() @@ -49,8 +50,10 @@ class NamedList: def __repr__(self): return ",".join("%.7g" % val for val in self.aslist()) + class PpmsSim: CHANNELS = 'st t mf pos r1 i1 r2 i2'.split() + def __init__(self): self.status = NamedList('t mf ch pos', 1, 1, 1, 1) self.st = 0x1111 @@ -176,7 +179,6 @@ class PpmsSim: if abs(self.t - self.temp.target) < 1: self.status.t = 6 # outside tolerance - if abs(self.pos - self.move.target) < 0.01: self.status.pos = 1 else: @@ -187,8 +189,7 @@ class PpmsSim: self.i1 = self.t % 10.0 self.r2 = 1000 / self.t self.i2 = math.log(self.t) - self.level.value = round(100 - (self.time - self.start) * 0.01 % 100, 1) - # print('PROGRESS T=%.7g B=%.7g x=%.7g' % (self.t, self.mf, self.pos)) + self.level.value = 100 - (self.time - self.start) * 0.01 % 100 def getdat(self, mask): mask = int(mask) & 0xff # all channels up to i2 @@ -198,6 +199,7 @@ class PpmsSim: output.append("%.7g" % getattr(self, chan)) return ",".join(output) + class QDevice: def __init__(self, classid): self.sim = PpmsSim() @@ -225,5 +227,6 @@ class QDevice: result = "OK" return result + def shutdown(): pass diff --git a/secop_psi/softcal.py b/secop_psi/softcal.py index d3bcefb..8095cbf 100644 --- a/secop_psi/softcal.py +++ b/secop_psi/softcal.py @@ -22,12 +22,12 @@ import math import os -from os.path import basename, exists, join +from os.path import basename, dirname, exists, join import numpy as np from scipy.interpolate import splev, splrep # pylint: disable=import-error -from secop.core import Attached, BoolType, Parameter, Readable, StringType +from secop.core import Attached, BoolType, Parameter, Readable, StringType, FloatRange def linear(x): @@ -74,13 +74,18 @@ class Parser340(StdParser): def parse(self, line): """scan header for data format""" if self.header: - if line.startswith("Data Format"): - dataformat = line.split(":")[1].strip()[0] - if dataformat == '4': - self.logx, self.logy = True, False # logOhm - elif dataformat == '5': - self.logx, self.logy = True, True # logOhm, logK - elif line.startswith("No."): + key, _, value = line.partition(':') + if value: # this is a header line, as it contains ':' + value = value.split()[0] + key = ''.join(key.split()).lower() + if key == 'dataformat': + if value == '4': + self.logx, self.logy = True, False # logOhm + elif value == '5': + self.logx, self.logy = True, True # logOhm, logK + elif value not in ('1', '2', '3'): + raise ValueError('invalid Data Format') + elif 'No.' in line: self.header = False return super().parse(line) @@ -104,7 +109,9 @@ class CalCurve: calibname = sensopt.pop(0) _, dot, ext = basename(calibname).rpartition('.') kind = None - for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','): + pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(',') + pathlist.append(join(dirname(__file__), 'calcurves')) + for path in pathlist: # first try without adding kind filename = join(path.strip(), calibname) if exists(filename): @@ -134,13 +141,26 @@ class CalCurve: cls, args = KINDS.get(kind, (StdParser, {})) args.update(optargs) - parser = cls(**args) - with open(filename) as f: - for line in f: - parser.parse(line) + try: + parser = cls(**args) + with open(filename) as f: + for line in f: + parser.parse(line) + except Exception as e: + raise ValueError('calib curve %s: %s' % (calibspec, e)) from e self.convert_x = nplog if parser.logx else linear self.convert_y = npexp if parser.logy else linear - self.spline = splrep(np.asarray(parser.xdata), np.asarray(parser.ydata), s=0) + x = np.asarray(parser.xdata) + y = np.asarray(parser.ydata) + if np.all(x[:-1] > x[1:]): # all decreasing + x = np.flip(x) + y = np.flip(y) + elif np.any(x[:-1] >= x[1:]): # some not increasing + raise ValueError('calib curve %s is not monotonic' % calibspec) + try: + self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1)) + except (ValueError, TypeError) as e: + raise ValueError('invalid calib curve %s' % calibspec) from e def __call__(self, value): """convert value @@ -156,7 +176,7 @@ class Sensor(Readable): calib = Parameter('calibration name', datatype=StringType(), readonly=False) abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True) - value = Parameter(unit='K') + value = Parameter(datatype=FloatRange(unit='K')) pollinterval = Parameter(export=False) status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) @@ -164,9 +184,17 @@ class Sensor(Readable): _value_error = None enablePoll = False + def checkProperties(self): + if 'description' not in self.propertyValues: + self.description = '_' # avoid complaining about missing description + super().checkProperties() + def initModule(self): + super().initModule() self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._calib = CalCurve(self.calib) + if self.description == '_': + self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib) def write_calib(self, value): self._calib = CalCurve(value) @@ -174,7 +202,7 @@ class Sensor(Readable): def update_value(self, value): if self.abs: - value = abs(value) + value = abs(float(value)) self.value = self._calib(value) self._value_error = None