plesty.lib.traffic.visa

VISA traffic manager for Plesty device communication.

Attributes

_MAV

_POLL_FIRST_S

_POLL_MAX_S

_INTERFACE_QUERIES

Exceptions

NoMessageError

The instrument never signalled a message, so none was requested.

Classes

VisaTrafficManager

A TrafficManager implementation for managing VISA communication.

Functions

_is_in_progress(→ bool)

Whether exc says the session still has a transfer in flight.

_resource_parts(→ list[str])

Split a VISA resource string into its non-empty :: components.

_interface_class(→ str)

Return the interface class of a resource token, without its board index.

_enumeration_query(→ str)

Return the narrowest enumeration query that can still match address.

_is_complete_resource(→ bool)

Return True when address is already a full resource string.

_normalise_component(→ str)

Normalise one address component for comparison.

_resource_matches(→ bool)

Return True when candidate is the resource requested asks for.

Module Contents

plesty.lib.traffic.visa._MAV = 16
plesty.lib.traffic.visa._POLL_FIRST_S = 0.001
plesty.lib.traffic.visa._POLL_MAX_S = 0.02
exception plesty.lib.traffic.visa.NoMessageError(address: str, command: str, waited_s: float, polls: int)

Bases: pyvisa.errors.VisaIOError

The instrument never signalled a message, so none was requested.

Deliberately a VisaIOError carrying the timeout status code: every existing caller already reads a timeout as “this instrument does not answer that”, and none of them need to change.

What it adds is a promise a plain timeout cannot make — nothing was left in flight. The read was never issued, so the device holds no unsatisfied REQUEST_DEV_DEP_MSG_IN. That is what lets the recovery ladder skip every rung: a clear and a reopen exist to rescue a stranded transfer, and here there is none to rescue.

Record what was asked, of what, and for how long.

Parameters:
  • address (str) – The VISA resource the command went to.

  • command (str) – The command that went unanswered.

  • waited_s (float) – How long MAV was polled for, in seconds.

  • polls (int) – How many status reads that took.

address
command
waited_s
polls
__str__() str

Say the instrument stayed silent, not that the link failed.

Return type:

str

plesty.lib.traffic.visa._is_in_progress(exc: pyvisa.errors.VisaIOError) bool

Whether exc says the session still has a transfer in flight.

Parameters:

exc (pyvisa.errors.VisaIOError) – The error a VISA operation raised.

Returns:

True for VI_ERROR_IN_PROGRESS. Matched on the numeric code rather than the message, which is localised by some backends.

Return type:

bool

class plesty.lib.traffic.visa.VisaTrafficManager(address: str, timeout: int = 5000, write_termination='\n', read_termination='\n', baud_rate: int | None = None)

Bases: plesty.lib.traffic.TrafficManager

A TrafficManager implementation for managing VISA communication.

This class provides methods to open, close, and communicate with VISA instruments.

Initialize with VISA address, timeout, and communication parameters.

Parameters:
  • address (str)

  • timeout (int)

  • baud_rate (int | None)

DISCOVERY_TIMEOUT_S: float = 10.0
OPEN_TIMEOUT_S: float = 15.0
write_termination = Multiline-String
Show Value
"""
"""
read_termination = Multiline-String
Show Value
"""
"""
baud_rate = None
rm = None
recovery_enabled = True
stb_gated_queries = True
_mav_proven: bool | None = None
_opened_in_progress = False
_ever_answered = False
discover_instrument(address: str) list[str]

Resolve address to a full VISA resource string.

Discovery is skipped entirely when the address is already complete — there is nothing to look up, and every enumeration is a chance to hang.

Otherwise the scan is filtered to the address’s own interface. An unfiltered list_resources() probes every ASRL, GPIB and TCPIP resource on the machine, including serial ports another device server already holds open; that call blocks inside the native VISA library and does not deliver KeyboardInterrupt, so the process can only be killed. A USB instrument is found by asking for USB instruments.

The scan also runs under a watchdog, so a wedged device on the bus turns into an actionable error rather than a frozen process.

Parameters:

address (str) – The VISA resource address to resolve. May be a complete resource string or a partial one such as "USB::0x1313::0x8078::INSTR".

Returns:

every failure raises, so a caller that gets a value has a resource to open. The first entry is the full, serial-bearing string and is logged, so it can be passed straight back as --address to skip discovery on the next run.

Return type:

The matching resource strings, most specific first. Never empty

