Skip to content

Camera images over PVA — client orientation

GEECS camera images are served live as EPICS pvAccess (PVA) PVs of the standard NTNDArray type by GeecsPvaGateway — a distributed service running on each Windows camera server, serving only that host's cameras. Any PVA-speaking consumer — a stock Phoebus Image widget, three lines of p4p, an ophyd-async signal — gets typed pixel arrays with normalized timestamps, with zero knowledge of GEECS internals.

LabVIEW camera device --loopback TCP--> per-server gateway --NTNDArray--> Phoebus / p4p / ophyd-async

The central GEECS CA gateway (scalars/controls) and the per-server image gateways are peers in one flat namespace: CA/PVA name search finds whichever server owns a PV, and nothing proxies pixels through a central box — image bandwidth stays at the edge, by design.

This page is a client-facing orientation. The authoritative detail lives in the package alongside the code — GeecsPvaGateway/DEPLOYMENT.md (fleet runbook, addressing) and GeecsPvaGateway/CLAUDE.md (architecture); treat those as the source of truth if anything disagrees.

Why not just read the camera's TCP stream yourself?

You could — the GEECS wire format is decodable, and for a one-off diagnostic script running on the camera server itself that remains legitimate. For everything else, the gateway exists so that the hard parts are solved exactly once:

  • The wire format is a hazard. Values that can't be comma-tokenized, latin-1 byte-mangling, and IMAQ image wrappers with multiple structural variants that have silently drifted before. Every bespoke decoder re-inherits all of it; the gateway's decoder is fixed in one place for everyone.
  • Fan-out economics. GEECS TCP push is per-connection — N direct subscribers make LabVIEW flatten and send every frame N times. The gateway subscribes once per stream variable and PVA fans out to any number of clients. Subscriptions are gated: a camera nobody is watching costs the device nothing at all.
  • The ecosystem is free. Phoebus renders these PVs with a stock widget; ophyd-async speaks PVA natively (the door to live images in scans); a "bespoke image GUI" starts at one p4p line instead of at a socket.

PV naming

Image PVs follow the same shared naming contract as the scalar gateway (lowercase components joined by : — see PV naming for the normalization rules):

[experiment:]device:variable            e.g. undulator:uc_tubein:image

A camera device typically serves several image-typed variables (image, processed_image, …); each is its own PV, gated independently — watching image costs nothing for processed_image.

Array variables (GeecsPvaGateway 0.12.0)

A device's 1darray-typed variables — a MagSpec camera's interpSpec / interpDiv lineouts, a Picoscope's scopeTrace.Channel0 — are served the same way, as NTNDArray PVs, minus the per-devicetype exclusions declared in geecs_core.db.device_streams (names the device never publishes, GUI twins, an axis that only repeats a lineout's column 0). The variable component of the PV name is normalized like any other: lower-cased, with runs of punctuation or whitespace collapsed to one underscore, so

undulator:uc_bcavemagspeccam1:interpspec          (from interpSpec)
undulator:u_bcaveict:scopetrace_channel0          (from scopeTrace.Channel0)

What you read:

  • A lineout is (rows, 2) float64 — column 0 the axis (energy in MeV, angle in mrad), column 1 the value — at its native length, never padded: the row count is the energy span over the configured ΔE, so it moves with the magnet current and the ΔE. A scan records one length (a frame of another is dropped and counted), so change either between scans. A single real row (the magnet-off default) is an ordinary frame.
  • A scope trace is (samples,) float64 in volts, at the device's configured record length; its time axis rides in the NTNDArray attribute list — x0 and dx in seconds, samples, and the raw offset / gain / channel name — so t = x0 + i * dx.
  • Phoebus's image widget shows a (rows, 2) lineout as a two-pixel-wide strip; use an XY plot on the two columns, or a 1-D plot for a trace.

Each gateway instance also serves four instance PVs for fleet health:

[experiment:]pvagateway:<host_token>:version     installed package version
[experiment:]pvagateway:<host_token>:heartbeat   counter, +1 per 5 s
[experiment:]pvagateway:<host_token>:restart     write 1 → clean relaunch
[experiment:]pvagateway:<host_token>:devices     the devices served right now (string array)

The served set follows the GEECS DB while the instance runs (GeecsPvaGateway 0.16.0): it is re-read every 60 s, so a device enabled or disabled in the DB gains or loses its PVs within a minute with no restart, and :devices is posted on every change. geecs-pva-gateway fleet shows it against the DB roster.

