diff --git a/frappy/modules.py b/frappy/modules.py
index 741b843..37f4dcc 100644
--- a/frappy/modules.py
+++ b/frappy/modules.py
@@ -420,6 +420,7 @@ class Module(HasAccessibles):
pobj.value = pobj.default
else:
# value given explicitly, either by cfg or as Parameter argument
+ pobj.given = True # for PersistentMixin
if hasattr(self, 'write_' + pname):
self.writeDict[pname] = pobj.value
if pobj.default is None:
@@ -706,11 +707,12 @@ class Module(HasAccessibles):
else:
loop = False # no slow polls ready
- def writeInitParams(self, started_callback=None):
+ def writeInitParams(self):
"""write values for parameters with configured values
- this must be called at the beginning of the poller thread
- with proper error handling
+ - does proper error handling
+
+ called at the beginning of the poller thread and for writing persistent values
"""
for pname in list(self.writeDict):
value = self.writeDict.pop(pname, Done)
@@ -730,8 +732,6 @@ class Module(HasAccessibles):
self.log.error('%s: %s', pname, str(e))
except Exception:
self.log.error(formatException())
- if started_callback:
- started_callback()
def setRemoteLogging(self, conn, level):
if self.remoteLogHandler is None:
diff --git a/frappy/persistent.py b/frappy/persistent.py
index d12c868..2a54957 100644
--- a/frappy/persistent.py
+++ b/frappy/persistent.py
@@ -21,7 +21,7 @@
# *****************************************************************************
"""Mixin for keeping parameters persistent
-For hardware not keeping parameters persistent, we might want to store them in Frappy.
+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
@@ -58,66 +58,79 @@ import json
from frappy.lib import generalConfig
from frappy.datatypes import EnumType
from frappy.params import Parameter, Property, Command
-from frappy.modules import HasAccessibles
+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 PersistentMixin(HasAccessibles):
- def __init__(self, *args, **kwds):
- super().__init__(*args, **kwds)
+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 = os.path.join(generalConfig.logdir, 'persistent')
os.makedirs(persistentdir, exist_ok=True)
self.persistentFile = os.path.join(persistentdir, '%s.%s.json' % (self.DISPATCHER.equipment_id, self.name))
self.initData = {} # "factory" settings
+ loaded = self.loadPersistentData()
for pname in self.parameters:
pobj = self.parameters[pname]
- flag = getattr(pobj, 'persistent', 0)
+ flag = getattr(pobj, 'persistent', False)
if flag:
if flag == 'auto':
def cb(value, m=self):
m.saveParameters()
self.valueCallbacks[pname].append(cb)
self.initData[pname] = pobj.value
- self.writeDict.update(self.loadParameters(write=False))
+ 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 loadParameters(self, write=True):
- """load persistent parameters
-
- :return: persistent parameters which have to be written
-
- is called upon startup and may be called from a module
- when a hardware powerdown is detected
- """
+ def loadPersistentData(self):
try:
with open(self.persistentFile, 'r', encoding='utf-8') as f:
self.persistentData = json.load(f)
- except Exception:
+ except (FileNotFoundError, ValueError):
self.persistentData = {}
- writeDict = {}
- for pname in self.parameters:
+ 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]
- if getattr(pobj, 'persistent', False) and pname in self.persistentData:
- try:
- value = pobj.datatype.import_value(self.persistentData[pname])
- pobj.value = value
- pobj.readerror = None
- if not pobj.readonly:
- writeDict[pname] = value
- except Exception as e:
- self.log.warning('can not restore %r to %r (%r)' % (pname, value, e))
- if write:
- self.writeDict.update(writeDict)
- self.writeInitParams()
- return writeDict
+ pobj.value = value
+ pobj.readerror = None
+ if hasattr(self, 'write_' + pname):
+ self.writeDict[pname] = value
+ self.writeInitParams()
+ return loaded
def saveParameters(self):
"""save persistent parameters
- - to be called regularely explicitly by the module
+ - 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
"""
@@ -125,6 +138,9 @@ class PersistentMixin(HasAccessibles):
# do not save before all values are written to the hw, as potentially
# factory default values were read in the mean time
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:
diff --git a/frappy_psi/trinamic.py b/frappy_psi/trinamic.py
index f51d1a6..0e562c5 100644
--- a/frappy_psi/trinamic.py
+++ b/frappy_psi/trinamic.py
@@ -270,7 +270,7 @@ class Motor(PersistentMixin, HasIO, Drivable):
elif not self._loading: # just powered up
try:
self._loading = True
- # get persistent values
+ # get persistent values and write to HW
writeDict = self.loadParameters()
finally:
self._loading = False
diff --git a/test/test_persistent.py b/test/test_persistent.py
new file mode 100644
index 0000000..2d58d4b
--- /dev/null
+++ b/test/test_persistent.py
@@ -0,0 +1,112 @@
+# -*- 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 json
+import os
+from os.path import join
+import pytest
+from frappy.config import Param
+from frappy.core import Module, ScaledInteger, IntRange, StringType, StructOf
+from frappy.lib import generalConfig
+from frappy.persistent import PersistentParam, PersistentMixin
+
+
+class DispatcherStub:
+ def announce_update(self, modulename, pname, pobj):
+ pass
+
+
+class LoggerStub:
+ def debug(self, fmt, *args):
+ print(fmt % args)
+ info = warning = exception = error = debug
+ handlers = []
+
+
+logger = LoggerStub()
+
+
+class ServerStub:
+ def __init__(self, equipment_id):
+ self.dispatcher = DispatcherStub()
+ self.dispatcher.equipment_id = equipment_id
+
+
+class Mod(PersistentMixin, Module):
+ flt = PersistentParam('', ScaledInteger(0.1), default=1.0)
+ stc = PersistentParam('', StructOf(i=IntRange(0, 10), s=StringType()))
+
+ def write_flt(self, value):
+ return value
+
+ def write_stc(self, value):
+ return value
+
+
+save_tests = [
+ ({'flt': Param(5.5), 'stc': Param({'i': 3, 's': 'test'})},
+ {'flt': 55, 'stc': {'i': 3, 's': 'test'}}),
+ ({'flt': Param(5.5)},
+ {'flt': 55, 'stc': {'i': 0, 's': ''}}), # saved default values
+]
+@pytest.mark.parametrize('cfg, data', save_tests)
+def test_save(tmpdir, cfg, data):
+ generalConfig.logdir = tmpdir
+
+ cfg['description'] = ''
+ m = Mod('m', logger, cfg, ServerStub('savetest'))
+ assert m.writeDict == {k: getattr(m, k) for k in data}
+ m.writeDict.clear() # clear in order to indicate writing has happened
+ m.saveParameters()
+ with open(join(tmpdir, 'persistent', 'savetest.m.json'), encoding='utf-8') as f:
+ assert json.load(f) == data
+
+
+load_tests = [
+ # check that value from cfg is overriding value from persistent file
+ ({'flt': Param(5.5), 'stc': Param({'i': 3, 's': 'test'})},
+ {'flt': 33, 'stc': {'i': 1, 's': 'bar'}},
+ {'flt': 5.5, 'stc': {'i': 3, 's': 'test'}}),
+ # check that value from file is taken when not in cfg
+ ({'flt': Param(3.5)},
+ {'flt': 35, 'stc': {'i': 2, 's': ''}},
+ {'flt': 3.5, 'stc': {'i': 2, 's': ''}}),
+ # check default is written when neither cfg is given nor persistent values present
+ ({},
+ {},
+ {'flt': 1.0, 'stc': {'i': 0, 's': ''}}),
+]
+@pytest.mark.parametrize('cfg, data, written', load_tests)
+def test_load(tmpdir, cfg, data, written):
+ generalConfig.logdir = tmpdir
+
+ os.makedirs(join(tmpdir, 'persistent'), exist_ok=True)
+ with open(join(tmpdir, 'persistent', 'loadtest.m.json'), 'w', encoding='utf-8') as f:
+ json.dump(data, f)
+ cfg['description'] = ''
+ m = Mod('m', logger, cfg, ServerStub('loadtest'))
+ assert m.writeDict == written
+ # parameter given in config must override values from file
+ for k, v in cfg.items():
+ assert getattr(m, k) == v['value']
+ for k, v in written.items():
+ assert getattr(m, k) == v