persistent module fixed
This commit is contained in:
parent
246ab99e12
commit
25b8780b11
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user