hitl_tester.modules.bms.m300_dmm

Provides controls for the Rigol M300 DMM.

(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 M300 DMM.
  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 time
 33
 34from hitl_tester.modules.bms_types import SafeResource, DMMImpedance
 35from hitl_tester.modules.logger import logger
 36
 37
 38class M300Dmm:
 39    """Rigol M300 DMM command wrapper."""
 40
 41    def __init__(self, resource: SafeResource, scan_list: list[dict[str, int]]):
 42        """
 43        Initialize the M300 wrapper with a specific PyVISA resource.
 44        This class does open the resource, don't open it for yourself!
 45        """
 46        self.resource = resource
 47        self.scan_list = scan_list
 48        self.scan_index = 0
 49        self.voltage_range = 20  # The maximum voltage that can be measured. Range(V) = 0.2, 2, 20, 200, 1000, AUTO
 50        self.impedance_config = DMMImpedance(2, 7)
 51        self.reset()
 52
 53    @property
 54    def _scan_list_str(self):
 55        """A string used by the VISA commands."""
 56        slot = self.scan_list[self.scan_index]["slot"]
 57        channel = self.scan_list[self.scan_index]["channel"]
 58        return f"(@{slot}{channel:02})"
 59
 60    def configure_voltage_trigger(self, timestamps: bool = False):
 61        """Set up trigger parameters. Up to 50,000 readings."""
 62        self.resource.lock.acquire()
 63        self.resource.write(f"CONF:VOLT:DC {self.voltage_range},MAX,{self._scan_list_str}")  # Fastest integration
 64        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
 65        self.resource.write("ZERO:AUTO OFF")  # No zero reading. Speeds up measurement
 66        if timestamps:
 67            self.resource.write("FORM:READ:TIME ON")  # Include relative timestamp
 68        # self.resource.write("TRIG:SOUR IMM")  # Software trigger
 69        self.resource.write("TRIG:SOUR TIM")  # Software trigger
 70        self.resource.write("TRIG:TIM 0.001")  # 0.001 seconds between measurements
 71        self.resource.write("TRIG:COUN 300")  # 1 to 50000 scans (~250 Hz)
 72        self.resource.write("*ESE 1")  # Enable status bit (helps determine when operation completed)
 73
 74    def send_trigger(self):
 75        """Send a software trigger."""
 76
 77        # FIXME(JA): Speed up measurements
 78        # self.resource.write(f"VOLT:DC:APER MIN,{self._scan_list_str}")
 79        # self.resource.write(f"VOLT:DC:NPLC MIN,{self._scan_list_str}")
 80        # self.resource.write(f"VOLT:DC:RES MAX,{self._scan_list_str}")
 81        # self.resource.write(f"ROUT:CHAN:DEL 0,{self._scan_list_str}")
 82
 83        self.resource.write("INIT")  # Run scans
 84        self.resource.write("*OPC")  # Set status bit to 1 when operation completes
 85        time.sleep(0.25)
 86
 87    def read_internal_memory(self) -> list[float]:
 88        """Return measured data from internal memory."""
 89
 90        # Wait until operation completes
 91        while int(self.resource.query("*ESR?")) & 1 != 1:
 92            time.sleep(1)
 93
 94        # Fetch measurements
 95        result = self.resource.query("FETC?")
 96
 97        # Decode measurements
 98        last_valid_num = 0
 99
100        def decode_num(string_num):
101            """Gracefully handle invalid numbers in returned values."""
102            nonlocal last_valid_num
103            try:
104                last_valid_num = float(string_num)
105            except ValueError:
106                logger.write_debug_to_report(f"Could not convert: {string_num}, using {last_valid_num}")
107            return last_valid_num
108
109        return list(map(decode_num, result.split(",")))
110
111    def configure_voltage_normal(self):
112        """The voltage settings for normal measurements."""
113        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
114        self.resource.write("ZERO:AUTO ON")  # Activate zero reading to increase accuracy
115        self.resource.write("VOLT:DC:NPLC 10")  # NPLC = 0.02, 0.2, 1, 10, 100, 200. 1PLC = 0.02s
116        self.resource.write("FORM:READ:TIME OFF")  # Remove relative timestamp
117        self.resource.lock.release()
118
119    @property
120    def volts(self) -> float:
121        """Measures DC voltage."""
122        return float(self.resource.query(f"MEAS:VOLT:DC? {self.voltage_range},DEF,{self._scan_list_str}"))
123
124    @property
125    def amps_ac(self) -> float:
126        """Measures AC current."""
127        return float(self.resource.query(f"MEAS:CURR:AC? AUTO,DEF,{self._scan_list_str}"))
128
129    def reset(self):
130        """Resets the instrument."""
131        self.resource.write("*CLS")  # Reset any errors
132        self.resource.write("*RST")  # Put the system in a known state
133        # resource.write("FORM:READ:TIME ON")  # Include relative timestamp
134        for self.scan_index in range(len(self.scan_list)):
135            self.resource.lock.acquire()
136            self.configure_voltage_normal()
137
138        for i in range(100, 600, 100):
139            logger.write_debug_to_report(f"Slot {i}: {self.resource.query(f'SYST:CTYP? {i}').rstrip(chr(10))}")
class M300Dmm:
 39class M300Dmm:
 40    """Rigol M300 DMM command wrapper."""
 41
 42    def __init__(self, resource: SafeResource, scan_list: list[dict[str, int]]):
 43        """
 44        Initialize the M300 wrapper with a specific PyVISA resource.
 45        This class does open the resource, don't open it for yourself!
 46        """
 47        self.resource = resource
 48        self.scan_list = scan_list
 49        self.scan_index = 0
 50        self.voltage_range = 20  # The maximum voltage that can be measured. Range(V) = 0.2, 2, 20, 200, 1000, AUTO
 51        self.impedance_config = DMMImpedance(2, 7)
 52        self.reset()
 53
 54    @property
 55    def _scan_list_str(self):
 56        """A string used by the VISA commands."""
 57        slot = self.scan_list[self.scan_index]["slot"]
 58        channel = self.scan_list[self.scan_index]["channel"]
 59        return f"(@{slot}{channel:02})"
 60
 61    def configure_voltage_trigger(self, timestamps: bool = False):
 62        """Set up trigger parameters. Up to 50,000 readings."""
 63        self.resource.lock.acquire()
 64        self.resource.write(f"CONF:VOLT:DC {self.voltage_range},MAX,{self._scan_list_str}")  # Fastest integration
 65        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
 66        self.resource.write("ZERO:AUTO OFF")  # No zero reading. Speeds up measurement
 67        if timestamps:
 68            self.resource.write("FORM:READ:TIME ON")  # Include relative timestamp
 69        # self.resource.write("TRIG:SOUR IMM")  # Software trigger
 70        self.resource.write("TRIG:SOUR TIM")  # Software trigger
 71        self.resource.write("TRIG:TIM 0.001")  # 0.001 seconds between measurements
 72        self.resource.write("TRIG:COUN 300")  # 1 to 50000 scans (~250 Hz)
 73        self.resource.write("*ESE 1")  # Enable status bit (helps determine when operation completed)
 74
 75    def send_trigger(self):
 76        """Send a software trigger."""
 77
 78        # FIXME(JA): Speed up measurements
 79        # self.resource.write(f"VOLT:DC:APER MIN,{self._scan_list_str}")
 80        # self.resource.write(f"VOLT:DC:NPLC MIN,{self._scan_list_str}")
 81        # self.resource.write(f"VOLT:DC:RES MAX,{self._scan_list_str}")
 82        # self.resource.write(f"ROUT:CHAN:DEL 0,{self._scan_list_str}")
 83
 84        self.resource.write("INIT")  # Run scans
 85        self.resource.write("*OPC")  # Set status bit to 1 when operation completes
 86        time.sleep(0.25)
 87
 88    def read_internal_memory(self) -> list[float]:
 89        """Return measured data from internal memory."""
 90
 91        # Wait until operation completes
 92        while int(self.resource.query("*ESR?")) & 1 != 1:
 93            time.sleep(1)
 94
 95        # Fetch measurements
 96        result = self.resource.query("FETC?")
 97
 98        # Decode measurements
 99        last_valid_num = 0
100
101        def decode_num(string_num):
102            """Gracefully handle invalid numbers in returned values."""
103            nonlocal last_valid_num
104            try:
105                last_valid_num = float(string_num)
106            except ValueError:
107                logger.write_debug_to_report(f"Could not convert: {string_num}, using {last_valid_num}")
108            return last_valid_num
109
110        return list(map(decode_num, result.split(",")))
111
112    def configure_voltage_normal(self):
113        """The voltage settings for normal measurements."""
114        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
115        self.resource.write("ZERO:AUTO ON")  # Activate zero reading to increase accuracy
116        self.resource.write("VOLT:DC:NPLC 10")  # NPLC = 0.02, 0.2, 1, 10, 100, 200. 1PLC = 0.02s
117        self.resource.write("FORM:READ:TIME OFF")  # Remove relative timestamp
118        self.resource.lock.release()
119
120    @property
121    def volts(self) -> float:
122        """Measures DC voltage."""
123        return float(self.resource.query(f"MEAS:VOLT:DC? {self.voltage_range},DEF,{self._scan_list_str}"))
124
125    @property
126    def amps_ac(self) -> float:
127        """Measures AC current."""
128        return float(self.resource.query(f"MEAS:CURR:AC? AUTO,DEF,{self._scan_list_str}"))
129
130    def reset(self):
131        """Resets the instrument."""
132        self.resource.write("*CLS")  # Reset any errors
133        self.resource.write("*RST")  # Put the system in a known state
134        # resource.write("FORM:READ:TIME ON")  # Include relative timestamp
135        for self.scan_index in range(len(self.scan_list)):
136            self.resource.lock.acquire()
137            self.configure_voltage_normal()
138
139        for i in range(100, 600, 100):
140            logger.write_debug_to_report(f"Slot {i}: {self.resource.query(f'SYST:CTYP? {i}').rstrip(chr(10))}")

Rigol M300 DMM command wrapper.

M300Dmm( resource: hitl_tester.modules.bms_types.SafeResource, scan_list: list[dict[str, int]])
42    def __init__(self, resource: SafeResource, scan_list: list[dict[str, int]]):
43        """
44        Initialize the M300 wrapper with a specific PyVISA resource.
45        This class does open the resource, don't open it for yourself!
46        """
47        self.resource = resource
48        self.scan_list = scan_list
49        self.scan_index = 0
50        self.voltage_range = 20  # The maximum voltage that can be measured. Range(V) = 0.2, 2, 20, 200, 1000, AUTO
51        self.impedance_config = DMMImpedance(2, 7)
52        self.reset()

Initialize the M300 wrapper with a specific PyVISA resource. This class does open the resource, don't open it for yourself!

resource
scan_list
scan_index
voltage_range
impedance_config
def configure_voltage_trigger(self, timestamps: bool = False):
61    def configure_voltage_trigger(self, timestamps: bool = False):
62        """Set up trigger parameters. Up to 50,000 readings."""
63        self.resource.lock.acquire()
64        self.resource.write(f"CONF:VOLT:DC {self.voltage_range},MAX,{self._scan_list_str}")  # Fastest integration
65        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
66        self.resource.write("ZERO:AUTO OFF")  # No zero reading. Speeds up measurement
67        if timestamps:
68            self.resource.write("FORM:READ:TIME ON")  # Include relative timestamp
69        # self.resource.write("TRIG:SOUR IMM")  # Software trigger
70        self.resource.write("TRIG:SOUR TIM")  # Software trigger
71        self.resource.write("TRIG:TIM 0.001")  # 0.001 seconds between measurements
72        self.resource.write("TRIG:COUN 300")  # 1 to 50000 scans (~250 Hz)
73        self.resource.write("*ESE 1")  # Enable status bit (helps determine when operation completed)

Set up trigger parameters. Up to 50,000 readings.

def send_trigger(self):
75    def send_trigger(self):
76        """Send a software trigger."""
77
78        # FIXME(JA): Speed up measurements
79        # self.resource.write(f"VOLT:DC:APER MIN,{self._scan_list_str}")
80        # self.resource.write(f"VOLT:DC:NPLC MIN,{self._scan_list_str}")
81        # self.resource.write(f"VOLT:DC:RES MAX,{self._scan_list_str}")
82        # self.resource.write(f"ROUT:CHAN:DEL 0,{self._scan_list_str}")
83
84        self.resource.write("INIT")  # Run scans
85        self.resource.write("*OPC")  # Set status bit to 1 when operation completes
86        time.sleep(0.25)

Send a software trigger.

def read_internal_memory(self) -> list[float]:
 88    def read_internal_memory(self) -> list[float]:
 89        """Return measured data from internal memory."""
 90
 91        # Wait until operation completes
 92        while int(self.resource.query("*ESR?")) & 1 != 1:
 93            time.sleep(1)
 94
 95        # Fetch measurements
 96        result = self.resource.query("FETC?")
 97
 98        # Decode measurements
 99        last_valid_num = 0
100
101        def decode_num(string_num):
102            """Gracefully handle invalid numbers in returned values."""
103            nonlocal last_valid_num
104            try:
105                last_valid_num = float(string_num)
106            except ValueError:
107                logger.write_debug_to_report(f"Could not convert: {string_num}, using {last_valid_num}")
108            return last_valid_num
109
110        return list(map(decode_num, result.split(",")))

Return measured data from internal memory.

def configure_voltage_normal(self):
112    def configure_voltage_normal(self):
113        """The voltage settings for normal measurements."""
114        self.resource.write(f"ROUT:SCAN {self._scan_list_str}")  # Select only this channel for the trigger
115        self.resource.write("ZERO:AUTO ON")  # Activate zero reading to increase accuracy
116        self.resource.write("VOLT:DC:NPLC 10")  # NPLC = 0.02, 0.2, 1, 10, 100, 200. 1PLC = 0.02s
117        self.resource.write("FORM:READ:TIME OFF")  # Remove relative timestamp
118        self.resource.lock.release()

The voltage settings for normal measurements.

volts: float
120    @property
121    def volts(self) -> float:
122        """Measures DC voltage."""
123        return float(self.resource.query(f"MEAS:VOLT:DC? {self.voltage_range},DEF,{self._scan_list_str}"))

Measures DC voltage.

amps_ac: float
125    @property
126    def amps_ac(self) -> float:
127        """Measures AC current."""
128        return float(self.resource.query(f"MEAS:CURR:AC? AUTO,DEF,{self._scan_list_str}"))

Measures AC current.

def reset(self):
130    def reset(self):
131        """Resets the instrument."""
132        self.resource.write("*CLS")  # Reset any errors
133        self.resource.write("*RST")  # Put the system in a known state
134        # resource.write("FORM:READ:TIME ON")  # Include relative timestamp
135        for self.scan_index in range(len(self.scan_list)):
136            self.resource.lock.acquire()
137            self.configure_voltage_normal()
138
139        for i in range(100, 600, 100):
140            logger.write_debug_to_report(f"Slot {i}: {self.resource.query(f'SYST:CTYP? {i}').rstrip(chr(10))}")

Resets the instrument.