hitl_tester.modules.cyber_6t.charger

Provides controls for the Rigol DL711 Power Supply.

(c) 2020-2024 TurnAround Factor, Inc.

#

CUI DISTRIBUTION CONTROL

Controlled by: DLA J68 R&D SBIP

CUI Category: Small Business Research and Technology

Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS

POC: GOV SBIP Program Manager Denise Price, 571-767-0111

Distribution authorized to U.S. Government Agencies only, to protect information not owned by the

U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that

it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests

for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,

Fort Belvoir, VA 22060-6221

#

SBIR DATA RIGHTS

Contract No.:SP4701-23-C-0083

Contractor Name: TurnAround Factor, Inc.

Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005

Expiration of SBIR Data Rights Period: September 24, 2029

The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer

software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights

in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause

contained in the above identified contract. No restrictions apply after the expiration date shown above. Any

reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce

the markings.

  1"""
  2Provides controls for the Rigol DL711 Power Supply.
  3
  4# (c) 2020-2024 TurnAround Factor, Inc.
  5#
  6# CUI DISTRIBUTION CONTROL
  7# Controlled by: DLA J68 R&D SBIP
  8# CUI Category: Small Business Research and Technology
  9# Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 10# POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 11# Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 12# U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 13# it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 14# for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 15# Fort Belvoir, VA 22060-6221
 16#
 17# SBIR DATA RIGHTS
 18# Contract No.:SP4701-23-C-0083
 19# Contractor Name: TurnAround Factor, Inc.
 20# Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 21# Expiration of SBIR Data Rights Period: September 24, 2029
 22# The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 23# software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 24# in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 25# contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 26# reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 27# the markings.
 28"""
 29
 30from __future__ import annotations
 31
 32import atexit
 33import time
 34import traceback
 35from time import sleep
 36from typing import cast
 37
 38import pyvisa as visa
 39from pyvisa.resources import MessageBasedResource
 40
 41from hitl_tester.modules.logger import logger
 42
 43
 44class Charger:
 45    """Keysight Power Supply command wrapper."""
 46
 47    ADDRESS = "Keysight Technologies,E36731A"
 48
 49    def __init__(self) -> None:
 50        """
 51        Initialize the Keysight wrapper from a PyVISA resource.
 52        """
 53
 54        # Find charger
 55        resource_manager = visa.ResourceManager()
 56        resources = resource_manager.list_resources()
 57        if not resources:
 58            raise RuntimeError("No PyVISA resources found")
 59        self.resource: MessageBasedResource | None = None
 60        for resource_address in resources:
 61            if resource_address.startswith("USB"):
 62                try:
 63                    resource = cast(MessageBasedResource, resource_manager.open_resource(resource_address))
 64                    if self.ADDRESS in resource.query("*IDN?"):
 65                        self.resource = resource
 66                        break
 67                except Exception as e:  # pylint: disable=broad-exception-caught
 68                    logger.write_debug_to_report(traceback.format_exception(e))
 69                    logger.write_debug_to_report(f"Could not open: {resource_address}")
 70        if not self.resource:
 71            raise RuntimeError("Could not find power supply")
 72
 73        # Set default values
 74        self.target_amps: float = 0
 75        self.target_volts: float = 0
 76        self.reset()
 77
 78        @atexit.register
 79        def __atexit__():
 80            """Configure a safe shut down for when the class instance is destroyed."""
 81            self.disable()
 82
 83    def __call__(self, amps: float, volts: float = 30):
 84        self.set_profile(volts, amps)
 85        return self
 86
 87    def __enter__(self):
 88        self.enable()
 89        return self
 90
 91    def __exit__(self, exc_type, exc_value, exc_traceback):
 92        self.disable()
 93
 94    def write(self, command: str) -> None:
 95        """Write SCPI command if possible."""
 96        try:
 97            if self.resource:
 98                self.resource.write(command)
 99        except Exception as e:  # pylint: disable=broad-exception-caught