Raises:
  • RuntimeError – If the bus was scanned but nothing matched — the address is wrong or the instrument is not connected — or if the VISA backend itself could not be scanned.

  • TimeoutError – If enumeration hung, which means a device on the bus is wedged. Kept distinct from “not found”, because a wedged bus and a wrong address need completely different responses.

_list_resources(query: str) list[str]

Enumerate resources matching query, under a watchdog.

list_resources runs in native code: when a device on the bus is wedged it can block forever, and no Python-level interrupt reaches it. Running it on a daemon thread means the caller gets a diagnosable timeout instead, and the interpreter can still exit.

Parameters:

query (str) – A VISA resource query such as "USB?*::INSTR".

Returns:

The resource strings found.

Raises:

TimeoutError – If enumeration did not finish within DISCOVERY_TIMEOUT_S.

Return type:

list[str]

_load_resource_manager()

Load the VISA Resource Manager, attempting to find the correct DLL on Windows.

_open()

Connect to the VISA instrument at the specified address.

_clear_on_open() None

Clear the device once the session is open, before any command.

A fresh USBTMC session inherits whatever state the previous one left: on the lab PM100D the first query after every open timed out and then succeeded on the retry that follows a device clear — reproducibly, for *IDN? and for the first parameter read alike. Clearing here costs one control transfer and removes a five-second stall from every connect, instead of leaving the recovery ladder to absorb it and hide that it is happening.

Best effort: an instrument that does not support the clear is no worse off than before.

One failure here is not best-effort news at all, and is reported as such. VI_ERROR_IN_PROGRESS on a session opened moments ago cannot describe anything this process did — a fresh session has issued no operation to still be in progress. It means the instrument was already holding a stranded transfer when the session opened, and that state outlives the session, the process and a driver-level re-enumeration. It is the earliest and clearest evidence available that what follows will be a wall of timeouts, and it is worth more than the wall.

Return type:

None

_note_open_in_progress(exc: pyvisa.errors.VisaIOError) None

Record that the clear-on-open found a transfer in flight.

This was previously reported at error level as a finished diagnosis: the instrument was wedged before the session existed, every command would time out, and only a power cycle would help. That reading does not survive contact with the logs. On one PM101R it was the opening state of ten of twelve consecutive starts, and connect then succeeded on all twelve — identity, sensor query and limit reads all answered normally. A message that tells someone to unplug a working instrument, at error level and with certainty, spends the credibility that the genuine remedy needs.

So the condition is recorded rather than interpreted. It is a real observation and worth keeping — a wedged instrument does present this way — but it is not evidence on its own, because a healthy one presents this way too. _diagnose_if_never_answered() states the diagnosis later, once a command has actually failed and nothing on the session has ever been answered, which is when the two cases finally separate.

Parameters:

exc (pyvisa.errors.VisaIOError) – The VI_ERROR_IN_PROGRESS the clear raised.

Return type:

None

_diagnose_if_never_answered(exc: pyvisa.errors.VisaIOError) None

Report a pre-existing wedge, once there is evidence for one.

Two facts together mean the instrument was unreachable before this session opened, and neither means it alone: the clear-on-open found a transfer in flight, and nothing on this session has ever been answered. A session that has answered even once was talking to a live instrument, so a later failure is an ordinary fault for the recovery ladder. A session that has never been answered and opened onto a transfer it did not create was talking to a wedged one from the start.

Said at error level because at that point it is a diagnosis rather than a symptom, and the remedy is spelled out because the two obvious attempts both fail: restarting rebuilds a session against the same stuck endpoint, and a driver-level re-enumeration (Windows: Disable-PnpDevice/Enable-PnpDevice, pnputil /restart-device) resets the port without ever dropping bus power, so a transfer held in the instrument’s own firmware rides straight through it. Both were tried on a wedged PM101R and both failed.

Parameters:

exc (pyvisa.errors.VisaIOError) – The error the failing command raised.

Return type:

None

_open_resource(address: str)

Open a VISA session on address, under a watchdog.

Opening a USBTMC session is not a local operation: it talks to the instrument, so a wedged device blocks here exactly as it blocks an enumeration — inside native code no Python interrupt can reach. This is the last unguarded call on the connect path.

Parameters:

address (str) – A full VISA resource string.

Returns:

The opened pyvisa resource.

