hitl_tester.test_cases.bms.test_d300

The test should sample voltage via the DMM at some desired rate and output a CSV with timestamp & voltage.

Used in these test plans:

  • m300_dmm ⠀⠀⠀(bms/m300_dmm.plan)
  • d300_dmm_2 ⠀⠀⠀(bms/d300_dmm_2.plan)
  • d300_dmm_3 ⠀⠀⠀(bms/d300_dmm_3.plan)

Example Command (warning: test plan may run other test cases):

  • ./hitl_tester.py m300_dmm -DSAMPLE_RATE=10 -DDISCHARGE_CUTOFF=1.0
  1"""
  2The test should sample voltage via the DMM at some desired rate and output a CSV with timestamp & voltage.
  3"""
  4
  5from __future__ import annotations
  6
  7import datetime
  8import time
  9from dataclasses import dataclass
 10from enum import Enum
 11
 12import pytest
 13from colorama import Fore
 14
 15from hitl_tester.modules.bms.bms_hw import BMSHardware
 16from hitl_tester.modules.bms.m300_dmm import M300Dmm
 17from hitl_tester.modules.bms.plateset import Plateset
 18from hitl_tester.modules.bms.csv_tables import CSVRecorders, NiCdQC
 19from hitl_tester.modules.file_lock import FileEvent
 20from hitl_tester.modules.logger import logger
 21
 22SAMPLE_RATE = 10
 23DISCHARGE_CUTOFF = 1.0
 24
 25bms_hardware = BMSHardware(pytest.flags)  # type: ignore[arg-type]
 26bms_hardware.init()
 27plateset = Plateset()
 28
 29
 30class CellState(Enum):
 31    """The current state of the NiCd cell."""
 32
 33    UNKNOWN = Fore.MAGENTA
 34    PASSED_20S = Fore.LIGHTGREEN_EX
 35    PASSED_17S_OR_20S = Fore.GREEN
 36    FAILED = Fore.LIGHTRED_EX
 37
 38
 39@dataclass
 40class Cell:
 41    """A NiCd cell attached to the HITL."""
 42
 43    index: int
 44    slot: int
 45    channel: int
 46    timestamp_1210_ma: float | None = None
 47    timestamp_1200_ma: float | None = None
 48    csv: CSVRecorders | None = None
 49    state = CellState.UNKNOWN
 50
 51    @property
 52    def voltage(self):
 53        """Measure the voltage for this cell."""
 54        bms_hardware.dmm.scan_index = self.index
 55        return bms_hardware.dmm.volts
 56
 57    def __str__(self) -> str:
 58        """Formatted string representation."""
 59        return f"{self.state.value}{self.state.name}{Fore.RESET}" f"[B{self.slot}:C{self.channel}, {self.voltage}V]"
 60
 61
 62def formatted_time(seconds: float | None, start: float | None = 0.0) -> str:
 63    """Return time formatted as hh:mm:ss."""
 64    if seconds is not None and start is not None:
 65        return str(datetime.timedelta(seconds=seconds - start)).partition(".")[0]
 66    return "N/A"
 67
 68
 69def test_record_voltage():
 70    """Record AC current data until killed."""
 71    start_time = time.perf_counter()
 72    while True:
 73        elapsed_time = time.perf_counter() - start_time
 74        volts = bms_hardware.dmm.volts
 75        logger.write_info_to_report(f"Elapsed Time(s): {elapsed_time:.3f}, Voltage(V): {volts}")
 76        time.sleep(SAMPLE_RATE)
 77
 78
 79def test_record_voltage_nicd():
 80    """Record all voltages."""
 81    assert isinstance(bms_hardware.dmm, M300Dmm)
 82    logger.write_info_to_report("Recording...")
 83    test_start = time.perf_counter()
 84    discharge_start = None
 85    kill_event = FileEvent("kill_discharge")
 86    discharge_event = FileEvent("nicd_discharging")
 87
 88    all_cells = [Cell(i, cell["slot"], cell["channel"]) for i, cell in enumerate(bms_hardware.dmm.scan_list)]
 89
 90    # Initialize cells
 91    for cell in all_cells:
 92        cell.csv = NiCdQC(bms_hardware)
 93        cell.csv.create_file(postfix=f"_{cell.slot:01}{cell.channel:02}")
 94
 95    # Use a file lock to communicate with main test (locked = test running, released = test ended)
 96    run_test = True
 97    discharging = False
 98    while run_test:
 99        time_remaining_seconds = max(0.0, (5 * 3600) - (time.perf_counter() - test_start))
