Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions example/limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
"""
Demonstrate a Handler which uses open() and post() to apply control.limits
to a pair of PVs.
Run the program and use
- `pvget demo:limit_open` to verify that although the initial value was set
to 14 the control.limitHigh means that it is set to 10.
- `pvmonitor demo:limit_post` to see the behaviour of a PV with control limits
set to restrict values to >=3 and <=7 as a post() sweeps it between 0 and 10.
- `pvput demo:limit_open control.limitHigh=5` to see that changing the control
limits is correctly supported.
"""

import time

from p4p import Value
from p4p.nt.scalar import NTScalar
from p4p.server import Server, ServerOperation
from p4p.server.raw import Handler
from p4p.server.thread import SharedPV


class LimitsHandler(Handler):
"""
A handler which applies the control.limits to a PV.
"""

def _eval_limits(self, value: Value):
"""Check the value against the control.limits and adjust if necessary."""

# Have "control.limitHigh" and "control.limitLow" been set? If they have
# then the changed flag will be set and we can use the value. If they
# haven't then the changed flag will not be set, they will have a default
# value of 0, and we shouldn't use them to test the value.

limit_high = value.get("control.limitHigh")
if (
limit_high is not None
and value.changed("control.limitHigh")
and value["value"] > limit_high
):
# print(f"Value {value['value']} is above the high limit of {limit_high}, setting to {limit_high}")
value["value"] = limit_high

limit_low = value.get("control.limitLow")
if (
limit_low is not None
and value.changed("control.limitLow")
and value["value"] < limit_low
):
# print(f"Value {value['value']} is below the low limit of {limit_low}, setting to {limit_low}")
value["value"] = limit_low

def open(self, value: Value):
"""Check any initial value against the limits."""
self._eval_limits(value)

def post(self, pv: SharedPV, value: Value):
"""Check changes to the value against the limits."""

# The value and control limits we need to evaluate are shared between
# the pv and the value. We need to check which fields are most current
# (value takes precedence) and set them in the value variable
# approppriately.
pv_value: Value = pv.current().raw
self._merge_field(value, pv_value, "value")
self._merge_field(value, pv_value, "control.limitHigh")
self._merge_field(value, pv_value, "control.limitLow")

self._eval_limits(value)

def _merge_field(self, value: Value, pv_value: Value, field: str):
if not value.changed(field) and pv_value.changed(field):
value[field] = pv_value[field]

def put(self, pv: SharedPV, op: ServerOperation):
"""Allow puts by simply forwarding to the post and invoking the associated handler."""

pv.post(op.value())
op.done()


def main():
"""
Create a pvaServer with two PVs with limits.
"""

# Construct a PV with a high limit of 10, but an initial value of 14.
# The handler will be responsible for enforcing the limit and ensuring
# that the value is set to 10 instead of 14.
open_pv = SharedPV(
nt=NTScalar("i", control=True),
handler=LimitsHandler(),
initial={
"value": 14,
"control.limitHigh": 10,
},
)

# Construct a PV starting within its limit but which we will use posts
# to step within a range of 0 to 10 every second. Observe the behaviour
# with a pvmonitor.
value = 5
post_pv = SharedPV(
nt=NTScalar("i", control=True),
handler=LimitsHandler(),
initial={
"value": value,
"control.limitLow": 3,
"control.limitHigh": 7,
},
)

pvs = {"demo:limit_open": open_pv, "demo:limit_post": post_pv}

server = None
try:
server = Server(providers=[pvs])
while True:
time.sleep(1)
value = value + 1
if value > 10:
value = 0
# post_pv.post(value)
except KeyboardInterrupt:
pass
finally:
for pv in pvs.values():
pv.close()
if server:
server.stop()


if __name__ == "__main__":
main()
107 changes: 107 additions & 0 deletions src/p4p/server/raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ class Handler(object):

Use of this as a base class is optional.
"""
def open(self, value):
"""
Called each time an Open operation is performed on this Channel

:param value: A Value, or appropriate object (see nt= and wrap= of the constructor).
"""
pass

def put(self, pv, op):
"""
Expand All @@ -50,6 +57,17 @@ def put(self, pv, op):
"""
op.done(error='Not supported')

def post(self, pv, value):
"""
Called each time a client issues a post
operation on this Channel.

:param SharedPV pv: The :py:class:`SharedPV` which this Handler is associated with.
:param value: A Value, or appropriate object (see nt= and wrap= of the constructor).
:param dict options: A dictionary of configuration options.
"""
pass

def rpc(self, pv, op):
"""
Called each time a client issues a Remote Procedure Call
Expand Down Expand Up @@ -77,6 +95,14 @@ def onLastDisconnect(self, pv):
pass


def close(self, pv):
"""
Called when the Channel is closed.

:param SharedPV pv: The :py:class:`SharedPV` which this Handler is associated with.
"""
pass

class SharedPV(_SharedPV):

