persistent module fixed

This commit is contained in:
zolliker 2021-06-08 07:39:46 +02:00
parent 246ab99e12
commit 25b8780b11
5 changed files with 110 additions and 54 deletions

View File

@ -21,5 +21,6 @@ acceleration=150.
movelimit=360 movelimit=360
speed=40 speed=40
encoder_tolerance=3.6 encoder_tolerance=3.6
free_wheeling=0.001 free_wheeling=0.1
power_down_delay=0.1
# pull_up=1 # pull_up=1

View File

@ -37,3 +37,4 @@ from secop.poller import AUTO, DYNAMIC, REGULAR, SLOW
from secop.properties import Property from secop.properties import Property
from secop.proxy import Proxy, SecNode, proxy_class from secop.proxy import Proxy, SecNode, proxy_class
from secop.stringio import HasIodev, StringIO from secop.stringio import HasIodev, StringIO
from secop.persistent import PersistentMixin, PersistentParam

View File

@ -242,8 +242,6 @@ class Module(HasAccessibles):
self.name = name self.name = name
self.valueCallbacks = {} self.valueCallbacks = {}
self.errorCallbacks = {} self.errorCallbacks = {}
self.persistentFile = None
self.persistentData = {}
errors = [] errors = []
# handle module properties # handle module properties
@ -399,8 +397,6 @@ class Module(HasAccessibles):
if '$' in dt.unit: if '$' in dt.unit:
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit)) dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
self.writeDict.update(self.loadParameters())
# 6) check complete configuration of * properties # 6) check complete configuration of * properties
if not errors: if not errors:
try: try:
@ -539,7 +535,6 @@ class Module(HasAccessibles):
self.log.error(str(e)) self.log.error(str(e))
except Exception: except Exception:
self.log.error(formatException()) self.log.error(formatException())
self.saveParameters()
if started_callback: if started_callback:
started_callback() started_callback()

View File

@ -19,30 +19,67 @@
# Markus Zolliker <markus.zolliker@psi.ch> # Markus Zolliker <markus.zolliker@psi.ch>
# #
# ***************************************************************************** # *****************************************************************************
"""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 = <get encoder from hardware>
if <power down/power up cycle detected>:
self.loadParameters()
<fix encoder turns by comparing loaded self.encoder with encoder from hw>
else:
self.saveParameters()
"""
import os import os
import json import json
from secop.errors import BadValueError, ConfigError, InternalError, \ from secop.errors import BadValueError, ConfigError, InternalError, \
ProgrammingError, SECoPError, SilentError, secop_error 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) super().__init__(*args, **kwds)
# self.persistentFile = None # write=False: write will happen later
# self.persistentData = {} self.initData = {}
self.writeDict.update(self.loadParameters()) 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): def loadParameters(self, write=True):
super().writeInitParams()
self.saveParameters()
if started_callback:
started_callback()
def loadParameters(self):
"""load persistent parameters """load persistent parameters
:return: persistent parameters which have to be written :return: persistent parameters which have to be written
@ -58,23 +95,34 @@ class PersistentMixin:
except FileNotFoundError: except FileNotFoundError:
self.persistentData = {} self.persistentData = {}
writeDict = {} writeDict = {}
for pname, pobj in self.parameters.items(): for pname in self.parameters:
if pobj.persistent and pname in self.persistentData: pobj = self.parameters[pname]
value = pobj.datatype.import_value(self.persistentData[pname]) if getattr(pobj, 'persistent', False) and pname in self.persistentData:
try: try:
value = pobj.datatype.import_value(self.persistentData[pname])
pobj.value = value pobj.value = value
if not pobj.readonly: if not pobj.readonly:
writeDict[pname] = value writeDict[pname] = value
except Exception as e: except Exception as e:
self.log.warning('can not restore %r to %r (%r)' % (pname, value, e)) self.log.warning('can not restore %r to %r (%r)' % (pname, value, e))
if write:
self.writeDict.update(writeDict)
self.writeInitParams()
return writeDict return writeDict
def saveParameters(self): def saveParameters(self):
"""save persistent parameters """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: if data != self.persistentData:
self.persistentData = data self.persistentData = data
persistentdir = os.path.basename(self.persistentFile) persistentdir = os.path.basename(self.persistentFile)
@ -92,3 +140,8 @@ class PersistentMixin:
except FileNotFoundError: except FileNotFoundError:
pass pass
@Command()
def factory_reset(self):
self.writeDict.update(self.initData)
self.writeInitParams()

View File

