bobview

A lean viewer for Phoebus Display Builder .bob screens, written in Rust with egui. It also opens the older CS-Studio BOY .opi screens. It shows live EPICS Channel Access and PV Access values, including areaDetector images, and is small enough to run on a phone, in the Android 16 Linux Terminal (Debian 13, aarch64). The same app runs in a browser, served by bobview-server.

Trends plot PVs' archived history from an EPICS Archiver Appliance as well as their live values. Scripts (Jython/JavaScript), display editing and alarm services are out of scope; see Scope and fidelity to Phoebus.

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 bobview, starts a simulated beamline on your own machine and walks you around its displays. It takes about ten minutes and needs no EPICS installation and no IOCs: bobview-testioc plays the part of the epics-containers example-services BL01T beamline.

You need an x86_64 or aarch64 Linux machine (an Android phone's Linux Terminal works), or a Mac. To build it with Cargo instead, see Install from crates.io.

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 bobview; macOS needs nothing). Then install bobview into ~/.local:

curl -sSfL https://raw.githubusercontent.com/gilesknap/bobview/main/install.sh | bash

Check that it is on your PATH:

bobview --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 simulated beamline

bobview-testioc &

This serves, over Channel Access and PV Access, a calculator IOC (bl01t:A, bl01t:B, bl01t:SUM), two motors and a simulated area detector that streams an image.

3. Open the displays

Tell the Channel Access client to look for PVs on this machine, and start the viewer:

EPICS_CA_ADDR_LIST=127.0.0.1 bobview

With no display named, bobview opens index.bob from the test screens it installed. You should see live values within a second or two.

4. Look around

  • Hover over a widget: a tooltip shows its PV and value.
  • Right-click a widget (long-press on a touch screen): copy the PV name or value, open the PV's details, or Trend it. A trend window plots the PV against time; leave it open. Given an archiver (--archiver URL), it shows the PV's history too: see Trend PVs.
  • Middle-click a widget to copy its PV name. On Linux a middle-click in a terminal pastes it; elsewhere, paste it as usual (Ctrl+V or ⌘V).
  • Click a button that opens another display. Use Back and Forward in the toolbar to move between them, and Home to return to index.bob.
  • Click the zoom in the toolbar to see the display at 100%, then Fit to fit it to the window again. On a touch screen, pinch to zoom.
  • Open the navigator from the toolbar to see a tree of every display reachable from the home display.

5. Look at the camera

From the home display, open the camera display. The image comes over PV Access from BL01T-DI-CAM-01:PVA:OUTPUT: drifting fringes with three spots moving across them. Use the image toolbar to zoom, pan and place a crosshair that reads out the pixel under it.

6. Change a value

From the home display, open the ea-test display. Type a new number into the text entry for bl01t:A and press Enter, or drag its slider. bl01t:SUM follows, as the calculator record adds A and B.

7. Finish

Close the window to quit bobview, then stop the IOC:

kill %1

Next steps

Serve displays to a browser

bobview also builds for the browser (WebAssembly, drawn with WebGL). bobview-server serves the web build, your .bob files and a PV Web Socket (PVWS) from one port. In this tutorial you serve the simulated beamline to a browser on your own machine.

Do the Quick start first, so that bobview and bobview-testioc are installed.

1. Start the IOC and the server

bobview-testioc &
EPICS_CA_ADDR_LIST=127.0.0.1 bobview-server --opi ~/.local/share/bobview/displays &

The server reaches PVs with bobview's own Channel Access and PV Access client, so it needs the same EPICS_* variables as the viewer. It listens on 127.0.0.1:8080, so only this machine can reach it.

2. Open the page

Browse to http://localhost:8080/?display=index.bob.

The same displays appear as in the desktop viewer, drawn in the page. Click through them as before. In a browser displays have no windows of their own: every link navigates in place.

3. Try page parameters

Add macros and a fixed zoom to the address:

http://localhost:8080/?display=demo.bob&macros=P=BL01T&zoom=1.5

All the parameters are listed in Browser page parameters.

4. Allow writes

Writes are off unless the server allows them, since they go out as the server's user. Stop the server and start it again with --write:

kill %2
EPICS_CA_ADDR_LIST=127.0.0.1 bobview-server --opi ~/.local/share/bobview/displays --write

It prints a random write token to the terminal. Open the page with it:

http://localhost:8080/?display=index.bob&token=TOKEN

The page removes the token from its address once it has read it, so a reload needs the token again.

Next steps

To let other machines use the server, read Deploy bobview-server for others before binding it to a public address.

Install from crates.io

This tutorial builds bobview from crates.io with Cargo, instead of downloading a release, and opens the test displays against the simulated beamline. Use it on a machine the release builds don't cover, or to build with your own Rust toolchain. It takes about fifteen minutes, most of it compiling.

1. Install Rust

Install Rust with rustup if you don't have it:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

and open a new terminal, so that ~/.cargo/bin is on your PATH. The build also needs a C compiler: sudo apt install build-essential on Debian and Ubuntu, sudo dnf install gcc on RHEL and Fedora, or xcode-select --install on macOS.

2. Install the graphics libraries

bobview needs these at run time, as the release does. On Debian or 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

macOS needs nothing.

3. Install bobview and the test IOC

cargo install bobview bobview-testioc

This builds and installs into ~/.cargo/bin:

commandwhat
bobviewthe viewer
bobview-probeprints live PV values from the command line
bobview-testioca pure-Rust IOC serving the example-services BL01T PVs

Check it works:

bobview --version

4. Start the simulated beamline

bobview-testioc &

5. Open the test displays

Cargo installs only the programs, not the test displays that a release installs. bobview opens displays over https, so open them from GitHub:

EPICS_CA_ADDR_LIST=127.0.0.1 bobview https://raw.githubusercontent.com/gilesknap/bobview/main/tests/displays/index.bob

Their links to other displays and images are fetched from the same place. You should see live values within a second or two. From here, carry on from step 4 of the Quick start.

Upgrade or remove

Run cargo install bobview bobview-testioc again to upgrade to the latest version, and cargo uninstall bobview bobview-testioc to remove them.

What Cargo does not install

Install bobview

bobview runs on x86_64 and aarch64 Linux (including the Android 16 Linux Terminal, Debian 13), on macOS 11 or later (Apple silicon or Intel), and on x86_64 Windows (see below). install.sh picks the release build to match the machine. To build it from crates.io with Cargo instead, see Install from crates.io.

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/bobview/main/install.sh | bash

This installs into ~/.local:

filewhat
bin/bobviewthe viewer
bin/bobview-sshruns the viewer with software OpenGL, for ssh -X and machines whose GPU can't be used; see Choose bobview or bobview-ssh
bin/bobview-testioca pure-Rust IOC serving the example-services BL01T PVs
bin/bobview-probeprints live PV values from the command line
bin/bobview-serverserves bobview to browsers, see Deploy bobview-server
share/bobview/displays/test screens; index.bob opens by default
share/bobview/web/the browser build that bobview-server serves

The Linux binaries are linked against glibc 2.28, so they run on RHEL 8+, Ubuntu 20.04+ and Debian 10+.

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; it replaces the binaries and the displays and web folders. To remove bobview, delete the bin/bobview* files and share/bobview from ~/.local.

Windows

The Windows build is new, and has not yet been tried against real IOCs.

Download bobview-x86_64-windows.zip from the latest release, unzip it anywhere, and run bin\bobview.exe. Nothing else needs installing. The zip holds the same files as the table above, as .exe, without bobview-ssh and install.sh; add its bin folder to PATH to run the commands from a terminal. To try it against the simulated beamline, in PowerShell:

Start-Process bin\bobview-testioc.exe
$env:EPICS_CA_ADDR_LIST = "127.0.0.1"
bin\bobview.exe

Windows may ask to let bobview-testioc and bobview through the firewall: Channel Access and PV Access search for PVs over UDP.

bobview draws with OpenGL, which a GPU driver provides. A virtual machine or remote desktop session without one has only Windows' built-in OpenGL 1.1, which is too old, and bobview fails to start. There, put Mesa's software opengl32.dll (llvmpipe, for example from mesa-dist-win) beside bobview.exe, and it draws on the CPU instead.

Layouts are saved in %APPDATA%\bobview\layouts (in %XDG_CONFIG_HOME%\bobview\layouts if that is set). DLS launcher scripts, which are shell scripts, are not run on Windows, so their command actions do nothing (see How BOY screens are translated). To remove bobview, delete the unzipped folder and %APPDATA%\bobview.

Connect to real IOCs

bobview uses the standard EPICS client variables (see Environment variables).

Search by UDP

Name the hosts (or broadcast addresses) to search:

EPICS_CA_ADDR_LIST="ioc1.example.com 10.0.0.255" bobview screen.bob
EPICS_PVA_ADDR_LIST=10.0.0.255 bobview screen.bob

Go through a gateway over TCP

