persistent module fixed

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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -19,30 +19,67 @@
# 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 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()

View File

@ -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'