"""Shared state Process Variable. Callback based implementation.
Expand Down Expand Up @@ -158,6 +184,16 @@ def open(self, value, nt=None, wrap=None, unwrap=None, **kws):
V = self._wrap(value, **kws)
except: # py3 will chain automatically, py2 won't
raise ValueError("Unable to wrap %r with %r and %r"%(value, self._wrap, kws))

# Guard goes here because we can have handlers that don't inherit from
# the Handler base class
try:
open_fn = self._handler.open
except AttributeError:
pass
Comment thread
mdavidsaver marked this conversation as resolved.
else:
open_fn(V)

_SharedPV.open(self, V)

def post(self, value, **kws):
Expand All @@ -174,8 +210,37 @@ def post(self, value, **kws):
V = self._wrap(value, **kws)
except: # py3 will chain automatically, py2 won't
raise ValueError("Unable to wrap %r with %r and %r"%(value, self._wrap, kws))

# Guard goes here because we can have handlers that don't inherit from
# the Handler base class
try:
post_fn = self._handler.post
except AttributeError:
pass
else:
post_fn(self, V)

_SharedPV.post(self, V)

def close(self, destroy=False):
"""Close PV, disconnecting any clients.

:param bool destroy: Indicate "permanent" closure. Current clients will not see subsequent open().

close() with destory=True or sync=True will not prevent clients from re-connecting.
New clients may prevent sync=True from succeeding.
Prevent reconnection by __first__ stopping the Server, removing with :py:meth:`StaticProvider.remove()`,
or preventing a :py:class:`DynamicProvider` from making new channels to this SharedPV.
"""
try:
close_fn = self._handler.close
except AttributeError:
pass
else:
close_fn(self)

_SharedPV.close(self)

def current(self):
V = _SharedPV.current(self)
try:
Expand Down Expand Up @@ -208,6 +273,13 @@ def __init__(self, pv, real):
self._pv = pv # this creates a reference cycle, which should be collectable since SharedPV supports GC
self._real = real

def open(self, value):
_log.debug('OPEN %s %s', self._pv, value)
try:
self._pv._exec(None, self._real.open, value)
except AttributeError:
pass

def onFirstConnect(self):
self._pv._exec(None, self._pv._onFirstConnect, None)
try: # user handler may omit onFirstConnect()
Expand Down Expand Up @@ -239,6 +311,20 @@ def rpc(self, op):
except AttributeError:
op.done(error="RPC not supported")

def post(self, value):
_log.debug('POST %s %s', self._pv, value)
try:
self._pv._exec(None, self._real.post, self._pv, value)
except AttributeError:
pass

def close(self):
_log.debug('CLOSE %s', self._pv)
try:
self._pv._exec(None, self._real.close, self._pv)
except AttributeError:
pass

@property
def onFirstConnect(self):
def decorate(fn):
Expand All @@ -253,6 +339,20 @@ def decorate(fn):
return fn
return decorate

@property
def on_open(self):
def decorate(fn):
self._handler.open = fn
return fn
return decorate

@property
def on_post(self):
def decorate(fn):
self._handler.post = fn
return fn
return decorate

@property
def put(self):
def decorate(fn):
Expand All @@ -267,6 +367,13 @@ def decorate(fn):
return fn
return decorate

@property
def on_close(self):
def decorate(fn):
self._handler.close = fn
return fn
return decorate

def __repr__(self):
if self.isOpen():
return '%s(value=%s)' % (self.__class__.__name__, repr(self.current()))
Expand Down
54 changes: 53 additions & 1 deletion src/p4p/test/test_sharedpv.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from ..wrapper import Value, Type
from ..client.thread import Context, Disconnected, TimeoutError, RemoteError
from ..server import Server, StaticProvider
from ..server.thread import SharedPV, _defaultWorkQueue
from ..server.thread import Handler, SharedPV, _defaultWorkQueue
from ..util import WorkQueue
from ..nt import NTScalar, NTURI
from .utils import RefTestCase
Expand Down Expand Up @@ -420,3 +420,55 @@ def testPVClose(self):

_log.debug('CLOSE')
self.assertFalse(self.H.conn)

class TestHandlerOpenPostClose(RefTestCase):
"""
Test Handler open(), post() and close() functions called correctly by SharedPV. Note that:
- TestRPC, TestFirstLast already test onFirstConnect() and onLastDisconnect().
- TestGPM, TestPVRequestMask already test put().
- TestRPC, TestRPC2 already test rpc().
"""

class TestHandler(Handler):
def __init__(self):
self.last_op = "init"

def open(self, value):
self.last_op = "open"
value["value"] = 17

def post(self, pv, value):
self.last_op = "post"
value["value"] = value["value"] * 2

def close(self, pv):
self.last_op = "close"

def setUp(self):
super(TestHandlerOpenPostClose, self).setUp()
self.handler = self.TestHandler()
self.pv = SharedPV(handler=self.handler, nt=NTScalar('d'))

def test_open(self):
# Setup sets the initial value to 5, but the Handler open() overrides
self.pv.open(5)
self.assertEqual(self.handler.last_op, "open")
self.assertEqual(self.pv.current(), 17.0)

def test_post(self):
self.pv.open(5)
self.pv.post(13.0)
self.assertEqual(self.handler.last_op, "post")
self.assertEqual(self.pv.current(), 26.0)

def test_close(self):
self.pv.open(5)
self.pv.close(sync=True)
self.assertEqual(self.handler.last_op, "close")

def tearDown(self):
self.pv.close(sync=True)
self.traceme(self.pv)
del self.pv

super(TestHandlerOpenPostClose, self).tearDown()