Until this month, running PLESTY on a lab bench was hand work: one virtual environment and one terminal per device server, uv pins maintained by hand, field-test reports copied around by hand. Nothing in the stack was missing — the operator's tooling was. August closed that gap with a new core project that installs hub device modules, launches and supervises their device servers, runs field tests against the instruments and keeps the logs.

74
commits
0.1.0 → 0.2.6
eight releases
2
PyPI distributions
3
platforms installed

A note on names. One repository, plesty/core/plesty-bench, publishes two packages with one tag: plesty-server is the bench application (the CLI plesty-server, the Qt console, the agent) and plesty-bench is the small remote client you install on your own machine. The bench runs the server; you drive it with the client.

It is not the booking portal, and it is not a device module. It sits beside the servers it runs, on the machine the instruments are plugged into:

experiment PC
experiment /
composite device
bench PC
device server  pm100d  :5551
device server  k10cr1  :5552
plesty-server  installs · starts · stops
               probes · logs · field-tests

An experiment still talks to the device servers directly — nothing on the experiment PC needs the bench application at all. What changed is that the bench itself now has an owner.

For users Bring up a bench

Install it on the bench PC from the project's releases — the Windows setup.exe, or install.sh on macOS and Linux. Both are ~2 MB and carry no application code of their own: they install uv if it is missing, run uv tool install "plesty-server[gui]==<version>", and add the Start-menu / Launchpad entry. If you already have uv, that one command is the installation.

The installer pins the release it was built for — never latest, never a development version. A bench is exactly the release it downloaded, and uv tool upgrade plesty-server is the only thing that moves it forward.

From nothing to an instrument an experiment can reach is five commands:

bench PC · shell
# 1. A fleet file to declare into — writes an annotated fleet.yaml here.
plesty-server fleet init

# 2. Declare the instrument: a name, the hub module it runs, and whatever
#    the driver needs to find the hardware. A port is allocated (5551 up),
#    because nothing here names one.
plesty-server declare pm100d plesty-pm100d --version 0.2.1 \
    --arg --address --arg "USB0::0x1313::…::INSTR"

# 3. Stock the bench with that module: its own environment, pinned.
plesty-server module install plesty-pm100d --version 0.2.1

# 4. Start it. Installs first if nothing serves the spec, then waits until
#    the server answers `describe` — not merely until the process exists.
plesty-server start pm100d

# 5. What does the bench think is running? And if it is not, why?
plesty-server status
plesty-server log pm100d -n 50

After step 4 the instrument is reachable from an experiment PC at this machine's address on the port status reports. Nothing further happens on the bench.

Those declarations accumulate in one file — fleet.yaml, the bench's configuration. It is what the CLI writes, the console reads and writes, and the headless service replays after a reboot:

fleet.yaml
version: 1
home: ~/.plesty/server                # optional
devices:
  pm100d:                            # instance name → env, log and state key
    package: plesty-pm100d            # PyPI distribution
    version: 0.2.1                   # pinned; omit for the newest release
    # git: https://gitlab.com/plesty/hub/devices/…/plesty-pm100d.git
    # ref: exp                        # branch/tag/sha; default tag v<version>
    args: [-a, "USB0::0x1313::…::INSTR"]
    env:                             # process environment; plesty-lib reads it
      PLESTY_REPORT_ARCHIVE: /shares/<archive>/reports
    autostart: true

A git: URL installs from the repository instead of PyPI — the form used for a module that has no release yet, and the form a developer uses for a branch. Both kinds live side by side: a bench holds several versions of a module at once, one environment per (package, version), so a pinned instance keeps running the release it was verified with while a newer one is installed next to it. Rolling back is pointing the instance at the version that is still there.

After a reboot, plesty-server fleet up installs and starts every device marked autostart; fleet run keeps them up headless. And if you would rather click than type, plesty-server gui is the same presenter behind a Qt window.

For users What “supervises” actually means

Several plesty-server processes look at the same bench at the same time — a command you type, the console, the headless service, the remote agent. Each has to answer is pm100d running?, what did the install print?, why did the server die? about processes another process started, possibly yesterday. Three design decisions follow from that, and they are what you feel in daily use:

Ports are the bench's job too, because the bench is the only thing that sees all of them at once: left alone, every device would bind the library default and the second server to start would fail. declare allocates from 5551 upwards (5550 belongs to the agent) and writes the number into the fleet file, so the port a client connected to yesterday is the port it finds today. When that port turns out to be taken at start — two benches on one machine, or a server left over from an earlier run — the two cases diverge:

PortOn start, port busyWhy
Named — in the declaration's args/envRefused, and said soThat number is passed to the device process; starting elsewhere would contradict its own arguments
Allocated — chosen by the benchMoved to a free port, recorded, started thereThe number was only ever the bench's choice, and a busy port serves no client

Everything the bench writes lives under one directory~/.plesty/server by default — so a bench can be wiped or moved as a whole, and so you always know where to look:

the bench home
~/.plesty/server/
├── fleet.yaml                  the bench's fleet
├── packages/plesty-pm100d/     installed modules, one env per version
│   ├── 0.2.1/ 0.2.1.json       the environment and its record
│   └── 0.3.0/ 0.3.0.json
├── repos/plesty-pm100d/exp/    git checkouts, one per (package, ref)
├── state/pm100d.json           what the server was started with
├── logs/pm100d.log             server stdout+stderr, appended per start
├── logs/jobs/<job-id>.log       one log per install / field test
└── jobs/<job-id>.json

Every record is a small JSON file on purpose: it is the simplest thing all those concurrent processes can read, and it survives the process that wrote it.

For developers Put your device on a bench

The good news is that there is nothing to implement. A device server is launched as:

what the supervisor runs
<environment python> -m plesty.<module> <args>

