Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional callbacks for SharedPV handlers #155

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
74 changes: 74 additions & 0 deletions example/auditor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""
In this example we setup a simple auditing mechanism that reports information
about the last channel changed (which channel, when, and who by). Since we
might need to know this information even when the program is not running we
persist this data to file, including information about when changes could
have been made.
"""

import time

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


class Auditor(Handler):
"""Persist information to file so we can audit when the program is closed"""

def open(self, value):
with open("audit.log", mode="a+") as f:
f.write(f"Auditing opened at {time.ctime()}\n")

def close(self, pv):
with open("audit.log", mode="a+") as f:
value = pv.current().raw["value"]
if value:
f.write(f"Auditing closed at {time.ctime()}; {value}\n")
else:
f.write(f"Auditing closed at {time.ctime()}; no changes made\n")


class Audited(Handler):
"""Forward information about Put operations to the auditing PV"""

def __init__(self, pv: SharedPV):
self._audit_pv = pv

def put(self, pv, op):
pv.post(op.value())
self._audit_pv.post(
f"Channel {op.name()} last updated by {op.account()} at {time.ctime()}"
)
op.done()


# Setup the PV that will make the audit information available.
# Note that there is no put in its handler so it will be externally read-only
auditor_pv = SharedPV(nt=NTScalar("s"), handler=Auditor(), initial="")

# Setup some PVs that will be audited and one that won't be
# Note that the audited handler does have a put so these PVs can be changed externally
pvs = {
"demo:pv:auditor": auditor_pv,
"demo:pv:audited_d": SharedPV(
nt=NTScalar("d"), handler=Audited(auditor_pv), initial=9.99
),
"demo:pv:audited_i": SharedPV(
nt=NTScalar("i"), handler=Audited(auditor_pv), initial=4
),
"demo:pv:audited_s": SharedPV(
nt=NTScalar("s"), handler=Audited(auditor_pv), initial="Testing"
),
"demo:pv:unaudted_i": SharedPV(nt=NTScalar("i"), initial=-1),
}

print(pvs.keys())
try:
Server.forever(providers=[pvs])
except KeyboardInterrupt:
pass
finally:
# We need to close the auditor PV manually, the server stop() won't do it for us
auditor_pv.close()
177 changes: 177 additions & 0 deletions example/ntscalar_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"""
A demonstration of using a handler to apply the Control field logic for an
Normative Type Scalar (NTScalar).

There is only one PV, but it's behaviour is complex:
- try changing and checking the value, e.g.
`python -m p4p.client.cli put demo:pv=4` and
`python -m p4p.client.cli get demo:pv`
Initially the maximum = 11, minimum = -1, and minimum step size = 2.
Try varying the control settings, e.g.
- `python -m p4p.client.cli put demo:pv='{"value":5, "control.limitHigh":4}'`
`python -m p4p.client.cli get demo:pv`
Remove the comments at lines 166-169 and try again.