100        time_remaining_string = formatted_time(time_remaining_seconds)
101        logger.write_info_to_report(f"Time remaining (H:M:S): {time_remaining_string}")
102        logger.write_info_to_report(", ".join(map(str, all_cells)))
103        for cell in all_cells:
104            if (volts := cell.voltage) < DISCHARGE_CUTOFF:
105                run_test = False
106
107            if discharging:
108                if not cell.timestamp_1210_ma and volts <= 1.21:
109                    cell.timestamp_1210_ma = time.perf_counter()
110                elif not cell.timestamp_1200_ma and volts <= 1.20:
111                    cell.timestamp_1200_ma = time.perf_counter()
112
113                    if cell.timestamp_1210_ma - discharge_start >= 5 * 3600:  # >5 hours above 1.21V = 20S
114                        cell.state = CellState.PASSED_20S
115                    elif cell.timestamp_1200_ma - discharge_start >= 5 * 3600:  # >5 hours above 1.2V = 17S/20S
116                        cell.state = CellState.PASSED_17S_OR_20S
117                    else:
118                        cell.state = CellState.FAILED
119            elif discharging := discharge_event.is_set():
120                discharge_start = time.perf_counter()
121
122            # Record CSV
123            cell.csv.record(
124                time.perf_counter() - test_start,
125                time.perf_counter() - discharge_start if discharge_start else None,
126                cell.slot,
127                cell.channel,
128                volts,
129                cell.state.name.title(),
130                int(cell.timestamp_1210_ma - discharge_start) if cell.timestamp_1210_ma else None,
131                int(cell.timestamp_1200_ma - discharge_start) if cell.timestamp_1200_ma else None,
132            )
133        time.sleep(SAMPLE_RATE)
134    kill_event.set()
135
136    # Log results
137    logger.write_info_to_report("Completed recording")
138    for cell in all_cells:
139        logger.write_info_to_report(
140            f"Board: {cell.slot}, "
141            f"Channel: {cell.channel}, "
142            f"Time to 1.21V (H:M:S): {formatted_time(cell.timestamp_1210_ma, discharge_start)}, "
143            f"Time to 1.20V (H:M:S): {formatted_time(cell.timestamp_1200_ma, discharge_start)}, "
144            f"Result: {cell.state.value}{cell.state.name}{Fore.RESET}"
145        )
146    time.sleep(5 * 60)  # Wait for process to notice event
SAMPLE_RATE = 10
DISCHARGE_CUTOFF = 1.0
class CellState(enum.Enum):
31class CellState(Enum):
32    """The current state of the NiCd cell."""
33
34    UNKNOWN = Fore.MAGENTA
35    PASSED_20S = Fore.LIGHTGREEN_EX
36    PASSED_17S_OR_20S = Fore.GREEN
37    FAILED = Fore.LIGHTRED_EX

The current state of the NiCd cell.

UNKNOWN = <CellState.UNKNOWN: '\x1b[35m'>
PASSED_20S = <CellState.PASSED_20S: '\x1b[92m'>
PASSED_17S_OR_20S = <CellState.PASSED_17S_OR_20S: '\x1b[32m'>
FAILED = <CellState.FAILED: '\x1b[91m'>
Inherited Members
enum.Enum
name
value
@dataclass
class Cell:
40@dataclass
41class Cell:
42    """A NiCd cell attached to the HITL."""
43
44    index: int
45    slot: int
46    channel: int
47    timestamp_1210_ma: float | None = None
48    timestamp_1200_ma: float | None = None
49    csv: CSVRecorders | None = None
50    state = CellState.UNKNOWN
51
52    @property
53    def voltage(self):
54        """Measure the voltage for this cell."""
55        bms_hardware.dmm.scan_index = self.index
56        return bms_hardware.dmm.volts
57
58    def __str__(self) -> str:
59        """Formatted string representation."""
60        return f"{self.state.value}{self.state.name}{Fore.RESET}" f"[B{self.slot}:C{self.channel}, {self.voltage}V]"

A NiCd cell attached to the HITL.

Cell( index: int, slot: int, channel: int, timestamp_1210_ma: float | None = None, timestamp_1200_ma: float | None = None, csv: hitl_tester.modules.bms.csv_tables.CSVRecorders | None = None)
index: int
slot: int
channel: int
timestamp_1210_ma: float | None = None
timestamp_1200_ma: float | None = None
state = <CellState.UNKNOWN: '\x1b[35m'>
voltage
52    @property
53    def voltage(self):
54        """Measure the voltage for this cell."""
55        bms_hardware.dmm.scan_index = self.index
56        return bms_hardware.dmm.volts

