The console is the desktop window of plesty-bench, the core project that manages a bench PC — the computer wired to the lab instruments. Until this month it managed devices. It now has an Experiments tab: create a run, fill in its settings as a form, start / pause / stop it, and watch the data arrive in the experiment's own plots — without writing a line of GUI code per experiment.

Two words used throughout: an experiment is a hub module that orchestrates several devices through a measurement plan; a run is one execution of it, which writes everything it measures into a run folder of its own (one line per finished step in records.jsonl, plus any raw files those lines name).

DeclarePick an installed experiment, or load one from a folder on disk.
Create runName it, choose a config file — copied into the run folder.
ConfigureA form built from what the experiment declares.
RunPause, Stop and Abort act at step boundaries.
WatchThe experiment's own viewer, docked in the console.

For users — running an experiment from the window

Users

Open the console (PLESTY Bench in the start menu / Launchpad, or plesty-server gui in a terminal). The Experiments tab is in front. The upper table lists one row per run, with its state (created, running, paused, completed, stopped, aborted) and progress such as 5/12 steps.

The Runs table of the Experiments tab, with a running, a completed and a created run of the pol-pl experiment
The Runs table: one row per run, with its state and progress.
  1. Create opens Create run: choose the experiment, a config file and a run id. The run is created, not started.
  2. Configure (or double-click the row) shows two tabs:
    • Experiment Config — the experiment's settings as a form: a spin box with its unit for a number, a drop-down for a choice, +/− rows for a list. Edit YAML… opens the file itself. Once a run has started its settings are read-only — Duplicate it to measure again with changes.
    • Device Setup — which benches the experiment runs across, and which device runs on which bench.
  3. The Create run dialog: experiment, repository, ref, config file and run id
    Create run: pick the experiment and its config; the run id is filled in.
    The Experiment Config tab: sample name and scan settings with units, generated from the experiment's schema
    Experiment Config: the form is generated from the experiment's own settings schema, units included.
  4. Run opens every instrument the experiment needs, then starts. Pause, Stop and Abort all wait for a step boundary — a step commits whole or not at all — so a request made during a long exposure is pending, not ignored. A stopped run offers Resume, skipping the steps already done.
  5. Four panes follow the selected run: Log (the run's output), Data (its last 50 records), Report and Visualization.

The Visualization pane

If the experiment ships a viewer, the pane shows that experiment's own plots, live, while the run goes on — the same views its author sees when running it from a terminal. If it ships none, the console draws plots of the run's recorded numbers instead.

An experiment's own viewer docked in the console: the spectrum with its fitted lines, a spectral map against half-wave-plate angle, and the fine-structure fit per line
An experiment that ships a viewer: its own spectrum, spectral map and fine-structure plots, docked in the console.
Generic plots of angle, peak wavelength and counts against step, drawn by the console for a run
Without a viewer, the console plots each recorded number against the step.
What changed this month. An experiment is installed in a Python environment of its own, not in the console's. The console now loads its viewer from that environment: it adds the module environment's packages behind its own, so the experiment and its dependencies come from there while plesty-lib, Qt and numpy stay the console's. If the views still cannot load — for example the environment uses a different Python version — the pane now says so and why, with the button to open them in their own window, instead of silently falling back to generic plots.

Operation forms send the right type

The same release fixed the device control forms the console opens: an argument is now edited according to its declared type, including optional ones such as float | None. An optional argument you leave untouched shows as device default and is left out of the call, so the device keeps its own value — previously it went out as 0, which a timeout reads as “give up immediately”.

From another machine

Alongside the tab, the console can now drive more than one bench. Each bench runs an agent — a network service guarded by a shared token. One bench is set as master and lists the others (Server ▸ Benches…); their devices appear in the same table under a Host column, and Fleet up / Fleet down reach them too. From a laptop without any window, the plesty-bench command line does the same:

uv tool install plesty-bench
export PLESTY_BENCH=lab-bench-03 PLESTY_BENCH_TOKEN=…   # the bench and its token
plesty-bench status
plesty-bench control pm100d                            # needs plesty-bench[control]

For developers — giving your experiment a form and a viewer

Developers & contributors

The console holds no code for any particular experiment. Everything it shows comes from two things your module declares.

1. The manifest: what the form is built from

An experiment built on plesty-lib describes itself in a Manifest. python -m <module> --manifest prints it without connecting anything, so the console can ask what the experiment takes before a run exists. The settings schema uses the same form as a device's parameters, which is why one form builder serves both. The viz field names the viewer module. From the polarization-PL experiment:

MANIFEST = Manifest(
    module="plesty.pol_pl",
    title="Polarization PL",
    schema=SCHEMA,                      # settings: type, unit, description
    environment=[EnvVar("SPEC_ADDRESS", "ZMQ address of the spectrometer server."), …],
    run_root=RUN_ROOT,
    viz="plesty.pol_pl.viz",            # the viewer the console docks
)

2. The viewer: Viz, RunData, Plot

The viewer API in plesty.lib.monitor was reduced this month from a stack of nine concepts to three you write. What remains:

run folderrecords.jsonl + raw filesthe experiment writes it
RunDataevery row so far; one per run, shared by all plotsyou write only reads=
Plota title and draw(data)you
drawablesLine, Scatter, Image, Scalar, Textyou choose
Vizfinds the run, holds the plots, refreshes themyou create it once
# plesty/my_exp/viz.py
import sys
from plesty.lib.monitor import Line, Viz

viz = Viz("Power scan", experiment="powerscan")

@viz.plot("Power")
def power(data):
    return [Line(x=data.column("wavelength_nm"), y=data.column("power_w"),
                 x_label="Wavelength (nm)", y_label="Power (W)")]

if __name__ == "__main__":
    sys.exit(viz.main())   # monitor / --replay / render to video
NeedUse
One field across all rowsdata.column("hwp_deg")
An array per row (a spectrum), as a 2-D mapdata.stack("counts")
Newest row that has a given fielddata.last("counts")
A row that only names a file on diskViz(..., reads=fn) — called once per row, cached
A plot that remembers state between refreshessubclass Plot, annotate draw() with Drawing
A panel that is not a plot (a table, readouts)override Plot.build() and return any Qt widget

Style travels with the values: each drawable carries its own label, color and axis labels, and color is a palette name (cyan, violet, mint, amber, rose, blue), never a hex code, so every experiment's views look alike. A single RunData per run means three plots of the same scan read the run's file once between them, and each sees every row.

How the console embeds it. The console calls load_viz("plesty.my_exp.viz"), which imports your module and expects exactly one Viz at module level, then viz.view(run) to place it in the pane. Keep the viewer importable without side effects and keep a single Viz per module, and it works both standalone and docked.

Testing it without a window

A plot is a plain function of the data, so it tests without a screen or the gui extra. Keep a small finished run — made once against simulated devices — in your tests:

from plesty.lib.monitor import Line, RunData
from plesty.my_exp.viz import power

def test_power_draws_one_point_per_step():
    data = RunData("tests/runs/powerscan_20260925-101534")
    data.poll()
    drawing = power(data)
    assert isinstance(drawing[0], Line)
Replay pacing. --replay now paces a finished run by records rather than lines of the file, so views that keep only a few records stay in step with the ones that keep them all. Rely on records, not file position, in your plots.

Contributors to the console itself: the Experiments tab lives in plesty-bench (plesty/server/view/ and plesty/server/presenter/); the viewer contract and the drawables live in plesty-lib under plesty.lib.monitor. A change to what a viewer can draw belongs in plesty-lib, so both the console and the stand-alone viewer get it.