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
p4pline 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,)float64in volts, at the device's configured record length; its time axis rides in the NTNDArrayattributelist —x0anddxin seconds,samples, and the rawoffset/gain/ channelname— sot = 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>"plusEPICS_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 macOSopen-launched app)
- Python / p4p / ophyd-async:
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.