Your first PV
✅ Verified ·
simple_server.rs·pvget.rs·demo_first_pv.py·demo_get.py· checkdocs_verify·The badge reports the whole
docs-verifysuite, not this chapter alone.
What you'll build
A server holding three PVs, and a client that reads one back. Two terminals.
The three records are deliberately of different kinds — one you can only read, two you can write — because that distinction is the first thing worth internalising.
Rust
The server
#![allow(unused)] fn main() { let server = PvaServer::builder() .ai("SIM:TEMPERATURE", 22.5) .ao("SIM:SETPOINT", 25.0) .bo("SIM:ENABLE", false) .build(); }
PvaServer::builder() collects records, .build() freezes them into a
server, and server.run().await binds the sockets and serves forever. The
full example adds a background task that walks SIM:TEMPERATURE toward
SIM:SETPOINT, but the four lines above are already a working IOC-like
server.
The client
#![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); }
That sits inside #[tokio::main] async fn main, with pv a String — both
the client and the server are async, so you need a Tokio runtime.
Python
The server
temp = spvirit.ai("SIM:TEMPERATURE", 22.5) # input — read-only to clients
setpoint = spvirit.ao("SIM:SETPOINT", 25.0) # output — clients may write
enable = spvirit.bo("SIM:ENABLE", False) # output — a writable bool
server = spvirit.Server(pvs=[temp, setpoint, enable])
server.run()
The client
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"])
The Python client is blocking — client.get(...) returns a value, no
await, no event loop. result.value is the whole NTScalar as a nested
dict, which is why the value itself is result.value["value"].
Run it
# Terminal 1
cargo run -p spvirit-server --example simple_server
# Terminal 2
cargo run -p spvirit-client --example pvget -- SIM:TEMPERATURE
Terminal 2 should print the whole structure — this is what success looks like:
$ 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={limitLow=0.000000, limitHigh=0.000000, description="", units="",
precision=0, form={index=0, choices=["Default", "String", "Binary",
"Decimal", "Hex", "Exponential", "Engineering"]}}, control={limitLow=0.000000,
limitHigh=0.000000, minStep=0.000000}, valueAlarm={active=false,
lowAlarmLimit=0.000000, lowWarningLimit=0.000000, highWarningLimit=0.000000,
highAlarmLimit=0.000000, lowAlarmSeverity=0, lowWarningSeverity=0,
highWarningSeverity=0, highAlarmSeverity=0, hysteresis=0}}
That is one long line, wrapped here to fit the page — nothing has been elided. Or with the Python pair:
python spvirit-py/examples/demo_first_pv.py # terminal 1
python spvirit-py/examples/demo_get.py # terminal 2
$ python spvirit-py/examples/demo_get.py
SIM:TEMPERATURE = 22.5
severity: 0
The two halves mix freely: the Rust client reads the Python server, spget
reads either, and so does pvget from EPICS Base.
What to notice
ai is an input; ao and bo are outputs. Input and output are named
from the server's point of view, so an input record is read-only to
clients. Try it:
$ spput SIM:SETPOINT 30
SIM:SETPOINT OK
$ spput SIM:TEMPERATURE 99
SIM:TEMPERATURE ERROR protocol error: PUT init error: Write access denied
The server enforces that from the record type alone — you did not configure any permissions.
You get more than a number back. A raw pvget prints the entire
NTScalar: value, alarm, timeStamp, display, control, valueAlarm. The
timestamp is there because the record layer stamped it for you.
$ spget SIM:TEMPERATURE
SIM:TEMPERATURE 2026-08-04 09:46:31.420 22.5
spget renders that structure for humans; the example client prints it
whole. Both received exactly the same bytes.
Nobody configured a port or an address. The client broadcast the PV name and the server answered. If that step fails, see the note on ports in Installation.
Next
Serving scalars — engineering units, precision, limits, and the metadata that makes a PV readable.