initial version of parameter table
This commit is contained in:
@ -61,6 +61,7 @@ PY += devsup/db.py
|
|||||||
PY += devsup/hooks.py
|
PY += devsup/hooks.py
|
||||||
PY += devsup/interfaces.py
|
PY += devsup/interfaces.py
|
||||||
PY += devsup/util.py
|
PY += devsup/util.py
|
||||||
|
PY += devsup/ptable.py
|
||||||
|
|
||||||
#===========================
|
#===========================
|
||||||
|
|
||||||
|
@ -23,4 +23,6 @@ except ImportError:
|
|||||||
epicsver = (0,0,0,0,"0","")
|
epicsver = (0,0,0,0,"0","")
|
||||||
pydevver = (0,0)
|
pydevver = (0,0)
|
||||||
|
|
||||||
|
INVALID_ALARM = UDF_ALARM = 0
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
338
devsupApp/src/devsup/ptable.py
Normal file
338
devsupApp/src/devsup/ptable.py
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
|
_tables = {}
|
||||||
|
|
||||||
|
from devsup.db import IOScanListThread
|
||||||
|
from devsup import INVALID_ALARM, UDF_ALARM
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Parameter',
|
||||||
|
'ParameterGroup',
|
||||||
|
'TableBase',
|
||||||
|
'build'
|
||||||
|
]
|
||||||
|
|
||||||
|
# Reason code to cause a record to read a new value from a table paramter
|
||||||
|
_INTERNAL = object()
|
||||||
|
|
||||||
|
# action types
|
||||||
|
# Default action, call whenever a record associated with the parameter
|
||||||
|
# is processed
|
||||||
|
_ONPROC = lambda n,o:True
|
||||||
|
# Run action when the value of a parameter changes
|
||||||
|
_ONCHANGE = lambda n,o:n!=o
|
||||||
|
_ISVALID = lambda n,o:n is not None
|
||||||
|
_ISNOTVALID = lambda n,o:n is None
|
||||||
|
|
||||||
|
def _add_action(self, act, fn):
|
||||||
|
try:
|
||||||
|
L = fn._ptable_action
|
||||||
|
except AttributeError:
|
||||||
|
L = fn._ptable_action = []
|
||||||
|
L.append((self,act,fn))
|
||||||
|
return fn
|
||||||
|
|
||||||
|
class Parameter(object):
|
||||||
|
"""Define a parameter in a table.
|
||||||
|
|
||||||
|
When a sub-class of TableBase is instancianted, parameters become
|
||||||
|
py:class:`_ParamInstance` instances.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A = Parameter()
|
||||||
|
B = Parameter(name='bb')
|
||||||
|
C = Parameter(iointr=True)
|
||||||
|
>>>
|
||||||
|
|
||||||
|
Defines a table with three parameters. The second 'B' defines a different
|
||||||
|
parameter name and attribute name. The parameter name 'bb' will be used
|
||||||
|
in .db files, which self.B will be used for access from the table instance.
|
||||||
|
|
||||||
|
When _iointr_ is True, then attached device support may use SCAN='I/O Intr',
|
||||||
|
which is triggered with the method self.B.notify().
|
||||||
|
|
||||||
|
This class has several methods which may be used decorate member functions
|
||||||
|
as actions when the value of a parameter is (possibly) changed.
|
||||||
|
"""
|
||||||
|
def __init__(self, name=None, iointr=False):
|
||||||
|
self.name, self._iointr = name, iointr
|
||||||
|
def onproc(self, fn):
|
||||||
|
"""Decorator run a member function action whenever
|
||||||
|
an attached device support processes.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A = Parameter()
|
||||||
|
@A.onproc
|
||||||
|
def action(self, oldval):
|
||||||
|
print 'A changed from',oldval,'to',self.A.value
|
||||||
|
"""
|
||||||
|
return _add_action(self, _ONPROC, fn)
|
||||||
|
def onchange(self, fn):
|
||||||
|
"Decorator to run an action when the value of a parameter is changed."
|
||||||
|
return _add_action(self, _ONCHANGE, fn)
|
||||||
|
def isvalid(self, fn):
|
||||||
|
"Decorator to run an action when the value is valid"
|
||||||
|
return _add_action(self, _ISVALID, fn)
|
||||||
|
def isnotvalid(self, fn):
|
||||||
|
"Decorator to run an action when the value is *not* valid"
|
||||||
|
return _add_action(self, _ISNOTVALID, fn)
|
||||||
|
def oncondition(self, cond):
|
||||||
|
"""Decorator which allows a custom condition function to be specified.
|
||||||
|
|
||||||
|
This function will be invoked with two argument cond(newval,oldval)
|
||||||
|
and is expected to retur a bool.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A = Parameter()
|
||||||
|
@A.oncondition(lambda n,o:n<5)
|
||||||
|
def action(self, oldval):
|
||||||
|
print self.A.value,'is less than 5'
|
||||||
|
"""
|
||||||
|
def decorate(fn, cond=cond, self=self):
|
||||||
|
return _add_action(self, cond, fn)
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
class ParameterGroup(object):
|
||||||
|
"""A helper for defining actions on groups of parameters
|
||||||
|
|
||||||
|
When a sub-class of TableBase is instancianted, parameter groups become
|
||||||
|
py:class:`_ParamGroupInstance` instances.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A = Parameter()
|
||||||
|
B = Parameter(name='bb')
|
||||||
|
grp = ParameterGroup([A,B])
|
||||||
|
>>>
|
||||||
|
|
||||||
|
This class has several methods which may be used decorate member functions
|
||||||
|
as actions based on the value of parameters in this group.
|
||||||
|
"""
|
||||||
|
def __init__(self, params, name=None):
|
||||||
|
self.params, self.name = params, name
|
||||||
|
def onproc(self, fn):
|
||||||
|
"""Decorator run a member function action whenever
|
||||||
|
a device support attached to any paramter in the group processes.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A, B = Parameter(), Parameter()
|
||||||
|
grp = ParameterGroup([A,B])
|
||||||
|
@grp.onproc
|
||||||
|
def action(self):
|
||||||
|
print self.A.value, self.B.value
|
||||||
|
"""
|
||||||
|
return _add_action(self, _ONPROC, fn)
|
||||||
|
def allvalid(self, fn):
|
||||||
|
"Decorator to run an action when all parameters have valid values"
|
||||||
|
return _add_action(self, (all, lambda p:p.isvalid), fn)
|
||||||
|
def anynotvalid(self, fn):
|
||||||
|
"Decorator to run an action when any parameters has an invalid value"
|
||||||
|
return _add_action(self, (any, lambda p:not p.isvalid), fn)
|
||||||
|
def oncondition(self, fmap, freduce=all):
|
||||||
|
"""Decorator for a custom condtion.
|
||||||
|
|
||||||
|
The condition is specified in two parts, a map function, and a reduce function.
|
||||||
|
The map function is applied to each parameter in the group. Then a list
|
||||||
|
of the results is passed to the reduce function. If not specified,
|
||||||
|
the default reducing function is all (map func must return bool).
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
A, B = Parameter(), Parameter()
|
||||||
|
grp = ParameterGroup([A,B])
|
||||||
|
@grp.oncondition(lambda v:v>5, any)
|
||||||
|
def action(self):
|
||||||
|
# either A or B is greater than 5
|
||||||
|
print self.A.value, self.B.value
|
||||||
|
"""
|
||||||
|
def decorate(fn, fmap=fmap, freduce=freduce, self=self):
|
||||||
|
return _add_action(self, (freduce, fmap), fn)
|
||||||
|
return decorate
|
||||||
|
|
||||||
|
class _ParamInstance(object):
|
||||||
|
"""Access to a parameter at runtime.
|
||||||
|
"""
|
||||||
|
def __init__(self, table, name, scan):
|
||||||
|
self.name = name
|
||||||
|
self.table, self.scan, self._value = table, scan, None
|
||||||
|
self.alarm, self.actions = 0, []
|
||||||
|
self._groups = set()
|
||||||
|
def _get_value(self):
|
||||||
|
return self._value
|
||||||
|
def _set_value(self, val):
|
||||||
|
self._value = val
|
||||||
|
self.alarm = 3 if val is None else 0
|
||||||
|
value = property(_get_value, _set_value, doc="The current parameter value")
|
||||||
|
@property
|
||||||
|
def isvalid(self):
|
||||||
|
"""Is the parameter value valid (not None and no INVALID_ALARM)
|
||||||
|
"""
|
||||||
|
return self.alarm < INVALID_ALARM and self._value is not None
|
||||||
|
def notify(self):
|
||||||
|
"""Notify attached records of parameter value change.
|
||||||
|
A no-op unless Parameter(iointr=True)
|
||||||
|
"""
|
||||||
|
if self.scan:
|
||||||
|
self.scan.interrupt(_INTERNAL)
|
||||||
|
def addAction(self, fn, cond=None):
|
||||||
|
"""Add an arbitrary action at runtime
|
||||||
|
"""
|
||||||
|
self.actions.append((cond, fn))
|
||||||
|
def _exec(self, oldval=None):
|
||||||
|
for C, F in self.actions:
|
||||||
|
if C(self.value, oldval):
|
||||||
|
F()
|
||||||
|
|
||||||
|
class _ParamGroupInstance(object):
|
||||||
|
"""Runtime access to a group of parameters.
|
||||||
|
"""
|
||||||
|
def __init__(self, table, name):
|
||||||
|
self.table, self.name = table, name
|
||||||
|
self.actions, self._params = [], None
|
||||||
|
def __iter__(self):
|
||||||
|
"Iterate over all parameter instances in this group"
|
||||||
|
return iter(self._params)
|
||||||
|
def addAction(self, fn, fmap, freduce=all):
|
||||||
|
"Add an arbitrary action at runtime"
|
||||||
|
self.actions.append((fmap, freduce, fn))
|
||||||
|
def _exec(self):
|
||||||
|
for M, R, F in self.actions:
|
||||||
|
if R(map(M, self._params)):
|
||||||
|
F()
|
||||||
|
def allValid(self):
|
||||||
|
"Quick test if all parameters are valid"
|
||||||
|
for P in self._params:
|
||||||
|
if not P.isvalid:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
class _ParamSup(object):
|
||||||
|
def __init__(self, inst, rec, info):
|
||||||
|
self.inst, self.info = inst, info
|
||||||
|
# Determine which field to use to store the value
|
||||||
|
fname = rec.info('pyfield','VAL')
|
||||||
|
self.raw = fname!='RVAL'
|
||||||
|
self.vfld = rec.field(fname)
|
||||||
|
self.vdata = None
|
||||||
|
if len(self.vfld)>1:
|
||||||
|
self.vdata = self.vfld.getarray()
|
||||||
|
def detach(self, rec):
|
||||||
|
pass
|
||||||
|
def allowScan(self, rec):
|
||||||
|
if self.inst.scan:
|
||||||
|
return self.inst.scan.add(rec)
|
||||||
|
def process(self, rec, reason=None):
|
||||||
|
with self.inst.table.lock:
|
||||||
|
if reason is _INTERNAL:
|
||||||
|
# sync table to record
|
||||||
|
self.inst.table.log.debug('-> %s (%s)', rec.NAME, self.inst.value)
|
||||||
|
nval = self.inst.value
|
||||||
|
if nval is not None:
|
||||||
|
if self.vdata is None:
|
||||||
|
self.vfld.putval(nval)
|
||||||
|
else:
|
||||||
|
if len(nval)>len(self.vdata):
|
||||||
|
nval = nval[:len(self.vdata)]
|
||||||
|
self.vdata[:len(nval)] = nval
|
||||||
|
self.vfld.putarraylen(len(nval))
|
||||||
|
if self.inst.alarm:
|
||||||
|
rec.setSevr(self.inst.alarm)
|
||||||
|
else:
|
||||||
|
# undefined value
|
||||||
|
rec.setSevr(INVALID_ALARM, UDF_ALARM)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# sync record to table
|
||||||
|
self.inst.table.log.debug('<- %s (%s)', rec.NAME, rec.VAL)
|
||||||
|
if self.vdata is None:
|
||||||
|
nval = self.vfld.getval()
|
||||||
|
else:
|
||||||
|
# A copy is made which can be used without locking the record
|
||||||
|
nval = self.vdata[:self.vfld.getarraylen()].copy()
|
||||||
|
|
||||||
|
oval, self.inst.value = self.inst.value, nval
|
||||||
|
|
||||||
|
# Execute actions
|
||||||
|
self.inst._exec(oval)
|
||||||
|
for G in self.inst._groups:
|
||||||
|
G._exec()
|
||||||
|
|
||||||
|
class TableBase(object):
|
||||||
|
"""Base class for all parameter tables.
|
||||||
|
|
||||||
|
Sub-class this and populate with :py:class:`Parameter` and :py:class:`ParameterGroup`.
|
||||||
|
|
||||||
|
When a table is instanciated it must be given a unique name.
|
||||||
|
|
||||||
|
>>> class MyTable(TableBase):
|
||||||
|
...
|
||||||
|
>>> x=MyTable(name='xyz')
|
||||||
|
>>>
|
||||||
|
"""
|
||||||
|
log = LOG
|
||||||
|
ParamSupport = _ParamSup
|
||||||
|
def __init__(self, **kws):
|
||||||
|
self.name = kws.pop('name')
|
||||||
|
if self.name in _tables:
|
||||||
|
raise KeyError("Table named '%s' already exists"%self.name)
|
||||||
|
self.lock = threading.Lock()
|
||||||
|
self._parameters = {}
|
||||||
|
|
||||||
|
# Find Parameters and ParameterGroup in the class dictionary
|
||||||
|
# and place approprate things in the instance dictionary
|
||||||
|
rparams = {}
|
||||||
|
rgroups = {}
|
||||||
|
for k,v in self.__class__.__dict__.items():
|
||||||
|
if isinstance(v, Parameter):
|
||||||
|
scan = None
|
||||||
|
if not v.name:
|
||||||
|
v.name = k
|
||||||
|
if v._iointr:
|
||||||
|
scan = IOScanListThread()
|
||||||
|
P = _ParamInstance(self, v.name, scan)
|
||||||
|
self._parameters[v.name] = P
|
||||||
|
rparams[v] = P
|
||||||
|
setattr(self, k, P)
|
||||||
|
elif isinstance(v, ParameterGroup):
|
||||||
|
if not v.name:
|
||||||
|
v.name = k
|
||||||
|
G = _ParamGroupInstance(self, v.name)
|
||||||
|
rgroups[v] = G
|
||||||
|
setattr(self, k, G)
|
||||||
|
|
||||||
|
# Populate groups with parameters
|
||||||
|
for g,G in rgroups.iteritems():
|
||||||
|
ps = G._params = [rparams[v] for v in g.params]
|
||||||
|
# reverse mapping from parameter to group(s)
|
||||||
|
for P in ps:
|
||||||
|
P._groups.add(G)
|
||||||
|
|
||||||
|
# second pass to attach actions
|
||||||
|
for k,v in self.__class__.__dict__.items():
|
||||||
|
if hasattr(v, '_ptable_action'):
|
||||||
|
for src,cond,cmeth in v._ptable_action:
|
||||||
|
# src is instance parameter or group
|
||||||
|
# cond is a callable for parameters, or a tuple for groups
|
||||||
|
# cmeth is the unbound method which is the action to take
|
||||||
|
if isinstance(src, Parameter):
|
||||||
|
P = rparams[src]
|
||||||
|
P.addAction(cmeth.__get__(self), cond)
|
||||||
|
elif isinstance(src, ParameterGroup):
|
||||||
|
G = rgroups[src]
|
||||||
|
G.addAction(cmeth.__get__(self), cond[1], cond[0])
|
||||||
|
|
||||||
|
super(TableBase, self).__init__(**kws)
|
||||||
|
self.log.info("Initialized ptable '%s'",self.name)
|
||||||
|
_tables[self.name] = self
|
||||||
|
|
||||||
|
def build(rec, args):
|
||||||
|
parts = args.split(None,2)
|
||||||
|
table, param = parts[:2]
|
||||||
|
info = None if len(parts)<3 else parts[2]
|
||||||
|
T = _tables[table]
|
||||||
|
P = T._parameters[param]
|
||||||
|
T.log.debug("Attaching ptable '%s, %s' to %s", T.name, P.name, rec.NAME)
|
||||||
|
return T.ParamSupport(P, rec, info)
|
@ -18,7 +18,7 @@ Contents:
|
|||||||
environment
|
environment
|
||||||
devsup
|
devsup
|
||||||
interfaces
|
interfaces
|
||||||
|
ptable
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
42
documentation/ptable.rst
Normal file
42
documentation/ptable.rst
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
ptable Package
|
||||||
|
==============
|
||||||
|
|
||||||
|
.. module:: devsup.ptable
|
||||||
|
|
||||||
|
The :py:mod:`devsup.ptable` module provides the means to define a Parameter Table,
|
||||||
|
which is something like a dictionary (parameter name <-> dict key)
|
||||||
|
where a parameter may be associated (attached) with zero or more EPICS records.
|
||||||
|
|
||||||
|
Changes to a parameter may be reflected in the attached records.
|
||||||
|
A change in an attached record will update the parameter value,
|
||||||
|
and optionally, involve some functions (actions) to make some use of the new value.
|
||||||
|
|
||||||
|
The basis of all tables is the :py:class:`TableBase` class. User code
|
||||||
|
will typically sub-class :py:class:`TableBase`.
|
||||||
|
|
||||||
|
Defining Parameters
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass:: Parameter
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: ParameterGroup
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: TableBase
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Runtime Access
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. autoclass :: _ParamInstance
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass :: _ParamGroupInstance
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Device Support
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
A general purpose device support is provided to access table parameters.
|
||||||
|
The input/output link format is "@devsup.ptable <tablename> <param> [optional]"
|
7
test.cmd
7
test.cmd
@ -3,6 +3,9 @@
|
|||||||
dbLoadDatabase("dbd/softIocPy.dbd")
|
dbLoadDatabase("dbd/softIocPy.dbd")
|
||||||
softIocPy_registerRecordDeviceDriver(pdbbase)
|
softIocPy_registerRecordDeviceDriver(pdbbase)
|
||||||
|
|
||||||
|
py "import logging"
|
||||||
|
py "logging.basicConfig(level=logging.DEBUG)"
|
||||||
|
|
||||||
py "import devsup; print devsup.HAVE_DBAPI"
|
py "import devsup; print devsup.HAVE_DBAPI"
|
||||||
py "import sys; sys.path.insert(0,'${PWD}/testApp')"
|
py "import sys; sys.path.insert(0,'${PWD}/testApp')"
|
||||||
py "print sys.path"
|
py "print sys.path"
|
||||||
@ -14,6 +17,10 @@ py "import test2"
|
|||||||
py "test2.addDrv('AAAA')"
|
py "test2.addDrv('AAAA')"
|
||||||
py "test2.addDrv('BBBB')"
|
py "test2.addDrv('BBBB')"
|
||||||
|
|
||||||
|
py "import test6"
|
||||||
|
py "test6.SumTable(name='tsum')"
|
||||||
|
|
||||||
dbLoadRecords("db/test.db","P=md:")
|
dbLoadRecords("db/test.db","P=md:")
|
||||||
|
dbLoadRecords("db/test6.db","P=tst:,TNAME=tsum")
|
||||||
|
|
||||||
iocInit()
|
iocInit()
|
||||||
|
@ -11,6 +11,7 @@ include $(TOP)/configure/CONFIG
|
|||||||
# Create and install (or just install) into <top>/db
|
# Create and install (or just install) into <top>/db
|
||||||
# databases, templates, substitutions like this
|
# databases, templates, substitutions like this
|
||||||
DB += test.db
|
DB += test.db
|
||||||
|
DB += test6.db
|
||||||
|
|
||||||
#----------------------------------------------------
|
#----------------------------------------------------
|
||||||
# If <anyname>.db template is not named <anyname>*.template add
|
# If <anyname>.db template is not named <anyname>*.template add
|
||||||
|
22
testApp/test6.db
Normal file
22
testApp/test6.db
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
record(ao, "$(P):A") {
|
||||||
|
field(DTYP, "Python Device")
|
||||||
|
field(OUT , "@devsup.ptable $(TNAME) A")
|
||||||
|
}
|
||||||
|
|
||||||
|
record(ao, "$(P):B") {
|
||||||
|
field(DTYP, "Python Device")
|
||||||
|
field(OUT , "@devsup.ptable $(TNAME) B")
|
||||||
|
}
|
||||||
|
|
||||||
|
record(ao, "$(P):C") {
|
||||||
|
field(DTYP, "Python Device")
|
||||||
|
field(OUT , "@$(TNAME) C")
|
||||||
|
info("pySupportMod", "devsup.ptable")
|
||||||
|
}
|
||||||
|
|
||||||
|
record(ai, "$(P):S") {
|
||||||
|
field(DTYP, "Python Device")
|
||||||
|
field(INP , "@devsup.ptable $(TNAME) S")
|
||||||
|
field(SCAN, "I/O Intr")
|
||||||
|
field(PINI, "YES")
|
||||||
|
}
|
34
testApp/test6.py
Normal file
34
testApp/test6.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import logging
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
import devsup.ptable as PT
|
||||||
|
|
||||||
|
class SumTable(PT.TableBase):
|
||||||
|
A = PT.Parameter()
|
||||||
|
B = PT.Parameter()
|
||||||
|
C = PT.Parameter()
|
||||||
|
S = PT.Parameter(iointr=True)
|
||||||
|
|
||||||
|
inputs = PT.ParameterGroup([A,B])
|
||||||
|
|
||||||
|
@C.onchange
|
||||||
|
def newC(self):
|
||||||
|
LOG.debug("C is %s", self.C.value)
|
||||||
|
|
||||||
|
@inputs.anynotvalid
|
||||||
|
def inval(self):
|
||||||
|
print self.A.isvalid, self.B.isvalid
|
||||||
|
LOG.debug("%s.update inputs not valid", self.name)
|
||||||
|
self.S.value = None
|
||||||
|
self.S.notify()
|
||||||
|
|
||||||
|
@inputs.allvalid
|
||||||
|
def update(self):
|
||||||
|
if not all(map(lambda P:P.isvalid, [self.A, self.B])):
|
||||||
|
self.inval()
|
||||||
|
return
|
||||||
|
self.S.value = self.A.value + self.B.value
|
||||||
|
LOG.debug("%s.S = %s", self.name, self.S.value)
|
||||||
|
self.S.notify()
|
Reference in New Issue
Block a user