215 lines
8.9 KiB
Python
215 lines
8.9 KiB
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 <markus.zolliker@psi.ch>
|
|
# *****************************************************************************
|
|
|
|
import time
|
|
from os.path import join
|
|
from frappyhistory.writer import Writer # pylint: disable=import-error
|
|
from secop.lib import clamp, formatExtendedTraceback
|
|
from secop.datatypes import IntRange, FloatRange, ScaledInteger,\
|
|
EnumType, BoolType, StringType, TupleOf, StructOf, ArrayOf, TextType
|
|
|
|
|
|
def make_cvt_list(dt, tail):
|
|
"""create conversion list
|
|
|
|
list of tuple (<conversion function>, <tail>, <curve options>)
|
|
tail is a postfix to be appended in case of tuples and structs
|
|
"""
|
|
if isinstance(dt, (IntRange, BoolType)):
|
|
return [(int, {'key': tail})]
|
|
if isinstance(dt, EnumType):
|
|
return [(int, {'key': tail, 'enum': dt.export_datatype()['members']})]
|
|
if isinstance(dt, (FloatRange, ScaledInteger)):
|
|
opts = {'key': tail}
|
|
if dt.unit:
|
|
opts['unit'] = dt.unit
|
|
opts['stepped'] = True
|
|
return [(dt.import_value, opts)]
|
|
if isinstance(dt, StringType):
|
|
opts = {'key': tail, 'kind': 'STR'}
|
|
if isinstance(dt, TextType):
|
|
opts['category'] = 'no'
|
|
else:
|
|
opts['category'] = 'string'
|
|
return [(lambda x: x, opts)]
|
|
if isinstance(dt, TupleOf):
|
|
result = []
|
|
for index, elmtype in enumerate(dt.members):
|
|
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, index)):
|
|
def conv(value, key=index, func=fun):
|
|
return func(value[key])
|
|
result.append((conv, opts))
|
|
return result
|
|
if isinstance(dt, ArrayOf):
|
|
result = []
|
|
for index in range(dt.maxlen):
|
|
for fun, opts in make_cvt_list(dt.members, '%s.%s' % (tail, index)):
|
|
opts['category'] = 'no'
|
|
|
|
def conv(value, key=index, func=fun):
|
|
return func(value[key])
|
|
result.append((conv, opts))
|
|
return result
|
|
if isinstance(dt, StructOf):
|
|
result = []
|
|
for subkey, elmtype in dt.members.items():
|
|
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
|
|
def conv(value, key=subkey, func=fun):
|
|
return func(value.get(key)) # None for missing struct key, should not be needed
|
|
result.append((conv, opts))
|
|
return result
|
|
return [] # other types (BlobType) are ignored: too much data, probably not used
|
|
|
|
|
|
class FrappyAbstractHistoryWriter:
|
|
"""abstract writer
|
|
|
|
doc only
|
|
"""
|
|
|
|
def put_def(self, key, kind='NUM', category='minor', **opts):
|
|
"""define or overwrite a new curve named <key> with options from dict <opts>
|
|
|
|
:param key: the key for the curve
|
|
:param kind: 'NUM' (default) for numeric values, 'STR' for strings
|
|
:param category: 'major' or 'minor': importance of curve
|
|
:param opts: a dict containing some of the following options
|
|
|
|
- label: a label for the curve in the chart
|
|
- 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'
|
|
True by default.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def put(self, timestamp, key, value):
|
|
"""add a data point
|
|
|
|
:param timestamp: the timestamp. must not decrease!
|
|
:param key: the curve name
|
|
:param value: the value to be stored (number or string), None indicates un undefined value
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def close(self, timestamp):
|
|
"""close the writer
|
|
|
|
:param timestamp:
|
|
indicate to the writer that all values are getting undefined after <timestamp>
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class FrappyHistory(Writer):
|
|
def __init__(self, history_path, modules, logger):
|
|
minsteps = {} # generalConfig.mintimesteps
|
|
super().__init__(history_path, logger, minsteps=minsteps)
|
|
self.__init_time = time.time()
|
|
self.__last_time = self.__init_time
|
|
# bad_timestep = set()
|
|
for modname, modobj in modules.items():
|
|
# if isinstance(modobj, HasComlog):
|
|
# modobj.enableComlog(join(history_path, 'comlog'))
|
|
for pname, pobj in modobj.parameters.items():
|
|
key = '%s:%s' % (modname, pname)
|
|
dt = pobj.datatype
|
|
cvt_list = make_cvt_list(dt, key)
|
|
|
|
given_opts = pobj.history
|
|
if isinstance(given_opts, dict):
|
|
given_opts = [given_opts] * len(cvt_list)
|
|
|
|
if pname == 'value':
|
|
for _, opts in cvt_list:
|
|
opts['key'] = opts['key'].replace(':value', '')
|
|
if pname == 'status':
|
|
# default labels '<modname>:status' and '<modname>:status_text'
|
|
for lbl, (_, opts) in zip([key, key + '_text'], cvt_list):
|
|
opts['label'] = lbl
|
|
|
|
label_set = set()
|
|
cvt_filtered = []
|
|
for given, (cvt_func, opts) in zip(given_opts, cvt_list):
|
|
result = dict(opts, **given)
|
|
|
|
stepped = result.pop('stepped', None)
|
|
if opts.get('stepped'): # True on floats
|
|
if pobj.readonly or stepped is False:
|
|
result['stepped'] = False
|
|
|
|
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:
|
|
result['category'] = 'minor'
|
|
else:
|
|
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:
|
|
# make sure time stamp is not decreasing, as a potentially decreasing
|
|
# value might bring the reader software into trouble
|
|
t = clamp(self.__last_time, t, time.time())
|
|
else:
|
|
t = time.time()
|
|
history.__last_time = t
|
|
if pobj.readerror: # error update
|
|
for _, opts in cvt:
|
|
self.put(t, opts['key'], None)
|
|
else:
|
|
for fun, opts in cvt:
|
|
self.put(t, opts['key'], fun(value))
|
|
|
|
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 close(self):
|
|
super().close(max(self.__last_time, time.time())) |