Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Reacting to writes

Verified · on_put.rs · on_put_reject.rs · demo_on_put.py · check docs_verify · docs-verify

The badge reports the whole docs-verify suite, not this chapter alone.

What you'll build

A writable PV that runs your code when a client writes to it — and, in the form that supports it, refuses writes it does not like.

Rust

#![allow(unused)]
fn main() {
    let server = PvaServer::builder()
        .ao("SIM:SETPOINT", 25.0)
        .on_put("SIM:SETPOINT", |pv, val| {
            println!("{pv} was set to {val:?}");
        })
        .build();
}

That is the builder form, and it is worth reading its signature carefully:

#![allow(unused)]
fn main() {
Fn(&str, &DecodedValue)
}

It returns (). It runs after the value has been applied, and it has no way to say no. It is a notification hook, not a validator.

To reject a write, use the typed handle form instead:

#![allow(unused)]
fn main() {
Fn(&Pv<T>, T) -> Result<(), String>
}
#![allow(unused)]
fn main() {
    let setpoint = Pv::ao("SIM:SETPOINT", 25.0)
        .units("degC")
        .drive_limits(0.0, 100.0)
        .on_put(|pv, value: f64| {
            if !(0.0..=100.0).contains(&value) {
                // Err rejects the PUT; the client's put() fails.
                return Err(format!("{} outside 0..100: {value}", pv.name()));
            }
            println!("{} accepted {value}", pv.name());
            Ok(())
        });

    let server = PvaServer::serve([setpoint.clone()]).build().await;
}

Err(msg) rejects the PUT on the wire and the client's put fails:

$ spput SIM:SETPOINT 500
SIM:SETPOINT ERROR protocol error: PUT failed: SIM:SETPOINT outside 0..100: 500

$ spget SIM:SETPOINT
SIM:SETPOINT 2026-08-04 10:08:05.123  30

The rejected value never reached the record. Ok(()) accepts it. You also get the value already converted to T instead of a raw DecodedValue.

The two forms are not interchangeable, and the builder form's inability to reject is the single most common surprise in this API. If you are validating, use handles.

Python

setpoint = spvirit.ao("SIM:SETPOINT", 25.0, drive_limits=(0.0, 100.0))


@setpoint.on_put
def _on_setpoint(pv, value):
    print(f"{pv.name} was set to {value}")
    if value > 100.0:
        return False  # reject the PUT on the wire


server = spvirit.Server(pvs=[setpoint])

Python has one form and it can reject: returning False — or raising — rejects the PUT, and anything else accepts it. The callback runs before the value is applied.

$ spput SIM:SETPOINT 30
SIM:SETPOINT OK

$ spput SIM:SETPOINT 500
SIM:SETPOINT ERROR protocol error: PUT failed: rejected by on_put

What to notice

Attach callbacks before serving. on_put, scan, and calc must be attached to a PV before it is handed to Server(...) / PvaServer::serve. Attaching afterwards is a silent no-op — the core logs a warning and carries on. Nothing raises, nothing fails; your callback simply never runs. This is true in both languages.

Validation is the only enforcement you get. Drive limits are advisory (see Serving scalars), so on_put is where range checking actually happens. If a PV must not accept 500, write that rule here.

The callback is not a place to block. It runs on the server's runtime. Long work belongs in a task you spawn from it.

A single client write can invoke your callback more than once. spput tries the full PUT flow first and silently falls back to the simple flow if that fails, so a rejected write arrives at the server twice and your callback logs twice:

$ spput SIM:SETPOINT 700
SIM:SETPOINT ERROR protocol error: PUT failed: rejected by on_put

# server log
SIM:SETPOINT was set to 700.0
SIM:SETPOINT was set to 700.0

Accepted writes run once; only the retry path doubles up. Pass --no-flow-fallback to suppress it. The general rule holds regardless of client: on_put callbacks should be idempotent, and side effects that must happen exactly once do not belong in one.

Array PVs do not support on_put or scan in Python. Calling either on an array PV raises TypeError. Drive arrays with pv.set(...) from your own loop instead — see Arrays and waveforms.

Run it

# Terminal 1
cargo run -p spvirit-server --example on_put
# or: python spvirit-py/examples/demo_on_put.py

# Terminal 2
spput SIM:SETPOINT 30
spput SIM:SETPOINT 500

on_put only observes — both writes succeed, and the interesting output is in terminal 1:

$ spput SIM:SETPOINT 30
SIM:SETPOINT OK

$ spput SIM:SETPOINT 500
SIM:SETPOINT OK
# terminal 1
SIM:SETPOINT was set to Structure([("value", Float64(30.0))])
SIM:SETPOINT was set to Structure([("value", Float64(500.0))])

The callback receives the whole submitted structure, not a bare number — a client may write value alone or several fields at once.

Now run on_put_reject instead, which returns an error for out-of-range values:

cargo run -p spvirit-server --example on_put_reject
$ spput SIM:SETPOINT 30
SIM:SETPOINT OK

$ spput SIM:SETPOINT 500
SIM:SETPOINT ERROR protocol error: PUT failed: SIM:SETPOINT outside 0..100: 500
Error: Protocol("PUT failed: SIM:SETPOINT outside 0..100: 500")

Your message crosses the wire verbatim, so write it for whoever is holding the terminal. spput also exits non-zero, which is why it prints that second Error: line — useful in a script.

Terminal 1 logs only the write it let through:

SIM:SETPOINT accepted 30

Next

Simulating a device.