Until this month, a device started from a bench saved into its server's working folder. The run's journal sat on one disk, its raw files on another, and its log in the bench's home directory. Now every bench knows the lab's shared disk. Each device writes into that bench's folder on it, and the run folder holds everything about the measurement: configuration, data, log, report, who took it and why. Any machine can open it, and it is still there after the bench that recorded it is uninstalled.
Three folders, one name each
The bench passes each device three environment variables. A device module reads them and never has to work out a path of its own:
The lab's shared disk, spelled the way this machine mounts it — a drive letter on Windows, a mount point on macOS or Linux.
This bench's folder inside the share. It is created when missing, and every device the bench starts writes below it.
The device's default folder: its fleet data_dir, or tmp/<device> when the fleet names none.
Paths are stored relative to the share, never as absolute paths. Each machine mounts the share somewhere different, so a path in a run's records only means something once the reading machine joins it to its own mount point.
For users: set it up once per bench
1 · Tell the bench where the share is
The Windows installer has a page for it, and you can skip it and set it later. In the console it is Server ▸ Configure… (Plesty share and Bench folder). From the command line:
# this bench
plesty-server data-path SHARE --folder FOLDER
plesty-server data-path # show what is set
plesty-server data-path --clear
# another bench, from your machine
plesty-bench -b BENCH data-path SHARE --folder FOLDER
The share must be mounted. The folder is a path inside it and is created if it does not exist. A fleet data_dir that is absolute or uses .. to climb out of the folder is refused. Before a share is set, a bench still gives its devices a folder under its home, data/, so a single PC with no share can still run the demo.
.env and the fleet's env: still override the folder the bench picks. An experiment's setup.yaml no longer does: where a device writes is decided by the bench that runs it.2 · Create a run: say who and why
Create run now asks for an Operator, which is required and pre-filled with whoever created the last run on this bench, and an optional Note. Both are stored in the run folder next to the data:
- the operator in
created.json; - notes in a plain
notes.txt, one entry per note with its time and author. Experiment ▸ Add note… appends one during the run or after it has ended.
The run table has two new columns, Operator and Note (the first line of the latest note). A duplicated run keeps the original's operator.
3 · Name it, configure it, without touching the module
- Rename before it starts. Double-click a run's row and the configuration dialog opens with its Run name first. When you save a new name, the run folder moves along with its configuration. A name an earlier run already used is refused, so that run's log can never be shown as this one's. Once a run has started its name is fixed, because its journal and data are written under it.
- Configure saves to the bench, never into the module. Configuring a run writes that run's
config.yaml. Configuring an experiment with no run selected writes the bench's own copy, which new runs start from. The installed module's checkout changes only through its repository. Saving rewrites only the values you edited, one line each, so comments and layout in the file survive.
4 · Read the log anywhere, after anything
When a run's job ends, however it ends, the bench copies the job's log into the run folder as job-logs/<job>.log, next to the report. When the bench showing a run has no job for it, the Log pane reads that copy. So the log is there for a run measured on another bench, or after an uninstall emptied the bench home. Runs measured before this version have no copy.
What a run folder holds
| File | What it is |
|---|---|
plan.json | The frozen plan and configuration the run started with |
journal.jsonl | The step-by-step history, appended while the run goes |
records.jsonl · data/ | The experiment's records and the data they point to |
raw/<device>/ | Files each device wrote, routed there by the run |
created.json · notes.txt | Who created the run, and the notes added to it |
job-logs/ · report | The run's own log and its measurement report |
from plesty.lib.experiment import Run, latest_run
run = Run(latest_run("/mnt/plesty/pol-pl", name="pol_pl"))
cfg = run.config # what it was run with
for record in run.records():
path = run.local(record["data_file"]) # share-relative → this machine's mount
Run.local finds the share from data_root=, then PLESTY_DATA, then the path the recording machine wrote in the journal, if that path exists here.
A ~ in PLESTY_DATA or a run root now means this machine's home directory (plesty-lib 0.6.0). Before, it was taken literally and the run landed in a folder named ~.
For developers: route devices, record share-relative paths
plesty-lib 0.6.0 changes the device and experiment side to match. The changes are breaking. Every item below is in the plesty-lib changelog with its migration.
A device writes only inside its bench folder
set_data_path(path) now takes a path relative to DEVICE_DATA_ROOT. It refuses an absolute path, a drive letter or ~, collapses .. and refuses anything that resolves outside the folder. It returns the folder relative to the share, for example bench-a/pol-pl/<run>/raw/cam, rather than an absolute path. If you subclass BaseDeviceSyncModel you get this without writing anything: take the three variables from the environment and let the base class check paths.
An experiment routes its devices, then records what they answered
async def setup(self) -> None:
await super().setup()
if self.raw_dir is not None: # None when this computer has no PLESTY_DATA
# replaces devices.set_data_path(self.raw_dir, subdirs=True, devices=[...])
self.route(self.devices, ["spec"])
# in a step: keep the device's file as a share-relative path, and return it to be recorded
data_file = self.raw_file("spec", reported_path) # → bench-a/pol-pl/<run id>/raw/spec/<name>
route()sends each named device toraw/<name>under the run and keeps the folder each one answered. Call it insetup(), because a resumed run callssetup()again.raw_file(device, reported)keeps only the file name from what the device reported (Windows or POSIX spelling) and joins it to that device's routed folder. It raises an error for a device that was not routed in this run.Setup.device_environment()has been removed. It gave a device the whole share as itsDEVICE_DATA_PATH, and the bench now sets that variable itself.
set_data_path returns as relative to the share, and replace any call to set_data_path(self.raw_dir, …) in an experiment with self.route(...) in setup(). The plesty-bench 0.5.0 line requires plesty-lib>=0.6.0.plesty-lib 0.6.1 makes a run's end match its record. A run the operator stopped used to print "Run completed" and exit 0. Now python -m <module> ends the way the run's journal does: a canceled run prints "Run canceled" and exits 130, as Ctrl-C does, and an aborted run prints "Run aborted" and exits 1. Both name the --resume that continues it, and @cli.finished runs only for a completed run.
Trying it without a share
To test the whole flow without a share, set PLESTY_DATA to a temporary folder and DEVICE_DATA_ROOT to a folder inside it. Then set_data_path("run-1/raw/cam") returns <bench folder>/run-1/raw/cam, and set_data_path("/abs") raises ValueError. The full guide is plesty-lib's Experiment ▸ Shared disk page.