Skip to content

Commit

Permalink
feat(errors): add EquipmentDriverError
Browse files Browse the repository at this point in the history
feat(errors): add CommunicationError
try:
    whatever()
except CommunicationError as e:
    print(f"High-level error: {e}")
    print(f"Communication error: {e.__cause__}")
    print(f"Original VISA error: {e.__cause__.__cause__}")
this allows you to catch the original error using __cause__

feat(retry): added _do_with_retry to core.
if max_retries is set this will rety communications automatically.
refactor(visa): refactor visa methods to use retry.
This moves all of the try-except blocks to one spot, a nice change!

feat(clear): add clear_status to ResourceCollection
allows the equipment.config to clear the equipment during init.

feat(clear): connect_resources uses clear during init if present

feat(error): connect_resources raise from error to allow diagnostic

refactor(lint): cleanup line lengths
docs(lint): get_error docstring

fix(trigger): HP_34401A trigger setup string was incorrect
not sure if this was working before, but the whitespace was needed
Using mixed 34401A's with 34461A's complained without it.

refactor(lint): resp_format line length and whitespace

docs(lint): keithley_2231A docstrings

fix(measure): keithley_2231A measure_current fails occasionally
With newer meter types such as BK9140 _update_channel causes the next response to be an empty string
  • Loading branch information
TedKus committed Oct 25, 2024
1 parent 32bcfd5 commit 43f557b
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 49 deletions.
62 changes: 26 additions & 36 deletions pythonequipmentdrivers/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import warnings
import pyvisa

from pythonequipmentdrivers.errors import ResourceConnectionError
from pythonequipmentdrivers.errors import (ResourceConnectionError,
CommunicationError)

# Globals
rm = pyvisa.ResourceManager()
Expand Down Expand Up @@ -112,8 +113,10 @@ class VisaResource:

idn: str # str: Description which uniquely identifies the instrument

def __init__(self, address: str, clear: bool = False, **kwargs) -> None:
def __init__(self, address: str, clear: bool = False,
max_retries: int = 0, **kwargs) -> None:
self.address = address
self.max_retries = max_retries