100            raise RuntimeError(traceback.format_exception(e)) from e
101
102    def set_profile(self, volts: float, amps: float):
103        """Sets (dis)charging profile"""
104        # Limit 30.9V, 4A
105        if volts >= 30.9:
106            logger.write_warning_to_html_report("Voltage too high, lowering.")
107            volts = 30.8
108        if not -4 <= amps <= 4:
109            raise RuntimeError("Current not in range +/- 4A.")
110        self.target_volts = volts
111        self.target_amps = amps
112        if amps < 0:  # Load mode
113            self.write("EMUL LOAD")
114            time.sleep(10)
115            self.amps = amps
116        else:
117            self.write("EMUL PSUP")
118            time.sleep(10)
119            self.write(f"APPL CH1, {volts}, {amps}")
120        sleep(10)  # The charger needs time to process the previous commands
121
122    def _amps_limit(self, new_amps_limit: float):
123        """Sets output current limit"""
124        self.write(f"CURR:LIM {new_amps_limit} (@1)")
125
126    def _volts_limit(self, new_volts_limit: float):
127        """Sets output voltage limit"""
128        self.write(f"VOLT:PROT {new_volts_limit}, (@1)")
129
130    amps_limit = property(None, _amps_limit)  # make this function accessible as a write-only attribute
131    volts_limit = property(None, _volts_limit)  # make this function accessible as a write-only attribute
132
133    @property
134    def amps(self) -> float:
135        """Reads the current being sourced"""
136        assert self.resource, "Resource does not exist."
137        return float(self.resource.query("MEAS:CURR? (@1)"))
138
139    @amps.setter
140    def amps(self, new_amps: float):
141        """Sets the power supply output current"""
142        self.target_amps = new_amps
143        self.write(f"CURR {new_amps}, (@1)")
144
145    @property
146    def volts(self) -> float:
147        """Reads the volts being sourced"""
148        assert self.resource, "Resource does not exist."
149        return float(self.resource.query("MEAS:VOLT? (@1)"))
150
151    @volts.setter
152    def volts(self, new_volts: float):
153        """Sets the power supply output voltage"""
154        self.target_volts = new_volts
155        self.write(f"VOLT {new_volts}, (@1)")
156
157    def enable(self):
158        """Enables power supply output"""
159        if self.target_amps > 0:
160            logger.write_info_to_report("Enabling charger")
161            self.write("OUTP ON, (@1)")
162        else:
163            logger.write_info_to_report("Enabling load")
164            self.write("INP ON, (@1)")
165        sleep(1)  # This instrument needs time to initialize
166
167    def disable(self):
168        """Disables power supply output"""
169        if self.target_amps > 0:
170            logger.write_info_to_report("Disabling charger")
171            self.write("OUTP OFF, (@1)")
172        else:
173            logger.write_info_to_report("Disabling load")
174            self.write("INP OFF, (@1)")
175        sleep(1)  # The charger needs time to process the previous commands
176
177    def reset(self):
178        """Resets the instrument"""
179        self.write("*RST")
180        sleep(3)
181        self.write("*CLS")
182        sleep(3)
183        self.write("SYST:RWL")
184        sleep(3)
class Charger:
 45class Charger:
 46    """Keysight Power Supply command wrapper."""
 47
 48    ADDRESS = "Keysight Technologies,E36731A"
 49
 50    def __init__(self) -> None:
 51        """
 52        Initialize the Keysight wrapper from a PyVISA resource.
 53        """
 54
 55        # Find charger
 56        resource_manager = visa.ResourceManager()
 57        resources = resource_manager.list_resources()
 58        if not resources:
 59            raise RuntimeError("No PyVISA resources found")
 60        self.resource: MessageBasedResource | None = None
 61        for resource_address in resources:
 62            if resource_address.startswith("USB"):
 63                try:
 64                    resource = cast(MessageBasedResource, resource_manager.open_resource(resource_address))
 65                    if self.ADDRESS in resource.query("*IDN?"):
 66                        self.resource = resource
 67                        break
 68                except Exception as e:  # pylint: disable=broad-exception-caught
 69                    logger.write_debug_to_report(traceback.format_exception(e))
 70                    logger.write_debug_to_report(f"Could not open: {resource_address}")
 71        if not self.resource:
 72            raise RuntimeError("Could not find power supply")
 73
 74        # Set default values
 75        self.target_amps: float = 0
 76        self.target_volts: float = 0
 77        self.reset()
 78
 79        @atexit.register
 80        def __atexit__():
 81            """Configure a safe shut down for when the class instance is destroyed."""
 82            self.disable()
 83
 84    def __call__(self, amps: float, volts: float = 30):
 85        self.set_profile(volts, amps)
 86        return self
 87
 88    def __enter__(self):
 89        self.enable()
 90        return self
 91
 92    def __exit__(self, exc_type, exc_value, exc_traceback):
 93        self.disable()
 94
 95    def write(self, command: str) -> None:
 96        """Write SCPI command if possible."""
 97        try:
 98            if self.resource:
 99                self.resource.write(command)
