# ***************************************************************************** # # 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 # # ***************************************************************************** """Mixin for keeping parameters persistent For hardware not keeping parameters persistent, we might want to store them in a file. The following example will make 'param1' and 'param2' persistent, i.e. whenever one of the parameters is changed, either by a change command or when reading back from the hardware, it is saved to a file, and reloaded after a power down / power up cycle. In order to make this work properly, there is a mechanism needed to detect power down (i.e. a reading a hardware parameter taking a special value on power up). An additional use might be the example of a motor with an encoder which looses the counts of how many turns already happened on power down. This can be solved by comparing the loaded encoder value self.encoder with a fresh value from the hardware and then adjusting the zero point accordingly. class MyClass(PersistentMixin, ...): param1 = PersistentParam(...) param2 = PersistentParam(...) encoder = PersistentParam(...) ... def read_encoder(self): encoder = if : self.loadParameters() else: self.saveParameters() """ import os import json from frappy.lib import generalConfig from frappy.datatypes import EnumType from frappy.params import Parameter, Property, Command, Limit from frappy.modules import Module class PersistentParam(Parameter): persistent = Property('persistence flag (auto means: save automatically on any change)', EnumType(off=0, on=1, auto=2), default=1) given = False class PersistentLimit(Limit, Parameter): pass class PersistentMixin(Module): persistentData = None # dict containing persistent data after startup def __init__(self, name, logger, cfgdict, srv): super().__init__(name, logger, cfgdict, srv) persistentdir = generalConfig.logdir / 'persistent' os.makedirs(persistentdir, exist_ok=True) self.persistentFile = persistentdir / f'{self.secNode.equipment_id}.{self.name}.json' self.initData = {} # "factory" settings loaded = self.loadPersistentData() for pname, pobj in self.parameters.items(): flag = getattr(pobj, 'persistent', False) if flag: if flag == 'auto': self.addCallback(pname, self.saveParameters) self.initData[pname] = pobj.value if not pobj.given: if pname in loaded: pobj.value = loaded[pname] if hasattr(self, 'write_' + pname): # a persistent parameter should be written to HW, even when not yet in persistentData self.writeDict[pname] = pobj.value self.__save_params() def loadPersistentData(self): try: with open(self.persistentFile, 'r', encoding='utf-8') as f: self.persistentData = json.load(f) except (FileNotFoundError, ValueError): self.persistentData = {} result = {} for pname, value in self.persistentData.items(): try: pobj = self.parameters[pname] if getattr(pobj, 'persistent', False): result[pname] = self.parameters[pname].datatype.import_value(value) except Exception as e: # ignore invalid persistent data (in case parameters have changed) self.log.warning('can not restore %r to %r (%r)', pname, value, e) return result def loadParameters(self): """load persistent parameters and write them to the HW, in case a write_ method is available may be called from a module when a hardware power down is detected """ loaded = self.loadPersistentData() for pname, value in loaded.items(): pobj = self.parameters[pname] pobj.value = value pobj.readerror = None if hasattr(self, 'write_' + pname): self.writeDict[pname] = value self.writeInitParams() return loaded def saveParameters(self, _=None): """save persistent parameters - to be called regularly explicitly by the module - the caller has to make sure that this is not called after a power down of the connected hardware before loadParameters dummy argument to avoid closure for callback """ if self.writeDict: # do not save before all values are written to the hw, as potentially # factory default values were read in the meantime return self.__save_params() def __save_params(self): data = {k: v.export_value() for k, v in self.parameters.items() if getattr(v, 'persistent', False)} if data != self.persistentData: self.persistentData = data persistentdir = self.persistentFile.parent tmpfile = self.persistentFile.parent / (self.persistentFile.name + '.tmp') if not persistentdir.is_dir(): persistentdir.mkdir(parents=True, exist_ok=True) try: with open(tmpfile, 'w', encoding='utf-8') as f: json.dump(self.persistentData, f, indent=2) f.write('\n') os.rename(tmpfile, self.persistentFile) finally: try: os.remove(tmpfile) except FileNotFoundError: pass @Command() def factory_reset(self): """reset to values from config / default values""" self.writeDict.update(self.initData) self.writeInitParams()