hitl_tester.modules.bms_types

Various types shared between modules.

(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"""
  2Various types shared between modules.
  3
  4(c) 2020-2024 TurnAround Factor, Inc.
  5
  6CUI DISTRIBUTION CONTROL
  7Controlled by: DLA J68 R&D SBIP
  8CUI Category: Small Business Research and Technology
  9Distribution/Dissemination Controls: PROTECTED BY SBIR DATA RIGHTS
 10POC: GOV SBIP Program Manager Denise Price, 571-767-0111
 11Distribution authorized to U.S. Government Agencies only, to protect information not owned by the
 12U.S. Government and protected by a contractor’s ‘limited rights’ statement, or received with the understanding that
 13it is not routinely transmitted outside the U.S. Government (determination made September 14, 2024). Other requests
 14for this document shall be referred to ACOR DLA Logistics Operations (J-68), 8725 John J. Kingman Rd., Suite 4317,
 15Fort Belvoir, VA 22060-6221
 16
 17SBIR DATA RIGHTS
 18Contract No.:SP4701-23-C-0083
 19Contractor Name: TurnAround Factor, Inc.
 20Contractor Address: 10365 Wood Park Ct. Suite 313 / Ashland, VA 23005
 21Expiration of SBIR Data Rights Period: September 24, 2029
 22The Government's rights to use, modify, reproduce, release, perform, display, or disclose technical data or computer
 23software marked with this legend are restricted during the period shown as provided in paragraph (b)(4) of the Rights
 24in Noncommercial Technical Data and Computer Software--Small Business Innovative Research (SBIR) Program clause
 25contained in the above identified contract. No restrictions apply after the expiration date shown above. Any
 26reproduction of technical data, computer software, or portions thereof marked with this legend must also reproduce
 27the markings.
 28"""
 29
 30from __future__ import annotations
 31
 32import inspect
 33import time
 34import traceback
 35from dataclasses import dataclass
 36from enum import Enum, auto, IntFlag, IntEnum
 37from pathlib import Path
 38from types import TracebackType
 39from typing import Protocol, runtime_checkable, Any, Type
 40
 41import _pytest
 42
 43try:
 44    from pyvisa.constants import StatusCode
 45except ModuleNotFoundError:
 46    pass  # Allow hitl_tester to import bms_types
 47
 48try:
 49    from hitl_tester.modules.logger import logger
 50    from hitl_tester.modules.file_lock import ResourceLock
 51except ModuleNotFoundError:
 52    pass  # Allow hitl_tester to import bms_types
 53
 54
 55# Test Hardware Type Definitions
 56@dataclass
 57class DMMImpedance:
 58    """Impedance config for DMM."""
 59
 60    pulse_current: int
 61    total_readings: int  # Used for averaging results
 62
 63
 64class BatteryType(Enum):
 65    """Supported battery types."""
 66
 67    UNDEFINED = auto()
 68    NICD = auto()
 69    LI = auto()
 70
 71
 72class NiCdChargeCycle(Enum):
 73    """Supported NiCad charge cycle types."""
 74
 75    UNDEFINED = 0
 76    STANDARD = 1  # 0.1C for 16 hours
 77    CUSTOM = 2
 78    DV_DT = 3  # C/2 for 2.5 hours
 79
 80
 81class DischargeType(Enum):
 82    """Supported discharge types."""
 83
 84    UNDEFINED = 0
 85    CONSTANT_CURRENT = 1
 86    CONSTANT_RESISTANCE = 2
 87    CONSTANT_VOLTAGE = 3
 88
 89
 90# Other
 91class StopWatch:
 92    """Record time with support for suspension."""
 93
 94    def __init__(self) -> None:
 95        self._start_time: float = time.perf_counter()
 96        self._suspend_time: float | None = None
 97
 98    @property
 99    def elapsed_time(self) -> float:
100        """How much time has passed since reset()."""
101        if self._suspend_time is None:
102            return time.perf_counter() - self._start_time
103        return self._suspend_time - self._start_time
104
105    def reset(self) -> None:
106        """Reset the timer to 0."""
107        self._start_time = time.perf_counter()
108
109    def start(self) -> None:
110        """Start the timer."""
111        if self._suspend_time is not None:
112            self._start_time += time.perf_counter() - self._suspend_time
113            self._suspend_time = None
114
115    def stop(self) -> None:
116        """Stop the timer."""
117        self._suspend_time = time.perf_counter()
118
119
120# Enums
121class ControlStatusRegister(IntFlag):
122    """Reset flags from the CSR."""
123
124    LOW_POWER = 0x80000000
125    WINDOW_WATCHDOG = 0x40000000
126    INDEPENDANT_WATCHDOG = 0x20000000
127    SOFTWARE = 0x10000000
128    POWER = 0x08000000
129    RESET_PIN = 0x04000000
130    OPTION_BYTE_LOADER = 0x02000000
131
132    def __str__(self) -> str:
133        return " | ".join(str(flag.name).title() for flag in ControlStatusRegister if self.value & flag)
134
135
136class CellState(IntEnum):
137    """The current state of a cell."""
138
139    UNKNOWN = 0
140    RESTING = 1
141    CHARGING_EXCITED = 2
142    DISCHARGING_EXCITED = 3
143    CHARGING_TIMER = 4
144    DISCHARGING_TIMER = 5
145
146    def __str__(self) -> str:
147        return self.name.title().replace("_", " ")
148
149    @classmethod
150    def _missing_(cls, _: object):
151        return cls(cls.UNKNOWN)
152
153
154class BMSState(IntEnum):
155    """The current state of the BMS."""
156
157    UNKNOWN = 0
158    SLOW_SAMPLE = 1
159    FAST_SAMPLE = 2
160    DEEP_SLUMBER = 3
161    CALIBRATION = 4
162    PREFAULT = 5
163    FAULT = 6
164    FAULT_SLUMBER = 7
165    FAULT_PERMANENT_DISABLE = 8
166
167    def __str__(self) -> str:
168        return self.name.title().replace("_", " ")
169
170    @classmethod
171    def _missing_(cls, _: object):
172        return cls(cls.UNKNOWN)
173
174
175# BMS HW Enums
176class CellCompMode(Enum):
177    """
178    This circuit compensates the output of the dc source according to the input capacitance of the device being tested
179    as well as the type of output connections being used.
180    """
181
182    HREMOTE = auto()  # (DEFAULT) Used for faster response with long load leads using remote sensing.
183    HLOCAL = auto()  # Use for faster response with short load leads or bench operation (no external cap needed).
184    LREMOTE = auto()  # Used for slower response with long load leads using remote sensing.
185    LLOCAL = auto()  # Used for slower response with short load leads or bench operation.
186
187
188class NGIMode(Enum):
189    """
190    This circuit compensates the output of the dc source according to the input capacitance of the device being tested
191    as well as the type of output connections being used.
192    """
193
194    SOURCE = 0  # N83624 source mode includes constant voltage.
195    CHARGE = 1  # Under Charge mode, battery charging and discharging.
196    SOC = 2  # The SOC function simulates battery charging.
197    SEQ = 3  # Executes sequentially according to the output parameters of a step file.
198
199
200# Exceptions
201class LoggingError(Exception):
202    """Log the exception before raising it."""
203
204    def __init__(self, message):
205        """Output error message and raise exception."""
206        logger.write_critical_to_report(f"{type(self).__name__} - {message}")
207        super().__init__(message)
208
209
210class PacketError(LoggingError):
211    """Raised when a packet in malformed."""
212
213
214class ResourceNotFoundError(LoggingError):
215    """Raised when a hardware resource is not found."""
216
217
218class OverTemperatureError(LoggingError):
219    """Raised when an over-temperature is triggered."""
220
221
222class OverVoltageError(LoggingError):
223    """Raised when an overvoltage is triggered."""
224
225
226class UnderVoltageError(LoggingError):
227    """Raised when an undervoltage is triggered."""
228
229
230class TimeoutExceededError(LoggingError):
231    """Some event did not occur in the specified amount of time."""
232
233
234class VisaSystemError(LoggingError):
235    """The instrument reported a system error."""
236
237
238class ValueLogError(LoggingError):
239    """A value error that gets logged."""
240
241
242class ChargerOrLoad(Protocol):
243    """Protocol for a charger or load object"""
244
245    def enable(self):
246        """Enables instrument output"""
247
248    def disable(self):
249        """Disables instrument output"""
250
251    def reset(self):
252        """Resets the instrument"""
253
254
255class SuppressExceptions:
256    """Suppresses all exceptions."""
257
258    def __enter__(self):
259        """NOP"""
260
261    def __exit__(
262        self, exc_type: Type[BaseException] | None, exc_value: BaseException | None, exc_traceback: TracebackType | None
263    ) -> bool:
264        """Suppress all exceptions by returning True."""
265        whitelist = [None, _pytest.outcomes.Exit, KeyboardInterrupt, SystemExit]
266        if result := exc_type not in whitelist:
267            exception_message = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
268            logger.write_debug_to_report(exception_message.rstrip("\n"))
269        return result
270
271
272class SafeResource:
273    """Gives multiprocess-safe access to Pyvisa resources without raising exceptions."""
274
275    def __init__(self, resource_address: str | tuple[str, str, str]):
276        self.resource_address = resource_address
277        self.lock = ResourceLock(resource_address)
278        self.default_result = "0"
279
280    def log_caller(self):
281        """Log the calling function."""
282        caller = (
283            f"File {inspect.stack()[2].filename}, line {inspect.stack()[2].lineno}, "
284            f"in {inspect.stack()[2].function}\n    {(''.join(inspect.stack()[2].code_context or [])).strip(chr(10))}"
285        )
286        logger.write_debug_to_report(caller)
287
288    @property
289    def baud_rate(self) -> int:
290        """The baud rate for the serial connection."""
291        return int(self.lock.baud_rate)
292
293    @baud_rate.setter
294    def baud_rate(self, new_baud_rate: int):
295        self.lock.baud_rate = new_baud_rate
296
297    @property
298    def last_status(self) -> StatusCode:
299        """PyVISA status code suppressing exceptions."""
300        with SuppressExceptions(), self.lock:
301            assert self.lock.resource
302            return self.lock.resource.last_status
303        return StatusCode.success
304
305    @property
306    def timeout(self) -> float:
307        """Get PyVISA timeout value."""
308        with SuppressExceptions(), self.lock:
309            assert self.lock.resource
310            return self.lock.resource.timeout
311        return 0.0
312
313    @timeout.setter
314    def timeout(self, new_timeout: float):
315        """Set PyVISA timeout value."""
316        with SuppressExceptions(), self.lock:
317            assert self.lock.resource
318            self.lock.resource.timeout = new_timeout
319
320    def write(self, command: str):
321        """PyVISA write suppressing exceptions."""
322        with SuppressExceptions(), self.lock:
323            assert self.lock.resource
324            self.lock.resource.write(command)
325            return
326        self.log_caller()
327
328    def query(self, command: str) -> str:
329        """PyVISA query suppressing exceptions."""
330        with SuppressExceptions(), self.lock:
331            assert self.lock.resource
332            return self.lock.resource.query(command)
333        self.log_caller()
334        return self.default_result
335
336    def read_raw(self) -> bytes:
337        """PyVISA read_raw suppressing exceptions."""
338        with SuppressExceptions(), self.lock:
339            assert self.lock.resource
340            return self.lock.resource.read_raw()
341        self.log_caller()
342        return b""
343
344
345# Flags
346@runtime_checkable
347class BMSFlags(Protocol):
348    """The flags passed from hitl_tester to bms_hw."""
349
350    plateset_id: str = ""
351    dry_run: bool = False
352    doc_generation: bool = False
353    cell_chemistry: str = ""
354    config: dict[str, str | dict[str, Any]] = {}  # FIXME(JA): break out into separate attributes with sane defaults
355    report_filename: Path = Path()
356    properties: dict[str, Any] | None = {}
357    test_plan: Path = Path()
358
359
360class TestFlags(BMSFlags):
361    """The flags to be passed from hitl_tester to bms_hw."""
362
363    def __init__(
364        self,
365        dry_run: bool = False,
366        doc_generation: bool = False,
367        cell_chemistry: str = "",
368        properties: dict[str, Any] | None = None,
369        test_plan: Path = Path(),
370    ):
371        self.dry_run = dry_run
372        self.doc_generation = doc_generation
373        self.cell_chemistry = cell_chemistry
374        self.properties = properties or {}
375        self.config = {}
376        self.test_plan = test_plan
@dataclass
class DMMImpedance:
57@dataclass
58class DMMImpedance:
59    """Impedance config for DMM."""
60
61    pulse_current: int
62    total_readings: int  # Used for averaging results

Impedance config for DMM.

DMMImpedance(pulse_current: int, total_readings: int)
pulse_current: int
total_readings: int
class BatteryType(enum.Enum):
65class BatteryType(Enum):
66    """Supported battery types."""
67
68    UNDEFINED = auto()
69    NICD = auto()
70    LI = auto()

Supported battery types.

UNDEFINED = <BatteryType.UNDEFINED: 1>
NICD = <BatteryType.NICD: 2>
LI = <BatteryType.LI: 3>
Inherited Members
enum.Enum
name
value
class NiCdChargeCycle(enum.Enum):
73class NiCdChargeCycle(Enum):
74    """Supported NiCad charge cycle types."""
75
76    UNDEFINED = 0
77    STANDARD = 1  # 0.1C for 16 hours
78    CUSTOM = 2
79    DV_DT = 3  # C/2 for 2.5 hours

Supported NiCad charge cycle types.

UNDEFINED = <NiCdChargeCycle.UNDEFINED: 0>
STANDARD = <NiCdChargeCycle.STANDARD: 1>
CUSTOM = <NiCdChargeCycle.CUSTOM: 2>
DV_DT = <NiCdChargeCycle.DV_DT: 3>
Inherited Members
enum.Enum
name
value
class DischargeType(enum.Enum):
82class DischargeType(Enum):
83    """Supported discharge types."""
84
85    UNDEFINED = 0
86    CONSTANT_CURRENT = 1
87    CONSTANT_RESISTANCE = 2
88    CONSTANT_VOLTAGE = 3

Supported discharge types.

UNDEFINED = <DischargeType.UNDEFINED: 0>
CONSTANT_CURRENT = <DischargeType.CONSTANT_CURRENT: 1>
CONSTANT_RESISTANCE = <DischargeType.CONSTANT_RESISTANCE: 2>
CONSTANT_VOLTAGE = <DischargeType.CONSTANT_VOLTAGE: 3>
Inherited Members
enum.Enum
name
value
class StopWatch:
 92class StopWatch:
 93    """Record time with support for suspension."""
 94
 95    def __init__(self) -> None:
 96        self._start_time: float = time.perf_counter()
 97        self._suspend_time: float | None = None
 98
 99    @property
100    def elapsed_time(self) -> float:
101        """How much time has passed since reset()."""
102        if self._suspend_time is None:
103            return time.perf_counter() - self._start_time
104        return self._suspend_time - self._start_time
105
106    def reset(self) -> None:
107        """Reset the timer to 0."""
108        self._start_time = time.perf_counter()
109
110    def start(self) -> None:
111        """Start the timer."""
112        if self._suspend_time is not None:
113            self._start_time += time.perf_counter() - self._suspend_time
114            self._suspend_time = None
115
116    def stop(self) -> None:
117        """Stop the timer."""
118        self._suspend_time = time.perf_counter()

Record time with support for suspension.

elapsed_time: float
 99    @property
100    def elapsed_time(self) -> float:
101        """How much time has passed since reset()."""
102        if self._suspend_time is None:
103            return time.perf_counter() - self._start_time
104        return self._suspend_time - self._start_time

How much time has passed since reset().

def reset(self) -> None:
106    def reset(self) -> None:
107        """Reset the timer to 0."""
108        self._start_time = time.perf_counter()

Reset the timer to 0.

def start(self) -> None:
110    def start(self) -> None:
111        """Start the timer."""
112        if self._suspend_time is not None:
113            self._start_time += time.perf_counter() - self._suspend_time
114            self._suspend_time = None

Start the timer.

def stop(self) -> None:
116    def stop(self) -> None:
117        """Stop the timer."""
118        self._suspend_time = time.perf_counter()

Stop the timer.

class ControlStatusRegister(enum.IntFlag):
122class ControlStatusRegister(IntFlag):
123    """Reset flags from the CSR."""
124
125    LOW_POWER = 0x80000000
126    WINDOW_WATCHDOG = 0x40000000
127    INDEPENDANT_WATCHDOG = 0x20000000
128    SOFTWARE = 0x10000000
129    POWER = 0x08000000
130    RESET_PIN = 0x04000000
131    OPTION_BYTE_LOADER = 0x02000000
132
133    def __str__(self) -> str:
134        return " | ".join(str(flag.name).title() for flag in ControlStatusRegister if self.value & flag)

Reset flags from the CSR.

LOW_POWER = <ControlStatusRegister.LOW_POWER: 2147483648>
WINDOW_WATCHDOG = <ControlStatusRegister.WINDOW_WATCHDOG: 1073741824>
INDEPENDANT_WATCHDOG = <ControlStatusRegister.INDEPENDANT_WATCHDOG: 536870912>
SOFTWARE = <ControlStatusRegister.SOFTWARE: 268435456>
POWER = <ControlStatusRegister.POWER: 134217728>
RESET_PIN = <ControlStatusRegister.RESET_PIN: 67108864>
OPTION_BYTE_LOADER = <ControlStatusRegister.OPTION_BYTE_LOADER: 33554432>
Inherited Members
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
enum.Enum
name
value
class CellState(enum.IntEnum):
137class CellState(IntEnum):
138    """The current state of a cell."""
139
140    UNKNOWN = 0
141    RESTING = 1
142    CHARGING_EXCITED = 2
143    DISCHARGING_EXCITED = 3
144    CHARGING_TIMER = 4
145    DISCHARGING_TIMER = 5
146
147    def __str__(self) -> str:
148        return self.name.title().replace("_", " ")
149
150    @classmethod
151    def _missing_(cls, _: object):
152        return cls(cls.UNKNOWN)

The current state of a cell.

UNKNOWN = <CellState.UNKNOWN: 0>
RESTING = <CellState.RESTING: 1>
CHARGING_EXCITED = <CellState.CHARGING_EXCITED: 2>
DISCHARGING_EXCITED = <CellState.DISCHARGING_EXCITED: 3>
CHARGING_TIMER = <CellState.CHARGING_TIMER: 4>
DISCHARGING_TIMER = <CellState.DISCHARGING_TIMER: 5>
Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
class BMSState(enum.IntEnum):
155class BMSState(IntEnum):
156    """The current state of the BMS."""
157
158    UNKNOWN = 0
159    SLOW_SAMPLE = 1
160    FAST_SAMPLE = 2
161    DEEP_SLUMBER = 3
162    CALIBRATION = 4
163    PREFAULT = 5
164    FAULT = 6
165    FAULT_SLUMBER = 7
166    FAULT_PERMANENT_DISABLE = 8
167
168    def __str__(self) -> str:
169        return self.name.title().replace("_", " ")
170
171    @classmethod
172    def _missing_(cls, _: object):
173        return cls(cls.UNKNOWN)

The current state of the BMS.

UNKNOWN = <BMSState.UNKNOWN: 0>
SLOW_SAMPLE = <BMSState.SLOW_SAMPLE: 1>
FAST_SAMPLE = <BMSState.FAST_SAMPLE: 2>
DEEP_SLUMBER = <BMSState.DEEP_SLUMBER: 3>
CALIBRATION = <BMSState.CALIBRATION: 4>
PREFAULT = <BMSState.PREFAULT: 5>
FAULT = <BMSState.FAULT: 6>
FAULT_SLUMBER = <BMSState.FAULT_SLUMBER: 7>
FAULT_PERMANENT_DISABLE = <BMSState.FAULT_PERMANENT_DISABLE: 8>
Inherited Members
enum.Enum
name
value
builtins.int
conjugate
bit_length
bit_count
to_bytes
from_bytes
as_integer_ratio
is_integer
real
imag
numerator
denominator
class CellCompMode(enum.Enum):
177class CellCompMode(Enum):
178    """
179    This circuit compensates the output of the dc source according to the input capacitance of the device being tested
180    as well as the type of output connections being used.
181    """
182
183    HREMOTE = auto()  # (DEFAULT) Used for faster response with long load leads using remote sensing.
184    HLOCAL = auto()  # Use for faster response with short load leads or bench operation (no external cap needed).
185    LREMOTE = auto()  # Used for slower response with long load leads using remote sensing.
186    LLOCAL = auto()  # Used for slower response with short load leads or bench operation.

This circuit compensates the output of the dc source according to the input capacitance of the device being tested as well as the type of output connections being used.

HREMOTE = <CellCompMode.HREMOTE: 1>
HLOCAL = <CellCompMode.HLOCAL: 2>
LREMOTE = <CellCompMode.LREMOTE: 3>
LLOCAL = <CellCompMode.LLOCAL: 4>
Inherited Members
enum.Enum
name
value
class NGIMode(enum.Enum):
189class NGIMode(Enum):
190    """
191    This circuit compensates the output of the dc source according to the input capacitance of the device being tested
192    as well as the type of output connections being used.
193    """
194
195    SOURCE = 0  # N83624 source mode includes constant voltage.
196    CHARGE = 1  # Under Charge mode, battery charging and discharging.
197    SOC = 2  # The SOC function simulates battery charging.
198    SEQ = 3  # Executes sequentially according to the output parameters of a step file.

This circuit compensates the output of the dc source according to the input capacitance of the device being tested as well as the type of output connections being used.

SOURCE = <NGIMode.SOURCE: 0>
CHARGE = <NGIMode.CHARGE: 1>
SOC = <NGIMode.SOC: 2>
SEQ = <NGIMode.SEQ: 3>
Inherited Members
enum.Enum
name
value
class LoggingError(builtins.Exception):
202class LoggingError(Exception):
203    """Log the exception before raising it."""
204
205    def __init__(self, message):
206        """Output error message and raise exception."""
207        logger.write_critical_to_report(f"{type(self).__name__} - {message}")
208        super().__init__(message)

Log the exception before raising it.

LoggingError(message)
205    def __init__(self, message):
206        """Output error message and raise exception."""
207        logger.write_critical_to_report(f"{type(self).__name__} - {message}")
208        super().__init__(message)

Output error message and raise exception.

Inherited Members
builtins.BaseException
with_traceback
add_note
args
class PacketError(LoggingError):
211class PacketError(LoggingError):
212    """Raised when a packet in malformed."""

Raised when a packet in malformed.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class ResourceNotFoundError(LoggingError):
215class ResourceNotFoundError(LoggingError):
216    """Raised when a hardware resource is not found."""

Raised when a hardware resource is not found.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class OverTemperatureError(LoggingError):
219class OverTemperatureError(LoggingError):
220    """Raised when an over-temperature is triggered."""

Raised when an over-temperature is triggered.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class OverVoltageError(LoggingError):
223class OverVoltageError(LoggingError):
224    """Raised when an overvoltage is triggered."""

Raised when an overvoltage is triggered.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class UnderVoltageError(LoggingError):
227class UnderVoltageError(LoggingError):
228    """Raised when an undervoltage is triggered."""

Raised when an undervoltage is triggered.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class TimeoutExceededError(LoggingError):
231class TimeoutExceededError(LoggingError):
232    """Some event did not occur in the specified amount of time."""

Some event did not occur in the specified amount of time.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class VisaSystemError(LoggingError):
235class VisaSystemError(LoggingError):
236    """The instrument reported a system error."""

The instrument reported a system error.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class ValueLogError(LoggingError):
239class ValueLogError(LoggingError):
240    """A value error that gets logged."""

A value error that gets logged.

Inherited Members
LoggingError
LoggingError
builtins.BaseException
with_traceback
add_note
args
class ChargerOrLoad(typing.Protocol):
243class ChargerOrLoad(Protocol):
244    """Protocol for a charger or load object"""
245
246    def enable(self):
247        """Enables instrument output"""
248
249    def disable(self):
250        """Disables instrument output"""
251
252    def reset(self):
253        """Resets the instrument"""

Protocol for a charger or load object

ChargerOrLoad(*args, **kwargs)
1767def _no_init_or_replace_init(self, *args, **kwargs):
1768    cls = type(self)
1769
1770    if cls._is_protocol:
1771        raise TypeError('Protocols cannot be instantiated')
1772
1773    # Already using a custom `__init__`. No need to calculate correct
1774    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1775    if cls.__init__ is not _no_init_or_replace_init:
1776        return
1777
1778    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1779    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1780    # searches for a proper new `__init__` in the MRO. The new `__init__`
1781    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1782    # instantiation of the protocol subclass will thus use the new
1783    # `__init__` and no longer call `_no_init_or_replace_init`.
1784    for base in cls.__mro__:
1785        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1786        if init is not _no_init_or_replace_init:
1787            cls.__init__ = init
1788            break
1789    else:
1790        # should not happen
1791        cls.__init__ = object.__init__
1792
1793    cls.__init__(self, *args, **kwargs)
def enable(self):
246    def enable(self):
247        """Enables instrument output"""

Enables instrument output

def disable(self):
249    def disable(self):
250        """Disables instrument output"""

Disables instrument output

def reset(self):
252    def reset(self):
253        """Resets the instrument"""

Resets the instrument

class SuppressExceptions:
256class SuppressExceptions:
257    """Suppresses all exceptions."""
258
259    def __enter__(self):
260        """NOP"""
261
262    def __exit__(
263        self, exc_type: Type[BaseException] | None, exc_value: BaseException | None, exc_traceback: TracebackType | None
264    ) -> bool:
265        """Suppress all exceptions by returning True."""
266        whitelist = [None, _pytest.outcomes.Exit, KeyboardInterrupt, SystemExit]
267        if result := exc_type not in whitelist:
268            exception_message = "".join(traceback.format_exception(exc_type, exc_value, exc_traceback))
269            logger.write_debug_to_report(exception_message.rstrip("\n"))
270        return result

Suppresses all exceptions.

class SafeResource:
273class SafeResource:
274    """Gives multiprocess-safe access to Pyvisa resources without raising exceptions."""
275
276    def __init__(self, resource_address: str | tuple[str, str, str]):
277        self.resource_address = resource_address
278        self.lock = ResourceLock(resource_address)
279        self.default_result = "0"
280
281    def log_caller(self):
282        """Log the calling function."""
283        caller = (
284            f"File {inspect.stack()[2].filename}, line {inspect.stack()[2].lineno}, "
285            f"in {inspect.stack()[2].function}\n    {(''.join(inspect.stack()[2].code_context or [])).strip(chr(10))}"
286        )
287        logger.write_debug_to_report(caller)
288
289    @property
290    def baud_rate(self) -> int:
291        """The baud rate for the serial connection."""
292        return int(self.lock.baud_rate)
293
294    @baud_rate.setter
295    def baud_rate(self, new_baud_rate: int):
296        self.lock.baud_rate = new_baud_rate
297
298    @property
299    def last_status(self) -> StatusCode:
300        """PyVISA status code suppressing exceptions."""
301        with SuppressExceptions(), self.lock:
302            assert self.lock.resource
303            return self.lock.resource.last_status
304        return StatusCode.success
305
306    @property
307    def timeout(self) -> float:
308        """Get PyVISA timeout value."""
309        with SuppressExceptions(), self.lock:
310            assert self.lock.resource
311            return self.lock.resource.timeout
312        return 0.0
313
314    @timeout.setter
315    def timeout(self, new_timeout: float):
316        """Set PyVISA timeout value."""
317        with SuppressExceptions(), self.lock:
318            assert self.lock.resource
319            self.lock.resource.timeout = new_timeout
320
321    def write(self, command: str):
322        """PyVISA write suppressing exceptions."""
323        with SuppressExceptions(), self.lock:
324            assert self.lock.resource
325            self.lock.resource.write(command)
326            return
327        self.log_caller()
328
329    def query(self, command: str) -> str:
330        """PyVISA query suppressing exceptions."""
331        with SuppressExceptions(), self.lock:
332            assert self.lock.resource
333            return self.lock.resource.query(command)
334        self.log_caller()
335        return self.default_result
336
337    def read_raw(self) -> bytes:
338        """PyVISA read_raw suppressing exceptions."""
339        with SuppressExceptions(), self.lock:
340            assert self.lock.resource
341            return self.lock.resource.read_raw()
342        self.log_caller()
343        return b""

Gives multiprocess-safe access to Pyvisa resources without raising exceptions.

SafeResource(resource_address: str | tuple[str, str, str])
276    def __init__(self, resource_address: str | tuple[str, str, str]):
277        self.resource_address = resource_address
278        self.lock = ResourceLock(resource_address)
279        self.default_result = "0"
resource_address
lock
default_result
def log_caller(self):
281    def log_caller(self):
282        """Log the calling function."""
283        caller = (
284            f"File {inspect.stack()[2].filename}, line {inspect.stack()[2].lineno}, "
285            f"in {inspect.stack()[2].function}\n    {(''.join(inspect.stack()[2].code_context or [])).strip(chr(10))}"
286        )
287        logger.write_debug_to_report(caller)

Log the calling function.

baud_rate: int
289    @property
290    def baud_rate(self) -> int:
291        """The baud rate for the serial connection."""
292        return int(self.lock.baud_rate)

The baud rate for the serial connection.

last_status: pyvisa.constants.StatusCode
298    @property
299    def last_status(self) -> StatusCode:
300        """PyVISA status code suppressing exceptions."""
301        with SuppressExceptions(), self.lock:
302            assert self.lock.resource
303            return self.lock.resource.last_status
304        return StatusCode.success

PyVISA status code suppressing exceptions.

timeout: float
306    @property
307    def timeout(self) -> float:
308        """Get PyVISA timeout value."""
309        with SuppressExceptions(), self.lock:
310            assert self.lock.resource
311            return self.lock.resource.timeout
312        return 0.0

Get PyVISA timeout value.

def write(self, command: str):
321    def write(self, command: str):
322        """PyVISA write suppressing exceptions."""
323        with SuppressExceptions(), self.lock:
324            assert self.lock.resource
325            self.lock.resource.write(command)
326            return
327        self.log_caller()

PyVISA write suppressing exceptions.

def query(self, command: str) -> str:
329    def query(self, command: str) -> str:
330        """PyVISA query suppressing exceptions."""
331        with SuppressExceptions(), self.lock:
332            assert self.lock.resource
333            return self.lock.resource.query(command)
334        self.log_caller()
335        return self.default_result

PyVISA query suppressing exceptions.

def read_raw(self) -> bytes:
337    def read_raw(self) -> bytes:
338        """PyVISA read_raw suppressing exceptions."""
339        with SuppressExceptions(), self.lock:
340            assert self.lock.resource
341            return self.lock.resource.read_raw()
342        self.log_caller()
343        return b""

PyVISA read_raw suppressing exceptions.

@runtime_checkable
class BMSFlags(typing.Protocol):
347@runtime_checkable
348class BMSFlags(Protocol):
349    """The flags passed from hitl_tester to bms_hw."""
350
351    plateset_id: str = ""
352    dry_run: bool = False
353    doc_generation: bool = False
354    cell_chemistry: str = ""
355    config: dict[str, str | dict[str, Any]] = {}  # FIXME(JA): break out into separate attributes with sane defaults
356    report_filename: Path = Path()
357    properties: dict[str, Any] | None = {}
358    test_plan: Path = Path()

The flags passed from hitl_tester to bms_hw.

BMSFlags(*args, **kwargs)
1767def _no_init_or_replace_init(self, *args, **kwargs):
1768    cls = type(self)
1769
1770    if cls._is_protocol:
1771        raise TypeError('Protocols cannot be instantiated')
1772
1773    # Already using a custom `__init__`. No need to calculate correct
1774    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1775    if cls.__init__ is not _no_init_or_replace_init:
1776        return
1777
1778    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1779    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1780    # searches for a proper new `__init__` in the MRO. The new `__init__`
1781    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1782    # instantiation of the protocol subclass will thus use the new
1783    # `__init__` and no longer call `_no_init_or_replace_init`.
1784    for base in cls.__mro__:
1785        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1786        if init is not _no_init_or_replace_init:
1787            cls.__init__ = init
1788            break
1789    else:
1790        # should not happen
1791        cls.__init__ = object.__init__
1792
1793    cls.__init__(self, *args, **kwargs)
plateset_id: str = ''
dry_run: bool = False
doc_generation: bool = False
cell_chemistry: str = ''
config: dict[str, str | dict[str, typing.Any]] = {}
report_filename: pathlib.Path = PosixPath('.')
properties: dict[str, typing.Any] | None = {}
test_plan: pathlib.Path = PosixPath('.')
class TestFlags(BMSFlags):
361class TestFlags(BMSFlags):
362    """The flags to be passed from hitl_tester to bms_hw."""
363
364    def __init__(
365        self,
366        dry_run: bool = False,
367        doc_generation: bool = False,
368        cell_chemistry: str = "",
369        properties: dict[str, Any] | None = None,
370        test_plan: Path = Path(),
371    ):
372        self.dry_run = dry_run
373        self.doc_generation = doc_generation
374        self.cell_chemistry = cell_chemistry
375        self.properties = properties or {}
376        self.config = {}
377        self.test_plan = test_plan

The flags to be passed from hitl_tester to bms_hw.

TestFlags( dry_run: bool = False, doc_generation: bool = False, cell_chemistry: str = '', properties: dict[str, typing.Any] | None = None, test_plan: pathlib.Path = PosixPath('.'))
364    def __init__(
365        self,
366        dry_run: bool = False,
367        doc_generation: bool = False,
368        cell_chemistry: str = "",
369        properties: dict[str, Any] | None = None,
370        test_plan: Path = Path(),
371    ):
372        self.dry_run = dry_run
373        self.doc_generation = doc_generation
374        self.cell_chemistry = cell_chemistry
375        self.properties = properties or {}
376        self.config = {}
377        self.test_plan = test_plan
dry_run = False
doc_generation = False
cell_chemistry = ''
properties = {}
config = {}
test_plan = PosixPath('.')