100        except Exception as e:  # pylint: disable=broad-exception-caught
101            raise RuntimeError(traceback.format_exception(e)) from e
102
103    def set_profile(self, volts: float, amps: float):
104        """Sets (dis)charging profile"""
105        # Limit 30.9V, 4A
106        if volts >= 30.9:
107            logger.write_warning_to_html_report("Voltage too high, lowering.")
108            volts = 30.8
109        if not -4 <= amps <= 4:
110            raise RuntimeError("Current not in range +/- 4A.")
111        self.target_volts = volts
112        self.target_amps = amps
113        if amps < 0:  # Load mode
114            self.write("EMUL LOAD")
115            time.sleep(10)
116            self.amps = amps
117        else:
118            self.write("EMUL PSUP")
119            time.sleep(10)
120            self.write(f"APPL CH1, {volts}, {amps}")
121        sleep(10)  # The charger needs time to process the previous commands
122
123    def _amps_limit(self, new_amps_limit: float):
124        """Sets output current limit"""
125        self.write(f"CURR:LIM {new_amps_limit} (@1)")
126
127    def _volts_limit(self, new_volts_limit: float):
128        """Sets output voltage limit"""
129        self.write(f"VOLT:PROT {new_volts_limit}, (@1)")
130
131    amps_limit = property(None, _amps_limit)  # make this function accessible as a write-only attribute
132    volts_limit = property(None, _volts_limit)  # make this function accessible as a write-only attribute
133
134    @property
135    def amps(self) -> float:
136        """Reads the current being sourced"""
137        assert self.resource, "Resource does not exist."
138        return float(self.resource.query("MEAS:CURR? (@1)"))
139
140    @amps.setter
141    def amps(self, new_amps: float):
142        """Sets the power supply output current"""
143        self.target_amps = new_amps
144        self.write(f"CURR {new_amps}, (@1)")
145
146    @property
147    def volts(self) -> float:
148        """Reads the volts being sourced"""
149        assert self.resource, "Resource does not exist."
150        return float(self.resource.query("MEAS:VOLT? (@1)"))
151
152    @volts.setter
153    def volts(self, new_volts: float):
154        """Sets the power supply output voltage"""
155        self.target_volts = new_volts
156        self.write(f"VOLT {new_volts}, (@1)")
157
158    def enable(self):
159        """Enables power supply output"""
160        if self.target_amps > 0:
161            logger.write_info_to_report("Enabling charger")
162            self.write("OUTP ON, (@1)")
163        else:
164            logger.write_info_to_report("Enabling load")
165            self.write("INP ON, (@1)")
166        sleep(1)  # This instrument needs time to initialize
167
168    def disable(self):
169        """Disables power supply output"""
170        if self.target_amps > 0:
171            logger.write_info_to_report("Disabling charger")
172            self.write("OUTP OFF, (@1)")
173        else:
174            logger.write_info_to_report("Disabling load")
175            self.write("INP OFF, (@1)")
176        sleep(1)  # The charger needs time to process the previous commands
177
178    def reset(self):
179        """Resets the instrument"""
180        self.write("*RST")
181        sleep(3)
182        self.write("*CLS")
183        sleep(3)
184        self.write("SYST:RWL")
185        sleep(3)

Keysight Power Supply command wrapper.

