# -*- 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 # ***************************************************************************** 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 (, , ) 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 with options from dict :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 """ 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 ':status' and ':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()))