-
Notifications
You must be signed in to change notification settings - Fork 46
Add additional callbacks for SharedPV handlers #155
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
Open
Monarda
wants to merge
14
commits into
epics-base:master
Choose a base branch
from
Monarda:post_handler
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
f1fd91d
Begun adding RulesHandler
Monarda 4ccfb7c
Implementation and example for nthandlers
Monarda df902e4
Merge branch 'epics-base:master' into master
Monarda 6c45ed9
Merge branch 'epics-base:master' into master
Monarda 406ede6
Add new handler functions with examples and tests
Monarda fecb939
Add another example showing how a handler.post can allow persistence
Monarda 6b67839
Duplicate pvs do work
Monarda c2a7f98
Minor formatting changes
Monarda 9aa3a0a
Change to how handler arguments are treated
Monarda 706602f
Most of the restore work can be done in the handler open()
Monarda 2f4a32c
Improvements to persist example
Monarda 49c7e90
Move control field initialisation to open initial. Improve comments
Monarda fdf3228
Merge remote-tracking branch 'upstream/master' into post_handler
Monarda 9a7cc63
Merge branch 'master' into post_handler
Monarda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like the end of this comment is missing? |
||
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]) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I note that this type hint doesn't work in Python2; I don't know how important it is for all the examples to still support Python2, but the overall library still does.