Reading and writing
✅ Verified ·
pvget.rs·pvput.rs·demo_get.py·demo_put.py· checkdocs_verify·The badge reports the whole
docs-verifysuite, 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