From 25b8780b11372eba305add76f58c2bcc0a5d49cc Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 8 Jun 2021 07:39:46 +0200 Subject: [PATCH] persistent module fixed --- cfg/trinamic.cfg | 3 +- secop/core.py | 1 + secop/modules.py | 5 --- secop/persistent.py | 89 ++++++++++++++++++++++++++++++++++--------- secop_psi/trinamic.py | 66 +++++++++++++++++--------------- 5 files changed, 110 insertions(+), 54 deletions(-) diff --git a/cfg/trinamic.cfg b/cfg/trinamic.cfg index 5431be7..4d38eb8 100644 --- a/cfg/trinamic.cfg +++ b/cfg/trinamic.cfg @@ -21,5 +21,6 @@ acceleration=150. movelimit=360 speed=40 encoder_tolerance=3.6 -free_wheeling=0.001 +free_wheeling=0.1 +power_down_delay=0.1 # pull_up=1 diff --git a/secop/core.py b/secop/core.py index e6ca57f..8a5d2ae 100644 --- a/secop/core.py +++ b/secop/core.py @@ -37,3 +37,4 @@ from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW from secop.properties import Property from secop.proxy import Proxy, SecNode, proxy_class from secop.stringio import HasIodev, StringIO +from secop.persistent import PersistentMixin, PersistentParam diff --git a/secop/modules.py b/secop/modules.py index 0fd2cb5..31fa80c 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -242,8 +242,6 @@ class Module(HasAccessibles): self.name = name self.valueCallbacks = {} self.errorCallbacks = {} - self.persistentFile = None - self.persistentData = {} errors = [] # handle module properties @@ -399,8 +397,6 @@ class Module(HasAccessibles): if '$' in dt.unit: dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit)) - self.writeDict.update(self.loadParameters()) - # 6) check complete configuration of * properties if not errors: try: @@ -539,7 +535,6 @@ class Module(HasAccessibles): self.log.error(str(e)) except Exception: self.log.error(formatException()) - self.saveParameters() if started_callback: started_callback() diff --git a/secop/persistent.py b/secop/persistent.py index 6d73ef2..352debd 100644 --- a/secop/persistent.py +++ b/secop/persistent.py @@ -19,30 +19,67 @@ # Markus Zolliker # # ***************************************************************************** -"""Define base classes for real Modules implemented in the server""" +"""Mixin for keeping parameters persistent +For hardware not keeping parameters persistent, we might want to store them in Frappy. + +The following example will make 'param1' and 'param2' persistent, i.e. whenever +one of the parameters is changed, either by a change command or by access from +an other interface to 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 cyclic reading of an +encoder value, 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 secop.errors import BadValueError, ConfigError, InternalError, \ ProgrammingError, SECoPError, SilentError, secop_error +from secop.lib import getGeneralConfig +from secop.params import Parameter, Property, BoolType, Command +from secop.modules import HasAccessibles -class PersistentMixin: - def __init__(*args, **kwds): +class PersistentParam(Parameter): + persistent = Property('persistence flag', BoolType(), default=True) + + +class PersistentMixin(HasAccessibles): + def __init__(self, *args, **kwds): super().__init__(*args, **kwds) - # self.persistentFile = None - # self.persistentData = {} - self.writeDict.update(self.loadParameters()) + # write=False: write will happen later + self.initData = {} + for pname in self.parameters: + pobj = self.parameters[pname] + if not pobj.readonly and getattr(pobj, 'persistent', False): + self.initData[pname] = pobj.value + self.writeDict.update(self.loadParameters(write=False)) + print('initData', self.initData) - def writeInitParams(self, started_callback=None): - super().writeInitParams() - self.saveParameters() - if started_callback: - started_callback() - - def loadParameters(self): + def loadParameters(self, write=True): """load persistent parameters :return: persistent parameters which have to be written @@ -58,23 +95,34 @@ class PersistentMixin: except FileNotFoundError: self.persistentData = {} writeDict = {} - for pname, pobj in self.parameters.items(): - if pobj.persistent and pname in self.persistentData: - value = pobj.datatype.import_value(self.persistentData[pname]) + for pname in self.parameters: + 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 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 def saveParameters(self): """save persistent parameters - to be called regularely explicitly by the module + - to be called regularely 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 """ - data = {k: v.export_value() for k, v in self.parameters.items() if v.persistent} + if self.writeDict: + # do not save before all values are written to the hw, as potentially + # factory default values were read in the mean time + return + 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 = os.path.basename(self.persistentFile) @@ -92,3 +140,8 @@ class PersistentMixin: except FileNotFoundError: pass + + @Command() + def factory_reset(self): + self.writeDict.update(self.initData) + self.writeInitParams() diff --git a/secop_psi/trinamic.py b/secop_psi/trinamic.py index e901f96..f4ad309 100644 --- a/secop_psi/trinamic.py +++ b/secop_psi/trinamic.py @@ -27,7 +27,7 @@ import struct from math import log10 from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \ - HasIodev, Parameter, Property, Drivable, TupleOf, Done + HasIodev, Parameter, Property, Drivable, TupleOf, Done, PersistentMixin, PersistentParam from secop.bytesio import BytesIO from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError @@ -53,16 +53,19 @@ CURRENT_SCALE = 2.8/250 ENCODER_RESOLUTION = 0.4 # 365 / 1024, rounded up -class HwParam(Parameter): +class HwParam(PersistentParam): adr = Property('parameter address', IntRange(0, 255), export=False) - scale = Property('parameter address', FloatRange(), export=False) + scale = Property('scale factor (physical value / unit)', FloatRange(), export=False) - def __init__(self, description, datatype, adr, scale=1, poll=True, persistent=False, **kwds): + def __init__(self, description, datatype, adr, scale=1, poll=True, + readonly=True, persistent=None, **kwds): """hardware parameter""" - if isinstance(datatype, FloatRange) and not kwds.get('fmtstr'): - datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale))) + if persistent is None: + persistent = not readonly + if isinstance(datatype, FloatRange) and datatype.fmtstr == '%g': + datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale) + 0.01)) super().__init__(description, datatype, poll=poll, adr=adr, scale=scale, - persistent=persistent, **kwds) + persistent=persistent, readonly=readonly, **kwds) def copy(self): res = HwParam(self.description, self.datatype.copy(), self.adr) @@ -71,50 +74,54 @@ class HwParam(Parameter): return res -class Motor(HasIodev, Drivable): +class Motor(PersistentMixin, HasIodev, Drivable): address = Property('module address', IntRange(0, 255), default=1) # limit_pin_mask = Property('input pin mask for lower/upper limit switch', # TupleOf(IntRange(0, 15), IntRange(0, 15)), # default=(8, 0)) - value = Parameter('motor position', FloatRange(unit='deg')) - zero = Parameter('zero point', FloatRange(unit='$'), readonly=False, default=0, persistent=True) - encoder = HwParam('encoder reading', FloatRange(unit='$'), + value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f')) + zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0) + encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'), 209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True) steppos = HwParam('position from motor steps', FloatRange(unit='$'), 1, ANGLE_SCALE, readonly=True, initwrite=False) target = Parameter('_', FloatRange(unit='$'), default=0) movelimit = Parameter('max. angle to drive in one go', FloatRange(unit='$'), - readonly=False, default=360, group='more', persistent=True) + readonly=False, default=360, group='more') tolerance = Parameter('positioning tolerance', FloatRange(unit='$'), readonly=False, default=0.9) encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance', FloatRange(0, 360., unit='$'), - 212, ANGLE_SCALE, readonly=False, group='more', persistent=True) + 212, ANGLE_SCALE, readonly=False, group='more') speed = HwParam('max. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), - 4, SPEED_SCALE, readonly=False, group='more', persistent=True) + 4, SPEED_SCALE, readonly=False, group='more') minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'), 130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam') currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'), 3, SPEED_SCALE, readonly=True, group='motorparam') maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'), - 6, CURRENT_SCALE, readonly=False, group='motorparam', persistent=True) + 6, CURRENT_SCALE, readonly=False, group='motorparam') standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'), 7, CURRENT_SCALE, readonly=False, group='motorparam') acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'), - 5, ACCEL_SCALE, readonly=False, group='motorparam', persistent=True) + 5, ACCEL_SCALE, readonly=False, group='motorparam') target_reached = HwParam('_', BoolType(), 8, group='hwstatus') move_status = HwParam('_', IntRange(0, 3), - 207, readonly=True, persistent=False, group='hwstatus') + 207, readonly=True, group='hwstatus') error_bits = HwParam('_', IntRange(0, 255), - 208, readonly=True, persistent=False, group='hwstatus') + 208, readonly=True, group='hwstatus') + # the doc says msec, but I believe the scale is 10 msec free_wheeling = HwParam('_', FloatRange(0, 60., unit='sec'), - 204, 0.001, readonly=False, group='motorparam', persistent=True) + 204, 0.01, default=0.1, readonly=False, group='motorparam') + power_down_delay = HwParam('_', FloatRange(0, 60., unit='sec'), + 214, 0.01, default=0.1, readonly=False, group='motorparam') baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}), readonly=False, default=0, poll=True, visibility=3, group='more') - pollinterval = Parameter(group='more', persistent=True) + pollinterval = Parameter(group='more') + iodevClass = BytesIO fast_pollfactor = 0.001 # poll as fast as possible when busy @@ -216,20 +223,13 @@ class Motor(HasIodev, Drivable): initialized = self.comm(GET_GLOB_PAR, 255, bank=2) if initialized: # no power loss self.saveParameters() - if any((v==0 for v in self.persistentData.values())): - print('SAVED', self.persistentData) + print('SAVED', self.persistentData) else: # just powered up # get persistent values writeDict = self.loadParameters() - # self.encoder now contains the last known (persistent) value self.log.info('set to previous saved values %r', writeDict) - for pname, value in writeDict.items(): - try: - getattr(self, 'write_' + pname)(value) - except Exception as e: - self.log.warning('can not write %r to %r (%r)' % (value, pname, e)) + # self.encoder now contains the last known (persistent) value self.fix_encoder(encoder) - value = self.encoder - self.zero self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag self._started = 0 self._need_reset = True @@ -352,6 +352,12 @@ class Motor(HasIodev, Drivable): def write_free_wheeling(self, value): return self.set('free_wheeling', value) + def read_power_down_delay(self): + return self.get('power_down_delay') + + def write_power_down_delay(self, value): + return self.set('power_down_delay', value) + def read_move_status(self): return self.get('move_status') @@ -382,7 +388,7 @@ class Motor(HasIodev, Drivable): return self.set('steppos', self.encoder - self.zero, check=False) self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE) - time.sleep(0.01) + time.sleep(0.1) if itry > 5: tol = self.tolerance self.status = self.Status.ERROR, 'reset failed'