Plesty Documentation

One Device

Start a single PLESTY device server with plesty-server and drive the instrument from a Python client.

Lesson 1 of 6. You are building the bench from nothing. The answer is on branch step-1-device if you want to check it — or start there and read.

Every instrument in PLESTY is wrapped by a device module: a Python package that knows the instrument's own protocol — its cables, its command language — and hides all of it behind ordinary Python methods.

That module is then run as a device server: a program that holds the instrument open and waits on a network port. Anything that wants a reading connects to that port and asks. This sounds like overkill on one PC, and on one PC it is; in a real lab the spectrometer is plugged into a Windows machine in the corner and the analysis runs somewhere else, so the network is there from the start rather than bolted on later.

A program that connects to a server and calls its operations is a client.

Start with one instrument: a Thorlabs PM100D, which measures how much light is falling on a sensor.

Find the module

No need to write the module. Check hub.plesty.net to find available modules that may match your needs. The hub lists every module on the platform, searchable by name and filterable by category, vendor and compliance standard.

Searching it for pm100d gives the powermeter and the four things anyone needs before using it: what it is called, where it comes from, which release, and how far it is held to the standard. Follow its docs link too — every module publishes documentation at docs.plesty.net, generated from the code so it cannot drift from what the server accepts. Functions lists the operations with their arguments, Parameters the settings with their units and limits, and Usage shows how to call them.

If nothing on the hub matches your instrument, that is when you write a module — Build a Device is that path.

Plesty Serving Platform

A module is a Python package; it does not run itself. plesty-server is the program that turns one into a running instrument — it installs modules, launches their servers, supervises them, and keeps the logs and the job history. It is the bench's own console, and it has a window.

Install it

macOS and Linux — one line:

curl -LsSf https://gitlab.com/api/v4/projects/85550357/packages/generic/plesty-server/v0.2.6/install.sh | sh

Windows — download plesty-server-v0.2.6-windows-setup.exe from the v0.2.6 release and run it. It installs per user and asks for no administrator rights.

Both do the same three things: install uv if it is missing, install the bench, and add an entry to the Start menu or Launchpad so the console opens like any other application.

Each release publishes its own installer, and the one above pins 0.2.6 — the version this lesson was written against, so the window you see matches the pictures here. When you would rather have the newest, take the installer from the latest release instead, or upgrade afterwards:

uv tool upgrade plesty-server

That is also why uv is not in the prerequisites: if you do not have it, the installer gives it to you. If you already do, this is the whole installation:

uv tool install "plesty-server[gui]==0.2.6"

The one thing the installer cannot supply on macOS or Linux is git, which the bench needs to fetch a module from a repository — as this demo does. On Windows it downloads a portable copy for itself; elsewhere, install one if git --version says you have none.

Open it

plesty-server gui

or the PLESTY Bench entry the installer added.

Install a module from the catalogue

Catalogue ▸ Search & install… (Ctrl+K) is the hub again, inside the bench:

The hub catalogue inside the console: every module, what this bench already holds, and where to install it from

Search or filter by vendor, select plesty-pm100d, choose the source — a release from PyPI or a branch from the repository — and Install module. The On bench column shows what this bench already holds; here the three modules of this demo, installed from the demo/mock-replay branch.

Installing stocks the bench. It does not decide that you want to run one — a bench may hold a module and run two instances of it, on two ports, against two instruments. Making one is the next step.

Add a device

Making an instance is Edit ▸ Add device… (Ctrl+N):

Add device: name, package, source, arguments and environment for a new instance

Field What it decides
Name The instance name — it also names the virtual environment, the log file and the process record, so it is unique per bench
Package Which installed module this is an instance of
Source A PyPI release, or a git repository at a ref
Args What the server is started with — how to reach the instrument, and anything the module takes
Env Environment for this server only, on top of the bench's own

For this lesson: name it pm100d, and the only argument it needs is --mock, which makes the module serve an in-memory simulator instead of reaching for a USB powermeter. That is why none of this needs hardware.

You do not choose a port. The bench allocates one — the lowest free port from 5551 up, skipping anything it already uses — and hands it to the server as --tcp-port when it starts, so what it probes and what the server binds cannot drift apart. Name a port in Args yourself and it takes yours instead.