— the entry point every hub device module already ships. The distribution name maps to the import path by the platform's flat-namespace convention (plesty-pm100dplesty.pm100d), so a module that follows the standard is declarable the day it exists. In practice a module needs three things to be a good bench citizen:

  1. A python -m entry point

    The scaffolded device server — what plesty init gives you.

  2. Everything the driver needs, in args or env

    Address, serial, report archive. The fleet file passes both through unchanged, and the same env is used for the server and for its field-test job — so the address and the archive come from one place.

  3. A .env.example and a release tag

    The console offers the example's keys when you declare the device. And because the registry knows a release from its tag before the package reaches PyPI, a release install that finds nothing on the index falls back to the repository at v<version> — a young module installs either way.

For developers Drive the bench from your own machine

The developer loop — install a branch, pull it again, run the field test, read the report — should not require walking over to the bench. So the bench runs an agent, and your machine gets the client:

bench PC / your machine
# on the bench — the agent listens on 5550
plesty-server agent

# on your machine — a client of a few hundred lines, and the only thing you need
uv tool install plesty-bench
export PLESTY_BENCH=lab-bench-03

plesty-bench status
plesty-bench update pm100d-dev --ref feat/drain     # job on the bench, followed here
plesty-bench field-test pm100d-dev --gates connect,drain
plesty-bench reports pm100d-dev --json
plesty-bench fetch repos/plesty-pm100d/feat-drain/reports/<instrument>/field-test.json -o .

The CLI is the API. The agent takes a plesty-server argument list and returns the exit code and the output, so anything the command line can do is remote and there is no second schema to keep in step. Three request types on the plesty-lib device-server wire (ZMQ, JSON) carry it: describe, exec and fetch — which also means any describe probe understands the agent without the client installed at all. Long work does not need streaming: a job id comes back at once and the client polls the job log by byte offset until the exit code arrives.

From Python, the same thing:

python
from plesty.bench import BenchClient

with BenchClient("lab-bench-03") as bench:
    job = bench.exec(["field-test", "pm100d-dev", "--no-wait"]).stdout.strip()
    code = bench.follow(job, print)
    report = bench.fetch(
        bench.exec(["reports", "pm100d-dev", "--json"]).json()[0]["home_relative"]
    )

Trust model: the lab network, the same as the device servers — an optional shared token, compared in constant time; fetch confined to the bench home and capped at 16 MB; exec refuses agent and gui. Where a second listening port is unwelcome, ssh <bench> plesty-server status needs neither the agent nor the client.

For developers Field tests, without walking over

A module scaffolded with plesty init field-test carries its own hardware-gated test. The bench runs it and puts the instrument back exactly as it was:

TierScriptNeedsPublishes
hosttests/field_test.pythe instrument itself — the device server must be stoppedfield-test.json
clienttests/field_test_client.pya running serverfield-test-client.json

The choreography around a host-tier run is the part worth knowing: the bench stops the server (an instrument has a single owner), runs the test as a job in the checkout with the instance's own environment, and then relaunches the server with the same arguments on the same port a client was connected to before. That is why the process record keeps the interpreter, module, args, env and port rather than just a pid. Both scripts live only in a git checkout — which is exactly why the bench installs from repositories as well as from PyPI.

For contributors The shape of the codebase

It is built as model – services – presenter – view, plus one module for everything the operating system makes different. Each layer answers one question about a piece of code: does it need the OS, the screen, or neither?

model
Data and rules only — the fleet file, device specs, installed versions, job and process records, field-test requests and reports. No subprocesses, no Qt; fully testable in memory.
services
The model's side effects: Installer, JobRunner, Supervisor, Catalogue, Agent. Model objects in, model objects back.
presenter
Bench — the choreography (install → start, stop → field test → relaunch, fleet up), driven by both the CLI and the console.
view
The Qt console on plesty-lib's palette and fonts. Renders state, emits intents, never touches a service.
host
The only module with platform branches: detaching, interrupting, killing, telling a pid is alive.

Dependencies point downwards only, and services never import each other's internals — a presenter composes them. Two conventions are worth knowing before your first merge request:

Getting a checkout running is uv sync --extra gui and uv run python -m plesty.server gui. The suite runs on Windows in CI as well as POSIX, and the project is on the quantum standard, so the usual gates apply.

For contributors Two distributions, one tag

Tagging a release publishes both packages through PyPI trusted publishing and creates the GitLab release; the split is made in the project metadata, not by a second repository.

DistributionContainsInstalled with
plesty-benchthe client — the wire and a thin CLI, on pyzmq and clickuv tool install plesty-bench (your machine)
plesty-serverthe bench application — model, services, presenter, CLI, console, agentuv tool install "plesty-server[gui]" (the bench)

The installers are the other half of the packaging story, and August spent real effort there — because a wizard is a process context of its own, and defects live in it that no other run reproduces. Both installers now have a test mode that builds the wheels from your working tree and carries them inside the installer, so a branch can be put through the real wizard before anything is tagged:

building an installer from a branch
# Windows
powershell -ExecutionPolicy Bypass -File .\packaging\windows\build-installer.ps1 -Mode Test

# macOS / Linux
./packaging/install.sh --mode=test          # --launcher-only for an install already there

The output is labelled TEST on every wizard page, so it cannot be mistaken for a release install. That mode paid for itself immediately: it is how a first install on a bare machine — no Python at all — was made to succeed in a single run, and how a class of Windows-only failures got fixed without shipping a release to reproduce them.

Where to start. The project's docs carry a page per concern — architecture, installing modules, process management, ports, cross-platform development, field tests, the command line, the console, remote benches, and packaging. The process-management and cross-platform primers assume no prior background and are the fastest way into the parts that are genuinely subtle.