When UDP search can't reach the IOCs, for example from a phone's Linux Terminal VM behind NAT, name a gateway reached over TCP. This works through NAT since the phone opens the connection:

EPICS_CA_NAME_SERVERS=gateway.example.com:9064 bobview screen.bob
EPICS_PVA_NAME_SERVERS=gateway.example.com:5075 bobview screen.bob

The example-services ca-gateway listens on port 9064 of its host.

Choose the protocol

PV names prefixed pva:// use PV Access; those with no prefix, or ca://, use Channel Access. The other prefixes are in Macros, rules and PV types.

Check a PV without the viewer

bobview-probe prints live values from the command line, and can write:

bobview-probe bl01t:SUM pva://bl01t:A
bobview-probe --seconds 10 --put bl01t:A=3.5 bl01t:SUM

Watch only, never write

Start the viewer with --read-only to refuse every write (see Writes and safety):

bobview --read-only screen.bob

For more output while diagnosing a connection, set BOBVIEW_LOG=debug.

Open displays over http(s)

A display can be a file or an http:// / https:// URL, on the command line or typed into Open…:

bobview http://host:8080/opi/demo.bob

Files that a display names (buttons, embedded displays, navigation tabs) are found relative to it, so a directory of screens served by any web server works as it does from disk. For example, to serve a folder of screens:

cd /path/to/screens && python3 -m http.server 8000
bobview http://localhost:8000/index.bob

Fetched screens are kept in memory for Back and Forward; Reload fetches them again.

A display loaded over http(s) writes PVs as a local file does, but no command is ever run for it. See Writes and safety.

Choose bobview or bobview-ssh

bobview draws with OpenGL. Two commands start it:

commanddraws withuse it
bobviewthe machine's own OpenGL: the GPU, or whatever driver the desktop providesnormally: on a local desktop, a laptop, a Mac, the phone
bobview-sshMesa in software, on the CPU of the machine bobview runs onwhen the OpenGL path can't work, such as over ssh -X

Both take the same arguments. Start with bobview, and switch to bobview-ssh if it fails to start or draws nothing.

When to use bobview-ssh

  • Over ssh -X or -Y. This is what it was named for. Forwarded OpenGL reaches your local X server as indirect GLX, which current X servers refuse, so bobview exits with glutin 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, on Wayland or X11 desktops with working graphics, in the Android Linux Terminal, or for --snapshot, unless one of the cases above applies.

What it does

bobview-ssh is a small script that runs bobview with:

  • LIBGL_ALWAYS_SOFTWARE=1, so Mesa renders on the CPU (llvmpipe);
  • __GLX_VENDOR_LIBRARY_NAME=mesa, and __EGL_VENDOR_LIBRARY_FILENAMES pointing 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 bobview, but bobview's displays are simple to draw, so it is usually quick enough. Over ssh -X each redraw sends the whole window over the network, so bobview redraws a window only for its own input and when a PV it shows changes, at most 10 times a second (see Pinned windows and the viewer).

Requirement

Mesa must be installed where bobview runs: mesa-dri-drivers on RHEL, libgl1-mesa-dri on Debian and Ubuntu (both are in the install prerequisites).

Example

ssh -X control-host
bobview-ssh /path/to/screen.bob -m P=BL01T

Layouts restore window positions over X forwarding; see Save and restore a layout.

Open BOY screens from an Eclipse project or DLS module

Any .opi file opens like a .bob:

bobview screen.opi

BOY screens often name each other through the CS-Studio workspace, though: an Eclipse project whose .project links folder names (mks937a, dlsPLC …) to the directories that hold each module's screens, so ../mks937a/gauge.opi means the mks937a link. This page shows how to open such screens so that their links work. How BOY screens are translated explains the rules behind it.

Through an Eclipse project

Open the screen through its project path:

bobview /workspaces/VacOPIS/vacuum_4-100/vacuum/diamondVacuumOverview.opi

for a project in /workspaces/VacOPIS/vacuum_4-100. bobview follows the project's links (the linked folders need not exist on disk; Open… lists them).

Or give the screen's real path and name the project with --project:

bobview -p /workspaces/VacOPIS/vacuum_4-100 /dls_sw/…/vacuumApp/opi/opi/diamondVacuumOverview.opi

A DLS module

DLS modules need neither. There is no .project on disk, but the generated launcher runcss.sh beside a module's screens (vacuumApp/opi/opi) lists the links it gives CS-Studio. bobview reads that list (it does not run the script), so just open the screen by its real path:

bobview /dls_sw/…/vacuum/4-100/vacuumApp/opi/opi/diamondVacuumOverview.opi

This works for a BOY screen reached by its real path from anywhere (the file chooser, a layout, an absolute path in another screen), with the launcher in its folder or up to two above.

To open another module's BOY screen through this module's links, give -p the module's screens folder (or its runcss.sh).

A screen that needs launcher macros

DLS screens are often opened by a launcher script, such as ./srCellVacuum 04, that supplies macros. For an .opi given on the command line in an Eclipse project (through its project path, with --project, or by its real path beside a DLS runcss.sh), bobview tries the launcher scripts in its directory to find them: the one named <screen>.sh first, with no arguments and with each number in the file name, bare and with an r. So:

screenmacros from
linacVacuum.opilinacVacuum.sh (dom=LI)
srCell05Vacuum.opisrCellVacuum 05
SR05C_rack.opisrCellVacuum 05r

Macros given with -m override these. Screens with no number in their name (srInjCellVacuum.opi is cell 01) need -m with the macros their launcher would give, or opening from the overview. Launcher scripts are not run on Windows, so there every such screen needs -m.

Save and restore a layout

A layout records the open windows so that a control-room arrangement can be put back with one command. Pinned windows and the viewer explains how windows are opened in the first place.

Save

Arrange the windows, then use the main toolbar's ☰ menu:

  • Save layout as… names a new layout (asking before replacing one).
  • Save layout saves to the layout last loaded or saved.
  • Save as default makes it the layout bobview starts with.
  • Save default on exit (off unless ticked) saves the default each time you quit.
  • Delete layout removes one.

Restore

Pick a layout from the toolbar's layout list; this closes the display and trend windows open now. From the command line:

bobview                     # restores the default layout if there is one
bobview DISPLAY             # opens DISPLAY; the default layout is not restored
bobview --layout rf-shift   # restores layouts/rf-shift.json
bobview --layout ~/shift.json   # restores the layout in that file
bobview --no-layout         # ignores the default layout

A --layout that has a directory in it or ends in .json is a layout file's path, so a layout kept elsewhere (under version control, say) can be restored without copying it into layouts. The toolbar shows it by its file name, and Save layout writes back to that file.

--snapshot ignores layouts.

What a layout holds

For each window: its display, Eclipse project and effective macros, its position and size, whether it is pinned, and whether a display window shows its toolbar; for the main window also its zoom and whether it shows the navigator. For each trend window: its PVs (with their colours, value axes and whether they are hidden), its time range (the last so long, live, or a fixed window), its value axes' ranges, log scales and names, its position and size. The archiver is not saved in a layout: trends use bobview's (see Trend PVs). A layout from before trends showed archived history restores each trend's PVs, live over its time span, with its log scale. A layout from before windows were pinned pins them all if it was saved in multi-window mode, and none if not.

Layouts are JSON files in $XDG_CONFIG_HOME/bobview/layouts (by default ~/.config/bobview/layouts, and on Windows %APPDATA%\bobview\layouts), so they can be copied between machines or kept under version control.

Positions on Wayland and with changed monitors

A saved position that is on none of the monitors now (a laptop that was docked) is dropped and the window manager places the window.

Wayland does not let an application read or set its windows' positions, so on a Wayland desktop a layout restores the windows and their sizes but the compositor places them, and bobview logs this once. To keep positions, run bobview under XWayland:

WAYLAND_DISPLAY= bobview

X11 desktops, including bobview-ssh over X forwarding, restore positions.

Trend PVs

A trend window plots one or more PVs against time: their archived history from an EPICS Archiver Appliance, joined to their live values. The plot is archiveview's, so a trend in bobview works as the standalone archiveview does.

Open a trend

  • Right-click a widget (long-press on a touch screen) and pick Trend for its PV, or Trend with rule PVs for its PV and the PVs its rules read. The same menu adds them to a trend already open.
  • In a BOY screen, a button that would open a Data Browser or StripTool opens a trend of its PVs instead (see How BOY screens are translated).

Several trends may be open at once, each in its own window over the display, and they stay open as you move between displays. Layouts save them.

Give bobview an archiver

Without an archiver a trend is live only: each PV's line starts when the window opens. To see history, name the appliance's retrieval service:

bobview --archiver http://archiver.example.com:17668 DISPLAY

/retrieval is added unless the URL ends with it, and Phoebus's pbraw://host:port/retrieval form works too. The archiver is chosen in this order:

  1. --archiver URL, which is also remembered for later runs (not with --snapshot);
  2. none with --no-archiver, for this run only;
  3. the BOBVIEW_ARCHIVER environment variable (not remembered), for a site-wide default set in a launcher script or profile;
  4. the last archiver given with --archiver or picked in a trend's archiver pill, from settings.json beside the layouts.