Raises:
  • RuntimeError – If the resource manager is not loaded.

  • TimeoutError – If opening did not complete within OPEN_TIMEOUT_S.

_close()

Close the VISA connection.

Clearing first discards any buffered response so the next session does not inherit it — but an instrument that is not answering can refuse the clear, and a session that cannot be cleared still has to be closed. So the clear is best effort, and the close goes through _release_session(), which falls back to the underlying handle when the session itself refuses. A device server shutting down on a wedged instrument otherwise leaves the interface claimed until the process dies.

_send_command(command: str, timeout=None, mode=None) str | bool

Send a command to the VISA instrument and return the response.

A VisaIOError triggers in-place recovery before the error ever reaches the caller: first a buffer discard and a USBTMC device clear (which unwedges most stalled interfaces), then a full session reopen — the command is retried after each rung. Plain SCPI writes and queries are idempotent, so the retries are safe. Only a third consecutive failure propagates. Physical intervention (replugging, power-cycling) must never be the documented remedy for a wedged instrument.

VI_ERROR_IN_PROGRESS skips straight to the reopen. It means the session still has a transfer in flight — a read the instrument never answered and the backend never abandoned — and every operation on such a session reports it, the clear included. Retrying there cannot succeed: on a PM101R with no sensor head it turned one timeout into a ladder that reported the wrong fault at every rung and burned a second timeout to learn nothing. Only a new session clears the condition.

Set recovery_enabled to False around an optional query. MIN/MAX limits are per-parameter optional in SCPI, and an instrument that does not implement one simply never answers: escalating that to a device clear and a session reopen costs three timeouts and a teardown to learn something the instrument was entitled not to support.

Parameters:

command (str)

Return type:

str | bool

_answered(result)

Record that the instrument responded, and pass result through.

The flag it sets is what stops a pre-existing wedge being diagnosed from a single failure: an instrument that has answered once was demonstrably reachable, so a later error is an ordinary fault rather than evidence it was stuck before this session opened.

Parameters:

result – Whatever the operation returned.

Returns:

result, unchanged.

_discard_buffers() None

Throw away whatever the last exchange left in the session buffers.

A timed-out query leaves its answer half-arrived: the instrument may still deliver it, and the backend may still hold part of it. Either remnant becomes the reply to the next command — a wavelength read answered with the previous power reading, which is worse than the timeout that caused it. The discard costs nothing and runs before the clear, which reaches the instrument but not the host-side buffers.

Best effort: a backend that does not implement flushing raises, and a session that cannot be flushed is no worse off than before.

Return type:

None

orphan(command: str) bool

Transmit command without reading its answer.

Parameters:

command (str) – A command the instrument will answer, so that the answer is the one left unread.

Returns:

True once transmitted.

Return type:

bool

_effective_timeout(timeout: int | None) int

Return the timeout to use, honouring any cap.

Parameters:

timeout (int | None) – A per-call timeout, or None for the session default.

Return type:

int

_guarded(mode: str, command: str)

Run one VISA operation.

This carried a watchdog: a timer that fired at twice the session timeout and issued a device clear from its own thread, on the theory that viRead can hang in native code past its own timeout and an asynchronous clear is the documented way to abort it.

It was removed because it cannot work. The watchdog only fires while the calling thread is still inside the read — that is the only situation it exists for — and VISA refuses a clear on a session another thread holds. Against one instrument it fired five times in a day and failed with VI_ERROR_RSRC_LOCKED every time, leaving a failed clear pending at the device; twice that took a USB re-enumeration to undo. It reliably produced the wedge it was meant to escape.

What does work is the recovery ladder in _send_command(): its clear runs on the calling thread, after the read has returned, and it has rescued every stall observed so far.

Parameters:
  • mode (str)

  • command (str)

_can_gate() bool

Whether this session can be asked for a status byte before reading.

Gating splits query into its three parts, so a resource that only offers the combined call cannot be gated. Checked before the write rather than caught after it: committing the command and then discovering the session cannot report status would leave exactly the stranded transfer this exists to prevent.

Returns:

True when write, read and a status read are all available. A resource without them keeps the unguarded path, which is what every interface did before this guard existed.

Return type:

bool

_query_when_ready(command: str) str

Write command, then read only once the instrument says it can answer.

