Preflight: is the instrument ready to run?

A module installs, configures and serves happily with no instrument behind it. The transport logs that it found nothing, the server binds regardless, and a client holds a resource against a device that was never there. The first sign of trouble is a field test that measures nothing, twenty minutes later.

A preflight is the question asked before anything is opened — and it is one question asked at three levels, not three separate checks.

Preflight across the layers: an experiment asks its rig, the rig asks each device server, a server asks its own transport, and one nested result comes back

The three layers

Layer

Entry point

Answers

Experiment

connect_rig(), Experiment.setup()

may this run start at all?

Device

CompositeDevice.preflight()

is each sub-device the device it claims, and does it answer?

Transport

TrafficManager.preflight()

is the instrument on this machine’s bus, port or network?

The transport layer is the only one that can see hardware, and it runs on the machine the hardware is attached to — which is usually not the machine the experiment runs on. That is the whole reason the layers have to know about each other: from the control machine, “the stage does not answer” and “the stage’s server is up and its controller has no driver bound” look identical, and they need different people to do different things.

One result, nested

Every layer answers with the same type, plesty.lib.traffic.preflight.Preflight. Each answer hangs under the one that asked for it, in parts:

report = rig.preflight()

report.ready            # the whole tree resolved
report.overall          # unreachable > unknown > ready, worst wins
report.problems()       # {"hwp": "on the USB bus, no driver bound"}
print(report.summary()) # the tree, indented, for the operator

BaseDeviceSyncModel.preflight() is exposed to the device API, so the composite asks each server what its transport sees and keeps the answer as a part. A server too old to expose it simply contributes no part — upgrading this library never breaks a bench that has not been upgraded with it.

Three states, and why unknown is not a failure

Reach

Means

A caller should

ready

the address resolved to something openable

proceed

unreachable

nothing matched, and reason says what to do

refuse

unknown

this layer cannot tell without opening

proceed, and log the reason

raise_for_reach() follows that: only unreachable raises. A transport with nothing to enumerate answers unknown, and a caller that refused on it would refuse every such transport.

Driver is the part of the diagnosis the serial layer cannot reach. A controller with no driver bound, one whose driver exposes no port, and one that is unplugged all look like “no port” from pyserial; where the device node can be asked, bound / unbound / absent tells them apart, and each has a different remedy.

Serial numbers

A serial number identifies an instrument in a specific lab, and a hub repository is world-readable. summary() prints them — its reader is the person at the bench, and saying which serials are present is most of the diagnosis. Anything else goes through to_dict(), which drops them as a field and removes them from the names, resolved addresses and the address itself, because a VISA resource string carries the unit’s serial inside it (USB0::0x1313::0x8078::P0024017::INSTR). That same projection is the wire format between a server and the composite that asked, so no serial crosses a machine boundary either.

Writing one for a new transport

Overriding preflight() is optional: the base answers unknown and needs no code. A transport that can enumerate returns what it saw — resolved for what it would open, seen for everything present, reason for one sentence naming the fix. See Traffic manager for the transport-level detail, and plesty/lib/traffic/apt.py for a worked example including the device-node query.