<host_token> is the server's IP with dots as underscores (192.168.6.100 → 192_168_6_100). A Phoebus fleet screen reading these is generated per experiment from the DB roster (GeecsPvaGateway/deploy/gen_fleet_status.py --experiment X → fleet_status_<x>.bob; HTU's fleet_status_undulator.bob is committed).

Each stream variable — image or array — also has a subscription-state PV (GeecsPvaGateway 0.10.0):

[experiment:]device:variable:connected   Idle | Disconnected | Connected

Idle means gated off — nobody is watching, so nothing is known. Disconnected (MAJOR alarm) means a watcher holds the subscription and the device is unreachable or dropped; Connected means it is live. To get the verdict for an idle camera, hold a monitor on its image PV for one gating round-trip (~1–2 s) and read this. It is the PVA gateway's own subscription state — distinct from the CA gateway's [experiment:]device:connected, which reports that gateway's subscription.

Reading images

Phoebus: add an Image widget and set its PV to pva://undulator:<camera>:image. That's the whole recipe.

Python (p4p) — note Context("pva"), not "ca":

import time

from p4p.client.thread import Context

ctx = Context("pva")
sub = ctx.monitor("undulator:uc_tubein:image", lambda v: print(v.shape, v.dtype))
time.sleep(5)  # hold the gate open — the first update is the current cached
               # value; the first real frame lands ~1-2 s later (1 Hz camera)
sub.close()

Use a held monitor, not a bare get

Subscriptions are gated on client interest: the gateway only subscribes to the camera while at least one client channel is open, and the first fresh frame arrives one gating round-trip after connecting (subscribe + next device push, ~1–2 s at 1 Hz). A bare get returns immediately with whatever is cached — the (1, 1) startup placeholder on a fresh instance, or a stale last frame from a previous watch — and never waits for that round-trip. Hold a monitor open for at least one push interval.

ophyd-async consumes the same PVs as standard PVA signals — live camera frames become readable device signals with no GEECS-specific code.

Addressing — who needs a PV address list

PVA name search is UDP broadcast, and broadcast is subnet-local. The camera-server fleet spans several lab subnets, so:

  • A client on the same subnet as a camera server finds it with zero config.
  • Any client that wants cameras across the whole fleet — control-room machines included, and all VPN/routed clients — needs the full fleet address list for unicast search:
    • Python / p4p / ophyd-async: EPICS_PVA_ADDR_LIST="<fleet list>" plus EPICS_PVA_AUTO_ADDR_LIST=NO
    • Phoebus: org.phoebus.pv.pva/epics_pva_addr_list=<fleet list> in the settings file (environment variables don't reach a macOS open-launched app)

The fleet list is kept once, in config.ini [pva] addr_list — the camera servers running an instance (the DB roster of camera-hosting endpoints, minus hosts where no instance was installed; see GeecsPvaGateway/DEPLOYMENT.md §Client access). Any process that imports geecs_bluesky exports it, together with [pva] file_plugin_addr_list, into EPICS_PVA_ADDR_LIST at import (broadcast search stays on; an explicit environment variable wins) — the worker, the scanner and the MCP server included; Phoebus and other non-Python clients still take the value by hand. The fleet tooling reads the key itself. The CA variables (EPICS_CA_*) belong to the scalar gateway and are unaffected — a client using both gateways sets both families.

Semantics worth knowing

  • Latest-wins, never backlogged: a slow consumer gets the newest frame and skips stale ones; nothing queues. The live stream is for watching — shot-complete data acquisition lives in the GEECS file path and scan system, not here.
  • Timestamps are normalized: each frame carries the device's own acquisition timestamp (GEECS's LabVIEW-epoch clock converted to Unix) when available, falling back to receive time — so NTNDArray timestamps line up with the scalar gateway's convention.
  • Uptime is unattended: instances run as auto-start Windows services that survive reboots and crashes, reinstall themselves from the lab's shared clone on every restart, and report liveness via the heartbeat/version PVs above.

Reading more

  • GEECS Gateway client orientation — the scalar/control side of the same namespace: naming rules, readbacks vs setpoints, alarms.
  • GeecsPvaGateway/DEPLOYMENT.md (in the source tree) — the fleet runbook: per-box onboarding, rollout via restart PVs, addressing detail.
  • GeecsPvaGateway/CLAUDE.md — architecture: gating, supervision, the frame path.