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

Your first PV

Verified · simple_server.rs · pvget.rs · demo_first_pv.py · demo_get.py · check docs_verify · docs-verify

The badge reports the whole docs-verify suite, 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 blockingclient.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.