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

Discovery and introspection

Verified · pvlist.rs · demo_discovery.py · demo_pvfind.py · check docs_verify · docs-verify

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

Next

Reacting to writes.