even more merges from gerrit

Change-Id: I4cfddc8fd4157ceae353789f2f60d834ec05974e
This commit is contained in:
2022-03-08 10:54:04 +01:00
parent 34b93adef0
commit 7c9296fe2e
9 changed files with 74 additions and 28 deletions

View File

@ -879,6 +879,8 @@ class StructOf(DataType):
:param optional: a list of optional members :param optional: a list of optional members
:param members: names as keys and types as values for all 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): def __init__(self, optional=None, **members):
super().__init__() super().__init__()
self.members = members self.members = members

View File

@ -253,6 +253,7 @@ class StringIO(IOBase):
if not self.is_connected: if not self.is_connected:
self.read_is_connected() # try to reconnect self.read_is_connected() # try to reconnect
if not self._conn: if not self._conn:
self.log.debug('can not connect to %r' % self.uri)
raise CommunicationSilentError('can not connect to %r' % self.uri) raise CommunicationSilentError('can not connect to %r' % self.uri)
try: try:
with self._lock: with self._lock:
@ -410,7 +411,7 @@ class BytesIO(IOBase):
:return: the full reply (replyheader + additional bytes) :return: the full reply (replyheader + additional bytes)
When the reply length is variable, :meth:`communicate` should be called 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 Typically this method determines then the length of additional bytes from
the already received bytes (replyheader) and/or the request and calls the already received bytes (replyheader) and/or the request and calls
:meth:`readBytes` to get the remaining bytes. :meth:`readBytes` to get the remaining bytes.

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ***************************************************************************** # *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under # 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 # 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 # Foundation; either version 2 of the License, or (at your option) any later
@ -17,6 +18,7 @@
# #
# Module authors: # Module authors:
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
#
# ***************************************************************************** # *****************************************************************************

View File

@ -41,7 +41,7 @@ from secop.logging import RemoteLogHandler, HasComlog
generalConfig.defaults['disable_value_range_check'] = False # check for problematic value range by default 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 """a special return value for a read/write function
indicating that the setter is triggered already""" indicating that the setter is triggered already"""

View File

@ -125,15 +125,22 @@ class Dispatcher:
"""registers new connection""" """registers new connection"""
self._connections.append(conn) self._connections.append(conn)
def remove_connection(self, conn): def reset_connection(self, conn):
"""removes now longer functional connection""" """remove all subscriptions for a connection
if conn in self._connections:
self._connections.remove(conn) to be called on the identification message
"""
for _evt, conns in list(self._subscriptions.items()): for _evt, conns in list(self._subscriptions.items()):
conns.discard(conn) conns.discard(conn)
self.set_all_log_levels(conn, 'off') self.set_all_log_levels(conn, 'off')
self._active_connections.discard(conn) 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): def register_module(self, moduleobj, modulename, export=True):
self.log.debug('registering module %r as %s (export=%r)' % self.log.debug('registering module %r as %s (export=%r)' %
(moduleobj, modulename, export)) (moduleobj, modulename, export))
@ -299,6 +306,10 @@ class Dispatcher:
self.log.error('should have been handled in the interface!') self.log.error('should have been handled in the interface!')
def handle__ident(self, conn, specifier, data): 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) return (IDENTREPLY, None, None)
def handle_describe(self, conn, specifier, data): def handle_describe(self, conn, specifier, data):

View File

@ -42,7 +42,6 @@ class Ls370Sim(Communicator):
for fmt, v in self.CHANNEL_COMMANDS: for fmt, v in self.CHANNEL_COMMANDS:
for chan in range(1,17): for chan in range(1,17):
self._data[fmt % chan] = v self._data[fmt % chan] = v
# mkthread(self.run)
def communicate(self, command): def communicate(self, command):
self.comLog('> %s' % command) self.comLog('> %s' % command)

View File

@ -168,7 +168,7 @@ class Channel(PpmsBase):
datatype=IntRange(1, 4), export=False) datatype=IntRange(1, 4), export=False)
def earlyInit(self): def earlyInit(self):
Readable.earlyInit(self) super().earlyInit()
if not self.channel: if not self.channel:
self.channel = self.name self.channel = self.name

View File

