history, as of 2022-02-01

Change-Id: I725a57546df8f7c9e5ffe04eb98872f2a609efe4
This commit is contained in:
zolliker 2022-02-02 09:52:01 +01:00
parent 8253fe471b
commit 05d0cfb193
3 changed files with 130 additions and 148 deletions

View File

@ -19,10 +19,8 @@
# ***************************************************************************** # *****************************************************************************
import time import time
try: from os.path import join
import frappyhistory # pylint: disable=import-error from frappyhistory.writer import Writer # pylint: disable=import-error
except ImportError:
pass # do not complain when used for tests
from secop.lib import clamp, formatExtendedTraceback from secop.lib import clamp, formatExtendedTraceback
from secop.datatypes import IntRange, FloatRange, ScaledInteger,\ from secop.datatypes import IntRange, FloatRange, ScaledInteger,\
EnumType, BoolType, StringType, TupleOf, StructOf, ArrayOf, TextType EnumType, BoolType, StringType, TupleOf, StructOf, ArrayOf, TextType
@ -41,13 +39,15 @@ def make_cvt_list(dt, tail):
if isinstance(dt, (FloatRange, ScaledInteger)): if isinstance(dt, (FloatRange, ScaledInteger)):
opts = {'key': tail} opts = {'key': tail}
if dt.unit: if dt.unit:
opts['group'] = dt.unit opts['unit'] = dt.unit
opts['stepped'] = True opts['stepped'] = True
return [(dt.import_value, opts)] return [(dt.import_value, opts)]
if isinstance(dt, StringType): if isinstance(dt, StringType):
opts = {'key': tail, 'kind': 'STR'} opts = {'key': tail, 'kind': 'STR'}
if isinstance(dt, TextType): if isinstance(dt, TextType):
opts['category'] = 'no' opts['category'] = 'no'
else:
opts['category'] = 'string'
return [(lambda x: x, opts)] return [(lambda x: x, opts)]
if isinstance(dt, TupleOf): if isinstance(dt, TupleOf):
result = [] result = []
@ -93,7 +93,8 @@ class FrappyAbstractHistoryWriter:
:param opts: a dict containing some of the following options :param opts: a dict containing some of the following options
- label: a label for the curve in the chart - label: a label for the curve in the chart
- group: grouping of the curves in charts (often equal to unit) - unit: the physical unit
- group: grouping of the curves in charts (unit by default)
- stepped: lines in charts should be drawn as stepped line. Only applicable when kind='NUM' - stepped: lines in charts should be drawn as stepped line. Only applicable when kind='NUM'
True by default. True by default.
""" """
@ -108,14 +109,6 @@ class FrappyAbstractHistoryWriter:
""" """
raise NotImplementedError() raise NotImplementedError()
def get(self, key):
"""get from cache
:param key: the curve name
:returns: the last stored value or None
"""
raise NotImplementedError()
def close(self, timestamp): def close(self, timestamp):
"""close the writer """close the writer
@ -125,32 +118,25 @@ class FrappyAbstractHistoryWriter:
raise NotImplementedError() raise NotImplementedError()
class FrappyHistoryHandler: class FrappyHistory(Writer):
def __init__(self, writer, modules, dispatcher, logger=None): def __init__(self, history_path, modules, logger):
self.writer = writer minsteps = {} # generalConfig.mintimesteps
self.log = logger super().__init__(history_path, logger, minsteps=minsteps)
self.cvt_lists = {} # dict <mod:param> of <conversion list> self.__init_time = time.time()
self.dispatcher = dispatcher self.__last_time = self.__init_time
self._init_time = time.time() # bad_timestep = set()
self._last_time = self._init_time
for modname, modobj in modules.items(): for modname, modobj in modules.items():
# if isinstance(modobj, HasComlog):
# modobj.enableComlog(join(history_path, 'comlog'))
for pname, pobj in modobj.parameters.items(): for pname, pobj in modobj.parameters.items():
ident = '%s:%s' % (modname, pobj.export)
key = '%s:%s' % (modname, pname) key = '%s:%s' % (modname, pname)
dt = pobj.datatype dt = pobj.datatype
cvt_list = make_cvt_list(dt, key) cvt_list = make_cvt_list(dt, key)
# create default opts given_opts = pobj.history
if pobj.history_category: if isinstance(given_opts, dict):
values = pobj.history_category.split(',') given_opts = [given_opts] * len(cvt_list)
else:
values = [None]
if len(values) == 1:
values *= len(cvt_list)
for cat, (_, opts) in zip(values, cvt_list):
if cat is None:
cat = opts.get('category', 'major' if pname in ('value', 'target') else 'minor')
opts['category'] = cat
if pname == 'value': if pname == 'value':
for _, opts in cvt_list: for _, opts in cvt_list:
opts['key'] = opts['key'].replace(':value', '') opts['key'] = opts['key'].replace(':value', '')
@ -158,75 +144,72 @@ class FrappyHistoryHandler:
# default labels '<modname>:status' and '<modname>:status_text' # default labels '<modname>:status' and '<modname>:status_text'
for lbl, (_, opts) in zip([key, key + '_text'], cvt_list): for lbl, (_, opts) in zip([key, key + '_text'], cvt_list):
opts['label'] = lbl opts['label'] = lbl
# overwrite opts based on history_* properties
if pobj.history_label: label_set = set()
for lbl, (_, opts) in zip(','.split(pobj.history_label), cvt_list): cvt_filtered = []
opts['label'] = lbl for given, (cvt_func, opts) in zip(given_opts, cvt_list):
if pobj.history_group: result = dict(opts, **given)
values = pobj.history_group.split(',')
if len(values) == 1: stepped = result.pop('stepped', None)
values *= len(cvt_list) if opts.get('stepped'): # True on floats
for grp, (_, opts) in zip(values, cvt_list): if pobj.readonly or stepped is False:
opts['group'] = grp result['stepped'] = False
if pobj.history_stepped:
values = pobj.history_stepped cat = given.get('category')
if cat is None:
if not pobj.export:
continue
cat = result.get('category')
if cat is None:
if pname in ('value', 'target'):
result['category'] = 'major'
elif pobj.readonly: elif pobj.readonly:
values = False result['category'] = 'minor'
if not isinstance(values, tuple):
values = [values] * len(cvt_list)
for stp, (_, opts) in zip(values, cvt_list):
if not stp and 'stepped' in opts: # only on floats
opts['stepped'] = False
cvt_list = [(key, opts) for key, opts in cvt_list if opts.get('category') != 'no']
for _, opts in cvt_list:
if opts.get('stepped'):
opts.pop('stepped', None)
writer.put_def(**opts)
if cvt_list:
self.cvt_lists[ident] = cvt_list
self.dispatcher.handle_activate(self, None, None)
self._init_time = None
def close_message(self, msg):
self.writer.close(time.time())
def send_reply(self, msg):
try:
action, ident, value = msg
assert action.endswith('update')
cvt_list = self.cvt_lists.get(ident)
if not cvt_list:
return
if self._init_time:
t = self._init_time # on initialisation, use the same timestamp for all
else: else:
t = value[1].get('t') result['category'] = 'param'
if cat == 'no':
continue
label = result.pop('label', None)
if label and label not in label_set:
result['label'] = label
label_set.add(label)
cvt_filtered.append((cvt_func, result))
# if result.get('timestep', 1) < minstep:
# bad_timestep.add('%s:%s' % (modname, pname))
self.put_def(**result)
if cvt_filtered:
def callback(value, p=pobj, history=self, cvt=cvt_filtered):
if self.__init_time:
t = self.__init_time # on initialisation, use the same timestamp for all
else:
t = p.timestamp
if t: if t:
# make sure time stamp is not decreasing, as a potentially decreasing # make sure time stamp is not decreasing, as a potentially decreasing
# value might bring the reader software into trouble # value might bring the reader software into trouble
t = clamp(self._last_time, t, time.time()) t = clamp(self.__last_time, t, time.time())
else: else:
t = time.time() t = time.time()
self._last_time = t history.__last_time = t
if action == 'update': if pobj.readerror: # error update
for fun, opts in cvt_list: for _, opts in cvt:
# we only look at the value, qualifiers are ignored for now self.put(t, opts['key'], None)
# we do not use the timestamp here, as a potentially decreasing value might else:
# bring the reader software into trouble for fun, opts in cvt:
# print('UPDATE', key, value, t) self.put(t, opts['key'], fun(value))
self.writer.put(t, opts['key'], fun(value[0]))
else: # error_update
for _, opts in cvt_list:
self.writer.put(t, opts['key'], None)
except Exception as e:
self.log.error('FrappyHistoryHandler.send_reply: %r with msg %r: ', repr(e), msg)
print(formatExtendedTraceback())
modobj.valueCallbacks[pname].append(callback)
modobj.errorCallbacks[pname].append(callback)
# if bad_timestep:
# if minstep < 1:
# logger.error('timestep < generalConfig.mintimestep')
# else:
# logger.error('timestep < 1, generalConfig.mintimestep not given?')
# logger.error('parameters: %s', ', '.join(bad_timestep))
#
self.__init_time = None
def add_writer(history_path, srv): def close(self):
# treat handler as a connection super().close(max(self.__last_time, time.time()))
logger = srv.log.getChild('history')
srv.dispatcher.add_connection(FrappyHistoryHandler(
frappyhistory.Writer(history_path, logger), srv.modules, srv.dispatcher, logger))

