/ˈspɪrɪt/ of the Machine
Spvirit is a Rust library for working with EPICS PVAccess protocol, including encoding/decoding and connection state tracking. It also includes tools for monitoring and testing PVAccess connections. These are not yet production ready , but they are available for anyone to use and contribute to.
Key areas of development in the near future include:
- More complete support for EPICS Normative Types (NT) and their associated metadata.
- softIOC-like server functionality, including parsing of EPICS db files and support for more complex PV types and behaviors, a prototype of which is already available in the
spservertool.
Because why not, admittedly I just wanted to learn Rust and this seemed like a fun project with a moderately useful outcome.
The project is structured as a Cargo workspace with three crates:
spvirit-types: Contains shared data model types for PVAccess Normative Types (NT).spvirit-codec: Contains the PVAccess protocol encoding/decoding logic and connection state tracking.spvirit-tools: Contains command-line tools for monitoring and testing PVAccess connections.
In the future I would like to split out the tools to client and server tools and add some more IOC-like functionality to the server tools, but for now they are all in one crate for simplicity.
# Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shgit clone https://github.com/ISISNeutronMuon/spviritcd spvirit
cargo build --releasecargo run --bin spmonitor my:pv:name
# or
./target/release/spmonitor my:pv:name
# or if installed
cargo install spvirit-tools
spmonitor my:pv:nameAdd the crates you need to your Cargo.toml:
[dependencies]
spvirit-tools = "0.1" # client/server library + CLI tools
spvirit-codec = "0.1" # low-level PVA protocol encode/decode
spvirit-types = "0.1" # shared Normative Type data modeluse spvirit_tools::{format_output, pvget, PvGetOptions, RenderOptions};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pv_name = std::env::args()
.nth(1)
.unwrap_or_else(|| "MY:PV:NAME".into());
let opts = PvGetOptions::new(pv_name);
let result = pvget(&opts).await?;
let render = RenderOptions::default();
println!("{}", format_output(&result.pv_name, &result.value, &render));
Ok(())
}use std::time::Duration;
use spvirit_tools::{build_auto_broadcast_targets, search_pv};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let pv_name = std::env::args()
.nth(1)
.unwrap_or_else(|| "MY:PV:NAME".into());
let targets = build_auto_broadcast_targets();
let addr = search_pv(&pv_name, 5076, Duration::from_secs(5), &targets, false).await?;
println!("Found server at {addr}");
Ok(())
}use spvirit_codec::{PvaPacket, PvaPacketCommand};
fn main() {
let raw: &[u8] = &[
0xCA, 0x02, 0x00, 0x01, // header: magic, version, flags (LE), command 1
0x09, 0x00, 0x00, 0x00, // payload length = 9
0x00, 0x40, 0x00, 0x00, // buffer_size = 16384
0xFF, 0x7F, // introspection_registry_size = 32767
0x00, 0x00, // qos = 0
0x00, // authz = "" (empty string)
];
let mut packet = PvaPacket::new(raw);
println!("command: {}", packet.header.command);
println!("endian: {}", if packet.header.flags.is_msb { "big" } else { "little" });
if let Some(cmd) = packet.decode_payload() {
match cmd {
PvaPacketCommand::ConnectionValidation(cv) => {
println!("buffer_size: {}", cv.buffer_size);
}
other => println!("{other:?}"),
}
}
}See the examples/ folders for runnable versions of each snippet:
cargo run --example pvget_example -p spvirit-tools MY:PV:NAME # requires a running IOC
cargo run --example search_example -p spvirit-tools MY:PV:NAME # requires a running IOC
cargo run --example decode_packet -p spvirit-codec # self-contained, no IOC needed| spvirit tool | EPICS Base equivalent | Description |
|---|---|---|
spget |
pvget |
Fetch the current value of a PV |
spput |
pvput |
Write a value to a PV |
spmonitor |
pvmonitor |
Subscribe to a PV and print value changes |
spinfo |
pvinfo |
Display field/metadata information for a PV |
splist |
pvlist |
List all available PVs on discovered servers |
spserver |
softIoc |
Not fully one-to-one - just a demo, it does parse some db file vocab |
spexplore |
Interactive TUI to browse servers, select PVs, and monitor values | |
spsearch |
TUI showing PV search network traffic for diagnostics | |
spsine |
Continuously write a sine wave to a PV (demo/testing) | |
spdodeca |
Server publishing a rotating 3D dodecahedron as an NTNDArray PV |
While not a full softIOC implementation, spserver is a simple PVAccess server that can serve some static PVs and parse a limited subset of EPICS db file syntax. It proves that the encoding/decoding and connection handling logic in spvirit-codec is sufficient to implement a server, and it can be used as a starting for a more full featured softIOC in the future. (hint hint PRs welcome :))
I have tested the tools in this repo against the following EPICS PVAccess servers:
- EPICS
- p4p (pvxs under the hood)
- PvAccessJava
- spvirit-scry — A Rust tool for capturing and analyzing pvAccess EPICS packets.
I used the following libraries and repos as refernce materials for PVAccess protocol:
| Section / Area | What Was Done With AI | Plans Ahead |
|---|---|---|
spvirit-types |
Hand coded, few types completed with AI, the prettified with AI | keep the same, fairly complete |
spvirit-codec |
Most was hand-coded, some restructuring and prettifying was done with AI. | keep the same, bring in any common helpers, maybe write a siplified API for users |
spvirit-tools |
Mostly AI generated, manually coded parts of Put an Get then let the Agents build on top. | Need to split it out into spvirit-IOC/server/client utils and the like to idiomatically conform to EPICS-base/pvxs client/servers. They do work and are ineter-op tested against other EPICS PVA implementations in C++/Java but don't have super clean/intuitive API's at the moment. |
| Testing | I wrote some basic tests, then used GenAI agents to generate more tests and test cases, which I then manually curated and edited. | Suite is fairly comprehensive so I will keep it as is. |