(The check-box at the bottom decides whether the device starts with the rest of the bench. One device has no rest of the bench; it earns its meaning in the next lesson.)

Run it

The console main view: the device table above, the selected device's detail below

The top pane lists what this bench declares — one device, for now. Select the row and it highlights; everything below acts on it.

Column Meaning
Name The instance name
Source The package, and the version actually installed
State running, stopped, exited
Port, PID, Started Filled in while it runs

Start (Ctrl+R) launches the server and waits until it answers — the state turns running, and the port and PID appear. Stop (Ctrl+T) shuts it down. Restart does both, which is what you want after changing a device's configuration.

The table refreshes itself every few seconds, so a server that dies shows up without you asking.

Edit the device config

Edit (Ctrl+E) reopens the same form, now with two tabs:

The device config dialog: name, package, source, ref, arguments and environment

Declaration is the bench's durable record of this device — what the form above wrote down. .env is the environment file handed to its server process, where a real bench keeps an instrument's address, and where the next lesson puts real work.

What the other buttons do

Install, Update and Uninstall act on the device's virtual environment rather than its process: creating it, pulling and re-syncing it, removing it. Each runs as a job, because they are slow and can fail in ways worth reading afterwards.

Field Test (Ctrl+F) runs the module's own hardware test against the real instrument and publishes a report — the artifact behind a module's field-test count on the hub.

Remove drops the device from the bench's declarations without touching what is installed. The two red buttons are the destructive ones.

Logs and jobs

The three tabs under the buttons are where you look when something is wrong:

Tab Shows
Log That device server's own output — what it said as it connected, what it has been asked to do, and any error it raised
Jobs Installs, updates and field tests, with their state, exit code and start time; opening one shows its output
Reports Field-test reports published for this device

A device that will not start is almost always answered by the Log tab; an install that failed, by the Jobs tab. Anything the bench itself wanted to tell you appears in the strip under the buttons, and View ▸ Messages… keeps the whole session's worth — so a warning that scrolled past is not lost.

Everything the window did is also a command — plesty-server declare, install, start, status, log. The console and the command line are two faces of one bench and share the same records, so you can use either, in any order.

Start the project

The device is running, but you have nowhere to put your own code — the script that talks to it, and everything the next two lessons add. That is a project, and its layout is not hand-written either: plesty init scaffolds it, the same command every module on the platform starts from.

That command is not part of the bench. plesty comes from plesty-sdk, the development side of the platform, and installing plesty-server does not bring it — so install it once:

uv tool install plesty-sdk

Then scaffold the project:

mkdir plesty-demo-bench && cd plesty-demo-bench
uv run plesty init

If that second command reports Failed to spawn: plesty, the tool is installed but its directory is not on your PATH. uv tool update-shell adds it, and a new shell picks it up. It is also worth checking that plesty --help lists an init command: a plesty from some older install may be found first, and older ones called it build.

The name of the directory matters: plesty init takes the module name from it, dropping the plesty- prefix — so plesty-demo-bench gives you plesty/demo_bench/, which is what the later lessons import from.

There is no git init to do. The scaffold creates the repository itself and makes the first commit, so the pre-push hook it installs has somewhere to live.

That writes pyproject.toml (already depending on plesty-lib), a plesty/demo_bench/ package, tests/, docs/, the licence files and a GitLab CI configuration. Every lesson adds to that; none of it is rewritten.

The SDK is in the project's dev dependencies from here on, so inside the project it is simply part of the environment: uv run plesty check runs the compliance gates, with no --with needed, and the pre-push hook runs that same command. Fetching it by hand was only to bring the project into existence.

Talk to it as a client

The server is now the instrument, as far as the rest of the platform is concerned. Your own code goes in the package the scaffold made — plesty/demo_bench/, the same place every module on the platform keeps its code. Write plesty/demo_bench/read_power.py:

"""Read the powermeter through its device server."""

from plesty.lib.service import build_client

#: The address the bench gave the powermeter server — check it with
#: `plesty-server status`, or read it off the device table.
PM_ADDRESS = "tcp://localhost:5551"


