From d714392bef910363e483c4ccc0c53a27b50c7cd9 Mon Sep 17 00:00:00 2001 From: TedKus Date: Wed, 16 Oct 2024 10:36:01 -0400 Subject: [PATCH 1/6] feat(remote): add set_access_remote to chroma --- pythonequipmentdrivers/source/Chroma_62000P.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pythonequipmentdrivers/source/Chroma_62000P.py b/pythonequipmentdrivers/source/Chroma_62000P.py index bec0aaa..64bcbd4 100644 --- a/pythonequipmentdrivers/source/Chroma_62000P.py +++ b/pythonequipmentdrivers/source/Chroma_62000P.py @@ -12,6 +12,18 @@ class Chroma_62000P(VisaResource): object for accessing basic functionallity of the Chroma_62000P DC supply """ + def set_access_remote(self, mode: bool = True) -> None: + """ + set_access_remote(mode) + + mode: str, interface method either 'remote' or 'local' + + set access to the device inferface to 'remote' or 'local' + """ + + self.write_resource( + f"""'CONFigure:REMote {'ON' if mode else 'OFF'}'""") + def set_state(self, state: bool) -> None: """ set_state(state) From 761e91261872d89316fd0f27e6b9f11bdadbf65f Mon Sep 17 00:00:00 2001 From: TedKus Date: Wed, 16 Oct 2024 10:36:43 -0400 Subject: [PATCH 2/6] feat(source): add BK 9140 source --- .../source/BKPrecision_9140.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 pythonequipmentdrivers/source/BKPrecision_9140.py diff --git a/pythonequipmentdrivers/source/BKPrecision_9140.py b/pythonequipmentdrivers/source/BKPrecision_9140.py new file mode 100644 index 0000000..3d59ecb --- /dev/null +++ b/pythonequipmentdrivers/source/BKPrecision_9140.py @@ -0,0 +1,18 @@ +from .Keithley_2231A import Keithley_2231A + + +# acts as an alias of Keithley_2231A +class BKPrecision_9140(Keithley_2231A): + """ + BKPrecision_9140(address) + + address : str, address of the connected power supply + + object for accessing basic functionallity of the B&K Precision DC supply + """ + + pass + + +if __name__ == '__main__': + pass From a657c26d417730e5a1fb4f6bf061e724cfa9f3c9 Mon Sep 17 00:00:00 2001 From: TedKus Date: Thu, 17 Oct 2024 12:39:17 -0400 Subject: [PATCH 3/6] feat(source): add bk 9140 source. it differs from the keithley in a few commands --- .../source/BKPrecision_9140.py | 221 +++++++++++++++++- 1 file changed, 217 insertions(+), 4 deletions(-) diff --git a/pythonequipmentdrivers/source/BKPrecision_9140.py b/pythonequipmentdrivers/source/BKPrecision_9140.py index 3d59ecb..ebb95a0 100644 --- a/pythonequipmentdrivers/source/BKPrecision_9140.py +++ b/pythonequipmentdrivers/source/BKPrecision_9140.py @@ -8,11 +8,224 @@ class BKPrecision_9140(Keithley_2231A): address : str, address of the connected power supply - object for accessing basic functionallity of the B&K Precision DC supply + object for accessing basic functionallity of the B&K Precision 9140 DC supply """ - pass + def set_access_remote(self, mode: str) -> None: + """ + set_access_remote(mode) + mode: str, interface method either 'remote' or 'RWLock' or 'local' + note that 'RWLock' will lock the front panel keys. -if __name__ == '__main__': - pass + set access to the device inferface to 'remote' or 'RWLock' or 'local' + """ + + if mode.lower() == "remote": + self.write_resource("SYSTem:REMote") + elif mode.lower() == "rwlock": + self.write_resource("SYSTem:RWLock") + elif mode.lower() == "local": + self.write_resource("SYSTem:LOCal") + else: + raise ValueError( + 'Unknown option for arg "mode", should be "remote" or "local"' + ) + + def set_channel(self, channel: int) -> None: + """ + set_channel(channel) + + channel: int, index of the channel to control. + valid options are 0-2 coresponding to 1-3 + + Selects the specified Channel to use for software control + """ + + self.write_resource(f"INST:SEL {channel}") + + def _update_channel(self, override_channel): + """Handles updating the device channel setting""" + + if override_channel is not None: + self.set_channel(override_channel - 1) + elif self.channel is not None: + self.set_channel(self.channel - 1) + else: + raise TypeError( + "Channel number must be provided if it is not provided during" + + "initialization" + ) + return + + def get_channel(self) -> int: + """ + get_channel() + + Get current selected Channel + + returns: int + """ + + response = self.query_resource("INST:SEL?") + return int(response) + 1 + + def set_state(self, state: bool, channel: int = None) -> None: + """ + set_state(state, channel) + + Enables/disables the output of the supply + + Args: + state (bool): Supply state (True == enabled, False == disabled) + channel (int): Index of the channel to control. valid options + are 1-3 + """ + + self._update_channel(channel) + self.write_resource(f"OUTP:STAT {1 if state else 0}") + + def get_state(self, channel: int = None) -> bool: + """ + get_state(channel) + + Retrives the current state of the output of the supply. + + Args: + channel (int): index of the channel to control. Valid options + are 1-3 + + Returns: + bool: Supply state (True == enabled, False == disabled) + """ + + self._update_channel(channel) + response = self.query_resource("OUTP:STAT?") + if response not in ("ON", "1"): + return False + return True + + def all_get_state(self) -> bool: + """ + all_get_state(channel) + + Retrives the current state of the output of the supply. + + Args: + None + + Returns: + bool: Supply state (True == enabled, False == disabled) + """ + + response = self.query_resource("OUTP:ALL?") + if response not in ("ON", "1"): + return False + return True + + def all_on(self) -> None: + """ + all_on() + + Enables the relay for ALL the power supply's outputs equivalent to + set_state(True). + + Args: + None + """ + + self.write_resource(f"OUTP:ALL {1}") + + def all_off(self) -> None: + """ + all_off() + + Disables the relay for ALL the power supply's outputs equivalent to + set_state(False). + + Args: + None + """ + + self.write_resource(f"OUTP:ALL {0}") + + def all_toggle(self) -> None: + """ + all_toggle() + + Reverses the current state of ALL the Supply's outputs + + Args: + None + """ + + if self.all_get_state(): + self.all_off() + else: + self.all_on() + + def set_slewrate(self, slewrate: float, channel: int = None) -> None: + """ + set_slewrate(current) + + slewrate: float/int, slew rate setpoint in volts per second. Valid options are 0.001 to 3200.0 V/s. + + channel: int=None, the index of the channel to set. Valid options are 1,2,3. + + sets the slew rate setting for the power supply in V/s + """ + if 0.001 < slewrate: + slewrate = 0.001 + elif slewrate > 3200: + slewrate = 3200 + + self._update_channel(channel) + self.write_resource(f"VOLT:SLOP {slewrate}") + + def get_slewrate(self, channel: int = None) -> float: + """ + get_slewrate() + + channel: int=None, the index of the channel to get. Valid options are 1,2,3. + + gets the slew rate setting for the power supply in V/s + + returns: float + """ + + self._update_channel(channel) + response = self.query_resource("VOLT:SLOP?") + return float(response) + + def set_current_slewrate(self, slewrate: float, channel: int = None) -> None: + """ + set_current_slewrate(current) + + slewrate: float/int, slew rate setpoint in Amps per second. Valid options are 1 to 800.0 A/s. + + channel: int=None, the index of the channel to set. Valid options are 1,2,3. + + sets the current slew rate setting for the power supply in A/s + """ + if 1 < slewrate: + slewrate = 1 + elif slewrate > 800: + slewrate = 800 + + self._update_channel(channel) + self.write_resource(f"CURR:SLOP {slewrate}") + + def get_current_slewrate(self, channel: int = None) -> float: + """ + get_current_slewrate() + + channel: int=None, the index of the channel to get. Valid options are 1,2,3. + + gets the current slew rate setting for the power supply in A/s + + returns: float + """ + + self._update_channel(channel) + response = self.query_resource("VOLT:SLOP?") + return float(response) From 8d0057f9fa8292749c417b13f937c30393bedd52 Mon Sep 17 00:00:00 2001 From: TedKus Date: Thu, 17 Oct 2024 14:04:12 -0400 Subject: [PATCH 4/6] fix(build): add manifest.in fix(pyproject): python 3.8 support fix(tox): python 3.8 support fix(type): revert typehint to python 3.8 style --- manifest.in | 2 ++ pyproject.toml | 2 +- .../temperaturecontroller/Koolance_EXC900.py | 9 ++++----- tox.ini | 8 +++++--- 4 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 manifest.in diff --git a/manifest.in b/manifest.in new file mode 100644 index 0000000..21ada19 --- /dev/null +++ b/manifest.in @@ -0,0 +1,2 @@ +include pythonequipmentdrivers/* +recursive-include pythonequipmentdrivers *.py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 3a5e6ab..c7ac601 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ ] description = "A library of software drivers to interface with various pieces of test instrumentation" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.8" classifiers = [ "Development Status :: 4 - Beta", "Environment :: Console", diff --git a/pythonequipmentdrivers/temperaturecontroller/Koolance_EXC900.py b/pythonequipmentdrivers/temperaturecontroller/Koolance_EXC900.py index 0ba80fc..ff7796e 100644 --- a/pythonequipmentdrivers/temperaturecontroller/Koolance_EXC900.py +++ b/pythonequipmentdrivers/temperaturecontroller/Koolance_EXC900.py @@ -1,7 +1,7 @@ import logging from dataclasses import dataclass from time import time -from typing import Literal +from typing import Literal, Dict from ..core import VisaResource @@ -94,7 +94,7 @@ def _write_data(self, data: bytes) -> None: self._last_read_data_time = None # trigger a refresh on the next read self.write_resource_raw(data) - def read_settings(self) -> dict[str, float]: + def read_settings(self) -> Dict[str, float]: """ read_settings() @@ -102,11 +102,10 @@ def read_settings(self) -> dict[str, float]: in a "key: value" format Returns: - dict[str, float]: dict of parameter names and their values + Dict[str, float]: dict of parameter names and their values """ - data = self._read_data() - out_dict: dict[str, float] = {} + out_dict: Dict[str, float] = {} for name, reg in self.DATA_REGISTER_MAP.items(): value = data[reg.offset : reg.offset + reg.n_bytes] diff --git a/tox.ini b/tox.ini index 485912a..29a6064 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,15 @@ [tox] env_list = format + py38 py39 py310 py311 py312 - + [testenv] base_python = + py38: python3.8-64 py39: python3.9-64 py310: python3.10-64 py311: python3.11-64 @@ -22,11 +24,11 @@ commands = [testenv:format] description = install black in a virtual environment and invoke it on the current folder -deps = +deps = black isort skip_install = true -commands = +commands = black . isort . From b9634b5a45cf03439eb0f6eb4e8c1a01893daf8e Mon Sep 17 00:00:00 2001 From: TedKus Date: Thu, 17 Oct 2024 14:19:27 -0400 Subject: [PATCH 5/6] fix(enum): add __str__ for python 3.8 This method returns the value of the Enum member as a string. changed `mode.value` to `str(mode)`. This calls the `__str__` method, returns the string value of the Enum member --- pythonequipmentdrivers/sink/Chroma_63600.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pythonequipmentdrivers/sink/Chroma_63600.py b/pythonequipmentdrivers/sink/Chroma_63600.py index 3b14fb5..4657217 100644 --- a/pythonequipmentdrivers/sink/Chroma_63600.py +++ b/pythonequipmentdrivers/sink/Chroma_63600.py @@ -29,6 +29,9 @@ class ValidModes(Enum): TIM = "TIM" SWD = "SWD" + def __str__(self): + return self.value + class Errors(Enum): OTP = 0 OVP = 1 @@ -452,7 +455,7 @@ def set_mode( raise ValueError(f"Invalid range: {range_setting}") self.set_channel(channel) - self.write_resource(f"MODE {mode.value}{range_setting}") + self.write_resource(f"MODE {str(mode)}{range_setting}") def get_mode(self, channel: int) -> Tuple[ValidModes, str]: """ From f866aab96a3693b020d4fc0acba17ab2fa93115d Mon Sep 17 00:00:00 2001 From: TedKus Date: Thu, 17 Oct 2024 15:59:04 -0400 Subject: [PATCH 6/6] fix(enum): json loads strings not values. add convert_to_enum Attempt to convert a string value to an Enum value if a matching Enum exists in the instance --- .../resource_collections.py | 24 ++++++++++++++++++- pythonequipmentdrivers/sink/Chroma_63600.py | 8 +++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/pythonequipmentdrivers/resource_collections.py b/pythonequipmentdrivers/resource_collections.py index 6594e71..1f2ed07 100644 --- a/pythonequipmentdrivers/resource_collections.py +++ b/pythonequipmentdrivers/resource_collections.py @@ -3,6 +3,7 @@ from pathlib import Path from types import SimpleNamespace from typing import Dict, Iterator, Tuple, Union +from enum import Enum from pyvisa import VisaIOError @@ -358,6 +359,20 @@ def get_callable_methods(instance) -> Tuple: return tuple(cmds) +def convert_to_enum(instance, value): + """ + Attempt to convert a string value to an Enum value if a matching Enum exists in the instance. + """ + for attr_name in dir(instance): + attr = getattr(instance, attr_name) + if isinstance(attr, type) and issubclass(attr, Enum): + try: + return attr[value.upper()] + except KeyError: + pass + return value + + def initiaize_device(instance, sequence) -> None: """ initiaize_device(instance, sequence) @@ -385,9 +400,16 @@ def initiaize_device(instance, sequence) -> None: if method_name in valid_cmds: try: func = getattr(instance, method_name) - func(**method_kwargs) + # Convert string values to Enum values where possible + converted_kwargs = { + key: convert_to_enum(instance, value) if isinstance(value, str) else value + for key, value in method_kwargs.items() + } + func(**converted_kwargs) except TypeError as error: # invalid kwargs print(error_msg_template.format(method_name, error)) + except ValueError as error: # invalid Enum value + print(error_msg_template.format(method_name, error)) else: print(error_msg_template.format(method_name, '"unknown method"')) diff --git a/pythonequipmentdrivers/sink/Chroma_63600.py b/pythonequipmentdrivers/sink/Chroma_63600.py index 4657217..85ae341 100644 --- a/pythonequipmentdrivers/sink/Chroma_63600.py +++ b/pythonequipmentdrivers/sink/Chroma_63600.py @@ -29,9 +29,6 @@ class ValidModes(Enum): TIM = "TIM" SWD = "SWD" - def __str__(self): - return self.value - class Errors(Enum): OTP = 0 OVP = 1 @@ -318,7 +315,8 @@ def set_dynamic_current_time(self, on_time: float, level: int = 0) -> None: else: self.write_resource(f"CURR:DYN:T{level} {on_time}") - def get_dynamic_current_time(self, level: int) -> Union[float, Tuple[float]]: + def get_dynamic_current_time(self, + level: int) -> Union[float, Tuple[float]]: """ get_dynamic_current_time(level) @@ -455,7 +453,7 @@ def set_mode( raise ValueError(f"Invalid range: {range_setting}") self.set_channel(channel) - self.write_resource(f"MODE {str(mode)}{range_setting}") + self.write_resource(f"MODE {mode.value}{range_setting}") def get_mode(self, channel: int) -> Tuple[ValidModes, str]: """