# 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](../imgs/preflight_layers.svg) ## 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`](reference/plesty/lib/traffic/preflight/index). Each answer hangs under the one that asked for it, in `parts`: ```python 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](../device/traffic_manager.md) for the transport-level detail, and `plesty/lib/traffic/apt.py` for a worked example including the device-node query.