archiveview
archiveview plots EPICS PVs against time. History comes from an EPICS Archiver Appliance and live values come from Channel Access or PV Access, joined into one line per PV. It is written in Rust with egui, in the style of bobview.
It covers what most people use the Phoebus Data Browser for, with a much smaller interface. The plot fills the window, a slim bar holds search and the time range, and everything else is direct manipulation: drag to pan, scroll to zoom, hover to read values. See Scope, and how it differs from the Data Browser.
New to archiveview? Install it, start the mock archiver and a simulated beamline, and plot your first PVs.
π§ How-to guides goal-orientedPractical steps: connect to your archiver and IOCs, find PVs, save views, take snapshots, embed the plot.
π‘ Explanations understanding-orientedBackground and design: what archiveview sets out to do and how archive and live data become one plot.
π Reference information-orientedCommand-line options, gestures and keys, config files, the archiver API, the mock archiver and the Rust API.
The Rust API docs for the workspace's crates are published alongside this book.
The docs follow the DiΓ‘taxis framework: four kinds of page for four needs, learning, doing, understanding and looking things up.
Quick start
This tutorial installs archiveview and plots archived and live PVs on your own machine. It takes about ten minutes and needs no EPICS installation, no IOCs and no real archiver:
archiveview-mockarchiverplays the part of an Archiver Appliance, serving 30 days of synthetic history for a handful of PVs;bobview-testioc(from bobview) serves the BL01T simulated beamline live, including PVs the mock also archives, so you can watch the archive and the live values join up.
You need an x86_64 or aarch64 Linux machine, or a Mac.
1. Install
On Debian or Ubuntu, first install the graphics libraries:
sudo apt install libxkbcommon0 libwayland-client0 libegl1 libgl1-mesa-dri
(on RHEL and Fedora see Install archiveview; macOS
needs nothing). Then install archiveview into ~/.local:
curl -sSfL https://raw.githubusercontent.com/gilesknap/archiveview/main/install.sh | bash
archiveview --version
If the command is not found, add export PATH=$HOME/.local/bin:$PATH to your
shell's start-up file and open a new terminal.
2. Start the mock archiver
archiveview-mockarchiver &
It listens on http://127.0.0.1:17668, the Archiver Appliance's usual
retrieval port. The PVs it serves are listed in
Mock archiver.
3. Plot some history
archiveview --archiver http://127.0.0.1:17668 MOCK:SINE MOCK:NOISE
The window opens on the last hour. Each PV is a line in its own colour, and a chip for each sits along the bottom with its latest value.
- Hover over the plot: a crosshair shows every trace's value and the time under the pointer.
- Scroll to zoom the time axis about the pointer, and drag to pan. As you zoom out, the archiver sends the data thinned to the plot's width; where a pixel holds many samples you see the line's mean with a shaded min-to-max band.
- Click 1w in the time-range pill to see the whole week, then double-click the plot to return to where you started.
- The pill reading β 127.0.0.1:17668 is the archiver: its dot is green while the archiver answers. Hover over it for details.
archiveview remembers --archiver, so from now on you can leave it out.
4. Find another PV
Press / (or click the search box) and type ramp. The archiver's
matching PVs appear as you type; pick MOCK:RAMP with the arrow keys and
Enter, or click it.
5. Add live values
Start the simulated beamline, and tell Channel Access to look for PVs on this machine:
bobview-testioc &
export EPICS_CA_ADDR_LIST=127.0.0.1 EPICS_CA_AUTO_ADDR_LIST=NO
archiveview --range 10m bl01t:SUM
(install the IOC first with cargo install bobview-testioc, or take it from a
bobview release).
The mock archives bl01t:SUM and the IOC serves it live. The line runs from
the archive into the live values at the right edge, and the β Live pill
shows a red dot while the plot follows the current time. Drag the plot to the left:
live following stops. Press L, or click the pill, to jump back to now.
6. Save the view
Open the ①menu, type demo into the Save as⦠field under Views
and press Enter. Next
time, archiveview --view demo reopens the same PVs and time range, from
the same archiver. Plain archiveview reopens the plot as you left it. See Save and load views.
7. Finish
Close the window, then stop the background programs:
kill %1 %2
Next steps
- Point archiveview at your own appliance and IOCs: Connect to an archiver and IOCs.
- Learn the rest of the gestures: Navigate the plot.
Install archiveview
archiveview runs on x86_64 and aarch64 Linux and on macOS 11 or later
(Apple silicon or Intel). install.sh picks the release build to match the
machine.
Install the prerequisites
On Debian and Ubuntu:
sudo apt install libxkbcommon0 libwayland-client0 libegl1 libgl1-mesa-dri
On RHEL, Rocky, Alma and Fedora:
sudo dnf install libxkbcommon libwayland-client libglvnd-egl mesa-dri-drivers
None are needed on macOS.
Install the release
curl -sSfL https://raw.githubusercontent.com/gilesknap/archiveview/main/install.sh | bash
This installs into ~/.local (set PREFIX to choose another place):
| file | what |
|---|---|
bin/archiveview | the plotter |
bin/archiveview-ssh | runs the plotter with software OpenGL, for ssh -X; see Run over ssh |
bin/archiveview-mockarchiver | a mock Archiver Appliance with synthetic history; see Mock archiver |
share/archiveview/ | the README, licence and THIRD_PARTY_LICENSES.txt |
The Linux binaries are linked against glibc 2.28, so they run on RHEL 8+, Ubuntu 20.04+ and Debian 10+.
Run from inside an unpacked release tarball, install.sh installs that
tarball rather than downloading one.
Or install with cargo
With a stable Rust toolchain (rustup), build the
plotter and the mock archiver from crates.io into ~/.cargo/bin:
cargo install --locked archiveview archiveview-mockarchiver
This builds for the machine it runs on, so it suits platforms with no
release build; --locked builds with the dependency versions CI tested. archiveview-ssh comes only with the release tarball.
Put it on your PATH
On macOS ~/.local/bin is not on PATH by default; add this to ~/.zshrc:
export PATH=$HOME/.local/bin:$PATH
Upgrade or remove
Run the install command again to upgrade. To remove archiveview, delete the
bin/archiveview* files and share/archiveview from ~/.local. Your saved
views and settings are in ~/.config/archiveview.
Connect to an archiver and IOCs
archiveview reads history from an Archiver Appliance over HTTP and live values from IOCs over Channel Access or PV Access. Either can be left out: with no archiver it plots live values only, and a PV the archiver does not know about still plots live.
Point it at the archiver
Pass the appliance's retrieval URL, normally port 17668:
archiveview --archiver http://archiver.example.org:17668 SR-DI-DCCT-01:SIGNAL
/retrieval is added for you, so http://host:17668 and
http://host:17668/retrieval mean the same. A single-container appliance
that serves everything on one port works the same way, e.g.
http://host:17665. See The archiver URL and API.
--archiver is remembered: archiveview writes it to settings.json as
archiver_url (see Config files), so later runs
use it without being told. Give --archiver again only to change
archivers, or --no-archiver for a run with live values only.
At start-up the archiver is chosen in this order:
--archiver URL;- none, with
--no-archiver; - the archiver the opened view was saved with (the
defaultview restored at start-up, or--view NAME); archiver_urlinsettings.json.
See and change the archiver
The pill right of Live in the top bar shows the archiver's host and a dot:
| dot | meaning |
|---|---|
| green | the archiver answers |
| amber | it does not: no connection, a gateway error, or a URL that is not an archiver |
| spinner | checking, at start-up or after a change |
| hollow grey, "No archiver" | none is set: live values only |
archiveview asks the appliance for its version (bpl/getVersion) at
start-up and after a change. After that every history fetch and search
updates the dot: a success turns it green, and a failure that looks like
the archiver's is checked with another version request before the dot
turns amber (one slow query timing out is not the archiver down). While
it is amber the archiver is checked again every minute, so it turns green
when the archiver comes back. A PV that is not archived (a 404 for that
PV) is not an archiver error.
Hover over the pill for the full retrieval URL, the appliance's version, and the last error and when it happened. Click it to switch archivers without restarting: type a URL and press Enter (or Use), pick one of the recently used archivers, or choose No archiver. Use on the archiver already in use checks it again at once. The plotted PVs' history is fetched again from the new archiver.
A switch to another archiver, in the pill or by opening a view saved with
one, is remembered in settings.json like --archiver. No archiver
is for this run only: the settings keep the last archiver, so the next
run without --archiver uses it again. In a narrow window the pill
shrinks to its dot.
Reach the IOCs
Live values use the standard EPICS client environment variables, as any CA or PVA client does:
export EPICS_CA_ADDR_LIST="10.0.0.10" EPICS_CA_AUTO_ADDR_LIST=NO
export EPICS_PVA_ADDR_LIST="10.0.0.10" EPICS_PVA_AUTO_ADDR_LIST=NO
A PV name with no prefix uses Channel Access. Prefix it with pva:// for PV
Access, e.g. pva://bl01t:SUM. The other prefixes of bobview's pv crate
also work for live values (sim://sine, loc://name), which is handy for
trying the plot out; the archiver is asked only about plain names.
archiveview never writes to PVs.
Check what is happening
The plot's chips show a PV's state: a spinner while its history loads, a
grey "β" when it is disconnected (hover for the reason), and a "live only"
mark when the archiver has no record of it (HTTP 404). A ! on a chip
means its last archive request failed; hover the chip for the error. For more detail run
with ARCHIVEVIEW_LOG=debug, which logs every archiver request.
Find and add PVs
Search the archiver
Press /, or click the search box at the left of the bar, and type part of
a name. After a short pause the box lists the archiver's matching PVs:
- The first match is highlighted:
β/βmove through the list,Enteradds the highlighted row, and a click adds the one clicked. - A PV whose name is exactly what you typed is always the first match, so
typing a full name and pressing
Enteradds that PV, even when other names containing it sort before it. - Adding a PV keeps the text and the list, so you can add several from one
search:
β,Enter,β,Enter, ... PVs already on the plot are dimmed with a β, and picking one again does nothing. Esccloses the list and keeps the text (βor a click brings the list back); a secondEsc, or the β in the box, clears it. A click elsewhere closes the list and keeps the text.- Coming back to the box (a click, or
/) selects its text, so typing starts a new search; pressβorEndto edit the old one instead.
| you type | archiver is asked for (regex) | matches |
|---|---|---|
dcct | (?i).*dcct.* | any name containing dcct, in any case (SR-DI-DCCT-01:SIGNAL) |
ES{Dif} | (?i).*ES\{Dif\}.* | as written: {, (, ., + and the like match only themselves |
SR-DI-*:SIGNAL | SR-DI-.*:SIGNAL | the whole name, with case; * is any run of characters |
BL0?T* | BL0.T.* | ? is any one character |
Text without * or ? is searched as a substring, ignoring case. With *
or ? it is a glob: it must match the whole name, case and all, and every
other character stands for itself.
Click the .* toggle inside the box to type a Java regular expression
instead. It must match the whole name: SR-DI matches nothing, while
SR-DI.* works. Start it with (?i) to ignore case.
The list shows the first 50 matches; type more of the name to narrow it.
Reuse a recent search
archiveview remembers a search once you add a PV from it (typos and
half-typed names are not kept), with its .* setting. It keeps the last
50, in history.json in the config directory, so
they survive a restart. Plain text that differs only in case (bl16i,
BL16I) is one search, kept as you last typed it.
- Click the empty box, or press
/: the list shows your recent searches under Recent. Pick one withβandEnter, or a click, to put it back in the box and search again. Nothing is added until you pick a match. - While typing: recent searches that start with (or contain) the text
come first, marked with a clock, above the archiver's matches.
Enterstill adds the first match; move up to a recent search to use it. So typingBL16IoffersBL16I-EA-*from yesterday. - Hover a recent search and click its β to forget it, or choose Clear search history in the β‘ menu to forget them all.
Add a PV the archiver doesn't know
Type the full name: as well as any archived matches, the list ends with an
Add "β¦" as typed row. Highlight it with β (or click it) and press
Enter. The text is added as written, so this is how to add a PV that is
not archived; it plots live only. A PV Access or simulated name
(pva://name, sim://sine), or several names separated by spaces or
commas, can only be added as typed, so that row comes first and Enter
adds it straight away.
Add PVs from another window
Select a PV name anywhere, then middle-click the plot or its legend: the PV is added, and a note at the bottom says so. This is the X11 and Wayland primary selection, the way EDM and MEDM pass PV names around:
- In bobview, middle-click a widget to select its PV name.
- In a terminal, an editor or a web page, select the name with the mouse (double-click selects a whole name in most terminals).
Several names separated by spaces, commas, semicolons or new lines add
several PVs. Quotes and brackets around a name are dropped, as are those
around a list ((SR:X, SR:Y)). A word can be a
PV name if, after an optional ca:// or pva://, it has only the
characters EPICS record names use (letters, digits, _ - + : [ ] < > { } $,
and . before a field, as in BL01T:X.VAL) and at least one letter.
Numbers, times and dates (12:30:00, 2026-10-17T13:41:52Z),
host:port (localhost:5064, ioc.example.org:5075), a::b, Python log
lines among other words (INFO:root:Connected), URLs, command-line flags
(-a) and words ending in . or : are left out. Numbered names such as
SR:BPM:01 and DLS:01 are names, and a ca:// or pva:// name is
always taken as it is.
bobview's local and simulated PVs are names too: loc://foo,
loc://x<VLong>(7), sim://ramp, sim://sine(0, 10, 0.1) (the commas and
spaces inside the brackets do not split it). A sim:// name must be one
of bobview's simulations: sine, sawtooth, ramp, noise,
gaussianNoise, flipflop or const. No archiver has them, so they
are plotted live only. Formulas (=β¦, eq://β¦) are not taken from a
selection; add them from bobview with right-click β Trend.
- A single word is added if it can be a name, so names with no
structure at all (
TEMP1,BEAMCURRENT) work: double-click one in a terminal and middle-click. - Several words add only those with a scheme or some structure: a
:,-,_, braces, a digit or a.FIELD. So a line of prose, a shell command or code selected with the names adds just the names ($ caget -a SR:XaddsSR:X).
If nothing is left, the note says so.
One paste adds at most 20 PVs. If the selection names more (most likely a screenful selected by accident), nothing is added and the note says how many it named; select fewer.
Middle-click in the search box pastes the selection there instead, where you click, so you can edit it or search with it. Text you select in the search box becomes the primary selection, so you can middle-click it into a terminal (the box selecting all its text when you click into it doesn't count).
Ctrl+V with the pointer over the plot (and the search box not focused)
adds the PVs in the clipboard, on every platform. Where there is no primary
selection at all (macOS, Windows, a Wayland desktop without the protocol),
middle-click uses the clipboard instead. With nothing selected it adds
nothing, rather than something copied long ago.
The selection is read in the background. If the application that owns it doesn't answer within 2 seconds, the note says it could not be read.
Over ssh -X the selection goes through your X server, so selecting a name
in a local window and middle-clicking the remote archiveview works.
Add PVs from the command line
Name them after the options:
archiveview --archiver http://archiver:17668 SR-DI-DCCT-01:SIGNAL SR-DI-DCCT-01:LIFETIME
Globs are not expanded on the command line; each argument is one PV.
Remove or hide a PV
Each PV has a chip along the bottom of the plot. Click a chip to hide its
trace (the dot turns hollow) and again to show it. Hover a chip to highlight
its trace: it is drawn bolder and on top, with its min/max band, while the
others dim. That finds a trace hidden under another with the same values,
or under the bands of busy PVs. Click its β to remove it.
With no archiver
Without an archiver, there is nothing to search:
the only row is Add "β¦" as typed, so type full names and press Enter.
Navigate the plot
Everything happens on the plot itself; there are no axis dialogs. The full list is in Gestures and keys.
Choose a time range
The pill in the bar holds the presets 1h, 8h, 1d and 1w. A preset shows that span ending now, following the current time.
The β― segment opens a small popover for anything else. Its Last
field has the keyboard, with the current span selected: type a span such as
90m, 2 days, 3w or 1 hour 30 min and press Enter to show it ending
now, following the current time. The units are s, m (minutes), h,
d, w, mo (30 days) and y (365 days), or their names (min, hour,
week, month, year, ...). The chips beside the label, 2h, 12h,
3d, 4w and 1y, show those spans in one click.
For a fixed range, fill in From and To as YYYY-MM-DD HH:MM (local
time, seconds optional) and press Enter in either. Tab moves from From to To.
An empty To, or now, means now, and the range then follows the current
time.
If a field can't be read, the error appears under it and the popover stays open to try again. Esc, or a click anywhere else, closes the popover without changing the range.
When you have zoomed or panned away from a preset, no segment is lit and the
pill shows the span you are looking at, such as 3h 20m.
Pan and zoom time
- Drag the plot left or right to pan. Let go while moving and it glides to a stop.
- Scroll, or pinch on a trackpad, to zoom about the pointer.
- A horizontal trackpad swipe pans.
βandβpan by a tenth of the span;+and-zoom about the middle.
Zoom to a box
Right-drag, or shift-drag, a box over the part you want. Both the time and value axes zoom to it.
Zoom values
Each value axis scales itself to its traces' visible data, including the min/max bands. Traces with different units, or values of very different sizes, get axes of their own; see Work with value axes.
- Drag up or down on an axis (its labels), or shift- or ctrl-scroll over it, to zoom that axis alone.
- Shift- or ctrl-scroll over the data zooms every axis about the pointer, and dragging the plot pans the values of any axis that is zoomed.
- Zooming an axis turns its autoscale off. The β€’ button, or
A, turns it back on for all of them.
Follow now, or look at the past
While the plot follows the current time, the β Live pill shows a red dot
and new values appear at the right edge. Panning or zooming into the past
stops following. Click the pill, press L, or drag back to the right edge
and let go to follow again.
Read values
Hover over the plot. A vertical hairline follows the pointer, with a card showing the time and each visible trace's value there, with its units, coloured by alarm severity when it is in alarm. Where the archiver has thinned the data into bins, the card also shows the bin's min and max.
Start again
Double-click the plot, press Home, or click β: the time range returns
to the last preset and autoscale turns on.
Zoom the interface
To make the text and controls bigger or smaller (not the data: that is
zooming time or values), use the Zoom β 100% + row at the top of the
β‘ menu, or Ctrl+= (or Ctrl++), Ctrl+β and Ctrl+0, as in a
browser. They work even while you type in the search box. Clicking the
percentage, or Ctrl+0, goes back to 100%. The steps run from 50% to 300%,
and each change shows "Zoom 125%" briefly.
The zoom is remembered for the next start (ui_zoom in
settings.json). --ui-scale 1.25
or ARCHIVEVIEW_UI_SCALE=1.25 starts at 125% whatever was saved.
Undo and redo
Explore freely: zoom in to look at something, then step back. βΆ and
β· in the bar undo and redo, as do Ctrl+Z and Ctrl+Shift+Z (or
Ctrl+Y), and the mouse's back and forward buttons, which suit stepping
through a zoom history. Each button's tooltip names the step, such as "Undo
zoom" or "Redo move SR-DI-DCCT-01:LIFETIME to Time (h)". In a narrow window
they are in the β‘ menu.
What can be undone is everything that changes what the plot shows:
- the time range: zooming and panning however done, presets, a custom range, and leaving or returning to Live (undo returns to Live if that is where the step started);
- each value axis: its zoom and pan, autoscale, log scale and a fixed range;
- a reset, as one step, so an accidental double-click can be undone;
- traces: adding PVs, removing one (it comes back with its colour, its axis, its place in the legend and its history, without fetching it again), hiding or showing one, moving one to another axis, and renaming an axis;
- opening a view, Start empty, and the restored session (the view's name in the β‘ menu comes back too; the archiver does not, so switch it back in the archiver pill if the view changed it).
One gesture is one step: a drag from press to release, or a run of scrolls or key presses with no pause longer than about half a second, or an arrow key held down. Time moving on while Live is not a step. A new change after undoing clears what could be redone. About 100 steps are kept, for the session only: they are not saved with views.
Settings and files are not undone: the search text and recent searches, the theme and UI zoom, the archiver, saving or deleting a view, the window's size and position, and an axis picked out by a click.
Work with value axes
You never have to set up a value axis: each PV picks one when its data first arrives, and the axes name themselves. When the choice is not the one you want, moving a trace takes a click or two.
How a new PV picks its axis
When a PV's data arrives (its archived history, or a second of live values if it has no history), archiveview compares it with the axes already on the plot. PVs pick in the order of their chips, whatever order the archiver answers in, so a view always comes up the same way; a PV is drawn once it has picked.
- Units first. A PV never joins an axis whose traces have other units: a beam current in mA and a lifetime in h get an axis each. This holds however many axes there are.
- Then size. With the same units (or none), it joins an axis whose values are within about a factor of ten of its own, a little more when their ranges overlap. SA:X and SA:Y of a BPM, both in mm, share an axis; a current of 300 mA and one of 0.3 mA do not.
- States apart. An enum or binary PV gets an axis of its own, named after it, with ticks at whole numbers. Only enums with the same states share one ("State").
- Log scale. A pressure, or a PV whose values span several decades, gets an axis on a log scale.
- About four axes. Past four axes of values, a new PV joins the closest axis with its units even when the sizes differ; with none, it still gets its own.
The choice is made once. Zooming, panning and reloading never move a trace to another axis; only you do.
The axes alternate sides: the first on the left, the second on the right, then a third left of the first and a fourth right of the second. The first chip's axis is always the first.
Move a trace to another axis
- Click the colour dot on its chip for a menu of the axes, and New axis.
- Or drag the chip onto an axis. Drop it anywhere else on the plot to give it an axis of its own.
An axis that loses its last trace disappears.
Pick out an axis' traces
Click an axis (its labels or name) to show its traces in full and dim the others. Click it again to show them all.
Zoom one axis
With the pointer over an axis, drag up or down or shift-scroll to
zoom that axis alone. Over the data, shift-scroll zooms every axis about the
pointer. β€’ or A autoscales them all again.
Names
An axis names itself from its traces:
- the quantity from the units, such as Current (mA), Position (mm), Time (h); unknown units show as they are, (counts);
- when two axes have the same units, the part of the PV names that tells them apart: Position X (mm) and Position Y (mm);
- without units, the PV's description, when the archiver gives one;
- otherwise Axis 1, Axis 2, ...
An axis of states is named after its PV, or State when several share it.
Hover a name to read it in full, with its units and its traces. Double-click a name to type your own; Enter keeps it, Esc does not. Clear it to go back to the automatic name.
An axis with one trace shows its name in the trace's colour. One with several shows a small dot in each trace's colour by its name.
Log scale
Right-click an axis for its own menu: Log scale, Autoscale and Renameβ¦. β‘ βΈ Log scale turns the log scale on or off for every axis.
Everything here (which trace is on which axis, each axis' zoom, log scale and name) is saved with the view.
Save and load views
A view is the set of PVs on the plot (with their colours and whether they
are shown), the time range and the value-axis settings. Views are saved as
small JSON files in ~/.config/archiveview/views/, one per name.
Save a view
Open the β‘ menu at the right of the bar. Under the Views heading:
- Save NAME saves over the view you opened.
- The Save as⦠field: type a name and press Enter (or Save). Names can't be empty, start with
., or contain/or\. - Save as default saves to
default, which opens when archiveview starts with no PVs and no--view.
A view also records the archiver in use when it was saved.
Open or delete a view
β‘ βΈ Open βΈ NAME opens a saved view (the default is listed first). If the view was saved with a different archiver, archiveview switches to it; the archiver pill shows the change. β‘ βΈ Delete βΈ NAME removes one. From the command line:
archiveview --view beam
Keep the default up to date
Save default on exit (under Views in the β‘ menu) is on unless you turn
it off: archiveview saves the current plot as default whenever you quit,
so starting it with no PVs and no --view reopens it as you left it, with
the same archiver. The choice is kept in settings.json.
When the default view is restored, a notice at the bottom says "Restored last session" for a few seconds. Its Start empty button clears the plot instead (keeping the archiver). PVs given on the command line start a new plot of just those PVs rather than adding to the restored one; with Save default on exit on, that plot becomes the default when you quit.
What is restored
| saved | restored as |
|---|---|
| a relative range (e.g. the last 8 hours) | that span ending now, following live |
| an absolute range | exactly those start and end times |
| which trace is on which value axis | as saved: traces do not pick again |
| an axis' autoscale, or its min and max, log scale and name | as saved |
| a trace's colour and hidden state | as saved |
| the archiver | switched to, unless --archiver or --no-archiver is given |
A run with --no-archiver saves the default view on exit with the
archiver it had before, so one live-only run doesn't make the next start
forget it. A view saved without an archiver (or by an older archiveview)
keeps the archiver in use, so the same view works against another appliance; so
does a hand-edited view with the archiver field removed.
Views can be edited by hand or shared by copying the file; see Config files and the view format.
Run over ssh
archiveview draws with OpenGL. Two commands start it:
| command | draws with | use it |
|---|---|---|
archiveview | the machine's own OpenGL: the GPU, or whatever driver the desktop provides | normally: on a local desktop, a laptop, a Mac |
archiveview-ssh | Mesa in software, on the CPU of the machine archiveview runs on | when the OpenGL path can't work, such as over ssh -X |
Both take the same arguments. Start with archiveview, and switch to
archiveview-ssh if it fails to start or draws nothing.
When to use archiveview-ssh
- Over
ssh -Xor-Y. This is what it was named for. Forwarded OpenGL reaches your local X server as indirect GLX, which current X servers refuse, soarchiveviewexits withglutin error: [2] BadValue. - Where a GPU driver is installed but can't be used, for example a remote desktop, VNC or Xvfb session on a machine with an NVIDIA driver: glvnd picks NVIDIA's GL, which then has no GPU display to draw on.
- Where the GPU driver is broken or misbehaves, as a workaround while you sort it out.
There is no need for it on macOS or on Wayland or X11 desktops with working
graphics. --snapshot needs neither: it draws without OpenGL or a display.
What it does
archiveview-ssh is a small script that runs archiveview with:
LIBGL_ALWAYS_SOFTWARE=1, so Mesa renders on the CPU (llvmpipe);__GLX_VENDOR_LIBRARY_NAME=mesa, and__EGL_VENDOR_LIBRARY_FILENAMESpointing at Mesa's EGL vendor file, so libglvnd picks Mesa rather than a GPU vendor's library such as NVIDIA's.
Over X forwarding the finished window then travels to your X server as plain images, which every X server accepts.
Software rendering costs CPU on the machine running archiveview, and over
ssh -X each redraw sends the whole window over the network. archiveview
redraws only for your input, when new data arrives, and, while following
now, at the pace the time axis scrolls, so this is usually quick enough.
Requirement
Mesa must be installed where archiveview runs: mesa-dri-drivers on RHEL,
libgl1-mesa-dri on Debian and Ubuntu (both are in the
install prerequisites).
Example
ssh -X control-host
archiveview-ssh --archiver http://archiver:17668 SR-DI-DCCT-01:SIGNAL
Take snapshots
--snapshot renders the plot to a PNG without opening a window, then
exits. It needs no display and no GPU, so it works in CI, over ssh and in
containers. Use it for documentation, regression checks, and reports.
archiveview --archiver http://archiver:17668 --snapshot beam.png --range 7d SR-DI-DCCT-01:SIGNAL
Everything else on the command line applies as usual: the PVs, --view,
--range and the archiver.
Size, scale and theme
archiveview --snapshot out.png --size 1920x1080 ...
archiveview --snapshot out.png --scale-factor 2 ... # as on a high-DPI screen
archiveview --snapshot out.png --theme dark ...
--size is in pixels and defaults to 1280x800. Snapshots use the light
theme unless --theme dark is given, and 100% interface zoom (not the one
saved from the β‘ menu) unless --ui-scale is given, e.g. --ui-scale 1.5.
Wait for data
The snapshot is taken after --wait seconds (default 2), and not before
every archive request has finished, so history is always drawn. It gives up
waiting after 60 seconds. Raise --wait to give live PVs time to connect
and gather a few values:
archiveview --snapshot out.png --wait 5 --range 10m bl01t:SUM
What CI does
CI starts the mock archiver and
bobview-testioc, then snapshots a set of views and uploads them as the
snapshots artifact of each run:
archiveview-mockarchiver --port 17668 &
bobview-testioc &
export EPICS_CA_ADDR_LIST=127.0.0.1 EPICS_CA_AUTO_ADDR_LIST=NO
archiveview --archiver http://127.0.0.1:17668 --snapshot snapshots/mock-1h.png \
--size 1280x800 --wait 5 --range 1h MOCK:SINE MOCK:NOISE MOCK:NAN
archiveview --archiver http://127.0.0.1:17668 --snapshot snapshots/empty.png --size 1280x800
kill %1 %2
The set covers the empty state, a short and a long range (raw steps and
min/max bands), the dark theme, a live trace joining its history, a narrow
window and a plot with no archiver. See .github/workflows/ci.yml for the
full list. Look at them, rather than only checking they were written: the
plot should fill the window, bands should shade binned data, the time labels
should read well and the chips should be tidy.
Embed the plot in an egui app
The plot is a library widget, archiveview::Plot, and the archiveview
program is a thin window around it. Any egui 0.36 app can show one, and as
many as it likes; bobview is meant to use it for its trend windows.
[dependencies]
# default-features = false: just the widget, without the standalone app's
# eframe, png and clap
archiveview = { git = "https://github.com/gilesknap/archiveview", default-features = false }
pv = { package = "bobview-pv", version = "1.8" }
egui = "0.36.2"
The host must use the same pv crate as archiveview, the bobview-pv
package: Cargo treats two packages with different names as different
crates, even from the same source, so a PvManager from a path dependency
on a package called pv cannot be passed to Plot::new. bobview itself
therefore names its in-tree crate's package bobview-pv (keeping pv as
the name it is imported by) and points the published one at it:
# bobview's workspace Cargo.toml
[patch.crates-io]
bobview-pv = { path = "crates/pv" }
#![allow(unused)] fn main() { use archiveview::{ArchiverClient, Plot}; // once, in the app's constructor; share the app's own PvManager let pvs = pv::PvManager::new(); let archiver = ArchiverClient::new("http://archiver:17668").ok(); let mut plot = Plot::new(&pvs, archiver); plot.add_pvs(&["SR-DI-DCCT-01:SIGNAL".to_string()]); // each frame, e.g. inside an egui::Window: give it a size, as the plot // fills whatever space it is offered (at least 360 x 200) egui::Window::new(plot.title()) .id(egui::Id::new("trend-1")) .default_size([720.0, 420.0]) .resizable(true) .show(ctx, |ui| plot.ui(ui)); }
What the widget does, and doesn't, assume:
-
It uses the host's PV context.
Plot::newtakes the host'spv::PvManager, subscribes through it, and runs archive requests on that manager's tokio runtime. It starts no runtime or threads of its own. -
No archiver is fine. With
Noneit plots live values only, like bobview's trend window.set_archiverchanges it later. -
The archiver is the host's unless it says otherwise. The archiver pill always shows whether the archiver can be reached (
plot.archiver_status()), but by default it is a status display only, andset_viewignores a view'sarchiver, so a layout restored later never moves the plot to another archiver.plot.set_archiver_switching(true)(as the standalone app does) lets the user switch archivers in the pill and makesset_viewfollow a view's archiver. The plot then switches itself, andplot.take_archiver_change()is true once afterwards so the host can remember the choice; give it the host's remembered archivers withplot.set_recent_archivers(..). Persisting them is the host's job: the widget writes no files. -
No globals. Every plot keeps its own state and egui ids, so several can be open at once.
Plot::newsalts its ids with a running count;Plot::with_id_salt(&pvs, archiver, "trend-1")uses the host's own salt, which keeps them stable when the host rebuilds its plots. -
The host owns the look. The plot follows
ui.visuals()(light or dark) and the host's fonts; it never sets the theme or installs fonts. -
It repaints itself when archive data arrives (in the viewport it was last drawn in, so a plot in its own native window is repainted, not the root one) and, while following now, at the pace the time axis scrolls. A host whose every repaint redraws more than the plot can slow it:
plot.set_min_repaint_interval(Duration::from_millis(250))floors every repaint the plot schedules itself (the scroll pace, the Live pill's pulse, spinners, debounce and probe waits). Easing after the user's own pans and zooms, and the repaint when data arrives, stay immediate; with a floor, a live rescale of the value axes moves once per tick. The floor does not cover repaints from the host's ownPvManagernotifier on live monitor updates: those are the host's to pace. -
It reads no clipboard. Ctrl+V over the plot works on its own (egui delivers the clipboard as a paste event). For middle-click, the host reads the primary selection:
plot.take_middle_click()says where a middle-click landed, and the host answers with the selection's text,plot.paste_pvs(&text)on the plot orplot.paste_into_search(&text)in the search box.plot.take_search_selection()gives text the user selected in the search box, for the host to make the primary selection, andplot.take_pasted()says what a paste added (.message()for a toast). A middle-click stays intake_middle_click()until taken (a later one replaces it);paste_pvsandpaste_into_searchask for a repaint themselves, so the host can answer between frames, from another thread's result. Read the selection off the UI thread: X11 asks its owner, which can take seconds. The standalone app does all this inapp.rs, witharchiveview::selection(aWorkerthread, a 2 s limit, and setting the primary only once a selection has been steady for 150 ms).
To save a plot in the host's own layout files, store plot.view() (it is
serde-serialisable) and restore it with plot.set_view(&view). A view
records the archiver in use; set_view switches to it only with archiver
switching on.
plot.is_loading() is true while archive requests are in flight.
The plot keeps its own undo and redo lists (in memory, about 100 steps),
with βΆ β· buttons in its bar, Ctrl+Z and Ctrl+Shift+Z, and the mouse's
back and forward buttons. Restoring a layout with set_view, and adding
PVs with add_pvs, are steps too. A host with an undo of its own can turn
the plot's controls off with plot.set_undo_controls(false) and drive it
instead: plot.undo() and plot.redo() return whether there was a step,
and plot.undo_label() and plot.redo_label() name it ("zoom"). A host
with no use for them can stop the recording (and the memory it keeps,
such as removed traces' data) with plot.set_undo_enabled(false). Give the
name of the layout shown with plot.set_view_name(..): each step keeps it,
so undoing an opened layout gives back the name of the one before
(plot.view_name()). Undo never switches the archiver back.
The search box remembers recent searches in memory only. To keep them
across runs, as the standalone app does in history.json, give the plot
the saved list with plot.set_search_history(history) and save
plot.search_history() (a serde-serialisable SearchHistory) whenever
plot.search_history_changed() returns true. A host with several plots
can share one list by passing it between them the same way.
See the Rust API for the full interface.
Build from source
You need a stable Rust toolchain (rustup).
git clone https://github.com/gilesknap/archiveview
cd archiveview
cargo build --release
cargo test --workspace
The binaries are in target/release. To try a change against the mock
archiver:
cargo run --bin archiveview-mockarchiver &
cargo run --bin archiveview -- --archiver http://127.0.0.1:17668 MOCK:SINE MOCK:NOISE
Build for RHEL 8 and other older Linux
With zig and cargo-zigbuild installed, link against glibc 2.28 as the release builds do:
cargo zigbuild --release --workspace --target x86_64-unknown-linux-gnu.2.28
Build these docs
The docs are an mdBook:
cargo install mdbook
scripts/build-docs.sh serve --open
This builds the Rust API docs into the book under api/, then serves it;
mdbook serve rebuilds and reloads the page as you edit. Run it again to
pick up changes to the API docs. scripts/build-docs.sh alone builds the
book into docs/book.
Checks CI runs
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
CI then snapshots the app against the mock archiver and bobview-testioc;
see Take snapshots.
How the docs are published
The Docs workflow builds the book and the API docs on every pull request.
On a push to main or a v* tag it publishes them to the gh-pages branch
in a folder named after the branch or tag, writes switcher.json (the list
of versions for the menu in the top bar) and leaves the other versions in
place. The site's root redirects to main/.
Make a release
Push a tag such as v0.1.0:
git tag v0.1.0
git push origin v0.1.0
CI creates the GitHub Release itself and builds with the version taken from
the tag, so Cargo.toml needs no bump. It builds on native runners for
each platform (ubuntu-24.04, ubuntu-24.04-arm and macOS) and attaches an
archiveview-<arch>-<os>.tar.gz for each, such as
archiveview-x86_64-linux.tar.gz and archiveview-aarch64-linux.tar.gz.
The Linux builds use cargo zigbuild to link against glibc 2.28, so they
run on RHEL 8+, Ubuntu 20.04+ and Debian 10+. Each tarball carries
THIRD_PARTY_LICENSES.txt, generated by scripts/third-party-licenses.py.
install.sh downloads the latest release for the machine it runs on.
Publish to crates.io
The same tag publishes archiveview-archiver, archiveview-mockarchiver and
archiveview to crates.io at the tag's version, once the check and build
jobs pass. The publish job uses crates.io
trusted publishing, so no token
is stored in GitHub.
Each crate on crates.io has a trusted publisher: GitHub, owner gilesknap,
repository archiveview, workflow ci.yml and environment release.
If the job fails after publishing some of the crates, a re-run fails on the
version already published. Publish the rest by hand instead, from a checkout
of the tag with an API token from https://crates.io/settings/tokens, after
setting the tag's version as CI does (Cargo.toml is not bumped in the
repository):
git checkout v0.2.0
V=0.2.0 perl -0pi -e 's/^version = .*$/version = "$ENV{V}"/m;' \
-e 's/^(archiver = \{.*version = ")[^"]*/$1$ENV{V}/m' Cargo.toml
cargo login
cargo publish -p archiveview --allow-dirty # each crate not yet published
git checkout Cargo.toml Cargo.lock
A new crate added to the workspace is published by hand the same way the first time, since trusted publishing can only be set up for a crate that already exists; then add its trusted publisher as above.
Docs
The tag also publishes a copy of these docs at
https://gilesknap.github.io/archiveview/<tag>/, which the version menu in the
docs' top bar then lists beside main.
Scope, and how it differs from the Data Browser
What archiveview is for
Looking at how PVs have behaved: what the beam current did overnight, when a vacuum gauge started to climb, whether a motor's readback settled. You name some PVs, choose a time range, and read the plot. For that it needs:
- history from an EPICS Archiver Appliance, and live values from Channel Access or PV Access, as one line per PV;
- several PVs on one shared time axis;
- fast pan and zoom from a week down to seconds;
- reading exact values and times off the plot;
- saving a set of PVs and a range to come back to.
What it leaves out
The Phoebus Data Browser does a great deal more, and exposes it through property sheets, trace tables, axis configuration dialogs, archive data source lists and long context menus. archiveview has none of these. In v1 it also leaves out:
- axis dialogs: value axes are chosen automatically, and moved from a chip's dot or by dragging the chip;
- exporting data (CSV, spreadsheet);
- statistics, formulas and waveform views;
.pltfiles;- more than one archiver at a time.
Some of these may come later, but only where they can be done without bringing back the configuration panels.
How it feels instead
archiveview takes its cues from modern charting tools such as Grafana, TradingView and Apple Stocks:
- The plot is the interface. It fills the window; a slim bar holds search, the time-range pill, the live toggle and two buttons.
- Direct manipulation replaces settings. Drag to pan, scroll or pinch to zoom, drag a box, drag the value axis; double-click to go back.
- Read by hovering. A crosshair card shows every trace's value, units and severity at the pointer's time.
- Traces are chips, not table rows: click to hide, hover to highlight, β to remove.
- Sensible defaults stand in for options: autoscale over what is
visible, colours from a palette that works on light and dark backgrounds,
min/max bands wherever the archiver has thinned the data, and time labels
that adapt from
14:30toTue 17toOct.
Relationship to bobview
archiveview shares bobview's look, fonts, palette and PV library, and its plot widget is meant to replace bobview's own trend window, so a trend opened from a bobview display gains history from the archiver.
Architecture
Crates
| crate | what |
|---|---|
archiver (package archiveview-archiver) | the Archiver Appliance retrieval client: builds URLs, makes blocking HTTP requests, and decodes the JSON tolerantly. It knows nothing of egui or live PVs. |
archiveview | the Plot widget, the standalone app around it (window, menu, saved views, settings), snapshot mode and the command line |
mockarchiver (package archiveview-mockarchiver) | a synthetic Archiver Appliance for tests, CI and demos |
Live values come from bobview's pv
crate (published as bobview-pv), which speaks Channel Access and PV Access
in pure Rust and runs its own small tokio runtime.
The mock deliberately shares no code with the client, so the client's tests exercise the real wire format, including its oddities, rather than a common definition of it.
From the time range to pixels
user input βββΊ Plot (time range, value range, traces)
β range changed? ββ300 ms debounceβββΊ archive fetch per trace
β (on the pv runtime, blocking HTTP)
β ββββββββββββ result + repaint ββββββββ
β
pv::Pv ββlistenerβββΊ live ring buffer per trace (IOC timestamps)
β
βΌ
each frame: history (cut at the first live sample) + live
β thinned to pixel columns β drawn
Fetching history
When the time range changes (a preset, a pan, a zoom, or the live view scrolling on), the plot waits 300 ms for things to settle, then asks the archiver for each PV's history over the visible range plus a quarter of the span either side, so small pans show data straight away. Each trace has at most one request in flight; a newer one makes older answers stale, and they are dropped when they arrive. The old history stays on screen until the new one lands, so nothing flickers.
For scalar numeric PVs the request is wrapped in the appliance's
optimized_N operator, with N twice the plot's width in device pixels (at
most 4000). The appliance then decides:
- if the range holds no more than N samples, it returns them raw, drawn as steps (each value holds until the next sample, which is what an archived sample means);
- otherwise it returns N bins, each with the mean, min, max and count, drawn as the mean line over a translucent min..max band.
So the amount of data sent is bounded by the plot's width, not the time range: a week of a 10 Hz PV costs the same as an hour.
Strings and enums are fetched raw (an enum's index). The plot asks the
archiver's getMetadata once per PV to learn its type, and falls back to
the shape of the first response.
Array (waveform) PVs are not plotted: a line of one element over time says little about a waveform, so their chip shows the latest array instead. When the metadata says a PV is an array, no data is fetched at all; without metadata, the first response tells, and its samples are dropped.
Joining live values
Each trace subscribes to its PV through the pv crate and records every
update into a ring buffer, stamped with the IOC's timestamp, the same
clock the archiver stored, so the two line up. History is cut at the first
live sample, so at the right edge the live values always win, and since both
are drawn as steps the join is seamless. A disconnect records a gap.
While the plot follows now, the history behind the live values is topped up: raw history every 30 seconds or so, by fetching only what is new since its last sample; binned history by fetching the (bounded) window again, but only once time has moved on by a few bins, so a week-long view asks the archiver for its week of bins every half hour, not every 30 seconds. Each trace has at most one request to the archiver at a time.
Drawing
Each frame, the visible part of every trace is thinned to at most four
points per pixel column (first, min, max, last), which keeps every peak
while drawing a bounded number of segments. Gaps (NaN values, and the
archiver's disconnection markers) break the line. Autoscale covers the
visible values including the bands, and the value axes animate to their new
ranges. A trace picks a value axis once, when its first data arrives: by
units, then by the size of its values, so a current in mA and a lifetime in
hours are both readable while the two planes of a BPM share one axis. The
rules are pure functions over a trace's value statistics (plot/axes.rs),
tested as a table.
The widget and the app
archiveview::Plot holds everything about one plot and nothing global, so
several can share one window or one program, and it borrows the host's PV
manager and runtime. ArchiveApp wraps one plot with the window chrome:
fonts, theme, the β‘ menu, and loading and saving views and settings in
~/.config/archiveview. main.rs adds the command line and either opens a
window or renders a snapshot.
Snapshots
--snapshot runs the same app in a bare egui context with no window, waits
for the archive requests to finish, then draws egui's triangles with a small
software rasteriser into a PNG. It is bobview's snapshot mode.
Command line
archiveview [OPTIONS] [PV ...]
archiveview-mockarchiver [--port PORT] [--bind ADDR]
archiveview --help prints the same list.
archiveview
| argument | meaning |
|---|---|
PV ... | PVs to plot. A plain name is Channel Access; pva://name is PV Access. With PVs given, archiveview starts a new view of them rather than opening the default view. |
-a, --archiver URL | the Archiver Appliance retrieval URL, e.g. http://host:17668. Phoebus's pbraw://host:17668/retrieval works too, as plain HTTP. /retrieval is added unless the URL ends with it. Overrides the opened view's archiver and archiver_url in settings.json, and is remembered there for later runs (not by --snapshot). With no archiver at all the plot is live only. |
--no-archiver | use no archiver, even if the view or settings.json names one: live values only. |
--view NAME | open the saved view NAME from ~/.config/archiveview/views/. Giving PVs as well is a usage error. |
-r, --range SPAN | the time span to show, ending now: 1h, 8h, 7d, 90m, 2 days, 1 hour 30 min, 3w, 6mo, 1y; units s, m (minutes), h, d, w, mo (30 days), y (365 days). Default 1h. |
--theme system|light|dark | the colour theme. Default: theme in settings.json, else system (follow the OS). |
--ui-scale F | start with the whole interface zoomed by F, e.g. 1.25 for 125%. Default: ARCHIVEVIEW_UI_SCALE, else the zoom last set in the β‘ menu (ui_zoom in settings.json), else 1. Snapshots start at 1 without it. See Zoom the interface. |
--version, --help |
Snapshot mode
| argument | meaning |
|---|---|
--snapshot OUT.png | render to a PNG without opening a window, then exit |
--size WxH | image size in pixels. Default 1280x800. |
--scale-factor F | pixels per point, as on a high-DPI screen. Default 1. |
--wait SECONDS | render at least this long before writing, and until archive requests finish (at most 60 s). Default 2. |
--hover X,Y | rest the pointer at this pixel, to show the crosshair readout |
--search TEXT | type TEXT into the search box, to show its matches |
Snapshots use the light theme unless --theme dark is given. See
Take snapshots.
Startup
- PVs on the command line start a new view of those PVs, over
--range. - Otherwise
--view NAMEopens that view. - Otherwise the saved
defaultview opens, if there is one. - Otherwise the plot starts empty, ready for a PV name.
Exit status
0 on success, 2 for a usage error (an unknown option or a bad value),
1 for any other failure, such as a snapshot that can't be written. A
--view that can't be read, or a bad archiver URL, is a usage error. Messages are printed as archiveview: message.
archiveview-mockarchiver
| argument | meaning |
|---|---|
--port PORT | the port to serve on. Default 17668. |
--bind ADDR | the address to listen on. Default 127.0.0.1; use 0.0.0.0 to serve other machines. |
It runs until killed. See Mock archiver.
Gestures and keys
The bar
| control | what it does |
|---|---|
| search box | find archived PVs as you type; Enter adds, keeping the search to add more. Empty, it lists your recent searches. Middle-click pastes the primary selection. See Find and add PVs |
.* (in the search box, shown while it has the focus) | search with a regular expression instead of a glob |
| β (in the search box) | clear it |
| 1h 8h 1d 1w | show that span ending now, following live |
| β― | a custom span (90m, 2 days, 1y, or a 2h 12h 3d 4w 1y chip) or a start and end date-time; Enter in a field shows it, Esc or a click away closes it unchanged. See Choose a time range |
| β Live | red dot while following now (it pulses for a few seconds, and while the pointer is over the plot); click to pause, or to jump to now and follow |
| β host | the archiver: green when it answers, amber when it fails, a spinner while checking, hollow grey for "No archiver". Hover for its URL, version and last error; click to switch archivers. See Connect |
| βΆ β· | undo and redo the last change to what the plot shows; the tooltip names it ("Undo zoom"). Greyed out with nothing to undo or redo. See Undo and redo |
| β€’ | autoscale every value axis |
| β | reset: back to the last preset, autoscale on |
| β‘ | Zoom β 100% + (the whole interface; click the percentage for 100%); Log scale (every value axis), Clear search history; then Views (Save NAME, the Save asβ¦ field, Save as default, Open βΈ, Delete βΈ, Save default on exit); then Theme; then a summary of the gestures |
In a narrow window the presets become a drop-down, the archiver pill shows only its dot, and βΆ β·, β€’ and β move into the β‘ menu.
On the plot
| gesture | effect |
|---|---|
| drag | pan time, with a glide after a quick release; stops following live |
| scroll wheel, trackpad pinch | zoom time about the pointer |
| horizontal trackpad swipe | pan time |
| shift- or ctrl-scroll | over an axis: zoom that axis; over the data: zoom every axis about the pointer; turns autoscale off |
| drag up or down on a value axis | zoom that axis; turns its autoscale off |
| right-drag, or shift-drag | zoom to the box, time and values |
| double-click | reset |
| click a value axis | pick out its traces, dimming the others; again to undo |
| hover a value axis' name | its full name, units and traces (with their colours), and the gestures |
| double-click a value axis' name | rename it (Enter keeps, Esc cancels, empty is automatic) |
| right-click a value axis | its menu: Log scale, Autoscale, Rename⦠|
| middle-click (here or on the legend) | add the PVs in the primary selection (the text last selected anywhere), or in the clipboard where there is no primary selection (macOS, Windows). At most 20 at once. See Add PVs from another window |
Ctrl+V (pointer over the plot) | add the PVs in the clipboard |
| mouse back and forward buttons | undo and redo, as in a browser |
| hover | crosshair: a hairline (snapping to a nearby sample), a time tag on the axis, and a card with every visible trace's value as its line shows it there ("β" in a gap) |
Legend chips
| gesture | effect |
|---|---|
| click | hide or show the trace |
| hover | highlight the trace (bolder, drawn on top, with its band) and dim the others |
| click β | remove the PV |
| click the colour dot | choose the trace's value axis, or New axis |
| drag onto a value axis | move the trace to that axis; dropped elsewhere on the plot, to a new axis of its own |
A chip shows the latest value and units, a spinner while history loads, a
grey "β" while disconnected, and a "live only" mark when the archiver has no
history of the PV. A long value is cut short with "β¦", and an array shows
its length and first elements, e.g. [936] 0, 0, 0, β¦. Arrays are not
plotted, and their chip says "not plotted". A ! in place of the β means
the last archive request failed. Hover a chip for its PV name, its live
and archive status, and these gestures.
Keys
These work whenever no text box has the focus.
| key | effect |
|---|---|
/ | focus the search box |
Esc | close the search list or popover; again, clear the search box |
β β | pan by a tenth of the span |
+ (or =) - | zoom time in and out about the middle |
L | follow now, or stop following |
A | autoscale |
Home | reset |
Ctrl+Z (βZ on a Mac) | undo |
Ctrl+Shift+Z or Ctrl+Y | redo |
These work even while a text box has the focus:
| key | effect |
|---|---|
Ctrl+= or Ctrl++ | zoom the interface in (to 300%) |
Ctrl+β | zoom the interface out (to 50%) |
Ctrl+0 | interface zoom back to 100% |
In the search list, β β move and Enter adds (or, on a recent search,
searches for it again).
Environment variables
| variable | used by | meaning |
|---|---|---|
ARCHIVEVIEW_LOG | archiveview | log level on stderr: error, warn (default), info, debug or trace. Below debug, the egui, eframe, winit and EPICS libraries log warnings only. |
ARCHIVEVIEW_UI_SCALE | archiveview | as --ui-scale: the interface zoom to start at, over the saved one |
XDG_CONFIG_HOME | archiveview | the config folder is $XDG_CONFIG_HOME/archiveview when set, else ~/.config/archiveview |
EPICS_CA_ADDR_LIST, EPICS_CA_AUTO_ADDR_LIST, EPICS_CA_SERVER_PORT, ... | live PVs | the standard Channel Access client settings |
EPICS_PVA_ADDR_LIST, EPICS_PVA_AUTO_ADDR_LIST, ... | live PVs | the standard PV Access client settings, for pva:// PVs |
LIBGL_ALWAYS_SOFTWARE and friends | archiveview-ssh | set by the script; see Run over ssh |
The archiver URL has no environment variable; use --archiver (which is
remembered in settings.json) or the archiver pill.
Config files and the view format
archiveview keeps its files in $XDG_CONFIG_HOME/archiveview when
XDG_CONFIG_HOME is set, otherwise ~/.config/archiveview:
~/.config/archiveview/
βββ settings.json
βββ history.json
βββ views/
βββ default.json
βββ beam.json
All are pretty-printed JSON, written atomically (to a hidden temporary file,
then renamed), so a crash never leaves half a file. Missing fields take
their defaults, so files can be edited by hand; fields archiveview doesn't
know are kept when it saves settings.json. A settings.json that is not
valid JSON gives the defaults for that run, with a warning, and is not
saved over until it is fixed.
settings.json
{
"archiver_url": "http://archiver.example.org:17668/retrieval",
"recent_archivers": [
"http://archiver.example.org:17668/retrieval",
"http://127.0.0.1:17668/retrieval"
],
"theme": "system",
"save_default_on_exit": true,
"ui_zoom": 1.25
}
| field | default | meaning |
|---|---|---|
archiver_url | none | the archiver used when neither --archiver nor the view opened at start-up names one. Written by --archiver, and by switching to another archiver in the pill or by opening a view (not by No archiver). |
recent_archivers | none | archivers used recently, newest first (at most 8), offered by the archiver pill |
theme | "system" | "system" (follow the OS), "light" or "dark"; set from the β‘ menu |
ui_zoom | 1 | the interface zoom set in the β‘ menu or with Ctrl+=/Ctrl+β/Ctrl+0, from 0.5 to 3; left out at 1. --ui-scale and ARCHIVEVIEW_UI_SCALE win over it. For now there is one for the app; once archiveview has windows and layouts, each window keeps its own in its layout. |
save_default_on_exit | true | save the plot as the default view on quit, so the next start (with no PVs or --view) reopens where you left off |
A missing or unreadable settings.json gives the defaults (an unreadable
one logs a warning).
history.json
The search box's recent searches, newest first, saved each time they change:
{
"searches": [
{ "text": "BL16I-EA-*" },
{ "text": "SR01C-DI-EBPM-01:SA:.*", "regex": true }
]
}
| field | meaning |
|---|---|
searches[].text | the text as typed |
searches[].regex | true when the .* toggle was on; default false |
At most 50 are kept. A missing or unreadable file means no recent searches (an unreadable one logs a warning, and is replaced the next time a search is remembered). Delete it, or use Clear search history in the β‘ menu, to forget them.
views/NAME.json
{
"version": 1,
"pvs": [
{ "name": "SR-DI-DCCT-01:SIGNAL", "axis": 0 },
{ "name": "SR-DI-DCCT-01:LIFETIME", "axis": 1 },
{ "name": "pva://bl01t:SUM", "color": "#d62728", "visible": false, "axis": 1 }
],
"range": { "kind": "relative", "secs": 28800 },
"y": { "autoscale": true },
"axes": [ { "autoscale": false, "min": 1, "max": 100, "log": true, "name": "Lifetime" } ],
"archiver": "http://archiver.example.org:17668/retrieval"
}
| field | meaning |
|---|---|
version | the format version, 1. A file with a newer version than the program knows is refused. |
pvs | the traces, in chip order |
pvs[].name | the PV as typed: NAME, ca://NAME, pva://NAME, sim://sine, ... |
pvs[].color | #rrggbb; when absent, the palette colour for its position |
pvs[].visible | false for a hidden trace; default true |
pvs[].axis | its value axis: 0 for y, 1 for the first of axes, ... When absent (as in files from before there were several axes), the trace picks an axis when its data arrives |
range | {"kind": "relative", "secs": S}: the last S seconds, following now; or {"kind": "absolute", "start": "...", "end": "..."} with RFC 3339 UTC times such as "2026-10-17T09:00:00Z" |
y | the first value axis |
axes | the value axes after the first, in order; omitted when there is one |
y.autoscale, axes[].autoscale | default true |
y.min, y.max | the value range when autoscale is false |
archiver | the retrieval URL in use when the view was saved; opening the view switches to it. Optional: older files lack it, and the format version is still 1. |
y.log | true for a log value axis; default false |
y.name | the name typed for the axis; when absent it names itself |
The value-axis fields were added without changing version: older files
load (their traces pick axes as they are added; y stays with the first
visible PV, and y.log applies to every axis), and an older archiveview
reads newer files with every trace on its unit's axis.
View names can't be empty, start with ., or contain / or \. The view
called default opens at startup when no PVs and no --view are given.
A host app that embeds the plot gets the same structure from
Plot::view() and can store it in its own files; see
Embed the plot.
The archiver URL and API
archiveview talks to the EPICS Archiver Appliance over its HTTP retrieval API. It needs only read access to retrieval; nothing on the management interface.
The URL
Give the retrieval base, as for any retrieval client:
--archiver | becomes |
|---|---|
http://host:17668 | http://host:17668/retrieval |
http://host:17668/ | http://host:17668/retrieval |
http://host:17668/retrieval | unchanged |
https://archiver.example.org/retrieval | unchanged (behind a proxy) |
An appliance installed as separate Tomcats serves retrieval on port 17668 (17665 is management). The single-container quick-start appliance serves everything on 17665.
Requests
With R the retrieval base:
| purpose | request |
|---|---|
| history | R/data/getData.json?pv=OP(PV)&from=T&to=T |
| search | R/bpl/getMatchingPVs?pv=GLOB&limit=50, or regex=RE |
| PV type | R/bpl/getMetadata?pv=PV, once per PV |
| is it there? | R/bpl/getVersion, at start-up and after a change of archiver, then every minute while it fails |
Times are sent in UTC as 2026-10-17T09:00:00.000Z. OP is
optimized_N for scalar numeric PVs, with N twice the plot's width in
device pixels, at most 4000; other PVs are fetched raw. See
Architecture.
What the client copes with
The appliance's JSON has some quirks, all handled:
NaN,Infinityand-Infinityappear as bare tokens, which is not valid JSON; they are read as gaps.- A range with no data returns
[ ], with no metadata. - Metadata values such as
PRECmay be strings ("3") or numbers. - The first sample may be earlier than
from: it is the value in force atfrom, and is drawn held from the left edge. optimizedbins carry[mean, std, min, max, count]as the value, timestamped at the bin's centre.- Severity 3 with a
cnxlostepsecsfield marks a lost connection, drawn as a gap.
Errors
| response | shown as |
|---|---|
404 from getData.json | the PV is not archived: it plots live only, and its chip says "live only" |
| any other non-2xx | the status and the first line of the reply, on the chip and in the status line |
| no connection | the transport error, likewise; live values carry on |
The archiver pill turns amber for errors that are the archiver's, once a
getVersion request confirms them: no connection, HTTP 502, 503 or 504,
or a URL that is not an archiver (404 for data and search alike). A 404
for one PV, a 400 for an odd name, or a 500 for a bad search pattern leave
it as it was. A server that answers getVersion with another HTTP error
(a proxy refusing it with 401 or 403, say) counts as reachable when it
answers a search.
Mock archiver
archiveview-mockarchiver is a small stand-in for an EPICS Archiver
Appliance. It serves synthetic, deterministic history for a fixed set of PVs
over the same HTTP API and JSON as the real appliance, quirks included. It
is used by the tests, by CI's snapshots, and by the
quick start.
archiveview-mockarchiver # http://127.0.0.1:17668
archiveview-mockarchiver --port 18000 --bind 0.0.0.0
Point archiveview at it with --archiver http://127.0.0.1:17668.
PVs
History starts 30 days before the mock starts; there is no data before that. Values are computed from the time, so the same request always gets the same answer.
| PV | signal |
|---|---|
MOCK:SINE | 1 Hz, a sine with a 10-minute period, units mA, precision 3 |
MOCK:RAMP | 0.1 Hz sawtooth, 0 to 100 |
MOCK:NOISE | 10 Hz noise |
MOCK:SLOW | one sample every 5 minutes, so optimized requests come back raw |
MOCK:ENUM | enum index, 0 to 3 |
MOCK:STRING | text |
MOCK:WAVE | an 8-element waveform |
MOCK:NAN | a sine with NaN every 50th sample and periodic disconnections |
bl01t:A, bl01t:B, bl01t:SUM, BL01T-MO-SIMC-01:M1.RBV, BL01T-MO-SIMC-01:M2.RBV | the names bobview-testioc serves live, so history and live values join up |
Endpoints
Every endpoint is on the one port.
| endpoint | notes |
|---|---|
/retrieval/data/getData.json | pv, from, to. Operators: raw (with the last sample before from), optimized_N, mean_N, nth_N, ncount, and the β¦Sample_N variants that leave empty bins out |
/retrieval/bpl/getMatchingPVs | pv (glob) or regex, limit |
/retrieval/bpl/getMetadata | pv |
/retrieval/bpl/getVersion | none; {"version": "Archiver Appliance Version mock-β¦"} |
/mgmt/bpl/getAllPVs | pv (comma-separated globs), regex, limit |
As the real appliance does, it answers 404 with an HTML page for an unknown
PV, 400 for an unparseable time or to before from, writes NaN as a bare
token, and marks disconnections with severity 3 and cnxlostepsecs.
In Rust tests
The mockarchiver library starts one on a free port:
#![allow(unused)] fn main() { let mock = mockarchiver::MockArchiver::start_local()?; let client = archiver::ArchiverClient::new(&mock.url())?; }
Rust API
The workspace's crates are documented with rustdoc, built into this book
under api/.
archiveview: thePlotwidget to embed in an egui app (see Embed the plot), the standaloneArchiveApp, views, config and snapshot modearchiver: the Archiver Appliance retrieval client: URLs, requests and tolerant JSON decodingmockarchiver: the mock appliance, for tests
The crates are not published to crates.io and their API is not yet stable.
In a local build the links work when the book is built with
scripts/build-docs.sh (see Build from source),
not with a plain mdbook serve.
Licences
archiveview is Apache-2.0.
The release binaries include third-party crates, among them bobview's pv
crate and the epics-rs CA and PVA
libraries (EPICS-style open licence), and embed the Liberation Sans fonts
and a symbol subset of Noto Sans Math (both SIL Open Font Licence) and
egui's default fonts. Their notices are in THIRD_PARTY_LICENSES.txt in
each release tarball, generated by scripts/third-party-licenses.py.