plesty.lib.traffic.preflight ============================ .. py:module:: plesty.lib.traffic.preflight .. autoapi-nested-parse:: What a transport can say about an instrument before it opens it. Discovery already knows why it failed and throws the answer away: the APT transport composes the ports present and the APT-capable candidates into a log line and returns ``False``, so nothing above the transport can report or act on it. The server binds anyway, and a client holds a resource against an instrument that was never there. A preflight returns those findings instead of logging them, and it is **one type for all three layers**: an experiment asks its rig, the rig asks each sub-device, and a sub-device's server asks its own transport on the machine the instrument is attached to. Each answer hangs under the one that asked for it, in :attr:`Preflight.parts`, so "no answer from the stage" and "the stage's server is up and its controller has no driver bound" are told apart by the experiment rather than by whoever is standing at the bench. Every transport answers the same three questions, in :class:`Preflight`: * Did the address resolve to something concrete, and to what? * What else is present that it could have resolved to? * Where the transport can tell, did the operating system bind a driver at all? The third is the one that cannot be answered from the serial layer. On the bench that prompted this, the stage sat on the USB bus with a failed driver install: there was no port to enumerate, so every layer above could only report that no candidate existed, which reads as an absent instrument and sends the operator looking for the wrong thing. :class:`Driver` is where a transport that can ask the device node says so. Nothing here opens a port. Identification uses descriptor metadata and enumeration only, for the reason :meth:`~plesty.lib.traffic.apt.AptTrafficManager.discover_instrument` gives: opening ports to find out what they are means opening one another device server holds, and on this platform that has hung processes uninterruptibly. **Serial numbers.** A serial number identifies a specific instrument in a specific lab, and a hub repository is world-readable. It is carried in :attr:`Candidate.serial` for the operator, printed by :meth:`Preflight.summary` where the operator is the only reader, and dropped by :meth:`Preflight.to_dict`, which is the projection anything written to a file goes through — the rule :mod:`plesty.lib.test.report_artifact` applies to the published field-test report. Exceptions ---------- .. autoapisummary:: plesty.lib.traffic.preflight.PreflightError Classes ------- .. autoapisummary:: plesty.lib.traffic.preflight.Reach plesty.lib.traffic.preflight.Driver plesty.lib.traffic.preflight.Candidate plesty.lib.traffic.preflight.Preflight Functions --------- .. autoapisummary:: plesty.lib.traffic.preflight.usb_driver_state Module Contents --------------- .. py:class:: Reach Bases: :py:obj:`enum.StrEnum` Whether the instrument can be reached, as far as the transport knows. Initialize self. See help(type(self)) for accurate signature. .. py:attribute:: READY :value: 'ready' The address resolved to something this transport can open. .. py:attribute:: UNREACHABLE :value: 'unreachable' Nothing matched, and :attr:`Preflight.reason` says what to do about it. .. py:attribute:: UNKNOWN :value: 'unknown' This transport cannot tell without opening the connection. Not a failure. A transport with no enumeration to offer answers this, and a caller that refuses to serve on it would refuse every such transport. .. py:class:: Driver Bases: :py:obj:`enum.StrEnum` What the operating system has done with the device, where it can be asked. Initialize self. See help(type(self)) for accurate signature. .. py:attribute:: BOUND :value: 'bound' the device has the port or resource it should have. :type: A driver is bound .. py:attribute:: UNBOUND :value: 'unbound' The device is on the bus and the OS bound no driver to it. The one state the serial layer cannot see, and the one whose remedy is a driver install rather than a cable. .. py:attribute:: ABSENT :value: 'absent' unplugged, powered off, or on another host. :type: No such device is on the bus .. py:attribute:: UNKNOWN :value: 'unknown' The question could not be asked here, so nothing is claimed either way. .. py:class:: Candidate One port or resource the transport enumerated. :ivar name: What the transport would open — ``"COM9"``, ``"/dev/ttyUSB0"``, ``"USB0::0x1313::0x8078::P0024017::INSTR"``. :ivar description: What the OS or the backend calls it, when it says. :ivar serial: The instrument serial number, where the descriptor carries one. Operator-facing only; :meth:`Preflight.to_dict` drops it. :ivar capable: Whether this could be the protocol's instrument at all. An APT controller is behind an FTDI bridge, and so is an unrelated USB adapter, so a capable candidate is a shortlist entry and not a match. .. py:attribute:: name :type: str .. py:attribute:: description :type: str :value: '' .. py:attribute:: serial :type: str :value: '' .. py:attribute:: capable :type: bool :value: False .. py:method:: to_dict() -> dict[str, Any] Return the publishable mapping: everything but the serial number. The serial is removed from :attr:`name` and :attr:`description` as well as dropped as a field. A VISA resource string carries the unit's serial as one of its components — ``USB0::0x1313::0x8078::P0024017::INSTR`` — so a projection that only dropped the field would publish it anyway. .. py:exception:: PreflightError Bases: :py:obj:`RuntimeError` The instrument cannot be reached, with the reason in the message. Initialize self. See help(type(self)) for accurate signature. .. py:class:: Preflight What one transport found when it looked for an instrument. :ivar protocol: The transport that produced this — ``"apt"``, ``"visa"``, ``"serial"``, ``"tcp"``. :ivar address: The address that was asked for; empty means "any instrument this transport can drive". :ivar reach: Whether the address resolved. See :class:`Reach`. :ivar driver: What the OS did with the device, where it could be asked. :ivar resolved: The concrete addresses to open, best match first. Empty unless :attr:`reach` is :attr:`Reach.READY`. :ivar seen: Everything the transport enumerated, matching or not. This is the half of a failure that says what to try instead. :ivar backend: The library doing the enumerating, where there is a choice of one — the VISA backend, for instance. Empty where there is not. :ivar reason: One sentence saying what is wrong and what to do about it. Empty when :attr:`reach` is :attr:`Reach.READY`. Carries no serial number, so it is safe wherever :meth:`to_dict` is. :ivar stalled: Whether the probe itself never finished. A bus with a wedged device on it stalls enumeration, and the instrument asked for may be perfectly healthy behind it, so this failure means "power-cycle the bus" where every other one means "the address is wrong". :ivar name: What this result is *of*, when it is one of several: the sub-device name in a rig, empty for a result standing alone. :ivar parts: The preflights this one is composed of. A rig's parts are its sub-devices; a sub-device's part is what its own transport reported from the machine it runs on. This is what makes the three layers one answer instead of three: an experiment asking its rig gets back the bus the stage is missing from, not merely "no answer". .. py:attribute:: protocol :type: str .. py:attribute:: address :type: str .. py:attribute:: reach :type: Reach .. py:attribute:: driver :type: Driver .. py:attribute:: resolved :type: tuple[str, Ellipsis] :value: () .. py:attribute:: seen :type: tuple[Candidate, Ellipsis] :value: () .. py:attribute:: backend :type: str :value: '' .. py:attribute:: reason :type: str :value: '' .. py:attribute:: stalled :type: bool :value: False .. py:attribute:: name :type: str :value: '' .. py:attribute:: parts :type: tuple[Preflight, Ellipsis] :value: () .. py:property:: overall :type: Reach The reach of this result and everything under it, worst first. One unreachable part makes the whole unreachable: a rig missing a stage is not a rig. An unknown part leaves the whole unknown, because a layer that could not look has not established that anything is there — and saying "ready" on its behalf is exactly the claim that let a server bind against an instrument that was never there. .. py:property:: ready :type: bool Whether this and every part of it resolved. .. py:property:: matched :type: str | None The address that will be opened, or None when nothing resolved. .. py:method:: problems() -> dict[str, str] Return the named parts that are unreachable, and why. The shape the device layer answered in before it spoke this one — sub-device name to a one-line problem — kept because it is what an operator prompt and a log line actually want, and because a caller that only needs "what is broken" should not have to walk a tree. .. py:method:: summary() -> str Return the operator-facing lines, most useful first. This is the one place a serial number is printed: the reader is the person at the bench, and telling them which serials *are* present is most of the diagnosis when the one they asked for is not. Parts are indented under the result they belong to, so a rig prints as one page: the sub-device that failed, and under it what its own transport saw on the machine it runs on. .. py:method:: to_dict() -> dict[str, Any] Return the publishable mapping of this result. Every serial number is dropped, here and in each candidate: this is the projection for anything written to a file in a repository that is world-readable. Port names, resource strings and host names remain — the module's own configuration already carries them — so a caller publishing an artifact still passes it through that artifact's own projection, as :mod:`plesty.lib.test.report_artifact` does. .. py:method:: from_dict(document: dict[str, Any]) -> Preflight :classmethod: Rebuild a result from :meth:`to_dict`. This is the wire format: a device server answers its own transport preflight as a mapping, and the composite that asked rebuilds it and hangs it under the sub-device it belongs to. What comes back has been through :meth:`to_dict`, so it carries no serial numbers and the candidates it rebuilds have none either. :param document: A mapping as :meth:`to_dict` produces. Missing keys take their defaults, so a server running an older version — which answers fewer of them — still produces a usable result. .. py:method:: raise_for_reach() -> Preflight Return self, raising when this or any part of it is unreachable. :attr:`Reach.UNKNOWN` does not raise. A layer that could not look has not found a problem; it has found nothing, and refusing on that would refuse every transport that cannot enumerate. :raises PreflightError: :attr:`overall` is :attr:`Reach.UNREACHABLE`; the message is :meth:`summary`, parts included. .. py:method:: _scrub(text: str, serials: list[str]) -> str :staticmethod: Return *text* with every serial this report saw taken out of it. A resolved VISA address is a resource string with the unit's serial in it, and the address asked for may be the same string. Both are removed by the serials the enumeration reported, which is the only way to know what in an opaque address string is the identifying part. :param text: The address or resolved name to clean. :param serials: Every serial number :attr:`seen` carries. .. py:method:: _describe() -> str Return the candidates as one line, serial numbers included. .. py:function:: usb_driver_state(hardware_id: str) -> Driver Return what the USB bus says about the device matching *hardware_id*. The serial layer cannot distinguish the three states that need three different fixes, because all of them look like "no port": a device with no driver bound, a device whose driver is bound but exposes no port, and a device that is not plugged in at all. The device node knows which, so it is asked directly — on Windows, which is where the question has an answer that can be had without root. :param hardware_id: A PnP hardware id prefix, such as ``"USB\\VID_0403"``. :returns: :attr:`Driver.BOUND` when the device is present and healthy, :attr:`Driver.UNBOUND` when it is present in a fault state, :attr:`Driver.ABSENT` when no such device is on the bus, and :attr:`Driver.UNKNOWN` where the platform cannot be asked — on which the caller claims nothing about the driver either way.