def main() -> None:
    """Print the instrument's identity, its operations, and one reading."""
    with build_client(PM_ADDRESS) as pm:
        print("identity:", pm.identity())
        print("operations:", ", ".join(sorted(pm.describe()["methods"])))

        # An operation: something the instrument does.
        reading = pm.measure_power(averaging=10)
        print(f"power: {reading['power'] * 1e6:.3f} uW")

        # A parameter: something it holds. Correct for the excitation
        # wavelength, then read the setting back off the instrument.
        pm.write("wavelength", 780.0)
        print("wavelength:", pm.query("wavelength"), "nm")


if __name__ == "__main__":
    main()

Run it:

uv run python -m plesty.demo_bench.read_power
identity: THORLABS,PM100D,MOCK0001,1.0.0
operations: check_errors, check_operatability, clear, configure_and_measure_power,
  get_sensor_info, identity, measure_current, measure_power, query, reset,
  set_data_path, write, zero_and_measure_power, zero_sensor
power: 1.513 uW
wavelength: 780.0 nm

-m runs it as part of the package rather than as a loose file, which is how everything on the platform is started — you will meet the same form in lesson 4 as python -m plesty.demo_pol_pl. uv run installs the project's dependencies the first time, so there is nothing to activate: plesty-lib, where build_client lives, is already a dependency of the scaffold.

Now look at the other side

Four lines of output is what the client saw. Go back to the console, select pm100d, and open its Log tab:

The device server's log while the client was talking to it

Read it against the script you just ran, line by line:

In the log What your script did
Allocated resource [] for client-65d9e2… build_client(...) connected, and the server gave that client a hold on the instrument
Command '*IDN?' sent pm.identity()
Running operation measure_power(averaging=10) then 'AVER:COUN 10', 'MEAS:POW?' pm.measure_power(averaging=10) — one call became two instrument commands
Writing 'wavelength' to '780.0' then 'CORR:WAV 780.0' pm.write("wavelength", 780.0)
Querying 'wavelength' then 'CORR:WAV?' pm.query("wavelength")
Released resource [] from client-65d9e2… leaving the with block

This is the part worth pausing on. Your script said measure_power(averaging=10); the instrument was sent AVER:COUN 10 and MEAS:POW?. That translation is the entire job of a device module — the SCPI, the units, the order commands must come in — and it is what you no longer have to know at the client end. Swap the PM100D for a different powermeter and the log changes completely while the script does not.

Two other things the log tells you. The server holds the instrument for one client at a time — allocated on connect, released on exit — which is how two experiments do not talk over each other. And it kept running afterwards: the client came and went, the server did not.

What the four lines show

The reading. About 1.5 µW, and slightly different every time you ask: the simulator answers a baseline with noise on it. It has no opinion about physics — nothing is shining on anything. Making the mock answer with measured data is the next lesson.

The vocabulary is the server's, not the client's. Nothing about a PM100D is built into build_client. It asks the server what operations exist and forwards the calls, which is why an experiment can drive an instrument nobody wrote a client for. describe() is the quick answer to "what can this thing do"; the considered one is the module's documentation at https://docs.plesty.net/plesty-pm100d/, generated from the code so it cannot drift from what the server accepts.

Operations and parameters are different things, and the split matters when you come to write a module:

Operations Parameters
What Things the instrument does Settings it holds
Called with pm.measure_power(...) pm.write(key, value) / pm.query(key)
Example zero_sensor() wavelength

The same bench, without the window

Everything you did in the console is a command, which is what a bench being scripted or brought up on boot actually uses:

plesty-server declare pm100d-spare plesty-pm100d --version 0.1.0 \
    --arg --mock                       # Edit ▸ Add device…
plesty-server install pm100d-spare     # Install
plesty-server start pm100d-spare       # Start
plesty-server status                   # the device table
plesty-server log pm100d-spare -n 20   # the Log tab

declare writes the same declaration the console's form does, and prints the port it allocated. --version takes a PyPI release; --git with --ref installs from a repository.

Stop it

Stop in the window, or:

plesty-server stop pm100d

One server is the whole platform in miniature: a module found on the hub, installed on a bench, running as a process on a port, answering a client, and keeping a log.

Next: three of them at once — where a bench stops being one device, and the three of them have to agree about what is on the optical table.