This exists because of what an abandoned read costs. A USBTMC query is two exchanges: the command out, then REQUEST_DEV_DEP_MSG_IN and a bulk-in read. An instrument is entitled to stay silent — an undefined header is answered with -113 in the error queue and no response at all — and when it does, the read times out and we walk away while the device still holds the request we made. Nothing in the recovery ladder retracts it: viClear cannot be queued behind a pending bulk-in and so reports VI_ERROR_IN_PROGRESS, and reopening the session replaces a host-side handle without touching the device. Five such queries in one session stopped a PM101R and a PM400 servicing USB-TMC at all, and only a power cycle brought them back.

Polling MAV first removes the cause rather than recovering from it: if the instrument has no message, no read is issued, and there is nothing to strand. Measured on a PM101R with an S155C, MEAS:ENER? — one of the five that wedged it — became a four-second non-event, the status byte sitting at 0x04 (EAV: error queued, no message) across forty polls, with the next three queries answering in single-digit milliseconds.

The guard will not fire until MAV has been seen at least once on this session. A status byte that never sets MAV is ambiguous — the instrument may have nothing to say, or may simply not maintain the bit — and only attempting the read distinguishes them. So the first such query falls back to reading, which is exactly today’s behaviour and no worse; if that read succeeds, the instrument is not using MAV and gating switches off for the rest of the session.

Parameters:

command (str) – The query to send.

Returns:

The instrument’s answer.

Raises:

NoMessageError – If MAV never set within the session timeout and the bit is known to work — the instrument is not going to answer, and nothing was left in flight.

Return type:

str

_reopen() None

Tear down the wedged session and open a fresh one — no discovery.

Deliberately avoids _open: its list_resources() discovery enumerates every USBTMC device and is exactly the call that can hang indefinitely while one of them is wedged. The address is already known-good from the original open, so the fresh session targets it directly — but through the same watchdog _open uses, because opening talks to the instrument and this rung runs precisely when the instrument is not answering.

Return type:

None

_release_session() None

Give up the current session, whether or not it consents.

close() is itself an operation on the session, so the one session that most needs closing — one with a transfer in flight — is the one that refuses. Dropping the reference there does not free it: the backend keeps the handle, the instrument keeps the interface, and the next reopen adds a second live session to the same device. Two rungs of the ladder left a PM101R holding three, at which point *IDN? stopped answering on all of them.

So a refused close falls back to closing the underlying VISA handle directly, which does not go through the session and so cannot be blocked by its state.

Return type:

None

plesty.lib.traffic.visa._INTERFACE_QUERIES: dict[str, str]
plesty.lib.traffic.visa._resource_parts(address: str) list[str]

Split a VISA resource string into its non-empty :: components.

Parameters:

address (str)

Return type:

list[str]

plesty.lib.traffic.visa._interface_class(token: str) str

Return the interface class of a resource token, without its board index.

"USB0" and "USB" both yield "USB", which is what makes a shipped default address comparable with an enumerated resource.

Parameters:

token (str)

Return type:

str

plesty.lib.traffic.visa._enumeration_query(address: str) str

Return the narrowest enumeration query that can still match address.

Falls back to "?*::INSTR" for an interface we do not recognise, which is still narrower than an unfiltered scan.

Parameters:

address (str)

Return type:

str

plesty.lib.traffic.visa._is_complete_resource(address: str) bool

Return True when address is already a full resource string.

A complete address carries a board index (USB0, not USB) and, for USB, the instrument serial. Such an address can be opened directly, so no enumeration is needed — and the enumeration that is skipped is the call that hangs on a wedged bus.

Parameters:

address (str)

Return type:

bool

plesty.lib.traffic.visa._normalise_component(value: str) str

Normalise one address component for comparison.

Vendor and product ids appear as 0x1313 in a shipped default and as 0x1313 or its decimal form in an enumerated resource depending on the backend, so numbers are compared by value and everything else casefolded.

Parameters:

value (str)

Return type:

str

plesty.lib.traffic.visa._resource_matches(requested: str, candidate: str) bool

Return True when candidate is the resource requested asks for.

Compares component by component rather than by substring. The substring test cannot work for the shipped defaults: "USB::0x1313::0x8078::INSTR" is not a substring of "USB0::0x1313::0x8078::P0001234::INSTR" — the board index and the serial segment both break it — so every VISA module’s default address failed to match a device that was plainly connected.

A requested component sequence matches when it appears in order in the candidate, which lets a serial-less address match the serial-bearing resource while still rejecting a different vendor or product id.

Parameters:
  • requested (str)

  • candidate (str)

Return type:

bool