Discovery and introspection
✅ Verified ·
pvlist.rs·demo_discovery.py·demo_pvfind.py· checkdocs_verify·The badge reports the whole
docs-verifysuite, not this chapter alone.
What you'll build
Four questions, in the order you actually hit them: what servers are on this
network, which one has my PV, what else does it serve, and what shape is that
PV? These are the library forms of splist and
spinfo — the same four calls those tools make.
Discovery, listing, and introspection are metadata-only — none of the Rust
steps below reads a value. The Python introspection snippet does end with a
get() to show the current value alongside the field table, but that call
is separate from introspect() itself.
Rust
Find servers. A UDP search with no PV name attached: every PVA server that hears it answers with its GUID and TCP address.
#![allow(unused)] fn main() { let servers = discover_servers(udp_port, timeout, &targets, false).await?; for server in &servers { let guid: String = server.guid.iter().map(|b| format!("{b:02X}")).collect(); println!("GUID 0x{guid} tcp {}", server.tcp_addr); } }
build_search_targets(None, None) produces the target list the way EPICS
Base does — EPICS_PVA_ADDR_LIST merged with auto-discovered broadcast
addresses, unless EPICS_PVA_AUTO_ADDR_LIST disables the latter. Pass
Some(ip) as the first argument to pin a single target.
Locate a PV. The same search, narrowed to one name:
#![allow(unused)] fn main() { let server_addr = search_pv(&pv, udp_port, timeout, &targets, false).await?; println!("{pv} is served by {server_addr}"); }
List a server's PVs. This one takes a SocketAddr, not a name — which is
why the two steps above come first:
#![allow(unused)] fn main() { let client = PvaClient::builder().timeout(timeout).build(); let (names, source) = client.pvlist_with_fallback(server_addr).await?; println!("{} PVs via {source:?}", names.len()); for name in &names { println!(" {name}"); } }
Describe a PV. This step builds its own PvaClient and resolves the PV
name directly, so it does not need the address search_pv found above.
pvinfo returns a StructureDesc; format_structure_tree renders it the way
spinfo does:
#![allow(unused)] fn main() { let client = PvaClient::builder().timeout(timeout).build(); let desc = client.pvinfo(&pv).await?; println!("{}", format_structure_tree(&desc)); }
Python
The same four beats. Discovery and listing live in spvirit.lowlevel:
print("\ndiscover_servers(timeout=1.5) ...")
try:
servers = ll.discover_servers(timeout=1.5)
except Exception as e: # noqa: BLE001
print(f" discovery failed: {e}")
servers = []
for s in servers:
print(f" guid={s['guid']} addr={s['addr']}")
if not servers:
print(" (no servers responded — run a spserver locally to see results)")
if len(sys.argv) > 1:
pv = sys.argv[1]
print(f"\nsearch_pv({pv!r}, timeout=2.0) ...")
try:
addr = ll.search_pv(pv, timeout=2.0)
print(f" -> {addr}")
except Exception as e: # noqa: BLE001
print(f" not found: {e}")
This continues straight from discovery, reusing the servers list that
discover_servers returned:
if servers:
addr = servers[0]["addr"]
print(f"\npvlist({addr!r}) ...")
try:
names, source = ll.pvlist(addr, timeout=3.0)
print(f" via {source}: {len(names)} names")
for n in names[:10]:
print(f" {n}")
except Exception as e: # noqa: BLE001
print(f" pvlist failed: {e}")
Introspection goes through a channel. demo_pvfind.py walks the returned
description with a small recursive walk() helper — defined just above this
block in the file — that yields one row per field, descending into
field.struct_desc wherever a field is itself a structure; formatting each
value's type goes through codec, spvirit.codec, imported at the top of the
same file:
with Channel.connect(pv_name, addr) as ch:
desc = ch.introspect()
print(f"\nPV : {pv_name}")
print(f"server : {addr} (sid={ch.sid})")
print(f"struct_id : {desc.struct_id}")
print(f"fields : {len(desc)}\n")
width = max(len(name) for name, _, _ in walk(desc))
for name, ftype, is_array in walk(desc):
suffix = "[]" if is_array and not ftype.endswith("[]") else ""
print(f" {name:<{width}} {ftype}{suffix}")
Channel.introspect() is the low-level route, and the one to use when you
want the channel open anyway for a subsequent get(). For a one-shot
summary there is also Client.info(pv_name), but it is flatter than
introspect()'s result: a top-level dict of {struct_id, fields: [{name, field_type}]}, with no nesting into sub-structures and no is_array flag.
Reach for Channel.introspect() when you need the full recursive
description; Client.info when a flat top-level summary is enough.
Client.pvlist(addr) is the __pvlist-only convenience — it returns just
the name list and has none of lowlevel.pvlist's fallback chain, so it
fails outright on servers where the fallback would have succeeded. Use
lowlevel.pvlist when you need that fallback chain or want to know which
route answered.
From the command line
$ splist
$ splist 127.0.0.1:5075
$ spinfo VAC:PRESSURE
What to notice
Listing needs an address, not a name. pvlist takes a SocketAddr
because listing is a question about a server, while a PV name is a question
about the network. If all you have is a PV name, search_pv (Rust) or
lowlevel.search_pv (Python) converts one into the other. Rust also offers
resolve_pv_server, which applies the full PvGetOptions — name servers,
explicit --server, the lot — rather than a bare broadcast.
The second return value names the route that worked. pvlist_with_fallback
tries four strategies in turn and tells you which answered:
PvListSource::PvList, GetField, ServerRpc, or ServerGet. Python returns
it as the second element of a (names, source) tuple, with source spelled
as one of the strings "pvlist", "getfield", "server_rpc", or
"server_get" — that is the source the Python snippet above prints. It
matters because the routes differ in completeness — a server answering by
ServerGet may be giving you a truncated view. The
splist page has the detail.
Introspection transfers no data. pvinfo uses CMD_GET_FIELD (0x11),
so the server replies with a type description and no value. That makes it
safe on PVs a GET would choke on — a 4-megapixel image, or a PV whose read
triggers expensive device I/O.
__pvlist is in every listing. It is the server's own introspection
channel, not one of your PVs. Filter it out if you are building a UI.
Discovery is a UDP broadcast. On a multi-homed host the search can leave
by the wrong interface and find nothing. build_search_targets(Some(ip), None)
or EPICS_PVA_ADDR_LIST pins it.
Run it
# Terminal 1 — something to talk to
cargo run -p spvirit-server --example complete_ioc
# Terminal 2
cargo run -p spvirit-client --example pvlist
# or, once you have an address from the line above
cargo run -p spvirit-client --example pvlist -- 127.0.0.1:5075
# or, to describe one PV
cargo run -p spvirit-client --example pvlist -- VAC:PRESSURE
# or, the Python equivalents
python spvirit-py/examples/demo_discovery.py
python spvirit-py/examples/demo_pvfind.py VAC:PRESSURE
Bare pvlist finds servers:
$ cargo run -p spvirit-client --example pvlist
GUID 0x60C70000640AEFC9B42DC918 tcp 10.64.23.134:5075
An address argument lists that server's PVs:
$ cargo run -p spvirit-client --example pvlist -- 127.0.0.1:5075
6 PVs via PvList
VAC:ERROR
VAC:LINK
VAC:PRESSURE
VAC:RGA
VAC:SETPOINT
__pvlist
A PV name searches for it, then describes it:
$ cargo run -p spvirit-client --example pvlist -- VAC:PRESSURE
VAC:PRESSURE is served by 10.64.23.134:5075
struct epics:nt/NTScalar:1.0
value: double
alarm: structure
severity: int
status: int
message: string
timeStamp: structure
secondsPastEpoch: long
nanoseconds: int
userTag: int
display: structure
limitLow: double
limitHigh: double
description: string
units: string
precision: int
form: structure
index: int
choices: string[]
control: structure
limitLow: double
limitHigh: double
minStep: double
valueAlarm: structure
active: boolean
lowAlarmLimit: double
lowWarningLimit: double
highWarningLimit: double
highAlarmLimit: double
lowAlarmSeverity: int
lowWarningSeverity: int
highWarningSeverity: int
highAlarmSeverity: int
hysteresis: ubyte
The GUID and address will differ on your machine.