Quick Start
The example below requires only the installation of the p4p and p4pillon Python packages. If using uv
the command uv run .\examples\quick_start\start.py
should start the server script.
Example
The example below creates two PVs, demo:pv:1
and demo:pv:2
. It sets the first up with alarm and control limits. The second PV is identical to the first except it has a different initial value and is set as read only. This script may be found in the examples/quick_start
directory and is called start.py
.
# /// script
# dependencies = [
# "p4p",
# "p4pillon@git+https://github.com/ISISNeutronMuon/p4pillon",
# ]
# ///
import asyncio
from p4p.server import Server, StaticProvider
from p4pillon.asyncio.pvrecipe import PVScalarRecipe
from p4pillon.definitions import PVTypes
loop = asyncio.new_event_loop() # create the asyncio event loop
pvrecipe_double = PVScalarRecipe(PVTypes.DOUBLE, "An example double PV", 5.0)
pvrecipe_double.initial_value = 17.5
pvrecipe_double.set_alarm_limits(low_warning=2, high_alarm=9)
pvrecipe_double.set_control_limits(low=-10, high=100)
pv_double1 = pvrecipe_double.create_pv()
pvrecipe_double.initial_value = -15.5
pvrecipe_double.read_only = True
pv_double2 = pvrecipe_double.create_pv()
provider = StaticProvider()
provider.add("demo:pv:1", pv_double1)
provider.add("demo:pv:2", pv_double2)
try:
server = Server((provider,))
with server:
done = asyncio.Event()
loop.run_until_complete(done.wait())
finally:
loop.close()
The initial state of the PVs may be examined using the commands:
$ python -m p4p.client.cli get demo:pv:1
demo:pv:1 Thu Aug 14 22:20:28 2025 17.5
$ python -m p4p.client.cli get demo:pv:2
demo:pv:2 Thu Aug 14 22:20:28 2025 -10.0
We can attempt to set values and verify what effect that has:
$ python -m p4p.client.cli put demo:pv:1=101
demo:pv:1=101 ok
$ python -m p4p.client.cli get demo:pv:1
demo:pv:1 Sun Aug 17 14:04:08 2025 100.0
$ python -m p4p.client.cli put demo:pv:2=13
demo:pv:2=13 Error: This PV is read-only
$ python -m p4p.client.cli get demo:pv:2
demo:pv:2 Sun Aug 17 13:59:55 2025 -10.0
To examine
How p4pillon extends p4p
The p4p library provides an Python interface to the pvAccess protocol and the structure of the Normative types. It does not implement the logic of the Normative Types. We illustrate what that means below.
SharedPV
We here repeat the simple “mailbox” PV from the p4p Server Example with some small changes to the SharedPV variable pv
. We add in additional fields (control
and valueAlarm
) and set them with initial values. This script is available in the examples/quick_start
directory as mailbox_sharedpv.py
. Note that this example uses threads, whereas the example in the previous section, above, uses asyncio.
from p4p.nt import NTScalar
from p4p.server import Server
from p4p.server.thread import SharedPV
pv = SharedPV(
nt=NTScalar("d", control=True, valueAlarm=True), # scalar double
initial={
"value": 2.2, # setting initial value also open()'s
"control.limitHigh": 10,
"valueAlarm.active": True,
"valueAlarm.highWarningLimit": 5,
"valueAlarm.highWarningSeverity": 1,
"valueAlarm.highAlarmLimit": 8,
"valueAlarm.highAlarmSeverity": 2,
},
)
@pv.put
def handle(pv, op):
pv.post(op.value()) # just store and update subscribers
op.done()
Server.forever(
providers=[
{
"demo:pv:name": pv, # PV name only appears here
}
]
) # runs until KeyboardInterrupt
Run the script above and, leaving it running, examine the PV demo:pv:name
that it creates. You can do this using the tools built into p4p.
python -m p4p.client.cli --raw get demo:pv:name
Note that we use the --raw
option to see the full structure of the PV rather than a summary.
You should see output like this:
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 2.2
struct "alarm_t" {
int32_t severity = 0
int32_t status = 0
string message = ""
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 0
int32_t nanoseconds = 0
int32_t userTag = 0
} timeStamp
struct {
double limitLow = 0
double limitHigh = 10
double minStep = 0
} control
struct {
bool active = true
double lowAlarmLimit = 0
double lowWarningLimit = 0
double highWarningLimit = 5
double highAlarmLimit = 8
int32_t lowAlarmSeverity = 0
int32_t lowWarningSeverity = 0
int32_t highWarningSeverity = 1
int32_t highAlarmSeverity = 2
double hysteresis = 0
} valueAlarm
}
This shows that the PV has been constructed as expected and is reporting the correct value and other settings. However, if we drop off the --raw
option in the get command and examine the output an issue becomes more obvious. Note that the timestamp is incorrect.
$ python -m p4p.client.cli get demo:pv:name
demo:pv:name Thu Jan 1 00:00:00 1970 2.2
Let’s now try putting a value to the PV and then checking the result:
$ python -m p4p.client.cli put demo:pv:name=6.6
demo:pv:name=6.6 ok
$ python -m p4p.client.cli get demo:pv:name
demo:pv:name Thu Jan 1 00:00:00 1970 6.6
We can observe that the value has correctly changed, but that the timestamp remains incorrect. Let’s take another more detailed look at the full structure:
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 6.6
struct "alarm_t" {
int32_t severity = 0
int32_t status = 0
string message = ""
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 0
int32_t nanoseconds = 0
int32_t userTag = 0
} timeStamp
struct {
double limitLow = 0
double limitHigh = 10
double minStep = 0
} control
struct {
bool active = true
double lowAlarmLimit = 0
double lowWarningLimit = 0
double highWarningLimit = 5
double highAlarmLimit = 8
int32_t lowAlarmSeverity = 0
int32_t lowWarningSeverity = 0
int32_t highWarningSeverity = 1
int32_t highAlarmSeverity = 2
double hysteresis = 0
} valueAlarm
}
Examine the PV’s alarm
field:
double value = 6.6
struct "alarm_t" {
int32_t severity = 0
int32_t status = 0
string message = ""
} alarm
The value of 6.6 is over the valueAlarm.highWarningLimit
of 5 but the alarm severity, status, and message do not reflect this.
Similarly if we set the value of the PV to 12.7…
$ python -m p4p.client.cli put demo:pv:name=12.7
demo:pv:name=12.7 ok
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 12.7
struct "alarm_t" {
int32_t severity = 0
int32_t status = 0
string message = ""
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 0
int32_t nanoseconds = 0
int32_t userTag = 0
} timeStamp
[...]
}
… we truncate some fields (not showing control
and valueAlarm
as they are unchanged).
The value has been set to 12.7, despite the control.limitHigh
being 10 which should not permit values above 10.0. Similarly the alarm severity, status, and message, and the timestamp remain unchanged.
The PV’s Normative Type fields are present, but the logic implied by their presence is not implemented.
SharedNT
Let’s try implementing the same simple “mailbox” server with p4pillon. This file is available in the examples/quick_start
directory and is called mailbox_sharednt.py
.
from p4p.nt import NTScalar
from p4p.server import Server
from p4pillon.thread.sharednt import SharedNT
pv = SharedNT(
nt=NTScalar("d", control=True, valueAlarm=True), # scalar double
initial={
"value": 2.2, # setting initial value also open()'s
"control.limitHigh": 10,
"valueAlarm.active": True,
"valueAlarm.highWarningLimit": 5,
"valueAlarm.highWarningSeverity": 1,
"valueAlarm.highAlarmLimit": 8,
"valueAlarm.highAlarmSeverity": 2,
},
) # setting initial value also open()'s
Server.forever(
providers=[
{
"demo:pv:name": pv, # PV name only appears here
}
]
) # runs until KeyboardInterrupt
Note that we have replaced the SharedPV
with a SharedNT
from p4pillon, and that we have removed the handle
function with the @pv.put
decorator.
Let’s examine the results of the same commands as above:
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 2.2
struct "alarm_t" {
int32_t severity = 0
int32_t status = 0
string message = ""
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 1755203073
int32_t nanoseconds = 363457202
int32_t userTag = 0
} timeStamp
struct {
double limitLow = 0
double limitHigh = 10
double minStep = 0
} control
struct {
bool active = true
double lowAlarmLimit = 0
double lowWarningLimit = 0
double highWarningLimit = 5
double highAlarmLimit = 8
int32_t lowAlarmSeverity = 0
int32_t lowWarningSeverity = 0
int32_t highWarningSeverity = 1
int32_t highAlarmSeverity = 2
double hysteresis = 0
} valueAlarm
}
$ python -m p4p.client.cli get demo:pv:name
demo:pv:name Thu Aug 14 21:24:33 2025 2.2
The timestamp is already being set correctly.
Let’s try the other commands…
$ python -m p4p.client.cli put demo:pv:name=6.6
demo:pv:name=6.6 ok
$ python -m p4p.client.cli get demo:pv:name
demo:pv:name Thu Aug 14 21:30:29 2025 6.6
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 6.6
struct "alarm_t" {
int32_t severity = 1
int32_t status = 0
string message = "highWarning"
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 1755203429
int32_t nanoseconds = 724358797
int32_t userTag = 0
} timeStamp
[...]
}
… again truncating fields which have not changed.
When you put
the value the timestamp on the PV should reflect that time. Notice that the alarm severity and message have been set.
$ python -m p4p.client.cli put demo:pv:name=12.7
demo:pv:name=12.7 ok
$ python -m p4p.client.cli get demo:pv:name
demo:pv:name Thu Aug 14 21:31:40 2025 10.0
$ python -m p4p.client.cli --raw get demo:pv:name
demo:pv:name struct "epics:nt/NTScalar:1.0" {
double value = 10
struct "alarm_t" {
int32_t severity = 2
int32_t status = 0
string message = "highAlarm"
} alarm
struct "time_t" {
int64_t secondsPastEpoch = 1755203500
int32_t nanoseconds = 87517261
int32_t userTag = 0
} timeStamp
[...]
}
The value has been limited to 10, the alarm severity has been updated to a value of 2 (i.e. MAJOR), and the timestamp has been updated appropriately.