default_settings = {
"open_timeout": int(1000 * kwargs.get("open_timeout", 1.0)), # ms
Expand All @@ -134,6 +137,18 @@ def __init__(self, address: str, clear: bool = False, **kwargs) -> None:

self.timeout = int(1000 * kwargs.get("timeout", 1.0)) # ms

def _do_with_retry(self, operation, *args, **kwargs):
"""Helper method to execute operations with retry logic"""
attempts = 0
while attempts <= self.max_retries:
try:
return operation(*args, **kwargs)
except pyvisa.VisaIOError as visa_error:
if attempts == self.max_retries:
raise CommunicationError(self.address,
self.idn) from visa_error
attempts += 1

def clear_status(self, **kwargs) -> None:
"""
clear_status(**kwargs)
Expand Down Expand Up @@ -258,10 +273,7 @@ def write_resource(self, message: str, **kwargs) -> None:
ascii characters
"""

try:
self._resource.write(message=message, **kwargs)
except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
self._do_with_retry(self._resource.write, message, **kwargs)

def write_resource_raw(self, message: bytes, **kwargs) -> None:
"""
Expand All @@ -273,10 +285,7 @@ def write_resource_raw(self, message: bytes, **kwargs) -> None:
message (bytes): data to write to the connected resource
"""

try:
self._resource.write_raw(message=message, **kwargs)
except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
self._do_with_retry(self._resource.write_raw, message, **kwargs)

def query_resource(self, message: str, **kwargs) -> str:
"""
Expand All @@ -295,12 +304,9 @@ def query_resource(self, message: str, **kwargs) -> str:
ascii characters
"""

try:
response: str = self._resource.query(message=message, **kwargs)
return response.strip()

except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
response: str = self._do_with_retry(
self._resource.query, message, **kwargs)
return response.strip()

def read_resource(self, **kwargs) -> str:
"""
Expand All @@ -313,12 +319,8 @@ def read_resource(self, **kwargs) -> str:
ascii characters
"""

try:
response: str = self._resource.read(**kwargs)
return response.strip()

except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
response: str = self._do_with_retry(self._resource.read, **kwargs)
return response.strip()

def read_resource_raw(self, **kwargs) -> bytes:
"""
Expand All @@ -332,13 +334,7 @@ def read_resource_raw(self, **kwargs) -> bytes:
Returns:
bytes: data recieved from a connected resource
"""

try:
response = self._resource.read_raw(**kwargs)
return response

except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
return self._do_with_retry(self._resource.read_raw, **kwargs)

def read_resource_bytes(self, n: int, **kwargs) -> bytes:
"""
Expand All @@ -351,13 +347,7 @@ def read_resource_bytes(self, n: int, **kwargs) -> bytes:
Returns:
bytes: data recieved from a connected resource
"""

try:
response = self._resource.read_bytes(count=n, **kwargs)
return response

except pyvisa.VisaIOError as error:
raise IOError("Error communicating with the resource\n", error)
return self._do_with_retry(self._resource.read_bytes, n, **kwargs)

def send_raw_scpi(self, command_str: str, **kwargs) -> None:
warnings.warn("send_raw_scpi is deprecated and may be removed in a "
Expand Down
24 changes: 24 additions & 0 deletions pythonequipmentdrivers/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,27 @@ def __init__(self, message="Device is not supported", *args):
class ResourceConnectionError(Exception):
def __init__(self, message="Could not connect to device", *args):
super().__init__(message, *args)


class EquipmentDriverError(IOError):
"""Base exception for all PythonEquipmentDrivers errors
Extends the existing IOError chain
Gets caught by any code looking for IOError
Maintains compatibility with existing error handling
Provides more specific error types while preserving IOError behavior
"""
pass


class CommunicationError(EquipmentDriverError):
"""Error during device communication"""
def __init__(self, address, idn=None, *args):
message = f"Failed to communicate with device at {address}"
if idn:
message += f"\nDevice: {idn}"
super().__init__(message, *args)


class ConfigurationError(EquipmentDriverError):
"""Error in device configuration or settings"""
pass
12 changes: 7 additions & 5 deletions pythonequipmentdrivers/multimeter/HP_34401A.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ class HP_34401A(VisaResource):
"P": "PER",
}

valid_ranges = {"AUTO", "MIN", "MAX", "DEF", "0.1", "1", "10", "100", "300"}
valid_ranges = {
"AUTO", "MIN", "MAX", "DEF", "0.1", "1", "10", "100", "300"}

valid_cranges = {"AUTO", "MIN", "MAX", "DEF", "0.01", "0.1", "1", "3"}

Expand Down Expand Up @@ -143,7 +144,7 @@ def get_error(self, **kwargs) -> str:
get_error
Returns:
[list]: last error in the buffer
str: last error in the buffer
"""
response = self.query_resource("SYSTem:ERRor?", **kwargs)
return self.resp_format(response, str)
Expand Down Expand Up @@ -200,7 +201,7 @@ def set_trigger(self, trigger: str, **kwargs) -> None:
trigger = trigger.upper()
if trigger not in self.valid_trigger:
raise ValueError("Invalid trigger option")
self.write_resource(f"TRIG:{self.valid_trigger[trigger]}")
self.write_resource(f"TRIG:SOUR {self.valid_trigger[trigger]}")

def set_trigger_source(self, trigger: str = "IMMEDIATE", **kwargs) -> None:
"""
Expand Down Expand Up @@ -537,7 +538,7 @@ def resp_format(
# that works out OK because data needs to be parsed from the first
# character anyway, so this is not an error, but I don't like
# that it isn't explicitly trying to find the correct character
response = list(map(resp_type, response[start + 1 : stop].split(",")))
response = list(map(resp_type, response[start + 1: stop].split(",")))

if len(response) == 1:
return response[0]
Expand All @@ -546,7 +547,8 @@ def resp_format(
def set_measure_time(self, measure_time: float = None):
if measure_time is None:
self.measure_time = (
self.sample_count * self.nplc_default * (1 / self.line_frequency) + 0.01
self.sample_count *
self.nplc_default * (1 / self.line_frequency) + 0.01
)
else:
self.measure_time = measure_time
Expand Down
21 changes: 19 additions & 2 deletions pythonequipmentdrivers/resource_collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ def set_local(self) -> None:
except (VisaIOError, AttributeError):
pass

def clear_status(self) -> None:
"""
clear_status()
Attempt to clear each resource in the collection.
"""
for resource in self:
try:
resource.clear_status()
except (VisaIOError, AttributeError):
pass


class DmmCollection(ResourceCollection):
"""
Expand Down Expand Up @@ -305,6 +317,9 @@ def connect_resources(config: Union[str, Path, dict],
if kwargs.get("init", False) and (init_sequence):
# get the instance in question
resource_instance = getattr(resources, name)
if kwargs.get("clear", False):
print(f"[CLEAR] {name}")
resource_instance.clear_status()

initiaize_device(resource_instance, init_sequence)
if kwargs.get("verbose", True):
Expand All @@ -316,7 +331,7 @@ def connect_resources(config: Union[str, Path, dict],
print(f"[FAILED CONNECTION] {name}")

if object_mask: # failed resource connection is required
raise ResourceConnectionError(error)
raise ResourceConnectionError(error) from error

except (ModuleNotFoundError, AttributeError) as error:

Expand Down Expand Up @@ -401,7 +416,9 @@ def initiaize_device(instance, sequence) -> None:
func = getattr(instance, method_name)
# Convert string values to Enum values where possible
converted_kwargs = {
key: convert_to_enum(instance, value) if isinstance(value, str) else value
key: convert_to_enum(instance,
value) if isinstance(value,
str) else value
for key, value in method_kwargs.items()
}
func(**converted_kwargs)
Expand Down
20 changes: 14 additions & 6 deletions pythonequipmentdrivers/source/Keithley_2231A.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,8 @@ def set_voltage(self, voltage: float, channel: int = None) -> None:
voltage: float or int, amplitude to set output to in Vdc
channel: int=None, the index of the channel to set. Valid options are 1,2,3.
channel: int=None, the index of the channel to set.
Valid options are 1,2,3.
set the output voltage setpoint of channel "channel" specified by
"voltage"
Expand All @@ -176,7 +177,8 @@ def get_voltage(self, channel: int = None) -> float:
"""
get_voltage()
channel: int=None, the index of the channel to set. Valid options are 1,2,3.
channel: int=None, the index of the channel to set.
Valid options are 1,2,3.
gets the output voltage setpoint in Vdc
Expand All @@ -193,7 +195,8 @@ def set_current(self, current: float, channel: int = None) -> None:
current: float/int, current limit setpoint in Adc
channel: int=None, the index of the channel to set. Valid options are 1,2,3.
channel: int=None, the index of the channel to set.
Valid options are 1,2,3.
sets the current limit setting for the power supply in Adc
"""
Expand All @@ -205,7 +208,8 @@ def get_current(self, channel: int = None) -> float:
"""
get_current()
channel: int=None, the index of the channel to set. Valid options are 1,2,3.
channel: int=None, the index of the channel to set.
Valid options are 1,2,3.
gets the current limit setting for the power supply in Adc
Expand Down Expand Up @@ -241,5 +245,9 @@ def measure_current(self, channel: int = None) -> float:
"""

self._update_channel(channel)
response = self.query_resource("MEAS:CURR?")
return float(response)
try:
response = float(self.query_resource("MEAS:CURR?"))
except ValueError:
# sometimes the device needs a repeat due to channel selection?
response = float(self.query_resource("MEAS:CURR?"))
return response

0 comments on commit 43f557b

Please sign in to comment.