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.
The three layers
Layer |
Entry point |
Answers |
|---|---|---|
Experiment |
|
may this run start at all? |
Device |
|
is each sub-device the device it claims, and does it answer? |
Transport |
|
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
|
Means |
A caller should |
|---|---|---|
|
the address resolved to something openable |
proceed |
|
nothing matched, and |
refuse |
|
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.