The gates prove the code. The field test proves the instrument. Every gate in

plesty check runs without hardware attached, so a module can be fully compliant and still

be unable to reach the device it was written for.

A field test drives the module against the instrument itself and writes a report that says what was verified and how it performed. It runs after the module passes its gates, and before the version that ships is tagged.

Two tiers, and why both

Tier Runs on Path exercised
host the machine the instrument is attached to the device class, in-process
client a different machine on the same network client → ZMQ → server → async wrapper → device

No experiment reaches an instrument the way the host tier does. Every one of them goes through the client tier's path, and defects live in the layers between — ZMQ sockets losing messages across threads, client resends collapsing a serialised server. Those need real network latency and do not reproduce over loopback, which is why the client tier belongs on a second machine.

A green host tier does not mean a working module.

Configuring the test

uv run plesty init field-test

An interview about the instrument: which operations are safe to call unattended, which settings survive being written back, what is slow enough to be worth interrupting. It generates tests/field_test.py and tests/field_test_client.py from the answers, each decision carried into the file with the reason it was given.

Answer for the instrument, not for the code, and pick what your experiments actually call. A parameter no experiment writes proves nothing by round-tripping cleanly.

The generated files are rewritten from the answers, so change an answer and re-run the interview — never edit the generated test.

Running it

Name the address. With no address given, the test falls back to the simulator, passes, and

publishes a report that looks exactly like a hardware run — same model, same tier, same gate

list. Check the log names a real port, and that the run took a physically plausible time.

On the instrument machine:

uv run python tests/field_test.py --address COM4     # or /dev/ttyUSB0, or a serial number

Then start the device server there and drive the client tier from another machine:

uv run python tests/field_test_client.py --address tcp://<instrument-host>:5551

Running it through a bench

Where the instrument is managed by plesty-bench, both tiers run as jobs and neither needs a person at the machine:

plesty-bench -b <host:port> ping                  # agent version, bench home
plesty-bench -b <host:port> list                  # declared devices, versions, state
plesty-bench -b <host:port> field-test <name> --tier host --address <address> --no-wait
plesty-bench -b <host:port> jobs log <job-id>     # follow it

A real host tier runs for minutes while the client's reply timeout is a minute, so --no-wait and poll rather than waiting on the call. The host tier stops a running device server first, because it needs the port itself, and relaunches it when it finishes.

The client tier reads the host tier's report to learn which operations and parameters actually reached this instrument, so fetch that report before running it:

plesty-bench -b <host:port> fetch "repos/<package>/<ref>/reports/<MODEL>/field-test.json" \
  -o reports/<MODEL>/field-test.json

The reports

A run writes three files and commits one of them:

performed, what it found wrong. This is the one you commit, and the hub reads it.

address and its serial number. Git-ignored on purpose: module repositories are world-readable. Set PLESTY_REPORT_ARCHIVE to copy them somewhere colleagues can read them.

Check the JSON for a serial or a host name before committing it.

Skipped gates are findings

A skip is the test saying the configuration does not fit the instrument, and each has a different answer:

not slow, so the gates that interrupt it have nothing to interrupt. Watch for a module that skips redundant work: a move to a position the stage already holds is slow the first time and instant on every repeat.

minimum and maximum with an empty range. That is the instrument, not the configuration.

threading modes would mean restarting the server.

What the run finds becomes an issue on the module, labelled field-test, and is fixed before the next release. That loop is the point: a field test that surfaces nothing anybody acts on has only cost the bench an afternoon.

What a green run still does not cover

Operations the interview excluded are not exercised — homing, anything that leaves the instrument changed, anything needing an argument the test would have to invent. A field test reports on the operations it was told were safe to call unattended, and says nothing about the rest.