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

Reading and writing

Verified · pvget.rs · pvput.rs · demo_get.py · demo_put.py · check docs_verify · docs-verify

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

What you'll build

Client code that reads a PV and writes one, plus the CLI equivalents you will reach for far more often than you expect.

Rust

Read

#![allow(unused)]
fn main() {
    let client = PvaClient::builder().build();
    let result = if fields.is_empty() {
        client.pvget(&pv).await?
    } else {
        let refs: Vec<&str> = fields.iter().map(String::as_str).collect();
        client.pvget_fields(&pv, &refs).await?
    };
    println!("{}: {}", result.pv_name, result.value);
}

Write

#![allow(unused)]
fn main() {
    let client = PvaClient::builder().build();
    if fields.is_empty() {
        client.pvput(&pv, value).await?;
    } else {
        let refs: Vec<&str> = fields.iter().map(String::as_str).collect();
        client.pvput_fields(&pv, value, &refs).await?;
    }
    println!("OK");
}

Both are async and both need a Tokio runtime. PvaClient::builder() takes the network settings — .port(), .udp_port(), timeouts — and .build() gives you a client you can reuse for many operations. Building one client per PV works, but you pay for discovery every time.

The _fields variants take a list of dotted paths and send them as a pvRequest, so the server returns only that subtree.

Python

Read

client = spvirit.Client()
result = client.get("SIM:TEMPERATURE")

# result.value is the whole NTScalar as a dict — value plus alarm,
# timeStamp, display, control and valueAlarm.
print(result.pv_name, "=", result.value["value"])
print("severity:", result.value["alarm"]["severity"])

Write

client = spvirit.Client()

client.put("SIM:SETPOINT", 30.0)
print("after put:", client.get("SIM:SETPOINT").value["value"])

The Python client is blocking — no await, no event loop. Both get and put take the same optional fields argument as their Rust counterparts.

From the command line

$ spget SIM:TEMPERATURE
SIM:TEMPERATURE 2026-08-04 09:46:31.420 22.5

$ spput SIM:SETPOINT 30
SIM:SETPOINT OK

spget formats the payload for humans; spget --fields value,alarm.severity narrows it. For everything the tools take, see Command-line tools.

What to notice

A GET returns the whole structure, not a number. In Rust result.value is a DecodedValue; in Python result.value is a nested dict, so the number itself is result.value["value"]. Everything else — alarm, timeStamp, display, control, valueAlarm — arrived in the same message and cost you nothing extra.

A PUT targets a field, and that field defaults to value. Python's put(pv, v) is put(pv, v, fields=["value"]). That is why writing to a PV does not wipe its alarm state or its units: you addressed one leaf of the structure.

Writing to an input record fails at the protocol level.

$ spput SIM:TEMPERATURE 99
SIM:TEMPERATURE ERROR protocol error: PUT init error: Write access denied

The refusal comes back on the PUT init exchange, before any value is sent. The record type alone decides it.

A client PUT always restamps the record. Server-side updates — store.set_value(), a scan callback, a .link() recomputation — set the record's timeStamp to now, and an accepted PUT does exactly the same, whether or not the value it carried actually moved:

$ spget SIM:SETPOINT
SIM:SETPOINT 2026-08-04 10:00:31.426 500
$ spput SIM:SETPOINT 42 && spget SIM:SETPOINT
SIM:SETPOINT OK
SIM:SETPOINT 2026-08-04 10:02:47.913  42

This matches EPICS Base, where recGblGetTimeStampSimm() runs unconditionally in process(). The one wrinkle: if the PUT carried its own non-zero timeStamp, that value is honoured instead of server time — a gateway relaying a reading from elsewhere can forward the originating acquisition time rather than the moment it happened to relay it.

Run it

# Terminal 1
cargo run -p spvirit-server --example scalar_metadata

# Terminal 2
cargo run -p spvirit-client --example pvget -- SIM:TEMPERATURE
cargo run -p spvirit-client --example pvput -- SIM:SETPOINT 30
python spvirit-py/examples/demo_put.py

The raw client prints the whole structure; pvput prints nothing but OK; and the Python pair prints just the field each one asked for:

$ cargo run -p spvirit-client --example pvget -- SIM:TEMPERATURE
SIM:TEMPERATURE: {value=22.500000, alarm={severity=0, status=0, message=""},
timeStamp={secondsPastEpoch=1786008647, nanoseconds=984899400, userTag=0},
display={...}, control={...}, valueAlarm={...}}

$ cargo run -p spvirit-client --example pvput -- SIM:SETPOINT 30
OK

$ python spvirit-py/examples/demo_put.py
after put: 30.0

(The {...} groups above are elided; see Your first PV for the untrimmed dump.)

A successful PUT returns no value — if you want to see the new number you have to read it back, which is exactly what demo_put.py does:

$ spput SIM:SETPOINT 30
SIM:SETPOINT OK

$ spget SIM:SETPOINT
SIM:SETPOINT 2026-08-06 09:30:35.355  30

Next

Monitoring changes.