The pill in each trend's top bar shows the archiver and whether it can be reached (green), is being checked, or cannot be reached (red). Click it to switch to another archiver, or to none; the switch applies to every trend window, and is remembered for the next run unless --archiver or BOBVIEW_ARCHIVER names another.

To try it without an appliance, run archiveview's mock archiver, which serves synthetic history for the test IOC's bl01t:SUM and a few MOCK: PVs:

cargo install archiveview-mockarchiver
archiveview-mockarchiver &
bobview --archiver http://127.0.0.1:17668

Use a trend

  • Time. The top bar's presets show the last hour, 8 hours, day or week; ⋯ takes a span such as 90m or 2 days, or two dates. Drag to pan and scroll to zoom time about the pointer. Moving into the past leaves Live; click it (or press L) to follow now again.
  • Values. Each value axis fits its data until you zoom it: drag an axis, or shift-scroll over it. PVs with different units or scales get axes of their own (up to four), on a log scale where that suits the values. Double-click the plot to reset.
  • PVs. Type in the search box to add a PV; with an archiver it also searches the archived names. The legend chips under the plot give each PV's colour, name and value: click one to hide its line, hover to pick it out, and × to remove it. Ctrl+V over the plot adds the PVs named in the clipboard.
  • Middle-click. On Linux (X11 or Wayland) a middle-click on the plot or legend adds the PVs named in the primary selection, as in the standalone archiveview: middle-click a widget in a display to select its PV name, or select names in a terminal or editor, then middle-click the trend. Channel Access, PV Access, loc:// and sim:// names are added (a sim:// one such as sim://sine(0, 10, 0.1) whole, with its arguments); a formula (= or eq://) isn't: use Trend in the widget's right-click menu. A middle-click in the search box pastes the selection there, and text selected in the search box becomes the primary selection. A note over the plot says what was added, or that the selection named no PVs (one naming more than 20 adds none). Windows and macOS have no primary selection, so there use Ctrl+V; in a snapshot a middle-click on a trend pastes nothing.
  • Undo. ↶ and ↷ (or Ctrl+Z and Ctrl+Shift+Z) undo and redo changes to the plot.
  • Hover over the plot for a readout of every PV at that time.

Limits

  • A PV the archiver doesn't hold is plotted live only, and its chip says so.
  • Waveforms are not plotted: their chip shows the latest array.
  • In the browser build trends are live only: a browser can't reach the archiver, and a middle-click pastes nothing: use Ctrl+V.

Deploy bobview-server for others

Serve displays to a browser runs bobview-server for this machine only. This guide makes it reachable from others, safely. All of its options are in Command line.

Run it where PVs are reachable

The server reaches PVs through bobview's own CA/PVA client, so run it anywhere caget works, with the usual EPICS_* variables:

bobview-server --opi /path/to/displays --bind 0.0.0.0

The release tarball includes the web build, which the server finds in share/bobview/web. Files in --opi take precedence over files of the same name in the web build (which has the test displays).

Choose who can reach it

By default it listens on --bind 127.0.0.1 --port 8080. Use --bind 0.0.0.0 (every interface) or a specific address to serve others, ideally behind a firewall or an authenticating reverse proxy, since reading PVs needs no login.

Browsers let any page open a websocket, so the server accepts only pages from itself (by Origin), plus any you name:

bobview-server --opi DIR --bind 0.0.0.0 --allow-origin https://ops.example.com

Those pages must also call the server by an IP address, localhost, this machine's name (as the kernel has it, without its domain, or in a domain of /etc/resolv.conf), the host of an --allow-origin, or a name given with --host NAME (repeatable), so that a page cannot reach it through a DNS name of its own.

Behind a reverse proxy

  • Give the public name the proxy passes on as Host with --host NAME.
  • Every client has the proxy's address, so raise --max-per-ip (default 10) and expect the logs to show the proxy.

Allow writes

Writes are off unless the server has --write, since they go out as the server's user. Each write must then carry a token. Set it in the environment rather than with --write-token, since arguments show in ps:

BOBVIEW_WRITE_TOKEN=s3cret bobview-server --opi DIR --write

With neither, the server makes a random token and prints it to stderr at startup; an empty BOBVIEW_WRITE_TOKEN counts as unset. Users open the page with &token=TOKEN. See Writes and safety for how the token is protected.

Limits it enforces

  • At most 100 clients and 50,000 subscribed PVs.
  • At most 10 clients from one address (or one IPv6 /64), --max-per-ip N.
  • A client subscribing to more than 1,000 new PVs a second, after its first 10,000, is slowed down.
  • A client that stops answering websocket pings, or takes over 10 s to take one message, is dropped.
  • PV names over 256 characters, or with spaces or non-ASCII characters, are refused.
  • Each client gets the PVs that changed at most every --throttle-ms (100 ms), each with its latest state.

It serves no loc:// or sim:// PVs: the page makes those itself. Images from PVA are not sent over PVWS yet.

Files it will not serve

Dot-files such as .git are never served, nor files reached through a symbolic link that leads out of --opi or --web (unless --follow-symlinks). Other sites cannot frame the pages (X-Frame-Options, frame-ancestors 'self').

Take snapshots

--snapshot renders a display to a PNG without opening a window, then exits. It is useful for documentation, regression checks and CI.

bobview --snapshot out.png screen.bob

Size and scale

bobview --snapshot out.png --size 1920x1080 screen.bob
bobview --snapshot out.png --size auto screen.bob       # as a window manager would size it
bobview --snapshot phone.png --scale-factor 2.75 screen.bob  # as on a high-DPI phone

--size defaults to 1280x800. auto starts at the window's default size and resizes it as a window manager would, on a monitor of 1920x1080 points.

Wait for PVs

PVs get 2 seconds to connect by default; give more with --wait:

bobview --snapshot out.png --wait 5 screen.bob

Click first

--click X,Y, --right-click X,Y and --middle-click X,Y (repeatable, in the order given) interact with the display before the snapshot, which is taken --settle seconds (default 1) after the last click. Positions are in pixels.

bobview --snapshot menu.png --right-click 200,150 --settle 0.5 screen.bob

Snapshot every test display

CI does this against bobview-testioc:

bobview-testioc &
export EPICS_CA_ADDR_LIST=127.0.0.1 EPICS_CA_AUTO_ADDR_LIST=NO
for f in tests/displays/*.bob; do
  bobview --snapshot "snapshots/$(basename "$f" .bob).png" --size 1280x800 --wait 5 "$f"
done
kill %1

Build from source

You need a stable Rust toolchain (rustup).

git clone https://github.com/gilesknap/bobview
cd bobview
cargo build --release
cargo test --workspace

The binaries are in target/release.

Cross-build for the phone

From x86_64, with zig and cargo-zigbuild installed:

cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.36

Build the browser version

./scripts/build-web.sh

writes dist/web. It needs the wasm32-unknown-unknown target and the wasm-bindgen CLI matching the version in Cargo.lock. In a checkout, bobview-server finds dist/web itself; --web DIR picks another.

Build these docs

The docs are an mdBook, with diagrams drawn by mdbook-mermaid. Install the versions CI uses (newer mdbook-mermaid needs a newer mdBook):

cargo install mdbook@0.4.40 mdbook-mermaid@0.15.0
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 clippy -p bobview-pv -p bobview --target wasm32-unknown-unknown -- -D warnings
cargo test --workspace

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

Set the version in the workspace's Cargo.toml, in both places: the workspace version and the = versions of bob and pv under [workspace.dependencies]. Merge that to main, then push a matching tag:

git tag v1.2.3
git push origin v1.2.3

CI checks that the tag matches the version in Cargo.toml and fails the build if not. It creates the GitHub Release itself and builds on a runner for each platform (ubuntu-24.04, ubuntu-24.04-arm, macos-15, which also cross-builds the x86_64 Mac build, and windows-2022), attaching a bobview-<arch>-<os>.tar.gz for each Linux and macOS build, such as bobview-x86_64-linux.tar.gz and bobview-aarch64-macos.tar.gz, and bobview-x86_64-windows.zip for Windows.

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 archive carries THIRD_PARTY_LICENSES.txt, generated by scripts/third-party-licenses.py. install.sh downloads the latest release for the machine it runs on.

The tag also publishes a copy of these docs at https://gilesknap.github.io/bobview/<tag>/, which the version menu in the docs' top bar then lists beside main.

Publish to crates.io

The workspace publishes five crates: bobview, bobview-server, the libraries they use, bobview-bob and bobview-pv, and bobview-testioc, which pv's tests use (the names bob and pv were taken; the code still calls them bob, pv and testioc). Check them first with

cargo publish --workspace --dry-run

which packages each crate and builds it as crates.io will.

The first time, publish by hand: log in to crates.io with GitHub, create an API token with the publish scope, then

cargo login
cargo publish --workspace

Cargo publishes them in dependency order. After that, CI can publish each release itself with trusted publishing, which needs no stored token:

  1. On crates.io, in each crate's Settings → Trusted Publishing, add the repository gilesknap/bobview and the workflow ci.yml.
  2. In the repository's Settings → Secrets and variables → Actions → Variables, set PUBLISH_CRATES to true.

The publish job then runs after the GitHub Release is made for each tag. A version can be published to crates.io only once, so fix a bad release with a new version.

archiveview

Trend windows embed archiveview from crates.io, and archiveview depends on the crates.io bobview-pv. The root Cargo.toml patches that to crates/pv, so both use one PvManager. That holds only while archiveview's bobview-pv requirement matches this workspace's version: if pv goes to a new major version, or its API that archiveview uses changes, release archiveview against it first. Then raise the archiveview version in the root Cargo.toml, run cargo update -p archiveview -p archiveview-archiver, and check cargo tree -i bobview-pv shows a single bobview-pv before tagging.

Scope and fidelity to Phoebus

What bobview is for

bobview is a viewer. It opens Phoebus Display Builder .bob screens and CS-Studio BOY .opi screens and shows live EPICS values on them, from a single small binary that starts quickly and runs on modest hardware: down to a phone, in the Android 16 Linux Terminal (Debian 13, aarch64). The same code builds for the browser.

What is out of scope

  • Scripts (Jython and JavaScript) are not run. The status line shows "N widgets with scripts ignored", counting widgets, not scripts. A few scripts that are common in DLS and techui-builder screens are recognised by name and done natively instead; see How BOY screens are translated and Widgets.
  • Display editing. Use Phoebus to edit screens.
  • Alarm services, and archivers other than the EPICS Archiver Appliance. Trends show history from an Archiver Appliance through archiveview; see Trend PVs.

Widgets bobview can't draw are shown as a grey placeholder labelled with the widget type and PV name, and the status line counts "N unsupported widgets". What is and isn't supported is listed in Widgets.

How close it is to Phoebus

Fidelity is approximate. Sizes, fonts (scaled by 0.95) and layout were tuned by eye, and output has not been compared with Phoebus's own pixel for pixel.

Where behaviour matters more than looks, bobview follows Phoebus closely: number formats, how typed text is parsed into a value (units dropped, 0x and 0b numbers, enum labels), macro expansion, and the precedence of operators in rules (as Phoebus's Jython for a .bob, as BOY's JavaScript for an .opi). Where it departs from Phoebus it says so in the reference, for example rules taking the integer part of a double for bitwise operators where Jython would fail.

Protocol maturity

Channel Access and PV Access come from the third-party epics-rs crates, which are young; see Architecture. bobview has been used against real Diamond Light Source systems: the screens of beamlines p47 and b01-1, and the accelerator's diagnostics and vacuum display hierarchies. In CI, the interop job (crates/pv/tests/interop.rs) reads a PV over CA and over PVA from a PVXS softIocPVX, writes one over CA and reads it back, and reads one over CA through a ca-gateway; everything else is tested against the in-repo bobview-testioc.

Architecture

bobview is a Cargo workspace of five crates. Three are published under longer names, as bob and pv were taken on crates.io, but keep their short names in code: bob is bobview-bob, pv is bobview-pv and testioc is bobview-testioc.

graph LR
    bobview["bobview<br/>viewer and bobview-probe"]
    server["bobview-server"]
    bob["bob<br/>display model"]
    pv["pv<br/>PV access"]
    testioc["testioc<br/>test IOC"]
    egui["egui / eframe"]
    archiveview["archiveview<br/>trend plot"]
    epics["epics-rs<br/>epics-ca-rs, epics-pva-rs"]
    pvws["PVWS"]

    bobview --> bob
    bobview --> pv
    bobview --> egui
    bobview --> archiveview
    archiveview --> pv
    server --> pv
    pv -- desktop --> epics
    pv -- browser --> pvws
    server -. serves .-> pvws
    testioc --> epics

    classDef wasm stroke-width:3px,stroke-dasharray:6 3
    class bobview,bob,pv wasm
cratewhat
bobthe display model: reads .bob files and translates .opi ones into the same form, expands macros, resolves Eclipse project links and DLS launchers
pvPV connections behind one interface: Channel Access, PV Access, PVWS for the browser, loc://, sim:// and formula PVs
bobviewthe viewer: an egui app that draws displays, plus bobview-probe
bobview-serverserves the web build, .bob files and a PV Web Socket from one port
testiocbobview-testioc, a pure-Rust IOC serving the example-services BL01T PVs

bobview's trend windows are archiveview's Plot widget, which fetches archived history from an EPICS Archiver Appliance and joins it to live values. It is a separate project that takes bobview's PvManager, so the workspace patches its crates.io bobview-pv to the one in crates/pv, making them the same type.

The crates with a dashed outline also build for wasm32-unknown-unknown. Which source file does what is listed in the source map.

From file to screen

graph LR
    fetch["fetch<br/>file or http(s)"] --> parse["bob<br/>parse; .opi translated;<br/>project links"]
    parse --> build["screen::Builder<br/>macros, rules"]
    build --> nodes["nodes<br/>with PV handles"]
    nodes --> draw["widgets draw<br/>each pass"]
    draw -- Requests --> app["the app<br/>open, trend, close …"]
  1. bobview reads a display from a file or over http(s), off the UI thread, and bob parses it into a tree of widgets with their properties. An .opi is translated as it is read, so everything after this sees only the .bob form. Files a display names are found through its Eclipse project's linked folders.
  2. The screen builder expands macros down the tree, compiles each widget's rules and asks pv for its PVs, giving a tree of nodes that each hold their PV handles. Embedded displays are built on threads of their own.
  3. Each pass, the widgets read their PVs' latest values and draw. A rule is evaluated again only when one of its PVs has changed. What a click asks for (open a display, show a trend, close the window) goes back to the app as a Request.

A window is drawn again only when something it shows changes; see Threads, windows and repaints.

Rules are expressions over a widget's PVs, evaluated in bobview, that set one of its properties. Formula PVs (=expr, eq://expr) are PVs made by pv from other PVs; see Macros, rules and PV types.

Protocols

pv's ca.rs and pva.rs are thin adapters over the epics-rs epics-ca-rs and epics-pva-rs crates, which do the protocol work. These are young, so protocol correctness rests largely on them; the interop CI job tests against other EPICS implementations.

In the browser

bob, pv and bobview build for the browser, drawn with WebGL. A browser can't speak CA or PVA, so there PVs come over a PV Web Socket (PVWS), as in Daedalus. bobview-server provides one at /pvws/pv, reaching PVs through the same pv crate as the desktop viewer; any other PVWS server works too. loc:// and sim:// PVs are made in the page itself. There is one window, and displays are fetched without blocking the page.

The test IOC

bobview-testioc serves enough of the epics-containers example-services BL01T beamline for its screens, over CA and PVA, with no C dependencies. It runs natively on an aarch64 phone, where the real amd64 IOC containers would need qemu, and CI uses it to snapshot every test display. See bobview-testioc.

Design choices

  • egui, immediate mode. Each pass draws the display from its nodes and the PVs' latest values, with no retained widget tree to keep in step. It gives one code base for the desktop, the browser and headless snapshots, from a small binary.
  • Thin adapters of our own over epics-rs, rather than bindings to the C libraries: no C toolchain, easy cross-builds, and one pv interface for every PV type.
  • Images are decimated to at most 2048 pixels a side before they are uploaded as a texture.
  • Fidelity is tuned by eye against Phoebus, not pixel for pixel; see Scope and fidelity to Phoebus.

Threads, windows and repaints

egui draws a window only when asked to. This page explains who asks, and the locking that lets several windows share one app. It is for contributors; the user's view is in Pinned windows and the viewer.

PV updates

A PvManager owns a tokio runtime (two worker threads, bobview-pv) with one task per CA or PVA PV, talking to the epics-rs client. Each monitor event updates the PV's shared state under its lock and bumps its version (Shared::update in crates/pv/src/lib.rs). The UI thread never waits on the network: each pass, widgets copy out the latest state.

Each window subscribes its PVs through its own handle, PvManager::tagged(notifier), so each PV knows which windows show it. An update calls the notifiers of those windows only. A window's notifier (Repaint in app.rs) asks egui for one repaint of that window, 100 ms later, and asks no more until the window's next pass begins; so a window is redrawn for its PVs at most about 10 times a second, however fast they change. When a window shows another display, its handle is retagged, so the old display's PVs stop repainting it. A notice (a refused or failed write) calls every window's notifier, so each shows it.

sequenceDiagram
    participant T as PV task (tokio)
    participant S as PV shared state
    participant R as child's Repaint
    participant E as egui / eframe (UI thread)
    participant A as BobApp (locked)
    T->>S: monitor event: update state, version += 1
    S->>R: call the tag's notifier
    R->>E: request_repaint_after_for(100 ms, child)<br/>only if not asked since its last pass
    E->>A: the child's deferred viewport callback:<br/>lock, display_pass
    A->>R: begin: may ask again
    A->>S: widgets read the latest state, and draw

Windows

The main window is eframe's root viewport. Each display and trend window is a deferred viewport (show_viewport_deferred) with a pass of its own, so it is redrawn for its own input and PVs, not when another window is. The app, BobApp, sits behind one Arc<Mutex<_>> (app::App): each window's pass locks it. eframe runs the passes one after another on the UI thread, so they never wait for each other.

Three rules keep this working:

  1. Never hold the lock while calling show_viewport_deferred. Where windows are embedded, egui runs the callback at once, inside the caller's pass, and the callback takes the lock. The main window's pass returns the windows to keep open, and App::draw registers them after letting go.
  2. Windows open from the main window's pass. A link in a child window adds the new window to the app, and the main window registers it on its next pass. So a pass that changes the windows open or their titles repaints the main window.
  3. A change to another window repaints it. Each pass compares a small Shape (windows, what each shows, trend titles, where the PV details dialog is) before and after, and repaints the windows whose part changed.

Embedded mode has one native window: the browser, snapshots and unit tests (a plain egui::Context embeds). There, display and trend windows are egui::Windows drawn in the main window's pass, and every repaint is of the main window.

Other repaint sources

Work on other threads asks for a repaint by viewport id, because a bare request_repaint there reaches whichever window egui is drawing:

  • the image-loader threads (symbol.rs), which read the SVG and PNG files of symbol and picture widgets, repaint each window waiting for an image;
  • the navigator's and command launchers' threads repaint the main window.

A window also polls while it waits: a display being fetched or built, an embedded display building on its thread, or a fetch in the browser asks for a repaint every 50 ms until it arrives. Strip charts scroll by asking for the next repaint when the plot has moved a pixel.

A trend window's plot is archiveview's Plot, built on the trend window's own tagged PvManager, so its PVs repaint only that window. It repaints itself for the rest: archive fetches run on the pv crate's runtime and ask for a repaint of the viewport the plot was last drawn in, and a live plot asks for the next repaint as it scrolls (at most 15 frames a second). Its Live pill also pulses, at about 15 frames a second, for 3 s after it goes live and while the pointer is over the plot: so a live trend in its own native window, with the pointer resting on it, repaints at about 15 frames a second, but only that window.

Where windows are embedded, a trend's plot is drawn in the main window, so each repaint it asks for itself redraws the whole display: there Trend::ui holds them to a strip chart's floor, MIN_SCROLL_REPAINT (0.25 s), with Plot::set_min_repaint_interval. A trend in its own native window repaints only itself, and keeps the plot's own pace. Repaints for the user's pans and zooms, and for a fetch finishing, are not held back.

Pinned windows and the viewer

For a control room, several displays can be open side by side, each in a window of its own, and the arrangement saved as a layout. The question is where a link opens its display: in place, or in another window? bobview answers it with one idea: each window is pinned or not.

The rule

A link (an action or menu button, a symbol click …) opens its display by these rules, whatever target the screen gives it:

ClickDisplay already open somewhereNot open
in a pinned windowraise that windowopen in the viewer, or a new window if every window is pinned
in an unpinned windowraise that windowopen in place
shift+clickraise that windowopen in a new window
ctrl+clickopen in placeopen in place

The viewer is the unpinned window used last (focused, opened or unpinned), so clicking a window once makes it where the next link goes. "Already open" means the same display with the same macros, in another window. The macros are all those the display would be opened with: the link's own, and those of the group, widget or embedded display it is on, so each gauge of a cell, opened through one gauge screen, gets its own window. A BOY display's $(DID) doesn't count, since each window gives it its own.

Why it works this way

The main window starts pinned, so it stays where it is while the displays it links to come and go in one other window: an overview on one side and a detail on the other. Pin that one too to keep it, and the next link opens a new viewer. Unpinned, the main window navigates in place, as a single window does. Holding shift opens every display in a window of its own, as the old multi-window mode did.

A display opened in place or in the viewer keeps that window's history for Back and Forward. A link to the display its own window shows reloads it in place, as a Refresh button wants, without adding to Back. BOY Exit buttons close the display window they are in (in the main window they go back, or quit bobview when there is nothing to go back to).

In a browser, and on a touch or small screen, displays have no windows of their own: every link navigates in place, and the pin and layout controls are not shown.

Where new windows go

A display window opens sized to its display at 100%, within the monitor, beside the window pinned last, so a row of small windows grows beside a big one: on the first of its right, below it, its left and above it where there is room on the monitor clear of the pinned windows. Failing that it goes beside the window whose link opened it, then beside the other pinned windows, or where it covers them least. The viewer is sized and placed so again for each display a pinned window's link opens in it.

Wayland doesn't let applications place their windows, so there the compositor does.

Redrawing

Each window is redrawn on its own: for its own input, and when a PV its display shows changes (at most 10 times a second), not when another window is. That matters most over ssh -X, where each redraw sends the whole window over the network.

The controls of each window are listed in The window and its controls.

How BOY screens are translated

One renderer for both formats

An .opi file (CS-Studio BOY) is translated as it is read into the .bob form, the way Phoebus's own legacy import converts it, so it is drawn by the same widgets as a .bob. It is accepted everywhere a .bob is: on the command line, in Open… (which lists .opi files too), in actions and linking containers, and over http(s). As in Phoebus, a .bob next to a named .opi file is opened in its place.

How each BOY widget is drawn is listed in BOY widgets.

Colours, fonts and rules

Colours keep the RGB the file gives; a colour with only a name is looked up in the EDM colors.list DLS converted its screens with, then in the Phoebus palette. Fonts in pixels keep their size, fonts in points are divided by 1.01, and the Phoebus named fonts (Default, Header 1 …) use the Phoebus sizes. Rules keep BOY's severity numbers (pvSev0 becomes pvLegacySev0) and widget.getValue() reads the widget's own PV.

Size and clipping

An .opi keeps its declared width and height, and widgets outside them are clipped, as in BOY. EDM conversions often leave stray widgets far off the display, which Phoebus (and bobview, for a .bob) would grow the display to show. This size is also what Fit, --size auto and an embedding container that sizes itself to its content use.

Scripts that are done natively

Scripts and JavaScript or Python actions are not run, with these exceptions, recognised and done by bobview itself:

  • The EDM Exit button's script (ScriptUtil.closeAssociatedOPI) goes back to the previous display, or closes the window.
  • A script that opens a Data Browser on some PVs (ScriptUtil.executeEclipseCommand of org.csstudio.trends.databrowser2.newWindow with ['pvnames', 'A,B'], as the DLS vacuum "Graph" buttons do) opens a trend of them, with their archived history when bobview has an archiver, as the Data Browser would show.
  • The DLS vacuum widget script get_pv_name.py, recognised by its file name. Its second PV, a vacuum space's link field such as SR03S-VA-SPACE-01:IONPSTA.INPA, names a group (SR03S-VA-GIONP-01:STA NPP NMS names SR03S-VA-GIONP-01). Each time it names a different one, a label shows the group's name, and any other widget reads the group's :STA and takes its name, so its tooltip, right-click menu and the $(name) its click passes as device follow. An EDM symbol of a gauge, ion pump or valve strip shows the frame for that status (0 when disconnected).

A click on a shape, symbol or LED with a BOY click hook runs its actions. A disconnected EDM symbol shows its first frame inside the disconnected border, without Phoebus' overlay.

Eclipse projects and DLS launchers

BOY screens name each other through the CS-Studio workspace: an Eclipse project whose .project links folder names to the directories that hold each module's screens. bobview resolves these links itself.

DLS modules have no .project on disk, but the generated launcher runcss.sh beside a module's screens lists the links it gives CS-Studio (links="${SCRIPT_DIR}=${project}/${module},…"). bobview reads that list (it does not run the script, which needs configure-ioc), taking version from the module's configure/VERSION (else dev). So …/vacuum/4-100/vacuumApp/opi/opi/diamondVacuumOverview.opi opens as …/vacuumApp/opi/opi/runcss.sh/vacuum_4-100/vacuum/diamondVacuumOverview.opi: the CS-Studio path /vacuum_4-100/vacuum/diamondVacuumOverview.opi below the launcher, and links to other modules work.

This is for BOY screens only: a .bob (even one Phoebus-style in place of an .opi, or one picked in a launcher project's folder) goes by its real path. A .project nearer the screen than the launcher, or beside it, wins when it links the screen (an empty one, as many DLS modules have, does not).

The steps to open such screens are in Open BOY screens from an Eclipse project or DLS module.

Launcher commands

DLS screens open others with command actions that run a launcher script, such as ./srCellVacuum 04, which ends by running runcss.sh DISPLAY -m MACROS to start CS-Studio. For a local display, bobview runs a copy of such a script beside a stand-in runcss.sh that only records its arguments, and opens that display with those macros. These are shell scripts, so they are not run on Windows, where such commands do nothing.

A command that starts StripTool (strip.py, strip.sh or buildStripToolconfig.sh and the PV names, as in the DLS vacuum, gauge and RGA screens) opens a trend of those PVs instead; a last word without a :, such as log_mbar, is ignored. Other commands are not run.

Running scripts is limited for safety; see Writes and safety.

Writes and safety

A viewer that can write to a control system needs to be predictable about when it does. This page explains the rules.

Every write takes one route. A control or a write_pv action calls Pv::write (or Pv::write_long_string, for text a string-format widget writes to a char array). Read-only mode refuses it there, unless it is to a loc:// PV. Otherwise it goes to the PV's writer: the queue of its backend task for CA and PVA, which converts the text to the PV's type and sends it; the value itself for loc://; or the PV Web Socket in a browser. On the desktop a refused or failed write becomes a message in the status line; the browser is below.

Read-only mode

--read-only refuses every write to a PV: controls are disabled and a write action says in the status line why it did nothing. loc:// PVs are exempt, as they live inside bobview and can't reach a control system.

Access rights

Channel Access reports whether a PV is writable, and controls follow it. PV Access does not report access rights, so such a PV is assumed writable (the PV details say "unknown"), and a put the server refuses is shown as a write error in the status line and the PV details.

Writes are never applied late

A write to a CA or PVA PV that was connected and has dropped is refused at once, rather than queued and applied when it comes back, possibly much later; the status line says so. So is a write while 16 earlier writes to the same PV still wait.

A write to a PV that has yet to connect (as a write_pv action to a PV not on the display makes) waits for it, up to 5 s over CA, without holding up the PV's other events. The writes behind it wait in that queue (so while it waits, a 17th is refused), and fail with it if it gives up.

Displays from the web

A write_pv action and the toggle script write at once, as in Phoebus, whether the display came from a local file or an http:// or https:// URL. To open displays without letting them write, use --read-only, which refuses every write.

Commands

No command is ever run for a display fetched over http(s), nor in a command_directory on the web. Only a launcher script that resolves (after macros, .. and project links) to a local file beside or under the display, or in a linked folder of its Eclipse project, is run. Anything else, such as an absolute path elsewhere or a ../.. escape, is refused and logged, as is a script that any user may change (world-writable, or in a world-writable folder). Each script run is logged.

In the browser

bobview-server writes as its own user, so it allows writes only with --write, and each write must carry a token. A page with a token loads displays only from its own server, and sends the token only to a PVWS on its own server, so a link naming someone else's display or &pvws= cannot write with it. The page removes the token from its address after reading it.

Writes to a PV that was connected and has dropped fail at once, and at most 16 may wait for one PV, as on the desktop. One to a PV that has yet to connect fails after 5 s, which the page sees only as the PV's write error, not as a message.

Command line

bobview

bobview [OPTIONS] [DISPLAY]

DISPLAY is a .bob or .opi file, or an http:// or https:// URL. With no display, bobview restores the default window layout when one has been saved, else opens index.bob from the bundled displays (~/.local/share/bobview/displays) when present, else a file chooser.

optionmeaning
-m, --macros P=X,R=Ymacros for the display
-p, --project DIRopen the display through the linked folders of the Eclipse project in DIR (its .project, or for a BOY .opi screen the links of a DLS runcss.sh in DIR); see Open BOY screens
-z, --zoom FACTORfixed zoom, 0.2 to 5, instead of fitting the window
-f, --fullscreenstart full screen
--read-onlyrefuse every write to a PV except loc:// ones; see Writes and safety
--archiver URLthe EPICS Archiver Appliance trends fetch history from, e.g. http://archiver:17668 (/retrieval is added); remembered for later runs (not with --snapshot). See Trend PVs
--no-archiverno archiver this run, even if BOBVIEW_ARCHIVER or the settings name one: trends are live only
--layout NAME or FILErestore the saved layout NAME, or the layout in the JSON file FILE (a path with a directory in it or ending in .json); Save layout then writes back to that file. See Save and restore a layout
--no-layoutdon't restore the default layout
--ui-scale F or autosize of the toolbar and menus relative to the desktop's scale, 1 being the desktop's own (default auto: smaller on a small high-DPI screen)
--snapshot OUT.pngrender to a PNG without a window and exit; see Take snapshots
--size WxH or autosnapshot size in pixels (default 1280x800); auto sizes it as a window manager would, on a 1920x1080 monitor
--scale-factor Fsnapshot as on a screen with this desktop scale, e.g. 2.75 for a phone (default 1)
--wait SECONDStime for PVs to connect before a snapshot (default 2)
--click X,Yclick here before the snapshot (repeatable)
--right-click X,Yright-click here before the snapshot (repeatable)
--middle-click X,Ymiddle-click here before the snapshot (repeatable)
--settle SECONDStime after the last click before the snapshot (default 1)
-h, --helpshow help
-V, --versionshow the version

bobview-ssh

bobview-ssh [OPTIONS] [DISPLAY]

Runs bobview with the same arguments, rendering with Mesa in software. See Choose bobview or bobview-ssh.

bobview-probe

bobview-probe [--seconds N] [--put NAME=VALUE]... NAME...

Prints live values of PVs through bobview's PV layer, for N seconds (default 5). --put writes a value first (repeatable). PV names take the same prefixes as in a display (pva://, loc:// …).

bobview-server

bobview-server [OPTIONS]

Serves the bobview web build, .bob files and a PV Web Socket on one port. Open http://HOST:PORT/?display=FILE.bob in a browser (default index.bob). See Deploy bobview-server for others.

optionmeaning
-p, --port PORTport to listen on (default 8080)
--bind ADDRaddress to listen on (default 127.0.0.1, this machine only; 0.0.0.0 is every interface)
--web DIRthe web build from scripts/build-web.sh (default ../share/bobview/web next to this binary, else dist/web)
--opi DIRserve .bob files from here; they take precedence over files of the same name in the web build
--follow-symlinksserve files through symbolic links that lead out of --opi or --web (refused by default)
--writelet browsers write to PVs (off by default, as in PVWS); every write must carry the write token
--write-token TOKENthe token --write wants (default: $BOBVIEW_WRITE_TOKEN if set and not empty, else a random one printed at startup); prefer the variable, as arguments show in ps
--allow-origin ORIGINalso let pages from ORIGIN connect, e.g. https://host.example.com (repeatable)
--host NAMElet browsers call this server NAME, as well as an IP address, localhost, this machine's name and the hosts of --allow-origin (repeatable)
--max-per-ip Nmost clients at once from one address (default 10)
--throttle-ms MSleast time between two batches of updates to one client (default 100)
-h, --helpshow help
-V, --versionshow the version

bobview-testioc

bobview-testioc [--only ea|motor|cam]...

Serves the simulated BL01T PVs; by default all three parts. See bobview-testioc.

Environment variables

EPICS

bobview, bobview-probe and bobview-server read the standard EPICS client variables:

variablemeaning
EPICS_CA_ADDR_LISThosts or broadcast addresses to search for CA PVs
EPICS_CA_AUTO_ADDR_LISTNO to search only EPICS_CA_ADDR_LIST
EPICS_CA_NAME_SERVERShost:port of CA name servers (gateways) reached over TCP
EPICS_PVA_ADDR_LISThosts or broadcast addresses to search for PVA PVs
EPICS_PVA_AUTO_ADDR_LISTNO to search only EPICS_PVA_ADDR_LIST
EPICS_PVA_NAME_SERVERShost:port of PVA servers reached over TCP

bobview-testioc reads the server variables: EPICS_CA_SERVER_PORT, EPICS_CAS_*, EPICS_PVAS_SERVER_PORT / EPICS_PVA_SERVER_PORT (TCP, default 5075) and EPICS_PVAS_BROADCAST_PORT (UDP search, default 5076).

bobview

variablemeaning
BOBVIEW_LOGlog level, e.g. debug for more output
BOBVIEW_UI_SCALEas --ui-scale
BOBVIEW_ARCHIVERthe archiver trends use when --archiver is not given (see Trend PVs); empty counts as unset
XDG_CONFIG_HOMElayouts are in $XDG_CONFIG_HOME/bobview/layouts (default ~/.config/bobview/layouts; on Windows %APPDATA%\bobview\layouts)
WAYLAND_DISPLAYset it empty to run under XWayland, so layouts keep window positions

Displays opened from files also fall back to environment variables for macros they don't define; see Macros, rules and PV types.

bobview-server

variablemeaning
BOBVIEW_WRITE_TOKENthe write token for --write; an empty value counts as unset

Tests

The interop tests in crates/pv/tests/interop.rs are skipped unless these name PVs to read:

variablemeaning
BOBVIEW_INTEROP_CA_PVa PV served by the IOC over Channel Access
BOBVIEW_INTEROP_PVA_PVa PV served by the IOC over PV Access (PVXS)
BOBVIEW_INTEROP_GW_PVa PV reached through ca-gateway (CA)
BOBVIEW_INTEROP_WRITE_PVoptional writable numeric PV; set to 7 and read back
BOBVIEW_INTEROP_REQUIRE1 to fail instead of skipping when a variable is missing

Widgets

The Phoebus Display Builder widgets bobview draws, and how far it goes with each. BOY .opi widgets are translated to these; see BOY widgets.

widgetsnotes
label, textupdate, textentryformats: default, decimal, exponential, engineering, hex, compact, string, sexagesimal (also HMS and DMS), binary, significant; units, precision; typed text is parsed as Phoebus does (units dropped, 0x/0b numbers, enum labels)
led, multi_state_led, byte_monitor
action_button, bool_button, slide_button, choice, radio, combo, checkboxopen display, write PV and open webpage actions; command actions only for DLS launcher scripts and StripTool (see How BOY screens are translated)
scaledslider, scrollbarset by a click or drag with the primary (left) button
spinnerits arrows (on the left with buttons_on_left), or Up and Down while it has focus, step the value by increment within its range; or type a value and press Enter. As in Phoebus, a drag does nothing
linearmeterscale, warning regions from the levels (the PV's alarm limits with limits_from_pv), limit markers, needle; horizontal or vertical
metera dial: scale (linear or log_scale), warning regions from the levels (the PV's alarm limits with limits_from_pv), needle
progressbar, tank, thermometerone shared bar renderer (tank and thermometer are vertical bars with their own default fill colours)
xyplotline, step and bar traces with markers against index or an x PV; several y axes, autoscale, log scale, grid, legend
stripchartits traces' PVs over the last time_range, sampled from when the display opened, on the first y axis (autoscale or fixed, log scale, grid), legend
rectangle, ellipse, arc, polyline, polygondashed and dotted line_style for rectangles and polylines
group, tabs, embedded, navtabsmacros pass down; embedded resize modes 0–2 and 4 (crop)
imagePVA NTNDArray (uncompressed, mono or RGB1) or a CA array shaped by data_width/data_height; Phoebus colour maps, autoscale or fixed range, unsigned, colour bar with a scale; toolbar to zoom, pan and place a crosshair that reads out the pixel value; decimated when a side is over 2048 pixels, to keep drawing fast
symbol, pictureSVG or PNG files, local or fetched next to the display; the PV value picks the symbol (initial_index, array_index, fallback_symbol; an empty one draws nothing), rotation, preserve_ratio/stretch_image, opacity, overlay for a disconnected PV; run_actions_on_mouse_click runs the symbol's actions when clicked; images inside an SVG are drawn only when embedded (data: URLs); a strip of frames sub_image_width wide (DLS EDM symbols) shows frame image_index

Common to all widgets

border_width/border_color borders, alarm borders, dashed borders for disconnected PVs, and label and text update rotation_step. Macros and rules are described in Macros, rules and PV types.

Unsupported

Unsupported widgets, such as table, webbrowser, array, fileselector and byte-array, are drawn as a grey placeholder labelled with the widget type and PV name, and counted in the status line.

Not yet

  • PV Access field paths (pva://name/field), and sys:// and other PV types (shown as not supported)
  • compressed (codec) images, image axes, ROIs and cursor PVs
  • the XY plot markers, error bars and toolbar
  • the strip chart's toolbar and second y axes
  • GIF and JPEG images (symbols and pictures show their fallback or a placeholder), and text inside SVG images
  • scripts: an execute action logs that it is not supported, except the "toggle the widget's PV" script that techui-builder puts in synoptic symbols, which bobview recognises and runs itself

BOY widgets

How each CS-Studio BOY widget in an .opi is drawn. Background on the translation is in How BOY screens are translated.

BOY widgetdrawn as
Rectangle, RoundedRectangle, Ellipse, arc, polyline, polygonthe shapes (BOY borders become outlines, points made relative, alpha)
Label, TextUpdate, TextInput, NativeTextlabel, textupdate, textentry (format_type, precision_from_pv, borders inside the bounds, rotation_angle); a text update or input with text and no PV is a label
groupingContainer, linkingContainergroup (style from border_style), embedded (opi_file, resize_behaviour; a title bar or group box border becomes a group around it)
ActionButton, NativeButton, MenuButton, BoolButton, ImageBoolButton, BoolSwitchaction_button (the push_action_index action first; an EDM message button, a BoolButton with no PV, becomes one that writes its push action), combo or action button with a menu, bool_button, slide_button
choiceButton, radioBox, checkbox, combo, spinner, scrollbarchoice, radio, checkbox, combo, spinner, scrollbar
LED, bytemonitor, progressbar, tank, thermometer, meter, gauge, scaledslider, knobled or multi_state_led, byte_monitor, progressbar with its value label, tank, thermometer, meter, scaledslider
edm.symbolwidget, MultistateMonitorWidget, BoolMonitorWidget, ImageBoolIndicator, Imagesymbol (an image strip, or a numbered set of images, nothing for a state without one), picture
detailpanel (DLS)a group with each row's name and content
xyGraph, intensityGraph, tab (TabbedContainer)xyplot (numbered axes and traces, rules on a trace's properties), image, tabs (one grouping container per tab)
edm.menumuxcombo on its first target PV (writes the item's label)
othersplaceholders

Macros, rules and PV types

Macros

  • $(X) and ${X}, with defaults.
  • Widget properties such as $(pv_name), and $(actions) in a tooltip.
  • Macros pass down through groups, tabs and embedded displays.
  • In displays opened from files, a macro that isn't defined falls back to the environment variable of the same name (in widget properties and the display name).
  • The display name, with macros expanded (or the file name if it has none), is the window title.

Give macros on the command line with -m P=X,R=Y, or in a browser with &macros=P=X,R=Y.

Rules

Rules are expressions over pv0, pvStr0, pvSev0, pvLegacySev0 … that set any property, or one item of a list such as symbols[0], including the position, size and PV. Output expression rules are supported, and macros in rules are expanded.

  • Operators: arithmetic, bitwise (including shifts) and logical.
  • Operators bind as in Phoebus's Jython for a .bob, or BOY's JavaScript for an .opi.
  • A lone = is == in a .bob, as Phoebus makes it.
  • Unlike Jython, bitwise operators take the integer part of a double such as pv0, where Jython fails the rule.
  • In an .opi, pvSev0 becomes pvLegacySev0 (BOY's severity numbers) and widget.getValue() reads the widget's own PV.

PV types

PV namewhat
nameChannel Access
ca://nameChannel Access
pva://namePV Access
loc://namea local PV inside bobview, never written to the control system
sim://…a simulated PV, such as sim://sine(0, 100, 0.1)
=expression, eq://expressiona formula PV

Not yet supported: sys:// and other PV types (shown as not supported), and PV Access field paths (pva://name/field). In a browser, CA and PVA PVs come over PVWS, and loc:// and sim:// PVs are made in the page.

The window and its controls

Main toolbar

From the left: Back, Forward, Home and Reload; zoom out, the zoom, zoom in and Fit; the navigator and Open…; the display's name; and on the right the window's pin, the layout list and ☰, a menu of the rest (Auto-size, Bring all windows to front, Close all other windows and the layout actions).

  • Clicking the zoom shows the display at 100% and sizes the window to it (and the navigator, when shown).
  • A narrow window keeps Back, Home, the zoom and Fit, and moves the rest into ☰.
  • On a small high-DPI screen the toolbar and menus are scaled down so they take a small share of it; --ui-scale F (or BOBVIEW_UI_SCALE=F) fixes their size.

Window size

The window opens at the size of the first display (when the screen has room). With Auto-size (in ☰, on by default) it is resized to each display you open, unless it is maximised or full screen; turn it off to keep the window's size and scale each display to fit. -z ZOOM fixes the zoom instead of fitting the window.

On a widget

  • Hover to see its PV and value.
  • Right-click (long-press on a touch screen) to copy the PV name, its value or a caget/pvget command, open the PV details, Trend it, Trend with rule PVs, or add it to an open trend. See Trend PVs for the trend window's own controls.
  • Middle-click to copy its PV name, as in EDM and MEDM: a middle-click in a terminal then pastes it, and one on a trend or in archiveview plots it (a Channel Access, PV Access, loc:// or sim:// name; for a formula use Trend). The widget flashes and the status line says "Copied name". See Copying a PV name.
  • On a touch screen, pinch to zoom.

Copying a PV name

A middle-click on a widget, or Copy PV name in its right-click menu, puts the PV name on both the clipboard (for Ctrl+V) and the primary selection (for a middle-click, on Linux). The name is the one other tools take:

  • macros expanded, and a Channel Access name bare and whole (BL01T-EA-DET-01:COUNT, not ca://BL01T-EA-DET-01:COUNT);
  • a PV Access name keeps its pva://;
  • a loc:// PV keeps its prefix but not its type and initial value (loc://x, not loc://x<VDouble>(1.5)); a sim:// PV is copied whole, its parameters too (sim://sine(0, 10, 0.1));
  • a formula copies the PVs it reads, each named as above, not its text.

A widget copies its own PV only, not the PVs its rules read. An XY plot or strip chart, which has no PV of its own, copies its traces' PVs. Several names are separated by spaces.

On X11 bobview holds the primary selection itself, so it is lost when bobview exits, as with any X application; this works over ssh -X too. On Wayland bobview uses the compositor's data-control protocol where it has one, and XWayland's primary selection otherwise. Windows, macOS and the browser have no primary selection, so there the name goes to the clipboard only (in the browser, if it allows the page to write to the clipboard). Middle-click in the browser is untested, and the browser may start autoscrolling on it.

Display windows

A display window (one opened beside the main window; see Pinned windows and the viewer) has the native title bar only: its toolbar (Back, Forward, Reload, zoom and Home) is hidden until you show it. While the pointer is over the window, small buttons appear in the top-right corner of the display: Back (while the toolbar is hidden), the window's pin, and a chevron that shows or hides the toolbar. The right-click menu on the display's background has Show toolbar and Close. Pinned windows have 📌 before their title.

Keys

keyaction
Ctrl+Rreload
Alt+← / Alt+→back / forward
Ctrl+Wclose the window (the main window quits bobview and closes its windows)
Ctrl+Tshow or hide a display window's toolbar
shift+click on a linkopen in a new window
ctrl+click on a linkopen in place

The navigator button shows, beside the main window's display, a tree of the displays the home display links to, the displays those link to, and so on.

  • A display's links are its widgets' Open display actions, including those in groups, tabs and embedded displays, each display and macros once, named by its button's text (or, in a menu, the action's description).
  • A link with no text of its own, or the same text as another link on its display (a row of identical device symbols), is named by the display it opens once that has been read: by the words of its title that macros fill in, such as LB-VA-IONP-04 from Ion Pump Controller - $(device), else by its whole title. The tooltip gives the title, file and macros.
  • Clicking a node opens its display as a link in the main window would, so with the main window pinned it goes to the viewer; shift and ctrl work as for a link. Displays open in a window are in bold.
  • Displays are read from their files, without connecting their PVs, as their nodes are shown, so the tree is read one level ahead of what is open. A display that links back to one above it is marked ↻ and not opened again; one that can't be read is marked ⚠. A command action's display (a DLS launcher script) is found by running its script, so only when its node is opened or clicked.
  • The filter box lists the displays read so far whose name or file matches; ⟳ reads them all again.
  • Showing or hiding the navigator widens or narrows the window by its width (unless maximised or full screen), so the display keeps its size and zoom.

Status line

Shows write errors and refusals, what a copy has just put on the clipboard, and counts "N widgets with scripts ignored" and "N unsupported widgets" for a display that has them.

Browser page parameters

The web build's page takes these query parameters:

http://HOST:PORT/?display=FILE.bob&macros=P=X,R=Y&zoom=1.5
parametermeaning
displaythe display to open, a path relative to the page or a URL (default index.bob); a URL on another server must allow cross-origin requests, and is refused when the page has a write token
macrosmacros, P=X,R=Y
zooma fixed zoom factor instead of fitting the page
pvwsanother PVWS server: host[:port] (meaning wss://host[:port]/pvws/pv, or ws:// when the page is plain http: and the port isn't 443), or a full ws:// or wss:// URL; default /pvws/pv on the page's own server
tokenthe write token of a bobview-server started with --write; removed from the address once read

Without bobview-server

The web build (dist/web from scripts/build-web.sh, or share/bobview/web in a release) is a static site: serve it from any web server and point pvws at any PVWS server. The display's server must allow the page's origin (CORS) unless it is the same server.

Differences from the desktop

  • Displays have no windows of their own: every link navigates in place, and the pin and layout controls are not shown.
  • Images from PVA are not sent over PVWS yet.

bobview-testioc

A pure-Rust EPICS IOC that serves, over Channel Access and PV Access, a subset of the PVs of the epics-containers example-services BL01T beamline, enough for its opi/demo.bob screen. It has no C dependencies, so it runs natively on an aarch64 phone where the real amd64 IOC containers would need qemu. Built on the epics-rs crates; uses about 15 MB RSS while streaming images.

bobview-testioc [--only ea|motor|cam]...    # default: all three

It needs no shell and keeps running with stdin closed. --only applies to both protocols. Its environment variables are in Environment variables.

PVs

partPVshow
eabl01t:A, bl01t:B, bl01t:SUMbl01t-ea-test-01's ioc.db verbatim, A=2.54, B=2.61
motorBL01T-MO-SIMC-01:M1, :M2real motor record (motor-rs) on a simulated axis
camBL01T-DI-CAM-01:DET:, :ARR:, :PVA:soft records plus a small driver loop

The camera is not a real simDetector: it serves the ADBase / NDPluginBase records the demo screen uses (Acquire, AcquireTime, AcquirePeriod, ArrayCounter, ImageMode, NumImages, DetectorState_RBV, FilePath, ...) and counts frames at the acquire period, honouring Single / Multiple / Continuous. As in its ioc.yaml it starts acquiring continuously at AcquireTime 0.1.

PV Access

Every record is also served over PVA the way QSRV does it: NTScalar for analog and long records (with display, control and alarm metadata), NTEnum for bi / bo / mbbi / mbbo, NTScalarArray for waveforms; gets, puts and monitors all work.

BL01T-DI-CAM-01:PVA:OUTPUT is an NTNDArray like NDPluginPVA's output. The image is simulated: 1024x1024 Int8 (byteValue, pixels 0..255 stored as i8 bit patterns, as an Int8 simDetector produces), uncompressed, with drifting diagonal fringes, three gaussian spots on Lissajous paths and some noise. A frame is posted for every frame counted (10 Hz by default) while DET:ArrayCallbacks and PVA:EnableCallbacks are both on; uniqueId is the frame counter. To look at a frame without a viewer:

cargo run -p bobview-testioc --example frame_png -- /tmp/frame 1 50

Build for the phone

From x86_64:

cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.36 -p bobview-testioc

Rust API

The workspace's crates are documented with rustdoc, built into this book under api/.

Libraries:

  • bob: the display model, .bob and .opi reading, macros and project links
  • pv: PV connections over CA, PVA, PVWS and local sources
  • testioc: the test IOC

Programs, documented with their private items, since that is all they have:

The crates are on crates.io (see Install from crates.io), but their API is not stable; it is documented for contributors.

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.

Source map

The main source files, for finding your way in. See Architecture for how they fit together.

filewhat
crates/bob/src/
lib.rsDisplay and Widget: a display kept close to its XML, parsed from a file or text
opi.rstranslates a BOY .opi, element by element, into the .bob form
macros.rs$(X) and ${X} expansion
source.rswhere a display comes from (file or URL), and finding the files it names
project.rsEclipse project linked folders and DLS launcher paths
group_link.rs, menu_mux.rs, opi_file_script.rs, trend.rsBOY scripts and launchers recognised and done natively
crates/pv/src/
lib.rsPvManager, Pv and PvState: subscriptions, writes and the per-window repaint tags
ca.rs, pva.rsChannel Access and PV Access backends over epics-rs
pvws.rs, pvws_msg.rsthe browser backend over a PV Web Socket
local.rs, sim.rs, formula.rsloc://, sim:// and formula PVs
name.rs, value.rsPV names and their prefixes; values and their conversions
crates/bobview/src/
main.rsthe command line, and starting the native or browser app
app.rsBobApp: the windows, layouts, requests and repaints (see Threads, windows and repaints)
view.rsone window's display, its loading and its Back and Forward
fetch.rsreading displays from files or over http(s), and caching them
screen.rsthe screen builder: widgets with positions, macros, PVs and rules
rules.rsthe rule expression language
widgets.rspainting and interaction for each widget type, and Request
image.rs, symbol.rs, svg.rsthe image widget; symbol and picture widgets, loaded on worker threads; safe SVG parsing
plot.rs, meter.rs, stripchart.rs, axis.rsXY plots, meters, the strip chart and their shared scales
trend.rstrend windows: an archiveview Plot each
format.rsvalues as text and back, as Phoebus formats them
tree.rsthe navigator
pvmenu.rsthe PV context menu and details window
layout.rssaved window layouts
launch.rsrunning DLS launcher scripts for .opi command actions
snapshot.rsheadless rendering to PNG
crates/bobview-server/src/
main.rs, files.rs, pvws.rsthe server, its file serving, and its PVWS onto pv

Licences

bobview is Apache-2.0.

The release binaries include third-party crates, among them 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 archive (tarball or zip), generated by scripts/third-party-licenses.py.

tests/displays/demo.bob and the test IOC's ea.db come from epics-containers example-services (Apache-2.0).

These docs draw their diagrams with Mermaid (MIT), in docs/theme/mermaid.min.js.