@ -26,6 +26,7 @@ import time
def num(string): def num(string):
return json.loads(string) return json.loads(string)
class NamedList: class NamedList:
def __init__(self, keys, *args, **kwargs): def __init__(self, keys, *args, **kwargs):
self.__keys__ = keys.split() self.__keys__ = keys.split()
@ -49,8 +50,10 @@ class NamedList:
def __repr__(self): def __repr__(self):
return ",".join("%.7g" % val for val in self.aslist()) return ",".join("%.7g" % val for val in self.aslist())
class PpmsSim: class PpmsSim:
CHANNELS = 'st t mf pos r1 i1 r2 i2'.split() CHANNELS = 'st t mf pos r1 i1 r2 i2'.split()
def __init__(self): def __init__(self):
self.status = NamedList('t mf ch pos', 1, 1, 1, 1) self.status = NamedList('t mf ch pos', 1, 1, 1, 1)
self.st = 0x1111 self.st = 0x1111
@ -176,7 +179,6 @@ class PpmsSim:
if abs(self.t - self.temp.target) < 1: if abs(self.t - self.temp.target) < 1:
self.status.t = 6 # outside tolerance self.status.t = 6 # outside tolerance
if abs(self.pos - self.move.target) < 0.01: if abs(self.pos - self.move.target) < 0.01:
self.status.pos = 1 self.status.pos = 1
else: else:
@ -187,8 +189,7 @@ class PpmsSim:
self.i1 = self.t % 10.0 self.i1 = self.t % 10.0
self.r2 = 1000 / self.t self.r2 = 1000 / self.t
self.i2 = math.log(self.t) self.i2 = math.log(self.t)
self.level.value = round(100 - (self.time - self.start) * 0.01 % 100, 1) self.level.value = 100 - (self.time - self.start) * 0.01 % 100
# print('PROGRESS T=%.7g B=%.7g x=%.7g' % (self.t, self.mf, self.pos))
def getdat(self, mask): def getdat(self, mask):
mask = int(mask) & 0xff # all channels up to i2 mask = int(mask) & 0xff # all channels up to i2
@ -198,6 +199,7 @@ class PpmsSim:
output.append("%.7g" % getattr(self, chan)) output.append("%.7g" % getattr(self, chan))
return ",".join(output) return ",".join(output)
class QDevice: class QDevice:
def __init__(self, classid): def __init__(self, classid):
self.sim = PpmsSim() self.sim = PpmsSim()
@ -225,5 +227,6 @@ class QDevice:
result = "OK" result = "OK"
return result return result
def shutdown(): def shutdown():
pass pass

View File

@ -22,12 +22,12 @@
import math import math
import os import os
from os.path import basename, exists, join from os.path import basename, dirname, exists, join
import numpy as np import numpy as np
from scipy.interpolate import splev, splrep # pylint: disable=import-error 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): def linear(x):
@ -74,13 +74,18 @@ class Parser340(StdParser):
def parse(self, line): def parse(self, line):
"""scan header for data format""" """scan header for data format"""
if self.header: if self.header:
if line.startswith("Data Format"): key, _, value = line.partition(':')
dataformat = line.split(":")[1].strip()[0] if value: # this is a header line, as it contains ':'
if dataformat == '4': value = value.split()[0]
self.logx, self.logy = True, False # logOhm key = ''.join(key.split()).lower()
elif dataformat == '5': if key == 'dataformat':
self.logx, self.logy = True, True # logOhm, logK if value == '4':
elif line.startswith("No."): 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 self.header = False
return return
super().parse(line) super().parse(line)
@ -104,7 +109,9 @@ class CalCurve:
calibname = sensopt.pop(0) calibname = sensopt.pop(0)
_, dot, ext = basename(calibname).rpartition('.') _, dot, ext = basename(calibname).rpartition('.')
kind = None 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 # first try without adding kind
filename = join(path.strip(), calibname) filename = join(path.strip(), calibname)
if exists(filename): if exists(filename):
@ -134,13 +141,26 @@ class CalCurve:
cls, args = KINDS.get(kind, (StdParser, {})) cls, args = KINDS.get(kind, (StdParser, {}))
args.update(optargs) args.update(optargs)
parser = cls(**args) try:
with open(filename) as f: parser = cls(**args)
for line in f: with open(filename) as f:
parser.parse(line) 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_x = nplog if parser.logx else linear
self.convert_y = npexp if parser.logy 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): def __call__(self, value):
"""convert value """convert value
@ -156,7 +176,7 @@ class Sensor(Readable):
calib = Parameter('calibration name', datatype=StringType(), readonly=False) calib = Parameter('calibration name', datatype=StringType(), readonly=False)
abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True) 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) pollinterval = Parameter(export=False)
status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) status = Parameter(default=(Readable.Status.ERROR, 'unintialized'))
@ -164,9 +184,17 @@ class Sensor(Readable):
_value_error = None _value_error = None
enablePoll = False enablePoll = False
def checkProperties(self):
if 'description' not in self.propertyValues:
self.description = '_' # avoid complaining about missing description
super().checkProperties()
def initModule(self): def initModule(self):
super().initModule()
self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._rawsensor.registerCallbacks(self, ['status']) # auto update status
self._calib = CalCurve(self.calib) self._calib = CalCurve(self.calib)
if self.description == '_':
self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib)
def write_calib(self, value): def write_calib(self, value):
self._calib = CalCurve(value) self._calib = CalCurve(value)
@ -174,7 +202,7 @@ class Sensor(Readable):
def update_value(self, value): def update_value(self, value):
if self.abs: if self.abs:
value = abs(value) value = abs(float(value))
self.value = self._calib(value) self.value = self._calib(value)
self._value_error = None self._value_error = None