Measure the voltage for this cell.

def formatted_time(seconds: float | None, start: float | None = 0.0) -> str:
63def formatted_time(seconds: float | None, start: float | None = 0.0) -> str:
64    """Return time formatted as hh:mm:ss."""
65    if seconds is not None and start is not None:
66        return str(datetime.timedelta(seconds=seconds - start)).partition(".")[0]
67    return "N/A"

Return time formatted as hh:mm:ss.

def test_record_voltage():
70def test_record_voltage():
71    """Record AC current data until killed."""
72    start_time = time.perf_counter()
73    while True:
74        elapsed_time = time.perf_counter() - start_time
75        volts = bms_hardware.dmm.volts
76        logger.write_info_to_report(f"Elapsed Time(s): {elapsed_time:.3f}, Voltage(V): {volts}")
77        time.sleep(SAMPLE_RATE)

Record AC current data until killed.

def test_record_voltage_nicd():
 80def test_record_voltage_nicd():
 81    """Record all voltages."""
 82    assert isinstance(bms_hardware.dmm, M300Dmm)
 83    logger.write_info_to_report("Recording...")
 84    test_start = time.perf_counter()
 85    discharge_start = None
 86    kill_event = FileEvent("kill_discharge")
 87    discharge_event = FileEvent("nicd_discharging")
 88
 89    all_cells = [Cell(i, cell["slot"], cell["channel"]) for i, cell in enumerate(bms_hardware.dmm.scan_list)]
 90
 91    # Initialize cells
 92    for cell in all_cells:
 93        cell.csv = NiCdQC(bms_hardware)
 94        cell.csv.create_file(postfix=f"_{cell.slot:01}{cell.channel:02}")
 95
 96    # Use a file lock to communicate with main test (locked = test running, released = test ended)
 97    run_test = True
 98    discharging = False
 99    while run_test:
100        time_remaining_seconds = max(0.0, (5 * 3600) - (time.perf_counter() - test_start))
101        time_remaining_string = formatted_time(time_remaining_seconds)
102        logger.write_info_to_report(f"Time remaining (H:M:S): {time_remaining_string}")
103        logger.write_info_to_report(", ".join(map(str, all_cells)))
104        for cell in all_cells:
105            if (volts := cell.voltage) < DISCHARGE_CUTOFF:
106                run_test = False
107
108            if discharging:
109                if not cell.timestamp_1210_ma and volts <= 1.21:
110                    cell.timestamp_1210_ma = time.perf_counter()
111                elif not cell.timestamp_1200_ma and volts <= 1.20:
112                    cell.timestamp_1200_ma = time.perf_counter()
113
114                    if cell.timestamp_1210_ma - discharge_start >= 5 * 3600:  # >5 hours above 1.21V = 20S
115                        cell.state = CellState.PASSED_20S
116                    elif cell.timestamp_1200_ma - discharge_start >= 5 * 3600:  # >5 hours above 1.2V = 17S/20S
117                        cell.state = CellState.PASSED_17S_OR_20S
118                    else:
119                        cell.state = CellState.FAILED
120            elif discharging := discharge_event.is_set():
121                discharge_start = time.perf_counter()
122
123            # Record CSV
124            cell.csv.record(
125                time.perf_counter() - test_start,
126                time.perf_counter() - discharge_start if discharge_start else None,
127                cell.slot,
128                cell.channel,
129                volts,
130                cell.state.name.title(),
131                int(cell.timestamp_1210_ma - discharge_start) if cell.timestamp_1210_ma else None,
132                int(cell.timestamp_1200_ma - discharge_start) if cell.timestamp_1200_ma else None,
133            )
134        time.sleep(SAMPLE_RATE)
135    kill_event.set()
136
137    # Log results
138    logger.write_info_to_report("Completed recording")
139    for cell in all_cells:
140        logger.write_info_to_report(
141            f"Board: {cell.slot}, "
142            f"Channel: {cell.channel}, "
143            f"Time to 1.21V (H:M:S): {formatted_time(cell.timestamp_1210_ma, discharge_start)}, "
144            f"Time to 1.20V (H:M:S): {formatted_time(cell.timestamp_1200_ma, discharge_start)}, "
145            f"Result: {cell.state.value}{cell.state.name}{Fore.RESET}"
146        )
147    time.sleep(5 * 60)  # Wait for process to notice event

Record all voltages.