This is also a demonstration of using the open(), put(), and post() callbacks
to implement this functionality, and particularly how it naturally partitions
the concerns of the three callback function:
- open() - logic based only on the input Value,
- post() - logic requiring comparison of cuurent and proposed Values
- put() - authorisation
"""

from p4p.nt import NTScalar
from p4p.server import Server
from p4p.server.raw import Handler
from p4p.server.thread import SharedPV
from p4p.wrapper import Value


class SimpleControl(Handler):
"""
A simple handler that implements the logic for the Control field of a
Normative Type.
"""

def __init__(self):
# The attentive reader may wonder why we are keeping track of state here
# instead of relying on control.limitLow, control.limitHigh, and
# control.minStep. There are three possible reasons a developer might
# choose an implementation like this:
# - As [Ref1] shows it's not straightforward to maintain state using
# the PV's own fields
# - A developer may wish to have the limits apply as soon as the
# Channel is open. If an initial value is set then this may happen
# before the first post().
# - It is possible to adapt this handler so it could be used without
# a Control field.
# The disadvantage of this simple approach is that clients cannot
# inspect the Control field values until they have been changed.
self._min_value = None # Minimum value allowed
self._max_value = None # Maximum value allowed
self._min_step = None # Minimum change allowed

def open(self, value) -> bool:
"""
This function manages all logic when we only need to consider the
(proposed) future state of a PV
"""
value_changed_by_limit = False

# Check if the limitHigh has changed. If it has then we have to
# reevaluate the existing value. Note that for this to work with a
# post() request we have to take the actions explained at Ref1
if value.changed("control.limitHigh"):
self._max_value = value["control.limitHigh"]
if value["value"] > self._max_value:
value["value"] = self._max_value
value_changed_by_limit = True

if value.changed("control.limitLow"):
self._min_value = value["control.limitLow"]
if value["value"] < self._min_value:
value["value"] = self._min_value
value_changed_by_limit = True

# This has to go in the open because it could be set in the initial value
if value.changed("control.minStep"):
self._min_step = value["control.minStep"]

# If the value has changed we need to check it against the limits and
# change it if any of the limits apply
if value.changed("value"):
if self._max_value and value["value"] > self._max_value:
value["value"] = self._max_value
value_changed_by_limit = True
elif self._min_value and value["value"] < self._min_value:
value["value"] = self._min_value
value_changed_by_limit = True

return value_changed_by_limit

def post(self, pv: SharedPV, value: Value):
"""
This function manages all logic when we need to know both the
current and (proposed) future state of a PV
"""
# [Ref1] This is where even our simple handler gets complex!
# If the value["value"] has not been changed as part of the post()
# operation then it will be set to a default value (i.e. 0) and
# marked unchanged. For the logic in open() to work if the control
# limits are changed we need to set the pv.current().raw value in
# this case.
if not value.changed("value"):
value["value"] = pv.current().raw["value"]
value.mark("value", False)

# Apply the control limits before the check for minimum change because:
# - the self._min_step may be updated
# - the value["value"] may be altered by the limits
value_changed_by_limit = self.open(value)

# If the value["value"] wasn't changed by the put()/post() but was
# changed by the limits then we don't check the min_step but
# immediately return
if value_changed_by_limit:
return

if (
self._min_step
and abs(pv.current().raw["value"] - value["value"]) < self._min_step
):
value.mark("value", False)

def put(self, pv, op):
"""
In most cases the combination of a put() and post() means that the
put() is solely concerned with issues of authorisation.
"""
# Demo authorisation.
# Only Alice may remotely change the Control limits
# Bob is forbidden from changing anything on this Channel
# Everyone else may change the value but not the Control limits
errmsg = None
if op.account() == "Alice":
pass
elif op.account() == "Bob":
op.done(error="Bob is forbidden to make changes!")
return
else:
if op.value().raw.changed("control"):
errmsg = f"Unauthorised attempt to set Control by {op.account()}"
op.value().raw.mark("control", False)

# Because we have not set use_handler_post=False in the post this
# will automatically trigger evaluation of the post rule and thus
# the application of
pv.post(op.value())
op.done(error=errmsg)


# Construct a PV with Control fields and use a handler to apply the Normative
# Type logic. Note that the Control logic is correctly applied even to the
# initial value, based on the limits set in the rest of the initial value.
pv = SharedPV(
nt=NTScalar("d", control=True),
handler=SimpleControl(),
initial={
"value": 12.0,
"control.limitHigh": 11,
"control.limitLow": -1,
"control.minStep": 2,
}, # Immediately limited to 11 due to handler
)


# Override the put in the handler so that we can perform puts for testing
# @pv.on_put
# def handle(pv, op):
# pv.post(op.value()) # just store and update subscribers
# op.done()


pvs = {
"demo:pv": pv,
}
print("PVs: ", pvs)
Server.forever(providers=[pvs])
Loading