View File

@ -26,7 +26,7 @@
import inspect import inspect
from secop.datatypes import BoolType, CommandType, DataType, \ from secop.datatypes import BoolType, CommandType, DataType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, \ DataTypeType, EnumType, IntRange, NoneOr, OrType, FloatRange, \
StringType, StructOf, TextType, TupleOf, ValueType, ArrayOf StringType, StructOf, TextType, TupleOf, ValueType, ArrayOf
from secop.errors import BadValueError, ProgrammingError from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
@ -94,6 +94,11 @@ class Accessible(HasProperties):
return '%s(%s)' % (self.__class__.__name__, ', '.join(props)) return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
historyStruct = StructOf(category=StringType(), label=StringType(), group=StringType(),
stepped=OrType(BoolType(), StringType()), timestep=FloatRange(0, 1),
record_unchanged=BoolType())
class Parameter(Accessible): class Parameter(Accessible):
"""defines a parameter """defines a parameter
@ -163,52 +168,35 @@ class Parameter(Accessible):
default None: write if given in config''', NoneOr(BoolType()), default None: write if given in config''', NoneOr(BoolType()),
export=False, default=None, settable=False) export=False, default=None, settable=False)
history_category = Property( history = Property(
'''[custom] category for history '''[custom] options for history
major: should be shown by default in a history chart, default for value and target for structured types, this is an array of options, to be applied in the order
minor: to be shown optionally in a history chart, default for other parameters of the created elements.
no: history is not saved. default for TextType and ArrayOf
list of options:
category
- major: should be shown by default in a history chart, default for value and target
- minor: to be shown optionally in a history chart, default for other parameters
- no: history is not saved. default for TextType and ArrayOf
category is ignored (forced to no) for BlobType category is ignored (forced to no) for BlobType
For structured types, the category may be a comma separated list, overwriting the label
default for the first or all curves.
If it does not contain a comma, it applies for all curves
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_label = Property(
'''[custom] label for history
default: <modname>:<parname> or <modname> for main value default: <modname>:<parname> or <modname> for main value
For structured types, the label may be a comma separated list, overwriting the group:
default for the first or all curves.
If it does not contain a comma, it applies for the first curve only.
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_group = Property(
'''[custom] group for history
default: unit default: unit
For structured types, the group may be a comma separated list, overwriting the stepped:
default for the first or all curves. If it does not contain a comma, it is whether a curve has to be drawn stepped or connected.
applies for all curves.
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_stepped = Property(
'''[custom] stepped curve
Whether a curve has to be drawn stepped or connected.
default: True when readonly=False, else False default: True when readonly=False, else False
Applicable to FloatRange and ScaledInteger only, other types are stepped by definition. timestep:
the desired time step for the curve storage. maximum and default value is 1 sec
For structured types, stepped may be a list, overwriting the default for the
first or all curves. If not, applieas to all curves.
''', ''',
OrType(BoolType(), ArrayOf(BoolType())), export=True, default=False, settable=False) OrType(historyStruct, ArrayOf(historyStruct)), export=True, default={}, settable=False)
# used on the instance copy only # used on the instance copy only
value = None value = None

View File

@ -114,6 +114,7 @@ class Server:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections))) self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles self._cfgfiles = cfgfiles
self._pidfile = os.path.join(cfg['piddir'], name + '.pid') self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
self.close_callbacks = []
def loadCfgFile(self, cfgfile): def loadCfgFile(self, cfgfile):
if not cfgfile.endswith('.cfg'): if not cfgfile.endswith('.cfg'):
@ -210,8 +211,7 @@ class Server:
self.interface.serve_forever() self.interface.serve_forever()
except KeyboardInterrupt as e: except KeyboardInterrupt as e:
self._restart = False self._restart = False
self.dispatcher.close() self.close()
self.interface.server_close()
if self._restart: if self._restart:
self.restart_hook() self.restart_hook()
self.log.info('restart') self.log.info('restart')
@ -227,6 +227,12 @@ class Server:
self._restart = False self._restart = False
self.interface.shutdown() self.interface.shutdown()
def close(self):
self.dispatcher.close()
self.interface.server_close()
for cb in self.close_callbacks:
cb()
def _processCfg(self): def _processCfg(self):
errors = [] errors = []
opts = dict(self.node_cfg) opts = dict(self.node_cfg)
@ -338,8 +344,13 @@ class Server:
self.log.info('all modules and pollers started') self.log.info('all modules and pollers started')
history_path = os.environ.get('FRAPPY_HISTORY') history_path = os.environ.get('FRAPPY_HISTORY')
if history_path: if history_path:
from secop_psi.historywriter import add_writer # pylint: disable=import-outside-toplevel try:
add_writer(history_path, self) from secop.historywriter import FrappyHistory # pylint: disable=import-outside-toplevel
history = FrappyHistory(history_path, self.modules, self.log.getChild('history'))
self.close_callbacks.append(history.close)
except ImportError:
raise
self.log.warning('FRAPPY_HISTORY is defined, but frappyhistory package not available')
# TODO: if ever somebody wants to implement an other history writer: # TODO: if ever somebody wants to implement an other history writer:
# - a general config file /etc/secp/secop.conf or <frappy repo>/etc/secop.conf # - a general config file /etc/secp/secop.conf or <frappy repo>/etc/secop.conf
# might be introduced, which contains the log, pid and cfg directory path and # might be introduced, which contains the log, pid and cfg directory path and