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.
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:
→TCP (ZMQ)
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.
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:
# 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 50After 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:
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: trueA 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.
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:
- Nothing is a child. Every server and every job is started detached through a tiny launcher, so no
plesty-serverprocess owns it. Closing the terminal that started an install does not kill the install; the exit code is left on disk for whoever asks next. - Liveness is a protocol probe.
statusasks the serverdescribeover ZMQ and waits forstatus: ok— a process that exists but cannot answer is not running, and a zombie pid never reads as healthy. - Stopping is graceful first. Interrupt → wait → terminate → kill. The interrupt is what lets the driver close the instrument session cleanly instead of leaving the hardware in whatever state it was in.
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:
| Port | On start, port busy | Why |
|---|---|---|
Named — in the declaration's args/env | Refused, and said so | That number is passed to the device process; starting elsewhere would contradict its own arguments |
| Allocated — chosen by the bench | Moved to a free port, recorded, started there | The 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:
~/.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>.jsonEvery 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.
The good news is that there is nothing to implement. A device server is launched as:
<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-pm100d → plesty.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:
- A
python -mentry pointThe scaffolded device server — what
plesty initgives you. - Everything the driver needs, in
argsorenvAddress, serial, report archive. The fleet file passes both through unchanged, and the same
envis used for the server and for its field-test job — so the address and the archive come from one place. - A
.env.exampleand a release tagThe 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.
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:
# 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:
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.
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:
| Tier | Script | Needs | Publishes |
|---|---|---|---|
| host | tests/field_test.py | the instrument itself — the device server must be stopped | field-test.json |
| client | tests/field_test_client.py | a running server | field-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.
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?
Installer, JobRunner, Supervisor, Catalogue, Agent. Model objects in, model objects back.Bench — the choreography (install → start, stop → field test → relaunch, fleet up), driven by both the CLI and the console.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:
- Platform branches go in
host, nowhere else. The type-check gate pins one platform so every machine sees the result CI sees; a Windows-only attribute reached for anywhere else will fail it. - Long work is a job, not a call. Install, update and field test all return a job id immediately and run detached as a hidden command group of the same program — which is why their output lands in a log the CLI, the console and a remote client can all tail.
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.
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.
| Distribution | Contains | Installed with |
|---|---|---|
| plesty-bench | the client — the wire and a thin CLI, on pyzmq and click | uv tool install plesty-bench (your machine) |
| plesty-server | the bench application — model, services, presenter, CLI, console, agent | uv 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:
# 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 thereThe 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.