Charger()
50    def __init__(self) -> None:
51        """
52        Initialize the Keysight wrapper from a PyVISA resource.
53        """
54
55        # Find charger
56        resource_manager = visa.ResourceManager()
57        resources = resource_manager.list_resources()
58        if not resources:
59            raise RuntimeError("No PyVISA resources found")
60        self.resource: MessageBasedResource | None = None
61        for resource_address in resources:
62            if resource_address.startswith("USB"):
63                try:
64                    resource = cast(MessageBasedResource, resource_manager.open_resource(resource_address))
65                    if self.ADDRESS in resource.query("*IDN?"):
66                        self.resource = resource
67                        break
68                except Exception as e:  # pylint: disable=broad-exception-caught
69                    logger.write_debug_to_report(traceback.format_exception(e))
70                    logger.write_debug_to_report(f"Could not open: {resource_address}")
71        if not self.resource:
72            raise RuntimeError("Could not find power supply")
73
74        # Set default values
75        self.target_amps: float = 0
76        self.target_volts: float = 0
77        self.reset()
78
79        @atexit.register
80        def __atexit__():
81            """Configure a safe shut down for when the class instance is destroyed."""
82            self.disable()

Initialize the Keysight wrapper from a PyVISA resource.

ADDRESS = 'Keysight Technologies,E36731A'
resource: pyvisa.resources.messagebased.MessageBasedResource | None
target_amps: float
target_volts: float
def write(self, command: str) -> None:
 95    def write(self, command: str) -> None:
 96        """Write SCPI command if possible."""
 97        try:
 98            if self.resource:
 99                self.resource.write(command)
100        except Exception as e:  # pylint: disable=broad-exception-caught
101            raise RuntimeError(traceback.format_exception(e)) from e

Write SCPI command if possible.

def set_profile(self, volts: float, amps: float):
103    def set_profile(self, volts: float, amps: float):
104        """Sets (dis)charging profile"""
105        # Limit 30.9V, 4A
106        if volts >= 30.9:
107            logger.write_warning_to_html_report("Voltage too high, lowering.")
108            volts = 30.8
109        if not -4 <= amps <= 4:
110            raise RuntimeError("Current not in range +/- 4A.")
111        self.target_volts = volts
112        self.target_amps = amps
113        if amps < 0:  # Load mode
114            self.write("EMUL LOAD")
115            time.sleep(10)
116            self.amps = amps
117        else:
118            self.write("EMUL PSUP")
119            time.sleep(10)
120            self.write(f"APPL CH1, {volts}, {amps}")
121        sleep(10)  # The charger needs time to process the previous commands

Sets (dis)charging profile

amps_limit
volts_limit
amps: float
134    @property
135    def amps(self) -> float:
136        """Reads the current being sourced"""
137        assert self.resource, "Resource does not exist."
138        return float(self.resource.query("MEAS:CURR? (@1)"))

Reads the current being sourced

volts: float
146    @property
147    def volts(self) -> float:
148        """Reads the volts being sourced"""
149        assert self.resource, "Resource does not exist."
150        return float(self.resource.query("MEAS:VOLT? (@1)"))

Reads the volts being sourced

def enable(self):
158    def enable(self):
159        """Enables power supply output"""
160        if self.target_amps > 0:
161            logger.write_info_to_report("Enabling charger")
162            self.write("OUTP ON, (@1)")
163        else:
164            logger.write_info_to_report("Enabling load")
165            self.write("INP ON, (@1)")
166        sleep(1)  # This instrument needs time to initialize

Enables power supply output

def disable(self):
168    def disable(self):
169        """Disables power supply output"""
170        if self.target_amps > 0:
171            logger.write_info_to_report("Disabling charger")
172            self.write("OUTP OFF, (@1)")
173        else:
174            logger.write_info_to_report("Disabling load")
175            self.write("INP OFF, (@1)")
176        sleep(1)  # The charger needs time to process the previous commands

Disables power supply output

def reset(self):
178    def reset(self):
179        """Resets the instrument"""
180        self.write("*RST")
181        sleep(3)
182        self.write("*CLS")
183        sleep(3)
184        self.write("SYST:RWL")
185        sleep(3)

Resets the instrument