@ -27,7 +27,7 @@ import struct
from math import log10 from math import log10
from secop.core import BoolType, Command, EnumType, FloatRange, IntRange, \ 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.bytesio import BytesIO
from secop.errors import CommunicationFailedError, HardwareError, BadValueError, IsBusyError 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 ENCODER_RESOLUTION = 0.4 # 365 / 1024, rounded up
class HwParam(Parameter): class HwParam(PersistentParam):
adr = Property('parameter address', IntRange(0, 255), export=False) 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""" """hardware parameter"""
if isinstance(datatype, FloatRange) and not kwds.get('fmtstr'): if persistent is None:
datatype.fmtstr = '%%.%df' % max(0, 1 - int(log10(scale))) 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, super().__init__(description, datatype, poll=poll, adr=adr, scale=scale,
persistent=persistent, **kwds) persistent=persistent, readonly=readonly, **kwds)
def copy(self): def copy(self):
res = HwParam(self.description, self.datatype.copy(), self.adr) res = HwParam(self.description, self.datatype.copy(), self.adr)
@ -71,50 +74,54 @@ class HwParam(Parameter):
return res return res
class Motor(HasIodev, Drivable): class Motor(PersistentMixin, HasIodev, Drivable):
address = Property('module address', IntRange(0, 255), default=1) address = Property('module address', IntRange(0, 255), default=1)
# limit_pin_mask = Property('input pin mask for lower/upper limit switch', # limit_pin_mask = Property('input pin mask for lower/upper limit switch',
# TupleOf(IntRange(0, 15), IntRange(0, 15)), # TupleOf(IntRange(0, 15), IntRange(0, 15)),
# default=(8, 0)) # default=(8, 0))
value = Parameter('motor position', FloatRange(unit='deg')) value = Parameter('motor position', FloatRange(unit='deg', fmtstr='%.3f'))
zero = Parameter('zero point', FloatRange(unit='$'), readonly=False, default=0, persistent=True) zero = PersistentParam('zero point', FloatRange(unit='$'), readonly=False, default=0)
encoder = HwParam('encoder reading', FloatRange(unit='$'), encoder = HwParam('encoder reading', FloatRange(unit='$', fmtstr='%.1f'),
209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True) 209, ANGLE_SCALE, readonly=True, initwrite=False, persistent=True)
steppos = HwParam('position from motor steps', FloatRange(unit='$'), steppos = HwParam('position from motor steps', FloatRange(unit='$'),
1, ANGLE_SCALE, readonly=True, initwrite=False) 1, ANGLE_SCALE, readonly=True, initwrite=False)
target = Parameter('_', FloatRange(unit='$'), default=0) target = Parameter('_', FloatRange(unit='$'), default=0)
movelimit = Parameter('max. angle to drive in one go', FloatRange(unit='$'), 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='$'), tolerance = Parameter('positioning tolerance', FloatRange(unit='$'),
readonly=False, default=0.9) readonly=False, default=0.9)
encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance', encoder_tolerance = HwParam('the allowed deviation between steppos and encoder\n\nmust be > tolerance',
FloatRange(0, 360., unit='$'), 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'), 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'), minspeed = HwParam('min. speed', FloatRange(0, MAX_SPEED, unit='$/sec'),
130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam') 130, SPEED_SCALE, readonly=False, default=SPEED_SCALE, group='motorparam')
currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'), currentspeed = HwParam('current speed', FloatRange(-MAX_SPEED, MAX_SPEED, unit='$/sec'),
3, SPEED_SCALE, readonly=True, group='motorparam') 3, SPEED_SCALE, readonly=True, group='motorparam')
maxcurrent = HwParam('_', FloatRange(0, 2.8, unit='A'), 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'), standby_current = HwParam('_', FloatRange(0, 2.8, unit='A'),
7, CURRENT_SCALE, readonly=False, group='motorparam') 7, CURRENT_SCALE, readonly=False, group='motorparam')
acceleration = HwParam('_', FloatRange(4.6 * ACCEL_SCALE, MAX_ACCEL, unit='deg/s^2'), 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') target_reached = HwParam('_', BoolType(), 8, group='hwstatus')
move_status = HwParam('_', IntRange(0, 3), move_status = HwParam('_', IntRange(0, 3),
207, readonly=True, persistent=False, group='hwstatus') 207, readonly=True, group='hwstatus')
error_bits = HwParam('_', IntRange(0, 255), 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'), 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)}), baudrate = Parameter('_', EnumType({'%d' % v: i for i, v in enumerate(BAUDRATES)}),
readonly=False, default=0, poll=True, visibility=3, group='more') readonly=False, default=0, poll=True, visibility=3, group='more')
pollinterval = Parameter(group='more', persistent=True) pollinterval = Parameter(group='more')
iodevClass = BytesIO iodevClass = BytesIO
fast_pollfactor = 0.001 # poll as fast as possible when busy 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) initialized = self.comm(GET_GLOB_PAR, 255, bank=2)
if initialized: # no power loss if initialized: # no power loss
self.saveParameters() self.saveParameters()
if any((v==0 for v in self.persistentData.values())):
print('SAVED', self.persistentData) print('SAVED', self.persistentData)
else: # just powered up else: # just powered up
# get persistent values # get persistent values
writeDict = self.loadParameters() writeDict = self.loadParameters()
# self.encoder now contains the last known (persistent) value
self.log.info('set to previous saved values %r', writeDict) self.log.info('set to previous saved values %r', writeDict)
for pname, value in writeDict.items(): # self.encoder now contains the last known (persistent) value
try:
getattr(self, 'write_' + pname)(value)
except Exception as e:
self.log.warning('can not write %r to %r (%r)' % (value, pname, e))
self.fix_encoder(encoder) self.fix_encoder(encoder)
value = self.encoder - self.zero
self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag self.comm(SET_GLOB_PAR, 255, 1, bank=2) # set initialized flag
self._started = 0 self._started = 0
self._need_reset = True self._need_reset = True
@ -352,6 +352,12 @@ class Motor(HasIodev, Drivable):
def write_free_wheeling(self, value): def write_free_wheeling(self, value):
return self.set('free_wheeling', 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): def read_move_status(self):
return self.get('move_status') return self.get('move_status')
@ -382,7 +388,7 @@ class Motor(HasIodev, Drivable):
return return
self.set('steppos', self.encoder - self.zero, check=False) self.set('steppos', self.encoder - self.zero, check=False)
self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE) self.comm(MOVE, 0, (self.encoder - self.zero) / ANGLE_SCALE)
time.sleep(0.01) time.sleep(0.1)
if itry > 5: if itry > 5:
tol = self.tolerance tol = self.tolerance
self.status = self.Status.ERROR, 'reset failed' self.status = self.Status.ERROR, 'reset failed'